blob: 62bfeb842f055f44af0b4ceb1ecd01c0a23e39cf [file] [log] [blame]
package org.apache.fulcrum.intake;
/*
* 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.
*/
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.Unmarshaller.Listener;
import javax.xml.bind.helpers.DefaultValidationEventHandler;
import javax.xml.validation.SchemaFactory;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.LogEnabled;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.commons.pool2.KeyedObjectPool;
import org.apache.commons.pool2.KeyedPooledObjectFactory;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.apache.fulcrum.intake.model.AppData;
import org.apache.fulcrum.intake.model.Field;
import org.apache.fulcrum.intake.model.Group;
/**
* This service provides access to input processing objects based on an XML
* specification.
*
* avalon.component name="intake"
* avalon.service type="org.apache.fulcrum.intake.IntakeService"
*
* @author <a href="mailto:jmcnally@collab.net">John McNally</a>
* @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
* @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
* @version $Id$
*/
public class IntakeServiceImpl extends AbstractLogEnabled implements
IntakeService, Configurable, Initializable, Contextualizable,
Serviceable
{
/** Map of groupNames -> appData elements */
private Map<String, AppData> groupNames;
/** The cache of group names. */
private Map<String, String> groupNameMap;
/** The cache of group keys. */
private Map<String, String> groupKeyMap;
/** The cache of property getters. */
private Map<String, Map<String, Method>> getterMap;
/** The cache of property setters. */
private Map<String, Map<String, Method>> setterMap;
/** AppData -> keyed Pools Map */
private Map<AppData, KeyedObjectPool<String, Group>> keyedPools;
/** The Avalon Container root directory */
private String applicationRoot;
/** List of configured xml specification files */
private List<String> xmlPathes = null;
/** Configured location of the serialization file */
private String serialDataPath = null;
/**
* Local Class to enable Avalon logging on the model classes
*
*/
private class AvalonLogEnabledListener extends Listener
{
/**
* @see javax.xml.bind.Unmarshaller.Listener#beforeUnmarshal(java.lang.Object, java.lang.Object)
*/
@Override
public void beforeUnmarshal(Object target, Object parent)
{
super.beforeUnmarshal(target, parent);
if (target instanceof LogEnabled)
{
((LogEnabled)target).enableLogging(getLogger());
}
}
}
/**
* Registers a given group name in the system
*
* @param groupName
* The name to register the group under
* @param group
* The Group to register in
* @param appData
* The app Data object where the group can be found
* @param checkKey
* Whether to check if the key also exists.
*
* @return true if successful, false if not
*/
private boolean registerGroup(String groupName, Group group,
AppData appData, boolean checkKey)
{
if (groupNames.containsKey(groupName))
{
// This name already exists.
return false;
}
boolean keyExists = groupNameMap.containsKey(group.getGID());
if (checkKey && keyExists)
{
// The key for this package is already registered for another group
return false;
}
groupNames.put(groupName, appData);
groupKeyMap.put(groupName, group.getGID());
if (!keyExists)
{
// This key does not exist. Add it to the hash.
groupNameMap.put(group.getGID(), groupName);
}
List<Field<?>> fields = group.getFields();
for (Field<?> field : fields)
{
String className = field.getMapToObject();
if (!getterMap.containsKey(className))
{
getterMap.put(className, new HashMap<String, Method>());
setterMap.put(className, new HashMap<String, Method>());
}
}
return true;
}
/**
* Tries to load a serialized Intake Group file. This can reduce the startup
* time of Turbine.
*
* @param serialDataPath
* The path of the File to load.
*
* @return A map with appData objects loaded from the file or null if the
* map could not be loaded.
*/
private Map<AppData, File> loadSerialized(String serialDataPath, long timeStamp)
{
getLogger().debug(
"Entered loadSerialized(" + serialDataPath + ", " + timeStamp
+ ")");
long timer = System.currentTimeMillis();
if (serialDataPath == null)
{
return null;
}
File serialDataFile = new File(serialDataPath);
if (!serialDataFile.exists())
{
getLogger().info("No serialized file found, parsing XML");
return null;
}
if (serialDataFile.lastModified() <= timeStamp)
{
getLogger().info("serialized file too old, parsing XML");
return null;
}
Map<AppData, File> serialData = null;
try (FileInputStream fin = new FileInputStream(serialDataFile);
ObjectInputStream in = new ObjectInputStream(fin))
{
Object o = in.readObject();
if (o instanceof Map)
{
@SuppressWarnings("unchecked") // checked with instanceof
Map<AppData, File> map = (Map<AppData, File>) o;
serialData = map;
}
else
{
// This could be old file from intake. Try to delete it
getLogger().info("Serialized object is not an intake map, ignoring");
in.close();
// Try to delete the file
boolean result = serialDataFile.delete();
if ( result == false )
{
getLogger().error("Unknown serialized file could not be removed");
}
}
}
catch (IOException e)
{
getLogger().error("Serialized File could not be read.", e);
// We got a corrupt file for some reason.
// Null out serialData to be sure
serialData = null;
}
catch (ClassNotFoundException e)
{
getLogger().error("Objects could not be read from serialized file.", e);
// This should not happen
// Null out serialData to be sure
serialData = null;
}
// Recreate transient loggers
if (serialData != null)
{
for (AppData appData : serialData.keySet())
{
for (Group group : appData.getGroups())
{
if (group instanceof LogEnabled)
{
((LogEnabled)group).enableLogging(getLogger());
}
for (Field<?> field : group.getFields())
{
if (field instanceof LogEnabled)
{
((LogEnabled)field).enableLogging(getLogger());
}
}
}
}
}
getLogger().info("Loaded serialized map object, ignoring XML");
getLogger().debug("Loading took " + (System.currentTimeMillis() - timer));
return serialData;
}
/**
* Writes a parsed XML map with all the appData groups into a file. This
* will speed up loading time when you restart the Intake Service because it
* will only unserialize this file instead of reloading all of the XML files
*
* @param serialDataPath
* The path of the file to write to
* @param appDataElements
* A Map containing all of the XML parsed appdata elements
*/
private void saveSerialized(String serialDataPath, Map<AppData, File> appDataElements)
{
getLogger().debug(
"Entered saveSerialized(" + serialDataPath
+ ", appDataElements)");
long timer = System.currentTimeMillis();
if (serialDataPath == null)
{
return;
}
File serialData = new File(serialDataPath);
try
{
boolean result = serialData.createNewFile();
if ( result == false )
{
getLogger().error("Could not create new serialized file");
}
// Try to delete the file
result = serialData.delete();
if ( result == false )
{
getLogger().error("Serialized file could not be removed");
}
}
catch (IOException e)
{
getLogger().info(
"Could not create serialized file " + serialDataPath
+ ", not serializing the XML data", e);
return;
}
try (FileOutputStream fout = new FileOutputStream(serialDataPath);
ObjectOutputStream out = new ObjectOutputStream(fout);
FileInputStream fin = new FileInputStream(serialDataPath);
ObjectInputStream in = new ObjectInputStream(fin))
{
// write the appData file out
out.writeObject(appDataElements);
out.flush();
// read the file back in. for some reason on OSX 10.1
// this is necessary.
/* Map dummy = (Map) */ in.readObject();
getLogger().debug("Serializing successful");
}
catch (IOException e)
{
getLogger().info(
"Could not write serialized file to " + serialDataPath
+ ", not serializing the XML data", e);
}
catch (ClassNotFoundException e)
{
getLogger().info(
"Could not re-read serialized file from " + serialDataPath, e);
}
getLogger().debug("Saving took " + (System.currentTimeMillis() - timer));
}
/**
* Gets an instance of a named group either from the pool or by calling the
* Factory Service if the pool is empty.
*
* @param groupName
* the name of the group.
* @return a Group instance.
* @throws IntakeException
* if recycling fails.
*/
@Override
public Group getGroup(String groupName) throws IntakeException
{
Group group = null;
AppData appData = groupNames.get(groupName);
if (groupName == null)
{
throw new IntakeException(
"Intake IntakeServiceImpl.getGroup(groupName) is null");
}
if (appData == null)
{
throw new IntakeException(
"Intake IntakeServiceImpl.getGroup(groupName): No XML definition for Group "
+ groupName + " found");
}
try
{
group = keyedPools.get(appData).borrowObject(groupName);
}
catch (Exception e)
{
throw new IntakeException("Could not get group " + groupName, e);
}
return group;
}
/**
* Puts a Group back to the pool.
*
* @param instance
* the object instance to recycle.
*
* @throws IntakeException
* The passed group name does not exist.
*/
@Override
public void releaseGroup(Group instance) throws IntakeException
{
if (instance != null)
{
String groupName = instance.getIntakeGroupName();
AppData appData = groupNames.get(groupName);
if (appData == null)
{
throw new IntakeException(
"Intake IntakeServiceImpl.releaseGroup(groupName): "
+ "No XML definition for Group " + groupName
+ " found");
}
try
{
keyedPools.get(appData).returnObject(groupName, instance);
}
catch (Exception e)
{
throw new IntakeException("Could not release group " + groupName, e);
}
}
}
/**
* Gets the current size of the pool for a group.
*
* @param groupName
* the name of the group.
*
* @throws IntakeException
* The passed group name does not exist.
*/
@Override
public int getSize(String groupName) throws IntakeException
{
AppData appData = groupNames.get(groupName);
if (appData == null)
{
throw new IntakeException(
"Intake IntakeServiceImpl.Size(groupName): No XML definition for Group "
+ groupName + " found");
}
KeyedObjectPool<String, Group> kop = keyedPools.get(appData);
return kop.getNumActive(groupName) + kop.getNumIdle(groupName);
}
/**
* Names of all the defined groups.
*
* @return array of names.
*/
@Override
public String[] getGroupNames()
{
return groupNames.keySet().toArray(new String[0]);
}
/**
* Gets the key (usually a short identifier) for a group.
*
* @param groupName
* the name of the group.
* @return the the key.
*/
@Override
public String getGroupKey(String groupName)
{
return groupKeyMap.get(groupName);
}
/**
* Gets the group name given its key.
*
* @param groupKey
* the key.
* @return groupName the name of the group.
*/
@Override
public String getGroupName(String groupKey)
{
return groupNameMap.get(groupKey);
}
/**
* Gets the Method that can be used to set a property.
*
* @param className
* the name of the object.
* @param propName
* the name of the property.
* @return the setter.
* @throws ClassNotFoundException if the class specified could not be loaded
* @throws IntrospectionException if the property setter could not be called
*/
@Override
public Method getFieldSetter(String className, String propName)
throws ClassNotFoundException, IntrospectionException
{
Map<String, Method> settersForClassName = setterMap.get(className);
if (settersForClassName == null)
{
throw new IntrospectionException("No setter Map for " + className
+ " available!");
}
Method setter = settersForClassName.get(propName);
if (setter == null)
{
PropertyDescriptor pd = new PropertyDescriptor(propName, Class
.forName(className));
synchronized (setterMap)
{
setter = pd.getWriteMethod();
settersForClassName.put(propName, setter);
if (setter == null)
{
getLogger().error(
"Intake: setter for '" + propName + "' in class '"
+ className + "' could not be found.");
}
}
// we have already completed the reflection on the getter, so
// save it so we do not have to repeat
synchronized (getterMap)
{
Map<String, Method> gettersForClassName = getterMap.get(className);
if (gettersForClassName != null)
{
Method getter = pd.getReadMethod();
if (getter != null)
{
gettersForClassName.put(propName, getter);
}
}
}
}
return setter;
}
/**
* Gets the Method that can be used to get a property value.
*
* @param className
* the name of the object.
* @param propName
* the name of the property.
* @return the getter.
* @throws ClassNotFoundException if the class specified could not be loaded
* @throws IntrospectionException if the property getter could not be called
*/
@Override
public Method getFieldGetter(String className, String propName)
throws ClassNotFoundException, IntrospectionException
{
Map<String, Method> gettersForClassName = getterMap.get(className);
if (gettersForClassName == null)
{
throw new IntrospectionException("No getter Map for " + className
+ " available!");
}
Method getter = gettersForClassName.get(propName);
if (getter == null)
{
PropertyDescriptor pd = null;
synchronized (getterMap)
{
pd = new PropertyDescriptor(propName, Class.forName(className));
getter = pd.getReadMethod();
gettersForClassName.put(propName, getter);
if (getter == null)
{
getLogger().error(
"Intake: getter for '" + propName + "' in class '"
+ className + "' could not be found.");
}
}
// we have already completed the reflection on the setter, so
// save it so we do not have to repeat
synchronized (setterMap)
{
Map<String, Method> settersForClassName = getterMap.get(className);
if (settersForClassName != null)
{
Method setter = pd.getWriteMethod();
if (setter != null)
{
settersForClassName.put(propName, setter);
}
}
}
}
return getter;
}
// ---------------- Avalon Lifecycle Methods ---------------------
/**
* Avalon component lifecycle method
*/
@Override
public void configure(Configuration conf) throws ConfigurationException
{
final Configuration xmlPaths = conf.getChild(XML_PATHS, false);
xmlPathes = new ArrayList<String>();
if (xmlPaths == null)
{
xmlPathes.add(XML_PATH_DEFAULT);
}
else
{
Configuration[] nameVal = xmlPaths.getChildren();
for (int i = 0; i < nameVal.length; i++)
{
String val = nameVal[i].getValue();
xmlPathes.add(val);
}
}
serialDataPath = conf.getChild(SERIAL_XML, false).getValue(SERIAL_XML_DEFAULT);
if (!serialDataPath.equalsIgnoreCase("none"))
{
serialDataPath = new File(applicationRoot, serialDataPath).getAbsolutePath();
}
else
{
serialDataPath = null;
}
getLogger().debug("Path for serializing: " + serialDataPath);
}
/**
* Avalon component lifecycle method Initializes the service by loading
* xml rule files and creating the Intake groups.
*
* @throws Exception
* if initialization fails.
*/
@Override
public void initialize() throws Exception
{
Map<AppData, File> appDataElements = null;
groupNames = new HashMap<String, AppData>();
groupKeyMap = new HashMap<String, String>();
groupNameMap = new HashMap<String, String>();
getterMap = new HashMap<String, Map<String,Method>>();
setterMap = new HashMap<String, Map<String,Method>>();
keyedPools = new HashMap<AppData, KeyedObjectPool<String, Group>>();
Set<File> xmlFiles = new HashSet<File>();
long timeStamp = 0;
getLogger().debug("logger is " + getLogger().getClass().getSimpleName());
for (String xmlPath : xmlPathes)
{
// Files are webapp.root relative
File xmlFile = new File(applicationRoot, xmlPath).getAbsoluteFile();
getLogger().debug("Path for XML File: " + xmlFile);
if (!xmlFile.canRead())
{
throw new Exception("Could not read input file with path "
+ xmlPath + ". Looking for file " + xmlFile);
}
xmlFiles.add(xmlFile);
getLogger().debug("Added " + xmlPath + " as File to parse");
// Get the timestamp of the youngest file to be compared with
// a serialized file. If it is younger than the serialized file,
// then we have to parse the XML anyway.
timeStamp = xmlFile.lastModified() > timeStamp ? xmlFile
.lastModified() : timeStamp;
}
Map<AppData, File> serializedMap = loadSerialized(serialDataPath, timeStamp);
if (serializedMap != null)
{
// Use the serialized data as XML groups. Don't parse.
appDataElements = serializedMap;
getLogger().debug("Using the serialized map");
}
else
{
long timer = System.currentTimeMillis();
// Parse all the given XML files
JAXBContext jaxb = JAXBContext.newInstance(AppData.class);
Unmarshaller um = jaxb.createUnmarshaller();
// Debug mapping
um.setEventHandler(new DefaultValidationEventHandler());
// Enable logging
Listener logEnabledListener = new AvalonLogEnabledListener();
um.setListener(logEnabledListener);
URL schemaURL = getClass().getResource("/intake.xsd");
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
um.setSchema(schemaFactory.newSchema(schemaURL));
appDataElements = new HashMap<AppData, File>();
for (File xmlFile : xmlFiles)
{
getLogger().debug("Now parsing: " + xmlFile);
try (FileInputStream fis = new FileInputStream(xmlFile))
{
AppData appData = (AppData) um.unmarshal(fis);
appDataElements.put(appData, xmlFile);
getLogger().debug("Saving AppData for " + xmlFile);
}
}
getLogger().debug("Parsing took " + (System.currentTimeMillis() - timer));
saveSerialized(serialDataPath, appDataElements);
}
int counter = 0;
AppData appData;
File dataFile;
for ( Entry<AppData, File> entry : appDataElements.entrySet() )
{
// Set the entry pair
appData = entry.getKey();
dataFile = entry.getValue();
int maxPooledGroups = 0;
List<Group> glist = appData.getGroups();
String groupPrefix = appData.getGroupPrefix();
for (ListIterator<Group> i = glist.listIterator(glist.size()); i.hasPrevious();)
{
Group g = i.previous();
String groupName = g.getIntakeGroupName();
boolean registerUnqualified = registerGroup(groupName, g, appData, true);
if (!registerUnqualified)
{
getLogger().info(
"Ignored redefinition of Group " + groupName
+ " or Key " + g.getGID() + " from "
+ dataFile);
}
if (groupPrefix != null)
{
StringBuilder qualifiedName = new StringBuilder();
qualifiedName.append(groupPrefix).append(':').append(groupName);
// Add the fully qualified group name. Do _not_ check
// for
// the existence of the key if the unqualified
// registration succeeded
// (because then it was added by the registerGroup
// above).
if (!registerGroup(qualifiedName.toString(), g,
appData, !registerUnqualified))
{
getLogger().error(
"Could not register fully qualified name "
+ qualifiedName
+ ", maybe two XML files have the same prefix. Ignoring it.");
}
}
// Init fields
for (Field<?> f : g.getFields())
{
f.initGetterAndSetter();
}
maxPooledGroups = Math.max(maxPooledGroups, g.getPoolCapacity());
}
KeyedPooledObjectFactory<String, Group> factory =
new Group.GroupFactory(appData);
GenericKeyedObjectPoolConfig<Group> poolConfig = new GenericKeyedObjectPoolConfig<Group>();
poolConfig.setMaxTotalPerKey(maxPooledGroups);
poolConfig.setJmxEnabled(true);
poolConfig.setJmxNamePrefix("fulcrum-intake-pool-" + counter++);
keyedPools.put(appData,
new GenericKeyedObjectPool<String, Group>(factory, poolConfig));
}
if (getLogger().isInfoEnabled())
{
getLogger().info("Intake Service is initialized now.");
}
}
/**
* Note that the avalon.entry key="urn:avalon:home"
* and the type is {@link java.io.File}
*
* @see org.apache.avalon.framework.context.Contextualizable#contextualize(org.apache.avalon.framework.context.Context)
*
* @param context the Context to use
* @throws ContextException if the context is not found
*/
@Override
public void contextualize(Context context) throws ContextException
{
this.applicationRoot = context.get("urn:avalon:home").toString();
}
/**
* Avalon component lifecycle method
*
* avalon.dependency type="org.apache.fulcrum.localization.LocalizationService"
*
* @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
*
* @param manager the service manager
* @throws ServiceException generic exception
*/
@Override
public void service(ServiceManager manager) throws ServiceException
{
IntakeServiceFacade.setIntakeService(this);
}
}