blob: 8b9f400fe92e3493e2a94c58cf9a3c63d1648373 [file] [log] [blame]
package org.apache.pluto.container.om.portlet.impl;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_ACT;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_DES;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_EVT;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_HDR;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_INI;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_REN;
import static org.apache.pluto.container.bean.processor.MethodDescription.METH_RES;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.ResourceBundle;
import java.util.Set;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.portlet.Portlet;
import javax.portlet.annotations.PortletApplication;
import javax.portlet.annotations.PortletConfiguration;
import javax.portlet.annotations.ServeResourceMethod;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.apache.pluto.container.bean.processor.AnnotatedMethod;
import org.apache.pluto.container.bean.processor.AnnotatedMethodStore;
import org.apache.pluto.container.bean.processor.MethodDescription;
import org.apache.pluto.container.bean.processor.MethodIdentifier;
import org.apache.pluto.container.bean.processor.MethodType;
import org.apache.pluto.container.om.portlet.EventDefinition;
import org.apache.pluto.container.om.portlet.EventDefinitionReference;
import org.apache.pluto.container.om.portlet.PortletApplicationDefinition;
import org.apache.pluto.container.om.portlet.PortletDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public abstract class ConfigurationProcessor {
/** Logger. */
private static final Logger LOG = LoggerFactory.getLogger(ConfigurationProcessor.class);
private static final boolean isDebug = LOG.isDebugEnabled();
private static final boolean isTrace = LOG.isTraceEnabled();
protected PortletApplicationDefinition pad;
public ConfigurationProcessor(PortletApplicationDefinition pad) {
this.pad = pad;
}
public PortletApplicationDefinition getPad() {
return pad;
}
/**
* Traverses the portlet deployment descriptor tree and returns the data in
* the form of a portlet application definition.
*
* @param rootElement
* Root element of portlet DD tree
* @return The parsed portlet application definition
* @throws IllegalArgumentException
* If there is a data validation error
*/
public abstract void process(JAXBElement<?> rootElement) throws IllegalArgumentException;
/**
* Validates the given portlet application definition. This method should only be called after
* the complete configuration has been read.
* <p>
* The validation method is designed to be called within the portlet application servlet context.
* It throws exceptions when specified classes cannot be loaded or other severe configuration
* problem is discovered. It logs warnings for less severe configuration problems.
* <p>
* The validation code is separate from the
* configuration reading code so that the config reading code won't cause exceptions when it
* is used by the maven-portlet-plugin packaging code.
*
* @throws IllegalArgumentException
* If there is a validation error.
*/
public abstract void validate() throws IllegalArgumentException;
/**
* Handle the locale the old-fashioned way (v1 & v2)
*/
protected Locale deriveLocale(String lang) {
Locale locale = Locale.ENGLISH;
if (lang != null) {
if (lang.contains("_") == true) {
// tolerate underscores to support old portlets
String[] parts = lang.split("_");
if (parts.length == 2) {
locale = new Locale(parts[0], parts[1]);
} else {
locale = new Locale(parts[0], parts[1], parts[2]);
}
} else {
locale = Locale.forLanguageTag(lang); //BCP47
}
}
return locale;
}
/**
* Check if input string is valid java identifier
*
* @param id
* @return
*/
protected boolean isValidIdentifier(String id) {
if (id == null || id.length() == 0) {
return false;
}
char[] chars = id.toCharArray();
if (!Character.isJavaIdentifierStart(chars[0])) {
return false;
}
for (char c : Arrays.copyOfRange(chars, 1, chars.length)) {
if (!Character.isJavaIdentifierPart(c) && (c != '.')) {
return false;
}
}
return true;
}
/**
* checks if class name is valid by trying to load it. If the optional
* argument <code>assignable</code> is provided, the method will check if the
* class can be assigned.
*
* @param clsName
* Class name string from configuration
* @param assignable
* Interface to which the class should be assignable
* @param msg
* Error message used when exception is thrown.
*/
protected void checkValidClass(String clsName, Class<?> assignable, String msg) {
StringBuilder txt = new StringBuilder(128);
txt.append(msg).append(", class name: ");
txt.append(clsName);
if (!isValidIdentifier(clsName)) {
txt.append(". Invalid java identifier.");
LOG.warn(txt.toString());
throw new IllegalArgumentException(txt.toString());
}
// Make sure the class can be loaded
Class<?> valClass = null;
try {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = this.getClass().getClassLoader();
}
valClass = cl.loadClass(clsName);
if (assignable != null && !assignable.isAssignableFrom(valClass)) {
txt.append(". Specified class is not a ");
txt.append(assignable.getCanonicalName());
throw new Exception();
}
} catch (Exception e) {
txt.append(" Exception: ").append(e.toString());
if (isDebug) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.flush();
txt.append("\n").append(sw.toString());
}
LOG.warn(txt.toString());
throw new IllegalArgumentException(txt.toString(), e);
}
}
/**
* checks if resource bundle name is valid by trying to load it.
*
* @param bundleName
* Class name string from configuration
*/
protected void checkValidBundle(String bundleName) {
StringBuilder txt = new StringBuilder(128);
txt.append("Bad resource bundle: ");
txt.append(bundleName);
if (!isValidIdentifier(bundleName)) {
txt.append(". Invalid java identifier.");
LOG.warn(txt.toString());
throw new IllegalArgumentException(txt.toString());
}
try {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = this.getClass().getClassLoader();
}
@SuppressWarnings("unused")
ResourceBundle rb = ResourceBundle.getBundle(bundleName, Locale.getDefault(), cl);
} catch (Exception e) {
LOG.warn(txt.toString());
throw new IllegalArgumentException(txt.toString(), e);
}
}
/**
* Generates a unique name for use in cases where the item is ordered by name, but the name
* is optional to from the point of view of the portlet developer. For example, the filter name
* need not be specified in the filter annotation, but if it is, the filter config can be
* modified through a corresponding specification in the portlet deployment descriptor.
*
* @return
*/
protected String genUniqueName() {
// create random name
final String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZüÜäÄöÖß";
StringBuilder txt = new StringBuilder(128);
txt.append("Generated:");
Random rand = new Random();
for (int ii = 0; ii < 32; ii++) {
txt.append(chars.charAt(rand.nextInt(chars.length())));
}
return txt.toString();
}
/**
* Reads web app deployment descriptor to extract the locale - encoding mappings
*
* @param in Input stream for DD
* @throws Exception If there is a parsing problem
*/
public void processWebDD(InputStream in) throws Exception {
// set up document
DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
fact.setValidating(false);
final DocumentBuilder builder = fact.newDocumentBuilder();
builder.setEntityResolver(new EntityResolver() {
public InputSource resolveEntity(String arg0, String arg1) throws SAXException, IOException {
return new InputSource(new StringReader(""));
}
});
final Document document = builder.parse(in);
final Element root = document.getDocumentElement();
// Generate xpath queries
final XPathFactory xpathFactory = XPathFactory.newInstance();
final XPath xpath = xpathFactory.newXPath();
final XPathExpression GET_LIST =
xpath.compile("//locale-encoding-mapping-list/locale-encoding-mapping");
final XPathExpression GET_LOC = xpath.compile("locale/text()");
final XPathExpression GET_ENC = xpath.compile("encoding/text()");
// get list of locale - encoding mappings and process them
NodeList nodes = (NodeList) GET_LIST.evaluate(root,
XPathConstants.NODESET);
int mappings = 0;
for (int jj = 0; jj < nodes.getLength(); jj++) {
Node node = nodes.item(jj);
String locstr = (String) GET_LOC.evaluate(node, XPathConstants.STRING);
String encstr = (String) GET_ENC.evaluate(node, XPathConstants.STRING);
Locale locale = deriveLocale(locstr);
pad.addLocaleEncodingMapping(locale, encstr);
mappings++;
}
LOG.debug("done parsing web DD, # mappings: " + mappings);
}
/**
* Extracts the data from the portlet application annotation and adds it to the
* portlet application definition structure.
* <p>
* The default method implementation does nothing. The V3 implementation will
* override this method to provide function.
* <p>
* This method is designed to be called before the portlet deployment descriptor
* is read so that data from the portlet DD can override that provided through annotations.
*
* @param pa The portlet application annotation
*/
public void processPortletAppAnnotation(PortletApplication pa) {
// default impl = do nothing
}
/**
* Extracts the data from the portlet annotation and adds it to a
* portlet definition structure. The portlet definition will be created if it does not
* already exist.
* <p>
* The default method implementation does nothing. The V3 implementation will
* override this method to provide function.
* <p>
* This method is designed to be called before the portlet deployment descriptor
* is read so that data from the portlet DD can override that provided through annotations.
*
* @param pc The portlet configuration annotation
* @param cls The annotated class
*/
public void processPortletConfigAnnotation(PortletConfiguration pc, Class<?> cls) {
// default impl = do nothing
}
/**
* Extracts the data from the portlet annotation and adds it to a
* portlet filter definition structure. The portlet filter definition will be created if it does not
* already exist.
* <p>
* The default method implementation does nothing. The V3 implementation will
* override this method to provide function.
* <p>
* This method is designed to be called before the portlet deployment descriptor
* is read so that data from the portlet DD can override that provided through annotations.
*
* @param cls The annotated class.
*/
public void processPortletFilterAnnotation(Class<?> cls) {
// default impl = do nothing
}
/**
* Extracts the data from the portlet annotation and adds it to a
* portlet listener definition structure. The portlet listener definition will be created if it does not
* already exist.
* <p>
* The default method implementation does nothing. The V3 implementation will
* override this method to provide function.
* <p>
* This method is designed to be called before the portlet deployment descriptor
* is read so that data from the portlet DD can override that provided through annotations.
*
* @param cls
*/
public void processListenerAnnotation(Class<?> cls) {
}
/**
* Processes PortletPreferencesValidator annotated classes.
*
* @param cls
*/
public void processValidatorAnnotation(Class<?> cls) {
}
/**
* reconciles the given annotated method store containing the bean configuration
* with the configuration as read from the portlet deployment descriptor and
* the corresponding type annotations.
* <p>
* Portlets that are defined in the bean config are added to the portlet application
* definition if not already present. Event reference information from the
* annotations is verified and added to the corresponding portlet definition.
* <p>
* Methods from portlet classes definied in the portlet definitions are
* added to the annotated method store.
*
* @param ams
*/
public void reconcileBeanConfig(AnnotatedMethodStore ams) {
Set<String> portletNames = ams.getPortletNames();
// the wildcard is only in the store for display / debug purposes. don't reconcile.
portletNames.remove("*");
if (isDebug) {
StringBuilder txt = new StringBuilder();
txt.append("Beginning reconciliation. Annotated portlets: ").append(portletNames.toString());
LOG.debug(txt.toString());
}
ams.setDefaultNamespace(pad.getDefaultNamespace());
for (String pn : portletNames) {
// copy data from the method store to the portlet definition
PortletDefinition pd = pad.getPortlet(pn);
if (pd == null) {
pd = new PortletDefinitionImpl(pn, pad);
}
// if one of the @serveResourceMethod annotations has its ayncSupported
// flag set to true, set the flag to true in the portlet definition
Set<MethodIdentifier> mis = ams.getMethodIDsForPortlet(pn);
for (MethodIdentifier mi : mis) {
if (mi.getType() == MethodType.RESOURCE) {
List<AnnotatedMethod> meths = ams.getMethods(mi);
for (AnnotatedMethod meth : meths) {
ServeResourceMethod srm = (ServeResourceMethod) meth.getAnnotation();
if (srm != null && srm.asyncSupported()) {
pd.setAsyncSupported(true);
}
}
}
}
// The processing event references
List<EventDefinitionReference> edrs = pd.getSupportedProcessingEvents();
for (QName qn : ams.getProcessingEventRefs(pn)) {
EventDefinition ed = pad.getEventDefinition(qn);
if (ed == null) {
StringBuilder txt = new StringBuilder(128);
txt.append("No event definition found for annotated processing event reference.");
txt.append(" Portlet name: ").append(pn);
txt.append(", QName: ").append(qn);
LOG.warn(txt.toString());
// remove the defective method from the store
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), qn, MethodType.EVENT);
ams.removeMethod(mi);
continue;
}
EventDefinitionReference newedr = new EventDefinitionReferenceImpl(qn);
if (!edrs.contains(newedr)) {
pd.addSupportedProcessingEvent(newedr);
}
}
// The publishing event references
edrs = pd.getSupportedPublishingEvents();
for (QName qn : ams.getPublishingEventRefs(pn)) {
EventDefinition ed = pad.getEventDefinition(qn);
if (ed == null) {
StringBuilder txt = new StringBuilder(128);
txt.append("No event definition found for annotated publishing event reference.");
txt.append(" Portlet name: ").append(pn);
txt.append(", QName: ").append(qn);
LOG.warn(txt.toString());
continue;
}
EventDefinitionReference newedr = new EventDefinitionReferenceImpl(qn);
if (!edrs.contains(newedr)) {
pd.addSupportedPublishingEvent(newedr);
}
}
pad.addPortlet(pd);
}
// Now add the declared portlet class methods to the store
List<PortletDefinition> badPortlets = new ArrayList<PortletDefinition>();
for (PortletDefinition pd : pad.getPortlets()) {
Class<?> cls = null;
String clsName = pd.getPortletClass();
if (isValidIdentifier(clsName)) {
// Make sure the class can be loaded
Class<?> valClass = null;
StringBuilder txt = new StringBuilder(128);
try {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = this.getClass().getClassLoader();
}
valClass = cl.loadClass(clsName);
if (Portlet.class.isAssignableFrom(valClass)) {
cls = valClass;
} else {
txt.append("Specified portlet class does not implement the Portlet interface.");
}
} catch (Exception e) {
txt.append("Specified portlet class could not be loaded.");
} finally {
if (cls == null) {
txt.append(" Portlet name: ").append(pd.getPortletName());
txt.append(", Portlet class: ").append(clsName);
LOG.warn(txt.toString());
}
}
}
if (cls != null) {
// extract the methods from the portlet class and add them to the method store
// as long there is no corresponding annotated method already present.
// (annotated methods take precedence over portlet class methods).
AnnotatedMethod am;
am = getMethod(cls, "init", METH_INI);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.INIT);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
am = getMethod(cls, "destroy", METH_DES);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.DESTROY);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
am = getMethod(cls, "processAction", METH_ACT);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.ACTION);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
am = getMethod(cls, "processEvent", METH_EVT);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.EVENT);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
am = getMethod(cls, "render", METH_REN);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.RENDER);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
am = getMethod(cls, "renderHeaders", METH_HDR);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.HEADER);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
am = getMethod(cls, "serveResource", METH_RES);
if (am != null) {
MethodIdentifier mi = new MethodIdentifier(pd.getPortletName(), "", MethodType.RESOURCE);
if (ams.getMethods(mi).size() == 0) {
ams.addMethod(mi, am);
}
}
}
// and finally make sure that the portlet has at least one render, header, or serveResource
// method. If not, delete it.
boolean methodsOK = false;
for (MethodIdentifier mi : ams.getMethodIDsForPortlet(pd.getPortletName())) {
if ((mi.getType() == MethodType.RENDER) || (mi.getType() == MethodType.RESOURCE) ||
(mi.getType() == MethodType.HEADER)) {
methodsOK = true;
break;
}
}
if (!methodsOK) {
ams.removeMethodsForPortlet(pd.getPortletName());
badPortlets.add(pd);
StringBuilder txt = new StringBuilder();
txt.append("Portlet does not have a render, resource, or header method, so cannot be taken into service. ");
txt.append("Portlet name: ").append(pd.getPortletName());
LOG.warn(txt.toString());
}
}
// if there are bad portlets, delete them from the config
for (PortletDefinition pd : badPortlets) {
pad.removePortlet(pd);
}
if (isDebug) {
StringBuilder txt = new StringBuilder();
txt.append("Finished reconciling bean config. ");
Set<String> finalNames = ams.getPortletNames();
finalNames.remove("*"); // don't display wildcard
txt.append("Resulting portlet list: ").append(finalNames.toString());
LOG.debug(txt.toString());
}
}
/**
* Activates the bean methods in the method store. Instantiates any portlet classes and fixes
* up the method store so that methods of the same class use the same class instance.
*/
public void instantiatePortlets(AnnotatedMethodStore ams, BeanManager bm) {
if (isDebug) {
StringBuilder txt = new StringBuilder();
txt.append("Instantiating the portlets.");
txt.append(" beanMgr: ").append((bm == null) ? "null" : "not null");
txt.append(", portlet names: ").append(Arrays.toString(ams.getPortletNames().toArray()));
LOG.debug(txt.toString());
}
ams.activateMethods(bm);
for (PortletDefinition pd : pad.getPortlets()) {
Set<Class<?>> processedClasses = new HashSet<Class<?>>();
String clsName = pd.getPortletClass();
Object instance = null;
if (clsName != null) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = this.getClass().getClassLoader();
}
try {
Class<?> cls = cl.loadClass(clsName);
processedClasses.add(cls);
// Let CDI instantiate the portlet to allow for injection.
// Get the single bean instance for the portlet class.
StringBuilder txt = new StringBuilder(128);
if (bm == null) {
txt.append("Could not get portlet bean. Bean manager is null.");
} else {
Set<Bean<?>> beans = bm.getBeans(cls);
if (beans == null || beans.size() == 0) {
txt.append("Could not get portlet bean. No beans found.");
} else {
Bean<?> bean = bm.resolve(beans);
if (bean == null) {
txt.append("Could not get portlet bean. Could not resolve bean.");
} else {
instance = bm.getReference(bean, bean.getBeanClass(), bm.createCreationalContext(bean));
if (instance == null) {
txt.append("Could not get portlet bean. Could not get bean instance.");
}
}
}
}
// If the instance is still null, the portlet class might not be in a valid bean
// archive, as a JSR 286 portlet might be. Try to get a regular old instance.
if (instance == null) {
LOG.debug("Could not create bean (possibly not in a valid bean archive). Now directly instantiating class: " + cls.getCanonicalName());
try {
instance = cls.newInstance();
} catch(Exception e) {
txt.append(" Exception creating instance of class: ").append(e.toString());
}
}
// If all went well, fix up the method store with the instance
if (instance != null) {
// The annotated method store might contain methods from the configured
// portlet class being processed. For example, this may occur when an action
// or event method in the portlet class is annotated to specify processing or
// publishing event references. Such annotated methods must use the same bean
// instance, so fix up the method store.
ams.setPortletClassInstance(pd.getPortletName(), cls, instance);
if (isTrace) {
StringBuilder str = new StringBuilder();
str.append("Updating class instances.");
str.append(" portlet name: ").append(pd.getPortletName());
str.append(", class: ").append(cls.getCanonicalName());
str.append(", instance: ").append((instance == null) ? "null" : "not null");
LOG.debug(str.toString());
}
}
// handle error situation
if (instance == null) {
txt.append(" Portlet name: ").append(pd.getPortletName());
txt.append(", portlet class: ").append(cls);
LOG.warn(txt.toString());
}
} catch (ClassNotFoundException e) {
LOG.debug("Could not instantiate portlet class: " + clsName);
}
}
if (bm == null) {
// Running in an environment without CDI support, the portlet classes
// need to be instantiated in such a way that all methods of a given portlet and
// a given class use the same instance of that class. Also, annotated portlet
// methods can appear in classes that do not implement the Portlet interface
// and those classes need to be instantiated as well.
for (MethodIdentifier mi : ams.getMethodIDsForPortlet(pd.getPortletName())) {
for (AnnotatedMethod am : ams.getMethods(mi)) {
Class<?> cls = am.getBeanClass();
if (!processedClasses.contains(cls)) {
processedClasses.add(cls);
try {
instance = cls.newInstance();
ams.setPortletClassInstance(pd.getPortletName(), cls, instance);
} catch (Exception e) {
StringBuilder txt = new StringBuilder(128);
txt.append("Exception creating instance of class: ").append(e.toString());
txt.append(" Portlet name: ").append(pd.getPortletName());
txt.append(", portlet class: ").append(cls);
LOG.warn(txt.toString());
}
}
}
}
}
}
}
/**
* helper method for extracting the portlet methods from the portlet class.
* @param cls
* @param name
* @param md
* @return
*/
private AnnotatedMethod getMethod(Class<?> cls, String name, MethodDescription md) {
AnnotatedMethod am = null;
try {
Method meth = cls.getMethod(name, md.getArgTypes());
am = new AnnotatedMethod(cls, null, meth, md);
} catch (Exception e) {
if (isDebug) {
StringBuilder txt = new StringBuilder();
txt.append("Could not retrieve method from portlet class.");
txt.append(" Method name: ").append(name);
txt.append(", Class: ").append(cls.getCanonicalName());
txt.append(", Argument types: ").append(md.getArgTypes());
LOG.debug(txt.toString());
}
}
return am;
}
}