/* Copyright (2005-2012) Schibsted ASA
* This file is part of Possom.
*
* Possom 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 3 of the License, or
* (at your option) any later version.
*
* Possom 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Possom. If not, see <http://www.gnu.org/licenses/>.
*
* AbstractResourceLoader.java
*
* Created on 23 January 2006, 10:57
*
*/
package no.sesat.search.site.config;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ByteArrayOutputStream;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.net.URL;
import javax.xml.parsers.DocumentBuilder;
import no.sesat.search.site.Site;
import no.sesat.search.site.SiteContext;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/** Utility class to handle loading different types of resources in a background thread.
* This avoids the problem of having to order loading of applications in the container because of static initialisers
* using resources from the search-front-config application.
*
* <br/>
*
* Because the loading is backgrounded it is important to wait until it is finished.
* This is done by calling abut()
*
* <br/><br/>
*
* Example usecases:<br/>
* (1)<pre>
* // load custom.properties into props in skin's WEB-INF/classes/
* Site site = ...;
* PropertiesContext context = ...;
* Properties props = new Properties();
* PropertiesLoader loader = context.newPropertiesLoader(site.getSiteContext(), "custom.properties", props);
* loader.abut();
* props.getProperty(...);
* <pre/>
* (2)<pre>
* // create document from my.xml located in skin's WEB-INF/classes/
* Site site = ...;
* DocumentContext context = ...;
* final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
* factory.setValidating(false);
* final DocumentBuilder builder = factory.newDocumentBuilder();
* loader = context.newDocumentLoader(site.getSiteContext(), "my.xml", builder);
* loader.abut();
* Document doc = loader.getDocument();
* </pre>
* (3)<pre>
* // read a class file from a jar file located in skin's WEB-INF/lib/
* Site site = ...;
* BytecodeContext context = ...;
* BytecodeLoader loader= context.newBytecodeLoader(site.getSiteContext(),"Example.class", "Example.jar");
* loader.abut()
* byte[] bytes = loader.getBytecode();
* </pre>
*
* @version $Id$
*
*/
public abstract class AbstractResourceLoader
implements Runnable, DocumentLoader, PropertiesLoader, BytecodeLoader {
private enum Polymorphism{
NONE,
FIRST_FOUND,
DOWN_HIERARCHY,
UP_HEIRARCHY
}
private enum Resource{
PROPERTIES(Polymorphism.UP_HEIRARCHY),
DOM_DOCUMENT(Polymorphism.NONE),
BYTECODE(Polymorphism.NONE);
final private Polymorphism polymorphism;
Resource(final Polymorphism polymorphism){
this.polymorphism = polymorphism;
}
Polymorphism getPolymorphism(){
return polymorphism;
}
}
// Constants --------------------------------------------------------
private static final String ERR_MUST_USE_PROPS_INITIALISER = "Must use properties initialiser to use this method!";
private static final String ERR_MUST_USE_XSTREAM_INITIALISER = "Must use xstream initialiser to use this method!";
private static final String ERR_MUST_USE_BYTECODE_INITIALISER = "Must use bytecode initialiser to use this method";
private static final String ERR_ONE_USE_ONLY = "This AbstractResourceLoader instance already in use!";
private static final String ERR_MUST_USE_CONTEXT_CONSTRUCTOR = "Must use constructor that supplies a context!";
private static final String ERR_INTERRUPTED_WAITING_4_RSC_2_LOAD = "Interrupted waiting for resource to load";
private static final String ERR_NOT_INITIALISED = "This AbstractResourceLoader has not been initialised. Nothing to wait for!";
private static final String WARN_USING_FALLBACK = "Falling back to default version for resource ";
private static final String FATAL_RESOURCE_NOT_LOADED = "Resource not found ";
private static final String WARN_PARENT_SITE = "Parent site is: ";
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private static final Logger LOG = Logger.getLogger(AbstractResourceLoader.class);
private static final String DEBUG_POOL_COUNT = "Pool size: ";
// Attributes ----------------------------------------------------
private final SiteContext context;
private String resource;
private Future future;
private Resource resourceType;
/** the properties resource holder. **/
protected Properties props;
/** DocumentBuilder builder. **/
protected DocumentBuilder builder;
/** Document. **/
protected Document document;
/** Bytecode **/
private byte[] bytecode;
/** Name of jar to load classes from **/
protected String jarFileName;
// Constructors --------------------------------------------------
/** Illegal Constructor. Must use AbstractResourceLoader(SiteContext). */
private AbstractResourceLoader() {
throw new IllegalArgumentException(ERR_MUST_USE_CONTEXT_CONSTRUCTOR);
}
/** Creates a new instance of AbstractResourceLoader.
*@param cxt the context that we supply us with which site we are dealing with.
*/
protected AbstractResourceLoader(final SiteContext cxt) {
context = cxt;
}
// Public --------------------------------------------------------
public abstract boolean urlExists(URL url);
public Properties getProperties() {
if (props == null) {
throw new IllegalStateException(ERR_MUST_USE_PROPS_INITIALISER);
}
return props;
}
public Document getDocument() {
if (builder == null) {
throw new IllegalStateException(ERR_MUST_USE_XSTREAM_INITIALISER);
}
return document;
}
public byte[] getBytecode() {
if (bytecode == null) {
throw new IllegalStateException(ERR_MUST_USE_BYTECODE_INITIALISER);
}
return bytecode;
}
public void init(final String resource, final Properties props) {
resourceType = Resource.PROPERTIES;
preInit(resource);
this.props = props;
postInit();
}
public void init(final String resource, final DocumentBuilder builder) {
resourceType = Resource.DOM_DOCUMENT;
preInit(resource);
this.builder = builder;
postInit();
}
public void initBytecodeLoader(String className, String jarFileName) {
resourceType = Resource.BYTECODE;
this.jarFileName = jarFileName;
preInit(className);
postInit();
}
public void abut() {
if (future == null) {
throw new IllegalStateException(ERR_NOT_INITIALISED);
}
try {
final long time = System.currentTimeMillis();
future.get();
LOG.debug("abut(" + (System.currentTimeMillis() - time) + "ms) for " + getResource(context.getSite()));
} catch (InterruptedException ex) {
LOG.error(ERR_INTERRUPTED_WAITING_4_RSC_2_LOAD, ex);
} catch (ExecutionException ex) {
LOG.error(ERR_INTERRUPTED_WAITING_4_RSC_2_LOAD, ex);
}
}
public void run() {
// Inheriting from Site & UniqueId from parent thread is meaningless in a thread pool.
MDC.put(Site.NAME_KEY, context.getSite());
MDC.remove("UNIQUE_ID");
switch(resourceType.getPolymorphism()){
case UP_HEIRARCHY:
// Properties inherent through the fallback process. Keys are *not* overridden.
boolean resourceFound = false;
for(Site site = getContext().getSite(); site != null; site = site.getParent()){
resourceFound |= loadResource(getResource(site));
}
if (!resourceFound) {
throw new ResourceLoadException("Could not find resource " + getResource(context.getSite()));
}
break;
case DOWN_HIERARCHY:
throw new UnsupportedOperationException("Not yet implemented");
case FIRST_FOUND:
// Default behavour: only load first found resource
Site site = getContext().getSite();
do {
if (loadResource(getResource(site))) {
break;
} else {
site = site.getParent();
if( null != site ){
LOG.warn(WARN_USING_FALLBACK + getResource(site));
LOG.warn(WARN_PARENT_SITE + site.getParent());
}
}
} while (site != null);
if (site == null) {
LOG.fatal(FATAL_RESOURCE_NOT_LOADED);
}
break;
case NONE:
// if the resource doesn't exist then fake an empty result.
if (!loadResource(getResource(getContext().getSite()))) {
loadEmptyResource(getResource(getContext().getSite()));
}
break;
}
}
// Protected -----------------------------------------------------
protected abstract URL getResource(final Site site);
protected abstract InputStream getInputStreamFor(final URL resource);
/** Get the SiteContext.
*@return the SiteContext.
**/
protected SiteContext getContext() {
return context;
}
/** Get the resource name/path this class is responsible for retrieving.
*@return the resource name/path.
**/
protected String getResource() {
return resource;
}
protected String readResourceDebug(final URL url){
return "Read Configuration from " + resource;
}
// Private -------------------------------------------------------
private void preInit(final String resource){
if (future != null && !future.isDone()) {
throw new IllegalStateException(ERR_ONE_USE_ONLY);
}
if (resourceType == Resource.BYTECODE) {
// Convert package structure to path.
if(!resource.endsWith(".jsp")){
this.resource = resource.replace(".", "/") + ".class";
}else{
this.resource = resource;
}
if (jarFileName != null) {
// Construct the path portion of a JarUrl.
this.resource = jarFileName + "!/" + this.resource;
}
} else {
this.resource = resource;
}
}
private void postInit(){
future = EXECUTOR.submit(this);
if(LOG.isTraceEnabled() && EXECUTOR instanceof ThreadPoolExecutor){
final ThreadPoolExecutor tpe = (ThreadPoolExecutor)EXECUTOR;
LOG.trace(DEBUG_POOL_COUNT + tpe.getActiveCount() + '/' + tpe.getPoolSize());
}
}
private boolean loadEmptyResource(final URL url) {
LOG.debug("Loading empty resource for " + resource);
switch(resourceType){
case PROPERTIES:
props.put(context.getSite().getName(), url);
break;
case DOM_DOCUMENT:
document = builder.newDocument();
break;
case BYTECODE:
bytecode = new byte[0];
break;
}
return true;
}
private boolean loadResource(final URL url) {
boolean success = false;
if(urlExists(url)){
final InputStream is = getInputStreamFor(url);
try {
switch(resourceType){
case PROPERTIES:
// only add properties that don't already exist!
// allows us to inherent back through the fallback process.
final Properties newProps = new Properties();
newProps.load(is);
props.put(context.getSite().getName(), url.toString());
for(Object p : newProps.keySet()){
if(!props.containsKey(p)){
final String prop = (String)p;
props.setProperty(prop, newProps.getProperty(prop));
}
}
break;
case DOM_DOCUMENT:
document = builder.parse( new InputSource(new InputStreamReader(is)) );
break;
case BYTECODE:
final ByteArrayOutputStream bytecodeOutputStream = new ByteArrayOutputStream();
for(int i = is.read(); i != -1; i = is.read()) {
bytecodeOutputStream.write(i);
}
bytecode = bytecodeOutputStream.toByteArray();
break;
}
LOG.info(readResourceDebug(url));
success = true;
} catch (NullPointerException e) {
LOG.warn(readResourceDebug(url), e);
} catch (IOException e) {
LOG.warn(readResourceDebug(url), e);
} catch (SAXParseException e) {
throw new ResourceLoadException(
readResourceDebug(url) + " at " + e.getLineNumber() + ":" + e.getColumnNumber(), e);
} catch (SAXException e) {
throw new ResourceLoadException(readResourceDebug(url), e);
}finally{
if( null != is ){
try{
is.close();
}catch(IOException ioe){
LOG.warn(readResourceDebug(url), ioe);
}
}
}
}
return success;
}
}