001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020
021 package org.apache.geronimo.system.plugin;
022
023 import java.io.File;
024 import java.io.FileOutputStream;
025 import java.io.IOException;
026 import java.io.InputStream;
027 import java.net.HttpURLConnection;
028 import java.net.MalformedURLException;
029 import java.net.URI;
030 import java.net.URL;
031 import java.net.URLConnection;
032 import java.util.ArrayList;
033 import java.util.Arrays;
034 import java.util.Comparator;
035 import java.util.List;
036
037 import javax.security.auth.login.FailedLoginException;
038 import javax.xml.parsers.DocumentBuilder;
039 import javax.xml.parsers.ParserConfigurationException;
040
041 import org.apache.geronimo.kernel.repository.Artifact;
042 import org.apache.geronimo.kernel.repository.FileWriteMonitor;
043 import org.apache.geronimo.kernel.repository.Version;
044 import org.apache.geronimo.kernel.repository.WriteableRepository;
045 import org.apache.geronimo.kernel.util.XmlUtil;
046 import org.apache.geronimo.system.plugin.model.PluginListType;
047 import org.apache.geronimo.crypto.encoders.Base64;
048 import org.w3c.dom.Document;
049 import org.w3c.dom.Element;
050 import org.w3c.dom.Node;
051 import org.w3c.dom.NodeList;
052 import org.xml.sax.SAXException;
053
054 /**
055 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
056 */
057 public class RemoteSourceRepository implements SourceRepository {
058
059 private final URI base;
060 private String username = null;
061 private String password = null;
062
063 public RemoteSourceRepository(URI base, String username, String password) {
064 if (!base.getPath().endsWith("/")) {
065 throw new IllegalArgumentException("base uri must end with '/', not " + base);
066 }
067 this.base = base;
068 this.username = username;
069 this.password = password;
070 }
071
072 public PluginListType getPluginList() {
073 try {
074 URL uri = base.resolve("geronimo-plugins.xml").toURL();
075 InputStream in = openStream(null, uri);
076 if (in != null) {
077 try {
078 return PluginXmlUtil.loadPluginList(in);
079 } finally {
080 in.close();
081 }
082 }
083 } catch (Exception e) {
084 // TODO: log it?
085 }
086 return null;
087 }
088
089 public OpenResult open(Artifact artifact, FileWriteMonitor monitor) throws IOException, FailedLoginException {
090
091 // If the artifact version is resolved then look for the artifact in the repo
092 if (artifact.isResolved()) {
093 URL location = getURL(artifact);
094 OpenResult result = open(artifact, location);
095 if (result != null) {
096 return result;
097 }
098 Version version = artifact.getVersion();
099 // Snapshot artifacts can have a special filename in an online maven repo.
100 // The version number is replaced with a timestmap and build number.
101 // The maven-metadata file contains this extra information.
102 if (version.toString().indexOf("SNAPSHOT") >= 0 && !(version instanceof SnapshotVersion)) {
103 // base path for the artifact version in a maven repo
104 URI basePath = base.resolve(artifact.getGroupId().replace('.', '/') + "/" + artifact.getArtifactId() + "/" + version + "/");
105
106 // get the maven-metadata file
107 Document metadata = getMavenMetadata(basePath);
108
109 // determine the snapshot qualifier from the maven-metadata file
110 if (metadata != null) {
111 NodeList snapshots = metadata.getDocumentElement().getElementsByTagName("snapshot");
112 if (snapshots.getLength() >= 1) {
113 Element snapshot = (Element) snapshots.item(0);
114 List<String> timestamp = getChildrenText(snapshot, "timestamp");
115 List<String> buildNumber = getChildrenText(snapshot, "buildNumber");
116 if (timestamp.size() >= 1 && buildNumber.size() >= 1) {
117 try {
118 // recurse back into this method using a SnapshotVersion
119 SnapshotVersion snapshotVersion = new SnapshotVersion(version);
120 snapshotVersion.setBuildNumber(Integer.parseInt(buildNumber.get(0)));
121 snapshotVersion.setTimestamp(timestamp.get(0));
122 Artifact newQuery = new Artifact(artifact.getGroupId(), artifact.getArtifactId(), snapshotVersion, artifact.getType());
123 location = getURL(newQuery);
124 return open(artifact, location);
125 } catch (NumberFormatException nfe) {
126 // log.error("Could not create snapshot version for " + artifact, nfe);
127 }
128 } else {
129 // log.error("Could not create snapshot version for " + artifact);
130 }
131 }
132 }
133 }
134 return null;
135 }
136
137 // Version is not resolved. Look in maven-metadata.xml and maven-metadata-local.xml for
138 // the available version numbers. If found then recurse into the enclosing method with
139 // a resolved version number
140 else {
141
142 // base path for the artifact version in a maven repo
143 URI basePath = base.resolve(artifact.getGroupId().replace('.', '/') + "/" + artifact.getArtifactId() + "/");
144
145 // get the maven-metadata file
146 Document metadata = getMavenMetadata(basePath);
147
148 // determine the available versions from the maven-metadata file
149 if (metadata != null) {
150 Element root = metadata.getDocumentElement();
151 NodeList list = root.getElementsByTagName("versions");
152 list = ((Element) list.item(0)).getElementsByTagName("version");
153 Version[] available = new Version[list.getLength()];
154 for (int i = 0; i < available.length; i++) {
155 available[i] = new Version(getText(list.item(i)));
156 }
157 // desc sort
158 Arrays.sort(available, new Comparator<Version>() {
159 public int compare(Version o1, Version o2) {
160 return o2.toString().compareTo(o1.toString());
161 }
162 });
163
164 for (Version version : available) {
165 URL location = getURL(new Artifact(artifact.getGroupId(), artifact.getArtifactId(), version, artifact.getType()));
166 OpenResult result = open(artifact, location);
167 if (result != null) {
168 return result;
169 }
170 }
171 }
172 }
173 return null;
174 }
175
176 private OpenResult open(Artifact artifact, URL location) throws IOException, FailedLoginException {
177 InputStream in = openStream(artifact, location);
178 return (in == null) ? null : new RemoteOpenResult(artifact, in);
179 }
180
181 private InputStream openStream(Artifact artifact, URL location) throws IOException, FailedLoginException {
182 int size = 0;
183 URLConnection con = location.openConnection();
184 if (con instanceof HttpURLConnection) {
185 HttpURLConnection http = (HttpURLConnection) con;
186 http.connect();
187 if (http.getResponseCode() == 401) { // need to authenticate
188 if (username == null || username.equals("")) {
189 throw new FailedLoginException("Server returned 401 " + http.getResponseMessage());
190 }
191 //TODO is it necessary to keep getting new http's ?
192 http = (HttpURLConnection) location.openConnection();
193 http.setRequestProperty("Authorization",
194 "Basic " + new String(Base64.encode((username + ":" + password).getBytes())));
195 http.connect();
196 if (http.getResponseCode() == 401) {
197 throw new FailedLoginException("Server returned 401 " + http.getResponseMessage());
198 } else if (http.getResponseCode() == 404) {
199 return null; // Not found at this repository
200 }
201 if (http.getContentLength() > 0) {
202 size = http.getContentLength();
203 }
204 } else if (http.getResponseCode() == 404) {
205 return null; // Not found at this repository
206 } else {
207 if (http.getContentLength() > 0) {
208 size = http.getContentLength();
209 }
210 }
211 return http.getInputStream();
212 }
213 return null;
214 }
215
216
217 private Document getMavenMetadata(URI base) throws IOException, FailedLoginException {
218 Document doc = null;
219 InputStream in = null;
220
221 try {
222 URL metaURL = base.resolve( "maven-metadata.xml").toURL();
223 in = openStream(null, metaURL);
224 if (in == null) { // check for local maven metadata
225 metaURL = base.resolve("maven-metadata-local.xml").toURL();
226 in = openStream(null, metaURL);
227 }
228 if (in != null) {
229 DocumentBuilder builder = XmlUtil.newDocumentBuilderFactory().newDocumentBuilder();
230 doc = builder.parse(in);
231 }
232 } catch (ParserConfigurationException e) {
233 throw (IOException)new IOException().initCause(e);
234 } catch (SAXException e) {
235 throw (IOException)new IOException().initCause(e);
236 } finally {
237 if (in == null) {
238 // log.info("No maven metadata available at " + base);
239 } else {
240 in.close();
241 }
242 }
243 return doc;
244 }
245
246
247 private URL getURL(Artifact configId) throws MalformedURLException {
248 String qualifiedVersion = configId.getVersion().toString();
249 if (configId.getVersion() instanceof SnapshotVersion) {
250 SnapshotVersion ssVersion = (SnapshotVersion) configId.getVersion();
251 String timestamp = ssVersion.getTimestamp();
252 int buildNumber = ssVersion.getBuildNumber();
253 if (timestamp != null && buildNumber != 0) {
254 qualifiedVersion = qualifiedVersion.replaceAll("SNAPSHOT", timestamp + "-" + buildNumber);
255 }
256 }
257 return base.resolve(configId.getGroupId().replace('.', '/') + "/"
258 + configId.getArtifactId() + "/" + configId.getVersion()
259 + "/" + configId.getArtifactId() + "-"
260 + qualifiedVersion + "." + configId.getType()).toURL();
261 }
262
263 /**
264 * Gets all the text contents of the specified DOM node.
265 */
266 private static String getText(Node target) {
267 NodeList nodes = target.getChildNodes();
268 StringBuffer buf = null;
269 for (int j = 0; j < nodes.getLength(); j++) {
270 Node node = nodes.item(j);
271 if (node.getNodeType() == Node.TEXT_NODE) {
272 if (buf == null) {
273 buf = new StringBuffer();
274 }
275 buf.append(node.getNodeValue());
276 }
277 }
278 return buf == null ? null : buf.toString();
279 }
280
281 /**
282 * Gets the text out of all the child nodes of a certain type. The result
283 * array has one element for each child of the specified DOM element that
284 * has the specified name.
285 *
286 * @param root The parent DOM element
287 * @param property The name of the child elements that hold the text
288 */
289 private static List<String> getChildrenText(Element root, String property) {
290 NodeList children = root.getChildNodes();
291 List<String> results = new ArrayList<String>();
292 for (int i = 0; i < children.getLength(); i++) {
293 Node check = children.item(i);
294 if (check.getNodeType() == Node.ELEMENT_NODE && check.getNodeName().equals(property)) {
295 NodeList nodes = check.getChildNodes();
296 StringBuffer buf = null;
297 for (int j = 0; j < nodes.getLength(); j++) {
298 Node node = nodes.item(j);
299 if (node.getNodeType() == Node.TEXT_NODE) {
300 if (buf == null) {
301 buf = new StringBuffer();
302 }
303 buf.append(node.getNodeValue());
304 }
305 }
306 results.add(buf == null ? null : buf.toString());
307 }
308 }
309 return results;
310 }
311
312 private static class RemoteOpenResult implements OpenResult {
313 private final Artifact artifact;
314 private final InputStream in;
315 private File file;
316
317 private RemoteOpenResult(Artifact artifact, InputStream in) {
318 this.artifact = artifact;
319 this.in = in;
320 }
321
322 public Artifact getArtifact() {
323 return artifact;
324 }
325
326 public File getFile() throws IOException {
327 if (file == null) {
328 file = downloadFile(in);
329 }
330 return file;
331 }
332
333 public void install(WriteableRepository repo, FileWriteMonitor monitor) throws IOException {
334 File file = getFile();
335 repo.copyToRepository(file, artifact, monitor);
336 if (!file.delete()) {
337 // log.warn("Unable to delete temporary download file " + tempFile.getAbsolutePath());
338 file.deleteOnExit();
339 }
340 }
341
342 public void close() {
343 if (in != null) {
344 try {
345 in.close();
346 } catch (IOException e) {
347 //ignore
348 }
349 }
350 }
351 /**
352 * Downloads to a temporary file so we can validate the download before
353 * installing into the repository.
354 *
355 * @param in source of download
356 // * @param monitor monitor to report results of download
357 * @return downloaded file
358 * @throws IOException if input cannot be read or file cannot be written
359 */
360 private File downloadFile(InputStream in/*, ResultsFileWriteMonitor monitor*/) throws IOException {
361 if (in == null) {
362 throw new IllegalStateException();
363 }
364 FileOutputStream out = null;
365 byte[] buf;
366 try {
367 // monitor.writeStarted(result.getArtifact().toString(), result.getFileSize());
368 File file = File.createTempFile("geronimo-plugin-download-", ".tmp");
369 out = new FileOutputStream(file);
370 buf = new byte[65536];
371 int count, total = 0;
372 while ((count = in.read(buf)) > -1) {
373 out.write(buf, 0, count);
374 // monitor.writeProgress(total += count);
375 }
376 // monitor.writeComplete(total);
377 in.close();
378 in = null;
379 out.close();
380 out = null;
381 return file;
382 } finally {
383 if (in != null) {
384 try {
385 in.close();
386 } catch (IOException ignored) {
387 //ignore
388 }
389 }
390 if (out != null) {
391 try {
392 out.close();
393 } catch (IOException ignored) {
394 //ignore
395 }
396 }
397 }
398 }
399 }
400 }