/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.processor; import org.apache.log4j.Logger; import org.orbeon.oxf.cache.*; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.http.Credentials; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.processor.impl.ProcessorOutputImpl; import org.orbeon.oxf.resources.ResourceManagerWrapper; import org.orbeon.oxf.resources.URLFactory; import org.orbeon.oxf.resources.handler.OXFHandler; import org.orbeon.oxf.util.*; import org.orbeon.oxf.xml.SAXStore; import org.orbeon.oxf.xml.XMLParsing; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.*; /** * Implementation of a caching transformer output that assumes that an output depends on a set * of URIs associated with an input document. * * Usage: an URIReferences object must be cached as an object associated with the config input. */ public abstract class URIProcessorOutputImpl extends ProcessorOutputImpl { public static Logger logger = LoggerFactory.createLogger(URIProcessorOutputImpl.class); private ProcessorImpl processorImpl; private String configInputName; private URIReferences localConfigURIReferences = null; // TODO: NIY public URIProcessorOutputImpl(ProcessorImpl processorImpl, String name, String configInputName) { super(processorImpl, name); this.processorImpl = processorImpl; this.configInputName = configInputName; } // private void log(String message) { // logger.info("URIProcessorOutputImpl (" + getClass().getName() + ") " + message); // } @Override public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) { final URIReferences uriReferences = getCachedURIReferences(pipelineContext); // log("uriReferences: " + uriReferences); if (uriReferences == null) return null; final List<CacheKey> keys = new ArrayList<CacheKey>(); // Handle config if read as input if (localConfigURIReferences == null) { final ProcessorImpl.KeyValidity configKeyValidity = processorImpl.getInputKeyValidity(pipelineContext, configInputName); // log("configKeyValidity: " + configKeyValidity); if (configKeyValidity == null) return null; keys.add(configKeyValidity.key); } // Add local key if needed if (supportsLocalKeyValidity()) { final ProcessorImpl.KeyValidity keyValidity = getLocalKeyValidity(pipelineContext, uriReferences); if (keyValidity == null) return null; keys.add(keyValidity.key); } // Handle local key // final ProcessorImpl.KeyValidity localKeyValidity = uriReferences.getLocalKeyValidity(); // if (localKeyValidity != null) // keys.add(localKeyValidity.key); // Handle dependencies if any // log("uriReferences.getReferences(): " + uriReferences.getReferences()); if (uriReferences.getReferences() != null) { for (final URIReference uriReference: uriReferences.getReferences()) { if (uriReference == null) return null; final CacheKey uriKey = getURIKey(pipelineContext, uriReference); // log("key: " + uriKey); keys.add(uriKey); } } final CacheKey[] outKeys = new CacheKey[keys.size()]; keys.toArray(outKeys); return new CompoundOutputCacheKey(getProcessorClass(), getName(), outKeys); } @Override protected Object getValidityImpl(PipelineContext pipelineContext) { final URIReferences uriReferences = getCachedURIReferences(pipelineContext); // log("uriReferences: " + uriReferences); if (uriReferences == null) return null; final List<Object> validities = new ArrayList<Object>(); // Handle config if read as input if (localConfigURIReferences == null) { final ProcessorImpl.KeyValidity configKeyValidity = processorImpl.getInputKeyValidity(pipelineContext, configInputName); // log("configKeyValidity: " + configKeyValidity); if (configKeyValidity == null) return null; validities.add(configKeyValidity.validity); } // Handle local validity // final ProcessorImpl.KeyValidity localKeyValidity = uriReferences.getLocalKeyValidity(); // if (localKeyValidity != null) // validities.add(localKeyValidity.validity); // Add local validity if needed if (supportsLocalKeyValidity()) { final ProcessorImpl.KeyValidity keyValidity = getLocalKeyValidity(pipelineContext, uriReferences); if (keyValidity == null) return null; validities.add(keyValidity.validity); } // Handle dependencies if any // log("uriReferences.getReferences(): " + uriReferences.getReferences()); if (uriReferences.getReferences() != null) { for (final URIReference uriReference: uriReferences.getReferences()) { if (uriReference == null) return null; final Object uriValidity = getURIValidity(pipelineContext, uriReference); // log("validity: " + uriValidity); validities.add(uriValidity); } } return validities; } /** * This method returns the key associated with an URI. * * @param pipelineContext current pipeline context * @param uriReference URIReference object containing the URI to process * @return key of the URI (including null) */ protected CacheKey getURIKey(PipelineContext pipelineContext, URIReference uriReference) { try { final String inputName = ProcessorImpl.getProcessorInputSchemeInputName(uriReference.spec); if (inputName != null) { // input: URIs return ProcessorImpl.getInputKey(pipelineContext, processorImpl.getInputByName(inputName)); } else { // Other URIs final String keyString = buildURIUsernamePasswordString(URLFactory.createURL(uriReference.context, uriReference.spec).toExternalForm(), uriReference.credentials); return new InternalCacheKey(processorImpl, "urlReference", keyString); } } catch (Exception e) { // If the file no longer exists, for example, we don't want to throw, just to invalidate // An exception will be thrown if necessary when the document is actually read // log("exception: " + e.getMessage()); return null; } } /** * This method returns the validity associated with an URI. * * @param pipelineContext current pipeline context * @param uriReference URIReference object containing the URI to process * @return validity of the URI (including null) */ protected Object getURIValidity(PipelineContext pipelineContext, URIReference uriReference) { try { final String inputName = ProcessorImpl.getProcessorInputSchemeInputName(uriReference.spec); if (inputName != null) { // input: URIs return ProcessorImpl.getInputValidity(pipelineContext, processorImpl.getInputByName(inputName)); } else { final URL url = URLFactory.createURL(uriReference.context, uriReference.spec); if (OXFHandler.PROTOCOL.equals(url.getProtocol())) { // oxf: URLs final String key = url.getFile(); final long result = ResourceManagerWrapper.instance().lastModified(key, false); // Zero and negative values often have a special meaning, make sure to normalize here return (result <= 0) ? null : result; } else if ("http".equals(url.getProtocol()) || "https".equals(url.getProtocol())) { // HTTP and HTTPS protocols: read and keep document // NOTE: We cache for this execution of this processor, so we don't make multiple accesses. The // cache is discarded once the processor gets out of scope. final URIReferencesState state = (URIReferencesState) processorImpl.getState(pipelineContext); final String urlString = url.toExternalForm(); readURLToStateIfNeeded(pipelineContext, url, state, uriReference.credentials); return state.getLastModified(urlString, uriReference.credentials); } else { // Other URLs return NetUtils.getLastModifiedAsLong(url); } } } catch (Exception e) { // If the file no longer exists, for example, we don't want to throw, just to invalidate // An exception will be thrown if necessary when the document is actually read // log("exception: " + e.getMessage()); return null; } } // public SAXStore getDocument(PipelineContext pipelineContext, String urlString) { // // Use cached state if possible // // NOTE: We cache just for this execution of this processor, so we don't make multiple accesses // final URIReferencesState state = (URIReferencesState) processorImpl.getState(pipelineContext); // if (state.isDocumentSet(urlString)) // return state.getDocument(urlString); // else // return null; // } private URIReferences getCachedURIReferences(PipelineContext pipelineContext) { // Check if config is external if (localConfigURIReferences != null) return localConfigURIReferences; // Make sure the config input is cacheable final ProcessorImpl.KeyValidity keyValidity = processorImpl.getInputKeyValidity(pipelineContext, configInputName); if (keyValidity == null) return null; // Try to find resource manager key in cache final URIReferences config = (URIReferences) ObjectCache.instance().findValid(keyValidity.key, keyValidity.validity); if (ProcessorImpl.logger.isDebugEnabled()) { if (config != null) ProcessorImpl.logger.debug("Config (URIReferences) found: " + config.toString()); else ProcessorImpl.logger.debug("Config (URIReferences) not found"); } return config; } public static class URIReference { public final String context; public final String spec; public final Credentials credentials; public URIReference(String context, String spec, Credentials credentials) { this.context = context; this.spec = spec; this.credentials = credentials; } @Override public String toString() { return "[" + context + ", " + spec + ", " + credentials + "]"; } } private static class DocumentInfo { public SAXStore saxStore; public Long lastModified; public DocumentInfo(SAXStore saxStore, Long lastModified) { this.saxStore = saxStore; this.lastModified = lastModified; } } public static class URIReferencesState { private Map<String, DocumentInfo> map; public void setDocument(String urlString, Credentials credentials, SAXStore documentSAXStore, Long lastModified) { if (map == null) map = new HashMap<String, DocumentInfo>(); map.put(buildURIUsernamePasswordString(urlString, credentials), new DocumentInfo(documentSAXStore, lastModified)); } public boolean isDocumentSet(String urlString, Credentials credentials) { return map != null && map.get(buildURIUsernamePasswordString(urlString, credentials)) != null; } public Long getLastModified(String urlString, Credentials credentials) { final DocumentInfo documentInfo = map.get(buildURIUsernamePasswordString(urlString, credentials)); return documentInfo.lastModified; } public SAXStore getDocument(String urlString, Credentials credentials) { final DocumentInfo documentInfo = map.get(buildURIUsernamePasswordString(urlString, credentials)); return documentInfo.saxStore; } } /** * This is the object that must be associated with the configuration input and cached. Users can * derive from this and store their own configuration inside. */ public static class URIReferences { private List<URIReference> references; /** * Add a URL reference. * * @param context optional context (can be null) * @param spec URL spec * @param credentials optional credentials */ public void addReference(String context, String spec, Credentials credentials) { if (references == null) references = new ArrayList<URIReference>(); references.add(new URIReference(context, spec, credentials)); } /** * Get URI references. * * @return references or null if none */ public List<URIReference> getReferences() { return references; } } protected boolean supportsLocalKeyValidity() { return false; } public ProcessorImpl.KeyValidity getLocalKeyValidity(PipelineContext pipelineContext, URIReferences uriReferences) { throw new UnsupportedOperationException(); } /** * This is called to handle "http:", "https:" and other URLs (but not "oxf:" and "input:"). It is possible to * override this method, for example to optimize HTTP access. * * @param pipelineContext current context * @param url URL to read * @param state state to read to * @param credentials optional credentials */ public void readURLToStateIfNeeded(PipelineContext pipelineContext, URL url, URIReferencesState state, Credentials credentials) { final String urlString = url.toExternalForm(); // Use cached state if possible if (!state.isDocumentSet(urlString, credentials)) { // We read the document and store it temporarily, since it will likely be read just after this anyway final SAXStore documentSAXStore; final Long lastModifiedLong; { // Perform connection final ExternalContext externalContext = (ExternalContext) pipelineContext.getAttribute(PipelineContext.EXTERNAL_CONTEXT); // Compute absolute submission URL final URI submissionURL; try { submissionURL = new URI( URLRewriterUtils.rewriteServiceURL(externalContext.getRequest(), urlString, ExternalContext.Response.REWRITE_MODE_ABSOLUTE) ); } catch (URISyntaxException e) { throw new OXFException(e); } // Open connection final IndentedLogger indentedLogger = new IndentedLogger(logger); final scala.collection.immutable.Map<String, scala.collection.immutable.List<String>> headers = Connection.jBuildConnectionHeadersCapitalizedIfNeeded( submissionURL.getScheme(), credentials != null, null, Connection.jHeadersToForward(), Connection.getHeaderFromRequest(externalContext.getRequest()), indentedLogger ); final ConnectionResult connectionResult = Connection.jApply("GET", submissionURL, credentials, null, headers, true, false, indentedLogger).connect(true); // Throw if connection failed (this is caught by the caller) if (connectionResult.statusCode() != 200) throw new OXFException("Got invalid return code while loading URI: " + urlString + ", " + connectionResult.statusCode()); // Read connection into SAXStore documentSAXStore = new SAXStore(); ConnectionResult.withSuccessConnection(connectionResult, true, new Function1Adapter<InputStream, Object>() { public Object apply(InputStream is) { XMLParsing.inputStreamToSAX(is, connectionResult.url(), documentSAXStore, XMLParsing.ParserConfiguration.PLAIN, true); return null; } }); // Obtain last modified lastModifiedLong = connectionResult.lastModifiedJava(); } // Cache document and last modified state.setDocument(urlString, credentials, documentSAXStore, lastModifiedLong); } } private static String buildURIUsernamePasswordString(String uriString, Credentials credentials) { // We don't care that the result is an actual URI if (credentials != null) return credentials.getPrefix() + uriString; else return uriString; } }