Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * 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.excalibur.source.factories; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Collections; import java.util.Iterator; import java.util.Map; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpState; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; import org.apache.commons.httpclient.util.DateParseException; import org.apache.commons.httpclient.util.DateUtil; import org.apache.commons.logging.Log; import org.apache.excalibur.source.ModifiableSource; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.SourceFactory; import org.apache.excalibur.source.SourceNotFoundException; import org.apache.excalibur.source.SourceParameters; import org.apache.excalibur.source.SourceResolver; import org.apache.excalibur.source.SourceUtil; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.AbstractLoggable; import org.apache.excalibur.source.validity.TimeStampValidity; /** * HTTP URL Source object, based on the Jakarta Commons * <a href="http://jakarta.apache.org/commons/httpclient/">HttpClient</a> * project. * * @author <a href="mailto:dev@avalon.apache.org">Avalon Development Team</a> * @version $Id: HTTPClientSource.java 641953 2008-03-27 19:09:20Z cziegeler $ */ public class HTTPClientSource extends AbstractLoggable implements ModifiableSource { /** * Constant used for identifying POST requests. */ public static final String POST = "POST"; /** * Constant used for identifying GET requests. */ public static final String GET = "GET"; /** * Constant used for configuring the proxy hostname. */ public static final String PROXY_HOST = "proxy.host"; /** * Constant used for configuring the proxy port number. */ public static final String PROXY_PORT = "proxy.port"; /** * Constant used when obtaining the Content-Type from HTTP Headers */ public static final String CONTENT_TYPE = "Content-Type"; /** * Constant used when obtaining the Content-Length from HTTP Headers */ public static final String CONTENT_LENGTH = "Content-Length"; /** * Constant used when obtaining the Last-Modified date from HTTP Headers */ public static final String LAST_MODIFIED = "Last-Modified"; /** * The URI being accessed. */ private final String m_uri; /** * Contextual parameters passed via the {@link SourceFactory}. */ private final Map m_parameters; /** * Optional http state passed from SourceFactory */ private final HttpState m_httpState; /** * The {@link HttpClient} object. */ private HttpClient m_client; /** * Proxy port if set via configuration. */ private int m_proxyPort = -1; /** * Proxy host if set via configuration. */ private String m_proxyHost; /** * Whether the data held within this instance is currently accurate. */ private boolean m_dataValid; /** * Whether the resource exists on the server. */ private boolean m_exists; /** * The mime type of the resource on the server. */ private String m_mimeType; /** * The content length of the resource on the server. */ private long m_contentLength; /** * The last modified date of the resource on the server. */ private long m_lastModified; /** * Stored {@link SourceValidity} object. */ private SourceValidity m_cachedValidity; /** * Cached last modification date. */ private long m_cachedLastModificationDate; /** * Constructor, creates a new {@link HTTPClientSource} instance. * * @param uri URI * @param parameters contextual parameters passed to this instance * @exception Exception if an error occurs */ public HTTPClientSource(final String uri, final Map parameters, final HttpState httpState) throws Exception { this.m_uri = uri; this.m_parameters = parameters == null ? Collections.EMPTY_MAP : parameters; this.m_httpState = httpState; } public void setProxyHost(String proxyHost) { this.m_proxyHost = proxyHost; } public void setProxyPort(int proxyPort) { this.m_proxyPort = proxyPort; } /** * Initializes this {@link HTTPClientSource} instance. * * @exception Exception if an error occurs */ public void initialize() throws Exception { this.m_client = new HttpClient(); if (this.m_proxyHost != null && this.m_proxyPort != -1) { this.m_client.getHostConfiguration().setProxy(this.m_proxyHost, this.m_proxyPort); } if (this.m_httpState != null) { this.m_client.setState(this.m_httpState); } this.m_dataValid = false; } /** * Method to discover what kind of request is being made from the * parameters map passed in to this Source's constructor. * * @return the method type, or if no method type can be found, * HTTP GET is assumed. */ private String findMethodType() { final String method = (String) this.m_parameters.get(SourceResolver.METHOD); return method == null ? GET : method; } /** * Helper method to create the required {@link HttpMethod} object * based on parameters passed to this {@link HTTPClientSource} object. * * @return a {@link HttpMethod} object. */ private HttpMethod getMethod() { final String method = this.findMethodType(); // create a POST method if requested if (POST.equals(method)) { return this.createPostMethod(this.m_uri, (SourceParameters) this.m_parameters.get(SourceResolver.URI_PARAMETERS)); } // default method is GET return this.createGetMethod(this.m_uri); } /** * Factory method to create a new {@link PostMethod} with the given * {@link SourceParameters} object. * * @param uri URI * @param params {@link SourceParameters} * @return a {@link PostMethod} instance */ private PostMethod createPostMethod(final String uri, final SourceParameters params) { final PostMethod post = new PostMethod(uri); if (params == null) { return post; } for (final Iterator names = params.getParameterNames(); names.hasNext();) { final String name = (String) names.next(); for (final Iterator values = params.getParameterValues(name); values.hasNext();) { final String value = (String) values.next(); post.addParameter(new NameValuePair(name, value)); } } return post; } /** * Factory method to create a {@link GetMethod} object. * * @param uri URI * @return a {@link GetMethod} instance */ private GetMethod createGetMethod(final String uri) { final GetMethod method = new GetMethod(uri); // add all parameters as headers for (final Iterator i = this.m_parameters.keySet().iterator(); i.hasNext();) { final String key = (String) i.next(); final String value = (String) this.m_parameters.get(key); if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Adding header '" + key + "', with value '" + value + "'"); } method.setRequestHeader(key, value); } return method; } /** * Factory method to create a {@link HeadMethod} object. * * @param uri URI * @return a {@link HeadMethod} instance */ private HeadMethod createHeadMethod(final String uri) { return new HeadMethod(uri); } /** * Factory method to create a {@link PutMethod} object. * * @param uri URI to upload <code>uploadFile</code> to * @param uploadFile {@link File} to be uploaded * @return a {@link PutMethod} instance * @exception IOException if an error occurs */ private PutMethod createPutMethod(final String uri, final File uploadFile) throws IOException { final PutMethod put = new PutMethod(uri); put.setRequestEntity(new InputStreamRequestEntity(new FileInputStream(uploadFile.getAbsolutePath()))); return put; } /** * Factory method to create a {@link DeleteMethod} object. * * @param uri URI to delete * @return {@link DeleteMethod} instance. */ private DeleteMethod createDeleteMethod(final String uri) { return new DeleteMethod(uri); } /** * Method to make response data available if possible without * actually making an actual request (ie. via HTTP HEAD). */ private void updateData() { // no request made so far, attempt to get some response data. if (!this.m_dataValid) { if (GET.equals(this.findMethodType())) { final HttpMethod head = this.createHeadMethod(this.m_uri); try { this.executeMethod(head); return; } catch (final IOException e) { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Unable to determine response data, using defaults", e); } } finally { head.releaseConnection(); } } // default values when response data is not available this.m_exists = false; this.m_mimeType = null; this.m_contentLength = -1; this.m_lastModified = 0; this.m_dataValid = true; } } /** * Executes a particular {@link HttpMethod} and updates internal * data storage. * * @param method {@link HttpMethod} to execute * @return response code from server * @exception IOException if an error occurs */ protected int executeMethod(final HttpMethod method) throws IOException { final int response = this.m_client.executeMethod(method); this.updateExists(method); this.updateMimeType(method); this.updateContentLength(method); this.updateLastModified(method); // all finished, return response code to the caller. return response; } /** * Method to update whether a referenced resource exists, after * executing a particular {@link HttpMethod}. * * <p>REVISIT: exists() would be better called canRead() * or similar, as a resource can exist but not be readable.</p> * * @param method {@link HttpMethod} executed. */ private void updateExists(final HttpMethod method) { final int response = method.getStatusCode(); // The following returns true, if the user can successfully get // an InputStream without receiving errors? ie. if we receive a // HTTP 200 (OK), 201 (CREATED), 206 (PARTIAL CONTENT) // REVISIT(MC): need a special way to handle 304 (NOT MODIFIED) // 204 & 205 in the future // resource does not exist if HttpClient returns a 404 or a 410 this.m_exists = (response == HttpStatus.SC_OK || response == HttpStatus.SC_CREATED || response == HttpStatus.SC_PARTIAL_CONTENT); } /** * Method to ascertain whether the given resource actually exists. * * @return <code>true</code> if the resource pointed to by the * URI during construction exists, <code>false</code> * otherwise. */ public boolean exists() { this.updateData(); return this.m_exists; } /** * Method to obtain an {@link InputStream} to read the response * from the server. * * @return {@link InputStream} containing data sent from the server. * @throws IOException if some I/O problem occurs. * @throws SourceNotFoundException if the source doesn't exist. */ public InputStream getInputStream() throws IOException, SourceNotFoundException { final HttpMethod method = this.getMethod(); int response = this.executeMethod(method); this.m_dataValid = true; // throw SourceNotFoundException - according to Source API we // need to throw this if the source doesn't exist. if (!this.exists()) { final StringBuffer error = new StringBuffer(); error.append("Unable to retrieve URI: "); error.append(this.m_uri); error.append(" ("); error.append(response); error.append(")"); throw new SourceNotFoundException(error.toString()); } return method.getResponseBodyAsStream(); } /** * Obtain the absolute URI this {@link Source} object references. * * @return the absolute URI this {@link String} object references. */ public String getURI() { return this.m_uri; } /** * Return the URI scheme identifier, ie. the part preceding the fist ':' * in the URI (see <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>). * * @return the URI scheme identifier */ public String getScheme() { return SourceUtil.getScheme(this.m_uri); } /** * Obtain a {@link SourceValidity} object. * * @return a {@link SourceValidity} object, or * <code>null</code> if this is not possible. */ public SourceValidity getValidity() { // Implementation taken from URLSource.java, Kudos :) final long lm = this.getLastModified(); if (lm > 0) { if (lm == this.m_cachedLastModificationDate) { return this.m_cachedValidity; } this.m_cachedLastModificationDate = lm; this.m_cachedValidity = new TimeStampValidity(lm); return this.m_cachedValidity; } return null; } /** * Refreshes this {@link Source} object. */ public void refresh() { this.recycle(); } /** * Method to update the mime type of a resource after * executing a particular {@link HttpMethod}. * * @param method {@link HttpMethod} executed */ private void updateMimeType(final HttpMethod method) { // REVISIT: should this be the mime-type, or the content-type -> URLSource // returns the Content-Type, so we'll follow that for now. final Header header = method.getResponseHeader(CONTENT_TYPE); this.m_mimeType = header == null ? null : header.getValue(); } /** * Obtain the mime-type for the referenced resource. * * @return mime-type for the referenced resource. */ public String getMimeType() { this.updateData(); return this.m_mimeType; } /** * Method to update the content length of a resource after * executing a particular {@link HttpMethod}. * * @param method {@link HttpMethod} executed */ private void updateContentLength(final HttpMethod method) { try { final Header length = method.getResponseHeader(CONTENT_LENGTH); this.m_contentLength = length == null ? -1 : Long.parseLong(length.getValue()); } catch (final NumberFormatException e) { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Unable to determine content length, returning -1", e); } this.m_contentLength = -1; } } /** * Obtain the content length of the referenced resource. * * @return content length of the referenced resource, or * -1 if unknown/uncalculatable */ public long getContentLength() { this.updateData(); return this.m_contentLength; } /** * Method to update the last modified date of a resource after * executing a particular {@link HttpMethod}. * * @param method {@link HttpMethod} executed */ private void updateLastModified(final HttpMethod method) { final Header lastModified = method.getResponseHeader(LAST_MODIFIED); try { this.m_lastModified = lastModified == null ? 0 : DateUtil.parseDate(lastModified.getValue()).getTime(); } catch (DateParseException e) { // we ignore this exception and simply set last modified to 0 this.m_lastModified = 0; } } /** * Get the last modification date of this source. This date is * measured in milliseconds since the Epoch (00:00:00 GMT, January 1, 1970). * * @return the last modification date or <code>0</code> if unknown. */ public long getLastModified() { this.updateData(); return this.m_lastModified; } /** * Recycles this {@link HTTPClientSource} object so that it may be reused * to refresh it's content. */ private void recycle() { this.m_dataValid = false; } /////////////////////////// ModifiableSource methods /** * Obtain an {@link OutputStream} to write to. The {@link OutputStream} * returned actually references a temporary local file, which will * be written to the server upon closing. * * The returned stream must be closed or cancelled by the calling code. * * @return an {@link OutputStream} instance * @exception IOException if an error occurs */ public OutputStream getOutputStream() throws IOException { final File tempFile = File.createTempFile("httpclient", "tmp"); return new WrappedFileOutputStream(tempFile, this.getLogger()); } /** * Internal class which extends {@link FileOutputStream} to * automatically upload the data written to it, upon a {@link #close} * operation. */ private class WrappedFileOutputStream extends FileOutputStream { /** * Reference to the File being written itself. */ private File m_file; /** * Reference to a logger. */ private Log m_logger; /** * Constructor, creates a new {@link WrappedFileOutputStream} * instance. * * @param file {@link File} to write to. * @param logger {@link Logger} reference. * @exception IOException if an error occurs */ public WrappedFileOutputStream(final File file, final Log logger) throws IOException { super(file); this.m_file = file; this.m_logger = logger; } /** * Closes the stream, and uploads the file written to the * server. * * @exception IOException if an error occurs */ public void close() throws IOException { super.close(); if (this.m_file != null) { this.upload(); this.m_file.delete(); this.m_file = null; } } /** * Method to test whether this stream can be closed. * * @return <code>true</code> if possible, false otherwise. */ public boolean canCancel() { return this.m_file != null; } /** * Cancels this stream. * * @exception IOException if stream is already closed */ public void cancel() throws IOException { if (this.m_file == null) { throw new IOException("Stream already closed"); } super.close(); this.m_file.delete(); this.m_file = null; } /** * Helper method to attempt uploading of the local data file * to the remove server via a HTTP PUT. * * @exception IOException if an error occurs */ private void upload() throws IOException { final HttpMethod uploader = HTTPClientSource.this.createPutMethod(HTTPClientSource.this.m_uri, this.m_file); if (this.m_logger.isDebugEnabled()) { this.m_logger.debug("Stream closed, writing data to " + HTTPClientSource.this.m_uri); } try { final int response = HTTPClientSource.this.executeMethod(uploader); if (!this.successfulUpload(response)) { throw new SourceException( "Write to " + HTTPClientSource.this.m_uri + " failed (" + response + ")"); } if (this.m_logger.isDebugEnabled()) { this.m_logger .debug("Write to " + HTTPClientSource.this.m_uri + " succeeded (" + response + ")"); } } finally { if (uploader != null) { uploader.releaseConnection(); } } } /** * According to RFC2616 (HTTP 1.1) valid responses for a HTTP PUT * are 201 (Created), 200 (OK), and 204 (No Content). * * @param response response code from the HTTP PUT * @return true if upload was successful, false otherwise. */ private boolean successfulUpload(final int response) { return response == HttpStatus.SC_OK || response == HttpStatus.SC_CREATED || response == HttpStatus.SC_NO_CONTENT; } } /** * Deletes the referenced resource. * * @exception SourceException if an error occurs */ public void delete() throws SourceException { final DeleteMethod delete = this.createDeleteMethod(this.m_uri); try { final int response = this.executeMethod(delete); if (!this.deleteSuccessful(response)) { throw new SourceException("Failed to delete " + this.m_uri + " (" + response + ")"); } if (this.getLogger().isDebugEnabled()) { this.getLogger().debug(this.m_uri + " deleted (" + response + ")"); } } catch (final IOException e) { throw new SourceException("IOException thrown during delete", e); } finally { delete.releaseConnection(); } } /** * According to RFC2616 (HTTP 1.1) valid responses for a HTTP DELETE * are 200 (OK), 202 (Accepted) and 204 (No Content). * * @param response response code from the HTTP PUT * @return true if upload was successful, false otherwise. */ private boolean deleteSuccessful(final int response) { return response == HttpStatus.SC_OK || response == HttpStatus.SC_ACCEPTED || response == HttpStatus.SC_NO_CONTENT; } /** * Method to determine whether writing to the supplied OutputStream * (which must be that returned from {@link #getOutputStream()}) can * be cancelled * * @return true if writing to the stream can be cancelled, * false otherwise */ public boolean canCancel(final OutputStream stream) { // with help from FileSource, dankeschoen lads :) if (stream instanceof WrappedFileOutputStream) { return ((WrappedFileOutputStream) stream).canCancel(); } throw new IllegalArgumentException("Output stream supplied was not created by this class"); } /** * Cancels any data sent to the {@link OutputStream} returned by * {@link #getOutputStream()}. * * After calling this method, the supplied {@link OutputStream} * should no longer be used. */ public void cancel(final OutputStream stream) throws IOException { if (stream instanceof WrappedFileOutputStream) { ((WrappedFileOutputStream) stream).cancel(); } else { throw new IllegalArgumentException("Output stream supplied was not created by this class"); } } }