blob: 418611aecea484fa93bb745adccf6753fbe768fd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. The ASF licenses this file to You
* 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
*/
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;
}
}