/*
* ============================================================================
* GNU Lesser General Public License
* ============================================================================
*
* Beanlet - JSE Application Container.
* Copyright (C) 2006 Leon van Zantvoort
*
* This library 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 library 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 this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*
* Leon van Zantvoort
* 243 Acalanes Drive #11
* Sunnyvale, CA 94086
* USA
*
* zantvoort@users.sourceforge.net
* http://beanlet.org
*/
package org.beanlet.impl;
import static org.beanlet.common.BeanletConstants.*;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.XMLConstants;
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.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.beanlet.BeanletApplicationException;
import org.beanlet.common.AbstractProvider;
import org.beanlet.plugin.BeanletConfiguration;
import org.beanlet.plugin.BeanletConfigurationValidator;
import org.beanlet.common.BeanletConfigurationValidators;
import org.beanlet.plugin.ClassResolver;
import org.beanlet.common.ClassResolvers;
import org.beanlet.common.ElementAnnotationFactories;
import org.beanlet.plugin.ElementAnnotationFactory;
import org.beanlet.plugin.spi.BeanletConfigurationValidatorProvider;
import org.beanlet.plugin.spi.ClassResolverProvider;
import org.beanlet.plugin.spi.ElementAnnotationFactoryProvider;
import org.jargo.ComponentAlias;
import org.jargo.ComponentConfiguration;
import org.jargo.ComponentUnit;
import org.jargo.deploy.Deployable;
import org.jargo.deploy.Deployer;
import org.jargo.spi.ComponentAliasProvider;
import org.jargo.spi.ComponentConfigurationProvider;
import org.jargo.spi.Provider;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* @author Leon van Zantvoort
*/
public final class BeanletConfigurationProviderImpl extends AbstractProvider
implements ComponentConfigurationProvider, ComponentAliasProvider, Deployer {
private static final Method FIND_RESOURCES;
private static final XPath xpath;
private static final XPathExpression IMPORT_RESOURCE_EXPRESSION;
private static final XPathExpression IMPORT_OPTIONAL_EXPRESSION;
private static final XPathExpression ALIAS_EXPRESSION;
static {
try {
try {
FIND_RESOURCES = AccessController.doPrivileged(
new PrivilegedExceptionAction<Method>() {
public Method run() throws Exception {
// PENDING: alternative: subclassing ClassLoader?
// PERMISSION: java.lang.RuntimePermission accessDeclaredMembers
Method method = ClassLoader.class.
getDeclaredMethod("findResources", String.class);
// PERMISSION: java.lang.reflect.ReflectPermission suppressAccessChecks
method.setAccessible(true);
return method;
}
});
} catch (PrivilegedActionException e) {
throw e.getException();
}
xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(BEANLET_NAMESPACE_CONTEXT);
try {
IMPORT_RESOURCE_EXPRESSION = xpath.compile(
"/:beanlets/:import/@resource");
IMPORT_OPTIONAL_EXPRESSION = xpath.compile("../@optional");
ALIAS_EXPRESSION = xpath.compile("/:beanlets/:alias");
} catch (XPathExpressionException e) {
throw new AssertionError(e);
}
} catch (Exception e) {
throw new AssertionError(e);
}
}
private final Logger logger;
private final List<ElementAnnotationFactoryProvider> annotationFactoryProviders;
private final AtomicReference<ElementAnnotationFactory> annotationFactory;
private final List<ClassResolverProvider> resolverProviders;
private final AtomicReference<ClassResolver> resolver;
private final List<BeanletConfigurationValidatorProvider> validatorProviders;
private final AtomicReference<BeanletConfigurationValidator> validator;
private final WeakHashMap<ClassLoader, List<Document>> documentMap;
public BeanletConfigurationProviderImpl() {
logger = Logger.getLogger(getClass().getName());
annotationFactoryProviders =
new CopyOnWriteArrayList<ElementAnnotationFactoryProvider>();
annotationFactory = new AtomicReference<ElementAnnotationFactory>(
new ElementAnnotationFactories(null));
resolverProviders = new CopyOnWriteArrayList<ClassResolverProvider>();
resolver = new AtomicReference<ClassResolver>(new ClassResolvers(null));
validatorProviders =
new CopyOnWriteArrayList<BeanletConfigurationValidatorProvider>();
validator = new AtomicReference<BeanletConfigurationValidator>(
new BeanletConfigurationValidators(null));
// The WeakHashMap's key is not reference by the map's value.
documentMap = new WeakHashMap<ClassLoader, List<Document>>();
}
public List<ComponentAlias> getComponentAliases(ComponentUnit unit) {
List<ComponentAlias> aliases = new ArrayList<ComponentAlias>();
try {
List<Document> documents = getDocuments(unit.getClassLoader());
for (Document document : documents) {
NodeList nodeList = (NodeList) ALIAS_EXPRESSION.evaluate(document,
XPathConstants.NODESET);
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
NamedNodeMap attributes = node.getAttributes();
Node nameNode = attributes.getNamedItem("name");
Node aliasNode = attributes.getNamedItem("alias");
Node overrideNode = attributes.getNamedItem("override");
if (nameNode == null) {
throw new XPathExpressionException("name attribute");
}
if (aliasNode == null) {
throw new XPathExpressionException("alias attribute");
}
final String name = nameNode.getNodeValue();
final String alias = aliasNode.getNodeValue();
final boolean override = overrideNode == null ?
false : Boolean.valueOf(overrideNode.getNodeValue());
aliases.add(new ComponentAlias() {
public String getComponentName() {
return name;
}
public String getComponentAlias() {
return alias;
}
public boolean override() {
return override;
}
});
}
}
} catch (XPathExpressionException e) {
assert false : e;
throw new BeanletApplicationException(e);
}
return Collections.unmodifiableList(aliases);
}
public List<ComponentConfiguration<?>> getComponentConfigurations(
ComponentUnit unit) {
List<ComponentConfiguration<?>> configurations =
new ArrayList<ComponentConfiguration<?>>();
List<XMLAnnotationDomain<?>> domains =
XMLAnnotationDomain.list(getDocuments(unit.getClassLoader()),
unit.getClassLoader(), annotationFactory.get(), resolver.get());
for (XMLAnnotationDomain<?> tmp : domains) {
@SuppressWarnings("unchecked")
XMLAnnotationDomain<Object> domain =
(XMLAnnotationDomain<Object>) tmp;
if (!domain.isAbstract()) {
BeanletConfiguration<?> configuration =
new BeanletConfigurationImpl<Object>(unit, domain);
if (logger.isLoggable(Level.FINEST)) {
logger.finest(String.valueOf(configuration));
}
validator.get().validate(configuration);
configurations.add(configuration);
}
}
return Collections.unmodifiableList(configurations);
}
public void setParent(Deployer deployer) {
// Do nothing.
}
public void deploy(Deployable deployable) throws Exception {
if (deployable instanceof ElementAnnotationFactoryProvider) {
annotationFactoryProviders.add((ElementAnnotationFactoryProvider)
deployable);
final List<ElementAnnotationFactory> tmp =
new ArrayList<ElementAnnotationFactory>();
for (ElementAnnotationFactoryProvider provider :
getSortedList(annotationFactoryProviders)) {
tmp.addAll(provider.getElementAnnotationFactories());
}
annotationFactory.set(new ElementAnnotationFactories(tmp));
}
if (deployable instanceof ClassResolverProvider) {
resolverProviders.add((ClassResolverProvider) deployable);
final List<ClassResolver> tmp = new ArrayList<ClassResolver>();
for (ClassResolverProvider provider : getSortedList(resolverProviders)) {
tmp.addAll(provider.getClassResolvers());
}
resolver.set(new ClassResolvers(tmp));
}
if (deployable instanceof BeanletConfigurationValidatorProvider) {
validatorProviders.add((BeanletConfigurationValidatorProvider) deployable);
final List<BeanletConfigurationValidator> tmp =
new ArrayList<BeanletConfigurationValidator>();
for (BeanletConfigurationValidatorProvider provider :
getSortedList(validatorProviders)) {
tmp.addAll(provider.getBeanletConfigurationValidators());
}
validator.set(new BeanletConfigurationValidators(tmp));
}
}
public void undeploy(Deployable deployable) throws Exception {
if (deployable instanceof ElementAnnotationFactoryProvider) {
annotationFactoryProviders.remove((ElementAnnotationFactoryProvider)
deployable);
List<ElementAnnotationFactory> tmp =
new ArrayList<ElementAnnotationFactory>();
for (ElementAnnotationFactoryProvider provider : getSortedList(
annotationFactoryProviders)) {
tmp.addAll(provider.getElementAnnotationFactories());
}
annotationFactory.set(new ElementAnnotationFactories(tmp));
}
if (deployable instanceof ClassResolverProvider) {
resolverProviders.remove((ClassResolverProvider) deployable);
List<ClassResolver> tmp = new ArrayList<ClassResolver>();
for (ClassResolverProvider provider : getSortedList(
resolverProviders)) {
tmp.addAll(provider.getClassResolvers());
}
resolver.set(new ClassResolvers(tmp));
}
if (deployable instanceof BeanletConfigurationValidatorProvider) {
validatorProviders.remove((BeanletConfigurationValidatorProvider)
deployable);
List<BeanletConfigurationValidator> tmp =
new ArrayList<BeanletConfigurationValidator>();
for (BeanletConfigurationValidatorProvider provider :
getSortedList(validatorProviders)) {
tmp.addAll(provider.getBeanletConfigurationValidators());
}
validator.set(new BeanletConfigurationValidators(tmp));
}
}
/**
* Sorts the specified {@code providers} list according to the rules of
* the {@code SequentialDeployable} interface.
*/
private <T extends Provider> List<T> getSortedList(
List<T> providers) {
List<T> newList =
new ArrayList<T>(providers);
Collections.sort(newList,
new SequentialDeployableComparator<T>());
return newList;
}
/**
* Returns all beanlet documents that are available through the specified
* class {@code loader}.
*/
private synchronized List<Document> getDocuments(ClassLoader loader) {
List<Document> list = documentMap.get(loader);
if (list == null) {
list = getDocuments(BEANLET_XML, loader);
documentMap.put(loader, list);
}
return list;
}
private List<Document> getDocuments(String resource, ClassLoader loader) {
List<Document> documents = new ArrayList<Document>();
if (loader != null) {
try {
List<URL> urls = new ArrayList<URL>();
@SuppressWarnings("unchecked")
Enumeration<URL> e1 = (Enumeration<URL>) FIND_RESOURCES.
invoke(loader, resource);
while (e1.hasMoreElements()) {
urls.add(e1.nextElement());
}
@SuppressWarnings("unchecked")
Enumeration<URL> e2 = (Enumeration<URL>) FIND_RESOURCES.
invoke(loader, "META-INF/" + resource);
while (e2.hasMoreElements()) {
urls.add(e2.nextElement());
}
Set<URL> dupes = new HashSet<URL>();
for (URL url : urls) {
documents.addAll(getDocuments(url, loader, dupes));
}
} catch (IllegalAccessException e) {
assert false : e;
} catch (InvocationTargetException e) {
throw new BeanletApplicationException("Failed to load resource: " +
resource, e.getTargetException());
} catch (FileNotFoundException e) {
throw new BeanletApplicationException("Failed to load resource: " +
resource + ".", e);
}
}
return documents;
}
private List<Document> getDocuments(URL url, final ClassLoader loader,
Set<URL> dupes) throws FileNotFoundException {
List<Document> documents = new ArrayList<Document>();
try {
if (dupes.add(url)) {
InputStream stream = null;
try {
logger.finest("Loading beanlets from " +
url.toExternalForm() + ".");
DocumentBuilderFactory factory = getDocumentBuilderFactory();
try {
factory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage",
XMLConstants.W3C_XML_SCHEMA_NS_URI);
} catch (IllegalArgumentException e) {
// PENDING: handle exception
throw e;
}
// Checks whether the document is well formed.
DocumentBuilder checkingBuilder = factory.newDocumentBuilder();
URLConnection c = url.openConnection();
c.setUseCaches(false);
InputStream s = new BufferedInputStream(c.getInputStream());
try {
checkingBuilder.parse(s, url.toExternalForm());
} finally {
s.close();
}
factory.setNamespaceAware(true);
factory.setValidating(true);
// Validates the document according to the schema definition.
DocumentBuilder validatingBuilder = factory.newDocumentBuilder();
validatingBuilder.setEntityResolver(new EntityResolver() {
public InputSource resolveEntity(String publicId,
String systemId) throws SAXException,IOException {
try {
final InputSource source;
URI uri = new URI(systemId);
String path = uri.getPath();
if (path != null) {
if (path.startsWith("/")) {
path = path.substring(1);
}
final String p = path;
InputStream stream = AccessController.
doPrivileged(new PrivilegedAction<InputStream>() {
public InputStream run() {
return loader.getResourceAsStream(p);
}
});
if (stream != null) {
source = new InputSource(stream);
} else {
source = new InputSource(systemId);
}
} else {
source = new InputSource(systemId);
}
source.setPublicId(publicId);
return source;
} catch (URISyntaxException e) {
throw new SAXException(e);
}
}
});
validatingBuilder.setErrorHandler(new ErrorHandler() {
public void error(SAXParseException e) throws SAXException {
throw e;
}
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
public void warning(SAXParseException e) throws SAXException {
logger.warning(e.getMessage());
}
});
URLConnection connection = url.openConnection();
connection.setUseCaches(false);
stream = rewriteStream(connection.getInputStream(), getProperties(loader));
Document document = validatingBuilder.parse(stream,
url.toExternalForm());
documents.add(document);
NodeList nodes = (NodeList) IMPORT_RESOURCE_EXPRESSION.
evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
Node optionalNode = (Node) IMPORT_OPTIONAL_EXPRESSION.evaluate(
nodes.item(i), XPathConstants.NODE);
String name = nodes.item(i).getNodeValue();
try {
final URL newUrl;
URI nameUri = new URI(name);
if (nameUri.isAbsolute()) {
newUrl = nameUri.toURL();
} else {
URI uri = url.toURI();
URI tmp = new URI(uri.getRawSchemeSpecificPart());
if (tmp.isAbsolute()) {
newUrl = new URL(uri.getScheme() + ":" +
tmp.resolve(name).toASCIIString());
} else {
newUrl = new URL(uri.resolve(name).
toASCIIString());
}
}
documents.addAll(getDocuments(newUrl, loader, dupes));
} catch (FileNotFoundException e) {
if (optionalNode == null || !Boolean.valueOf(
optionalNode.getNodeValue())) {
throw new BeanletApplicationException(
"Import not found. Document: " + url +
". Import: " + name + ".", e);
}
} catch (MalformedURLException e) {
throw new BeanletApplicationException(
"Import not valid. Document: " + url +
". Import: " + name + ".", e);
} catch (URISyntaxException e) {
throw new BeanletApplicationException(
"Import not valid. Document: " + url +
". Import: " + name + ".", e);
}
}
} finally {
if (stream != null) {
stream.close();
}
}
}
} catch (FileNotFoundException e) {
throw e;
} catch (BeanletApplicationException e) {
throw e;
} catch (Exception e) {
throw new BeanletApplicationException("Failed to load document: " +
url + ".", e);
}
return documents;
}
private Properties getProperties(ClassLoader loader) {
try {
final Properties properties = new Properties();
List<URL> urls = new ArrayList<URL>();
@SuppressWarnings("unchecked")
Enumeration<URL> e2 = (Enumeration<URL>) FIND_RESOURCES.
invoke(loader, "META-INF/" + BEANLET_PROPERTIES);
while (e2.hasMoreElements()) {
urls.add(e2.nextElement());
}
for (ListIterator<URL> i = urls.listIterator(urls.size());
i.hasPrevious();) {
URLConnection c = i.previous().openConnection();
c.setUseCaches(false);
InputStream s = c.getInputStream();
if (s != null) {
try {
s = new BufferedInputStream(s);
properties.load(s);
} finally {
s.close();
}
}
}
try {
AccessController.doPrivileged(
new PrivilegedExceptionAction<Object>() {
public Object run() throws Exception {
File f = new File(BEANLET_PROPERTIES);
// PERMISSION: java.io.FilePermission beanlet.properties read
if (f.canRead()) {
InputStream s = new BufferedInputStream(
new FileInputStream(BEANLET_PROPERTIES));
try {
properties.load(s);
} finally {
s.close();
}
}
return null;
}
});
} catch (PrivilegedActionException e) {
throw e.getException();
}
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
// PERMISSION: java.util.PropertyPermission * read,write
properties.putAll(System.getProperties());
return null;
}
});
return properties;
} catch (Exception e) {
throw new BeanletApplicationException("Failed to read beanlet " +
"properties.", e);
}
}
private InputStream rewriteStream(InputStream stream,
Properties properties) throws IOException {
try {
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(
new InputStreamReader(stream));
String line = null;
while ((line = reader.readLine()) != null) {
builder.append(replace(line, properties) + "\n");
}
return new ByteArrayInputStream(builder.toString().getBytes());
} finally {
stream.close();
}
}
private String replace(String line, Properties properties) throws
BeanletApplicationException {
String modified = line;
int x = 0;
while ((x = modified.indexOf("${", x)) != -1) {
if (x > 0 && modified.charAt(x - 1) == '\\') {
x++;
continue;
}
int y = x + 2;
modified = modified.substring(0, y) + replace(modified.substring(y),
properties);
while ((y = modified.indexOf("}", y)) != -1) {
if (modified.charAt(y - 1) == '\\') {
y++;
continue;
}
final String placeholder = modified.substring(x + 2, y);
final String key;
final String defaultValue;
final String value;
String[] s = placeholder.split("=", 2);
key = s[0];
if (s.length == 2) {
defaultValue = s[1];
} else {
defaultValue = null;
}
String tmp = properties.getProperty(key);
if (defaultValue == null) {
if (tmp == null) {
throw new BeanletApplicationException(
"Beanlet property not set: '" + key + "'. " +
"Specify property in beanlet.properties, " +
"or system properties.");
} else {
value = tmp;
}
} else {
if (tmp == null) {
value = defaultValue;
} else {
value = tmp;
}
}
modified = modified.replace("${" + placeholder + "}", value);
x = y;
break;
}
x++;
}
return modified;
}
/**
* Returns the default JDK DocumentBuilderFactory.
*/
private static DocumentBuilderFactory getDocumentBuilderFactory() {
try {
return (DocumentBuilderFactory) Class.forName(
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl").
newInstance();
} catch (Exception e) {
// Fallback.
return DocumentBuilderFactory.newInstance();
}
}
}