| /* |
| * Copyright 2004-2005 The Apache Software Foundation or its licensors, |
| * as applicable. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.jackrabbit.net; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarInputStream; |
| |
| import javax.jcr.Property; |
| import javax.jcr.RepositoryException; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| /** |
| * The <code>JCRJarURLConnection</code> extends the |
| * {@link org.apache.jackrabbit.net.JCRURLConnection} class to support accessing |
| * archive files stored in a JCR Repository. |
| * <p> |
| * Just like the base class, this class requires the URL to resolve, either |
| * directly or through primary item chain, to a repository <code>Property</code>. |
| * <p> |
| * Access to this connections property and archive entry content is perpared |
| * with the {@link #connect()}, which after calling the base class implementation |
| * to find the property tries to find the archive entry and set the connection's |
| * fields according to the entry. This implementation's {@link #connect()} |
| * method fails if the named entry does not exist in the archive. |
| * <p> |
| * The {@link #getInputStream()} method either returns an stream on the archive |
| * entry or on the archive depending on whether an entry path is specified |
| * in the URL or not. Like the base class implementation, this implementation |
| * returns a new <code>InputStream</code> on each invocation. |
| * <p> |
| * If an entry path is defined on the URL, the header fields are set from the |
| * archive entry: |
| * <table border="0" cellspacing="0" cellpadding="3"> |
| * <tr><td><code>Content-Type</code><td>Guessed from the entry name or |
| * <code>application/octet-stream</code> if the type cannot be guessed |
| * from the name</tr> |
| * <tr><td><code>Content-Encoding</code><td><code>null</code></tr> |
| * <tr><td><code>Content-Length</code><td>The size of the entry</tr> |
| * <tr><td><code>Last-Modified</code><td>The last modification time of the |
| * entry</tr> |
| * </table> |
| * <p> |
| * If no entry path is defined on the URL, the header fields are set from the |
| * property by the base class implementation with the exception of the |
| * content type, which is set to <code>application/java-archive</code> by |
| * the {@link #connect()} method. |
| * <p> |
| * <em>Note that this implementation does only support archives stored in the |
| * JCR Repository, no other contained storage such as </em>file<em> or |
| * </em>http<em> is supported.</em> |
| * <p> |
| * This class is not intended to be subclassed or instantiated by clients. |
| * |
| * @author Felix Meschberger |
| * @version $Rev:$, $Date:$ |
| */ |
| public class JCRJarURLConnection extends JCRURLConnection { |
| |
| /** default log category */ |
| private static final Log log = LogFactory.getLog(JCRJarURLConnection.class); |
| |
| /** |
| * The name of the MIME content type for this connection's content if |
| * no entry path is defined on the URL (value is "application/java-archive"). |
| */ |
| protected static final String APPLICATION_JAR = "application/java-archive"; |
| |
| /** |
| * Creates an instance of this class for the given <code>url</code> |
| * supported by the <code>handler</code>. |
| * |
| * @param url The URL to base the connection on. |
| * @param handler The URL handler supporting the given URL. |
| */ |
| JCRJarURLConnection(URL url, JCRJarURLHandler handler) { |
| super(url, handler); |
| } |
| |
| /** |
| * Returns the path to the entry contained in the archive or |
| * <code>null</code> if the URL contains no entry specification in the |
| * path. |
| */ |
| String getEntryPath() { |
| return getFileParts().getEntryPath(); |
| } |
| |
| /** |
| * Connects to the URL setting the header fields and preparing for the |
| * {@link #getProperty()} and {@link #getInputStream()} methods. |
| * <p> |
| * After calling the base class implemenation to get the basic connection, |
| * the entry is looked for in the archive to set the content type, content |
| * length and last modification time header fields according to the named |
| * entry. If no entry is defined on the URL, only the content type header |
| * field is set to <code>application/java-archive</code>. |
| * <p> |
| * When this method successfully returns, this connection is considered |
| * connected. In case of an exception thrown, the connection is not |
| * connected. |
| * |
| * @throws IOException if an error occurrs retrieving the data property or |
| * any of the header field value properties or if any other errors |
| * occurrs. Any cuasing exception is set as the cause of this |
| * exception. |
| */ |
| public synchronized void connect() throws IOException { |
| |
| if (!connected) { |
| |
| // have the base class connect to get the jar property |
| super.connect(); |
| |
| // we assume the connection is now (temporarily) connected, |
| // thus calling the getters will not result in a recursive loop |
| Property property = getProperty(); |
| String contentType = getContentType(); |
| String contentEncoding = getContentEncoding(); |
| int contentLength = getContentLength(); |
| long lastModified = getLastModified(); |
| |
| // mark as not connected to not get false positives if the |
| // following code fails |
| connected = false; |
| |
| // Get hold of the data |
| try { |
| |
| JarInputStream jins = null; |
| try { |
| |
| // try to get the jar input stream, fail if no jar |
| jins = new JarInputStream(property.getStream()); |
| |
| String entryPath = getEntryPath(); |
| if (entryPath != null) { |
| |
| JarEntry entry = findEntry(jins, entryPath); |
| |
| if (entry != null) { |
| |
| contentType = guessContentTypeFromName(entryPath); |
| if (contentType == null) { |
| contentType = APPLICATION_OCTET; |
| } |
| |
| contentLength = (int) entry.getSize(); |
| lastModified = entry.getTime(); |
| |
| } else { |
| |
| throw failure("connect", entryPath + |
| " not contained in jar archive", null); |
| |
| } |
| |
| } else { |
| |
| // replaces the base class defined content type |
| contentType = APPLICATION_JAR; |
| |
| } |
| |
| } finally { |
| if (jins != null) { |
| try { |
| jins.close(); |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| |
| log.debug("connect: Using atom '" + property.getPath() |
| + "' with content type '" + contentType + "' for " |
| + String.valueOf(contentLength) + " bytes"); |
| |
| // set the fields |
| setContentType(contentType); |
| setContentEncoding(contentEncoding); |
| setContentLength(contentLength); |
| setLastModified(lastModified); |
| |
| // mark connection open |
| connected = true; |
| |
| } catch (RepositoryException re) { |
| |
| throw failure("connect", re.toString(), re); |
| |
| } |
| } |
| } |
| |
| /** |
| * Returns an input stream that reads from this open connection. If not |
| * entry path is specified in the URL, this method returns the input stream |
| * providing access to the archive as a whole. Otherwise the input stream |
| * returned is a <code>JarInputStream</code> positioned at the start of |
| * the named entry. |
| * <p> |
| * <b>NOTES:</b> |
| * <ul> |
| * <li>Each call to this method returns a new <code>InputStream</code>. |
| * <li>Do not forget to close the return stream when not used anymore for |
| * the system to be able to free resources. |
| * </ul> |
| * <p> |
| * Calling this method implicitly calls {@link #connect()} to ensure the |
| * connection is open. |
| * |
| * @return The <code>InputStream</code> on the archive or the entry if |
| * specified. |
| * |
| * @throws IOException if an error occurrs opening the connection through |
| * {@link #connect()} or creating the <code>InputStream</code> on the |
| * repository <code>Property</code>. |
| */ |
| public InputStream getInputStream() throws IOException { |
| |
| // get the input stream on the archive itself - also enforces connect() |
| InputStream ins = super.getInputStream(); |
| |
| // access the entry in the archive if defined |
| String entryPath = getEntryPath(); |
| if (entryPath != null) { |
| // open the jar input stream |
| JarInputStream jins = new JarInputStream(ins); |
| |
| // position at the correct entry |
| findEntry(jins, entryPath); |
| |
| // return the input stream |
| return jins; |
| } |
| |
| // otherwise just return the stream on the archive |
| return ins; |
| } |
| |
| //----------- internal helper to find the entry ------------------------ |
| |
| /** |
| * Returns the <code>JarEntry</code> for the path from the |
| * <code>JarInputStream</code> or <code>null</code> if the path cannot |
| * be found in the archive. |
| * |
| * @param zins The <code>JarInputStream</code> to search in. |
| * @param path The path of the <code>JarEntry</code> to return. |
| * |
| * @return The <code>JarEntry</code> for the path or <code>null</code> |
| * if no such entry can be found. |
| * |
| * @throws IOException if a problem occurrs reading from the stream. |
| */ |
| static JarEntry findEntry(JarInputStream zins, String path) |
| throws IOException { |
| |
| JarEntry entry = zins.getNextJarEntry(); |
| while (entry != null) { |
| if (path.equals(entry.getName())) { |
| return entry; |
| } |
| |
| entry = zins.getNextJarEntry(); |
| } |
| // invariant : nothing found in the zip matching the path |
| |
| return null; |
| } |
| } |