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    }