| /* |
| * Copyright (c) 2007, 2020 Oracle and/or its affiliates. All rights reserved. |
| * Copyright (c) 2021 Contributors to the Eclipse Foundation |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0, which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the |
| * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, |
| * version 2 with the GNU Classpath Exception, which is available at |
| * https://www.gnu.org/software/classpath/license.html. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| */ |
| |
| package org.jvnet.hk2.config; |
| |
| import org.glassfish.hk2.api.*; |
| import org.glassfish.hk2.utilities.AbstractActiveDescriptor; |
| import org.glassfish.hk2.utilities.AliasDescriptor; |
| import org.glassfish.hk2.utilities.DescriptorImpl; |
| import org.glassfish.hk2.utilities.ServiceLocatorUtilities; |
| import org.glassfish.hk2.utilities.cache.CacheUtilities; |
| import org.glassfish.hk2.utilities.cache.Computable; |
| import org.glassfish.hk2.utilities.cache.ComputationErrorException; |
| import org.glassfish.hk2.utilities.cache.WeakCARCache; |
| import org.jvnet.hk2.config.provider.internal.Creator; |
| import org.jvnet.hk2.config.provider.internal.CreatorImpl; |
| |
| import jakarta.inject.Singleton; |
| import jakarta.validation.constraints.NotNull; |
| import javax.xml.stream.Location; |
| import javax.xml.stream.XMLStreamException; |
| import javax.xml.stream.XMLStreamReader; |
| import javax.xml.stream.XMLStreamWriter; |
| |
| import java.beans.PropertyVetoException; |
| import java.lang.reflect.*; |
| import java.lang.annotation.Annotation; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.*; |
| import java.util.regex.Pattern; |
| |
| /** |
| * {@link Inhabitant} that loads configuration from XML. |
| * |
| * <p> |
| * This object also captures all the configuration values in a typeless way, |
| * so that the loading of the actual classes can be deferred as much as possible. |
| * |
| * <p> |
| * This is the {@link ActiveDescriptor} that gets registered into {@link ServiceLocator}, |
| * so one can access this object by {@link ServiceLocator#getServiceHandle(Class, String)} family |
| * of methods. |
| * |
| * @author Kohsuke Kawaguchi |
| */ |
| public class Dom extends AbstractActiveDescriptor implements InvocationHandler, ObservableBean { |
| /** |
| * Model drives the interpretation of this DOM. |
| */ |
| public final ConfigModel model; |
| |
| private final Dom parent; |
| |
| private ActiveDescriptor<Dom> domDescriptor; |
| |
| private ServiceHandle<Dom> serviceHandle; |
| /** |
| * This flag indicates whether a Dom object should be |
| * written to domain.xml. By default everything is written |
| * to domain.xml unless someone explicitly calls the |
| * skipFromXml method |
| */ |
| private boolean writeToXml = true; |
| |
| /** |
| * This method should be invoked if this Dom should not be persisted |
| * to the domain.xml file. |
| */ |
| public void skipFromXml() { |
| writeToXml = false; |
| } |
| |
| /** |
| * This method should be invoked if this Dom needs to be persisted to |
| * domain.xml file |
| */ |
| public void writeToXml() { |
| writeToXml = true; |
| } |
| |
| static abstract class Child { |
| final String name; |
| |
| Child(String name) { |
| this.name = name; |
| } |
| |
| /** |
| * Writes this node to XML. |
| */ |
| protected abstract void writeTo(XMLStreamWriter w) throws XMLStreamException; |
| |
| /** |
| * Returns a deep copy of itself. |
| * @return a deep copy of itself. |
| */ |
| protected abstract Child deepCopy(Dom parent); |
| |
| /** |
| * Returns true if it is an empty child, meaning |
| * all its attributes are either null or the default value |
| * as well as for all the descendants. |
| */ |
| protected abstract boolean isEmpty(); |
| |
| @Override |
| public String toString() { |
| return "Dom.Child(" + name + "," + System.identityHashCode(this) + ")"; |
| } |
| } |
| |
| static final class NodeChild extends Child { |
| final Dom dom; |
| |
| NodeChild(String name, Dom dom) { |
| super(name); |
| this.dom = dom; |
| } |
| |
| protected void writeTo(XMLStreamWriter w) throws XMLStreamException { |
| dom.writeTo(name,w); |
| } |
| |
| @Override |
| protected Child deepCopy(Dom parent) { |
| |
| return new NodeChild(name, dom.copy(parent)); |
| } |
| |
| @Override |
| protected boolean isEmpty() { |
| return dom.isEmpty(); |
| } |
| |
| @Override |
| public String toString() { |
| return "Dom.NodeChild(" + dom.getImplementation() + "," + super.toString() + ")"; |
| } |
| } |
| |
| static final class LeafChild extends Child { |
| /** |
| * Raw element text value before {@link Translator} processing. |
| */ |
| final String value; |
| |
| LeafChild(String name, String value) { |
| super(name); |
| this.value = value; |
| } |
| |
| protected void writeTo(XMLStreamWriter w) throws XMLStreamException { |
| w.writeStartElement(name); |
| w.writeCharacters(value); |
| w.writeEndElement(); |
| } |
| |
| @Override |
| protected Child deepCopy(Dom parent) { |
| return new LeafChild(name, value); |
| } |
| |
| @Override |
| protected boolean isEmpty() { |
| return false; |
| } |
| } |
| |
| public void initializationCompleted() { |
| } |
| |
| /* package */ @SuppressWarnings({ "unchecked" }) |
| void register() { |
| ServiceLocator locator = getServiceLocator(); |
| |
| ActiveDescriptor<?> myselfReified = locator.reifyDescriptor(this); |
| |
| DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class); |
| DynamicConfiguration dc = dcs.createDynamicConfiguration(); |
| |
| // habitat.add(this); |
| HK2Loader loader = this.model.classLoaderHolder; |
| |
| Set<Type> ctrs = new HashSet<Type>(); |
| ctrs.add(myselfReified.getImplementationClass()); |
| |
| if (ConfigBean.class.isAssignableFrom(this.getClass())) { |
| ctrs.add(ConfigBean.class); |
| } |
| |
| DomDescriptor<Dom> domDesc = new DomDescriptor<Dom>(this, ctrs, Singleton.class, |
| getImplementation(), new HashSet<Annotation>()); |
| domDesc.setLoader(loader); |
| domDescriptor = dc.addActiveDescriptor(domDesc, false); |
| |
| String key = getKey(); |
| for (String contract : model.contracts) { |
| ActiveDescriptor<Dom> alias = new AliasDescriptor<Dom>(locator, domDescriptor, contract, key); |
| dc.addActiveDescriptor(alias, false); |
| } |
| if (key!=null) { |
| ActiveDescriptor<Dom> alias = new AliasDescriptor<Dom>(locator, domDescriptor, model.targetTypeName, key); |
| dc.addActiveDescriptor(alias, false); |
| } |
| |
| dc.commit(); |
| |
| serviceHandle = getHabitat().getServiceHandle(domDescriptor); |
| } |
| |
| /** |
| * When a new Dom object is created, ensures that all @NotNull annotated |
| * elements have a value. |
| * |
| */ |
| public void addDefaultChildren() { |
| List<Dom.Child> children = new ArrayList<Dom.Child>(); |
| ensureConstraints(children); |
| if (!children.isEmpty()) { |
| setChildren(children); |
| } |
| } |
| |
| /* package */ void ensureConstraints(List<Child> children) { |
| Set<String> nullElements = new HashSet<String>(model.getElementNames()); |
| for (Child child : children) { |
| nullElements.remove(child.name); |
| } |
| |
| for (String s : nullElements) { |
| ConfigModel.Property p = model.getElement(s); |
| for (String annotation : p.getAnnotations()) { |
| if (annotation.equals(NotNull.class.getName())) { |
| if (p instanceof ConfigModel.Node) { |
| ConfigModel childModel = ((ConfigModel.Node) p).model; |
| Dom child = document.make(getHabitat(), null, this, childModel); |
| child.register(); |
| |
| children.add(new Dom.NodeChild(s, child)); |
| |
| // recursive call to ensure the children constraints are also respected |
| List<Child> grandChildren = new ArrayList<Child>(); |
| child.ensureConstraints(grandChildren); |
| if (!grandChildren.isEmpty()) { |
| child.setChildren(grandChildren); |
| } |
| |
| child.initializationCompleted(); |
| } |
| } |
| } |
| } |
| |
| } |
| |
| |
| |
| /** |
| * All attributes and their raw values before {@link Translator} processing. |
| */ |
| private Map<String,String> attributes = new HashMap<String, String>(); |
| /** |
| * List of all child elements, both leaves and nodes. |
| * |
| * <p> |
| * The list is read-only and copy-on-write to support concurrent access. |
| */ |
| private volatile List<Child> children = Collections.emptyList(); |
| private final Location location; |
| |
| /** |
| * Owner of the DOM tree. |
| */ |
| public final DomDocument document; |
| |
| private final ServiceLocator habitat; |
| |
| /** |
| * @param in |
| * If provided, this is used to record the source location where this DOM object is loaded from. |
| * Otherwise this can be null. |
| */ |
| public Dom(ServiceLocator habitat, DomDocument document, Dom parent, ConfigModel model, XMLStreamReader in) { |
| super(createDescriptor( |
| model.targetTypeName, model.injector.getLoader(), model.injector.getMetadata())); |
| |
| this.habitat = habitat; |
| |
| if (in!=null) { |
| this.location = new LocationImpl(in.getLocation()); |
| } else { |
| this.location=null; |
| } |
| this.model = model; |
| this.document = document; |
| this.parent = parent; |
| |
| // TODO: This code is disabled as it does fail from time to time when assertions are enabled |
| // assert (parent==null || parent.document==document); // all the nodes in the tree must belong to the same document |
| } |
| |
| public Dom(ServiceLocator habitat, DomDocument document, Dom parent, ConfigModel model) { |
| this(habitat, document, parent, model, null); |
| } |
| |
| public ServiceLocator getHabitat() { |
| return habitat; |
| } |
| |
| /** |
| * Copy constructor, used to get a deep copy of the passed instance |
| * @param source the instance to copy |
| */ |
| public Dom(Dom source, Dom parent) { |
| this(source.getHabitat(), source.document, parent, source.model); |
| List<Child> newChildren = new ArrayList<Child>(); |
| synchronized (source) { |
| for (Child child : source.children) { |
| newChildren.add(child.deepCopy(this)); |
| } |
| } |
| setChildren(newChildren); |
| attributes.putAll(source.attributes); |
| } |
| |
| /** |
| * Returns a copy of itself providing the parent for the new copy. |
| * |
| * @param parent the parent instance for the cloned copy |
| * @return the cloned copy |
| */ |
| protected <T extends Dom> T copy(T parent) { |
| return (T) new Dom(this, parent); |
| } |
| |
| /** |
| * Unwraps the proxy and returns the underlying {@link Dom} object. |
| * |
| * @return |
| * null if the given instance is not actually a proxy to a DOM. |
| */ |
| public static Dom unwrap(ConfigBeanProxy proxy) { |
| ConfigBeanProxy configBeanProxy = ConfigSupport.revealProxy(proxy); |
| InvocationHandler ih = Proxy.getInvocationHandler(configBeanProxy); |
| if (ih instanceof Dom) |
| return (Dom) ih; |
| if (ih instanceof ConfigView) { |
| return (Dom) ((ConfigView)ih).getMasterView(); |
| } |
| return null; |
| } |
| |
| /** |
| * Obtains the actual key value from this {@link Dom}. |
| */ |
| public String getKey() { |
| String k = model.key; |
| if(k==null) return null; |
| |
| switch(k.charAt(0)) { |
| case '@': |
| return attribute(k.substring(1)); |
| case '<': |
| return leafElement(k.substring(1,k.length()-1)); |
| default: |
| throw new IllegalStateException("Invalid key value:"+k); |
| } |
| } |
| |
| /** |
| * If this DOM is a child of another DOM, the parent pointer. |
| * Otherwise null. |
| */ |
| public Dom parent() { |
| return parent; |
| } |
| |
| /*package*/ void fillAttributes(XMLStreamReader in) { |
| for( int i=in.getAttributeCount()-1; i>=0; i-- ) { |
| String n = in.getAttributeLocalName(i); |
| if(model.attributes.containsKey(n)) { |
| if(attributes==null) |
| attributes = new HashMap<String, String>(); |
| attributes.put(n,in.getAttributeValue(i)); |
| } |
| } |
| if(attributes==null) |
| attributes = Collections.emptyMap(); |
| } |
| |
| /** |
| * Where was this {@link Dom} loaded from? |
| */ |
| public Location getLocation() { |
| return location; |
| } |
| |
| /** |
| * Returns the list of attributes with a value on this config instance. |
| * This is by definition a subset of the attributes names as known |
| * to the model {@see ConfigModel.getAttributeNames}. |
| * |
| * @return list of attributes names which have values on this config instance |
| */ |
| public Set<String> getAttributeNames() { |
| return Collections.unmodifiableSet( attributes.keySet() ); |
| } |
| |
| /** |
| * Returns the children name associated with this config instance. |
| * This is by definition a subset of the element names as known to the |
| * model {#see ConfigModel.getElementNames(). |
| * |
| * @Return list of elements names associated with this config instance |
| */ |
| public synchronized Set<String> getElementNames() { |
| Set<String> names = new HashSet<String>(); |
| for (Child child : children) { |
| names.add(child.name); |
| } |
| return names; |
| } |
| |
| /** |
| * Performs translation with null pass-through. |
| */ |
| private String t(String s) { |
| if(s==null) return null; |
| return document.getTranslator().translate(s); |
| } |
| |
| /** |
| * Obtains the attribute value, after variable expansion. |
| * |
| * @return |
| * null if the attribute is not found. |
| */ |
| public String attribute(String name) { |
| return t(rawAttribute(name)); |
| } |
| |
| /** |
| * Obtians the attribute value without variable expansion. |
| * |
| * @return |
| * null if the attribute is not found. |
| */ |
| public String rawAttribute(String name) { |
| String value = attributes.get(name); |
| if (value==null && model.attributes.containsKey(name)) { |
| value = model.attributes.get(name).getDefaultValue(); |
| } |
| return value; |
| } |
| |
| /** |
| * Obtains the plural attribute value. Values are separate by ',' and surrounding whitespaces are ignored. |
| * |
| * @return |
| * null if the attribute doesn't exist. This is a distinct state from the empty list, |
| * which indicates that the attribute was there but no values were found. |
| */ |
| public List<String> attributes(String name) { |
| String v = attribute(name); |
| if(v==null) return null; |
| List<String> r = new ArrayList<String>(); |
| StringTokenizer tokens = new StringTokenizer(v,","); |
| while(tokens.hasMoreTokens()) |
| r.add(tokens.nextToken().trim()); |
| return r; |
| } |
| |
| /** |
| * Updates the attribute value. |
| * |
| * This would trigger the re-injection of the value. |
| */ |
| public void attribute(String name, String value) { |
| if (value==null) { |
| attributes.remove(name); |
| } else { |
| attributes.put(name,value); |
| // TODO: |
| // this re-injection has two problems. First, it forces an instantiation |
| // even if that hasn't happened yet. Second, if the component is scoped, |
| // this won't work correctly (but then, there's no way to make that work, |
| // since we can't enumerate all scope instances.) |
| getInjector().injectAttribute(this,name,get()); |
| } |
| } |
| |
| /** |
| * Returns the child element by name |
| * @param name of the element |
| * @return child element |
| */ |
| public synchronized Dom element(String name) { |
| |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| for (Child child : children) { |
| if (child.name.equals(name)) { |
| return ((NodeChild) child).dom; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Picks up one leaf-element value. |
| */ |
| public String leafElement(String name) { |
| return t(rawLeafElement(name)); |
| } |
| |
| private ActiveDescriptor<Dom> addWithAlias(ServiceLocator locator, AbstractActiveDescriptor<?> descriptor, Class<?> contract, String name) { |
| ActiveDescriptor<Dom> added = ServiceLocatorUtilities.findOneDescriptor(locator, descriptor); |
| |
| if (added == null) { |
| if (ConfigBean.class.isAssignableFrom(this.getClass())) { |
| if (!descriptor.getAdvertisedContracts().contains(ConfigBean.class.getName())) { |
| descriptor.addContractType(ConfigBean.class); |
| } |
| } |
| |
| added = ServiceLocatorUtilities.addOneDescriptor(locator, descriptor); |
| } |
| |
| AliasDescriptor<Dom> alias = new AliasDescriptor<Dom>(locator, added, contract.getName(), name); |
| |
| ServiceLocatorUtilities.addOneDescriptor(locator, alias); |
| |
| return added; |
| } |
| |
| /** |
| * Inserts a new {@link Dom} node right after the given DOM element. |
| * |
| * @param reference |
| * If null, the new element will be inserted at the very beginning. |
| * @param name |
| * The element name of the newly inserted item. "*" to indicate that the element |
| * name be determined by the model of the new node. |
| */ |
| public synchronized void insertAfter(Dom reference, String name, Dom newNode) { |
| // TODO: reparent newNode |
| if(name.equals("*")) name=newNode.model.tagName; |
| NodeChild newChild = new NodeChild(name, newNode); |
| |
| if (children.size()==0) { |
| children = new ArrayList<Child>(); |
| } |
| if(reference==null) { |
| children.add(0, newChild); |
| newNode.domDescriptor = addWithAlias(getHabitat(), newNode, newNode.getProxyType(), newNode.getKey()); |
| return; |
| } |
| |
| ListIterator<Child> itr = children.listIterator(); |
| while(itr.hasNext()) { |
| Child child = itr.next(); |
| if (child instanceof NodeChild) { |
| NodeChild nc = (NodeChild) child; |
| if(nc.dom==reference) { |
| itr.add(newChild); |
| newNode.domDescriptor = addWithAlias(getHabitat(), newNode, newNode.getProxyType(), newNode.getKey()); |
| |
| return; |
| } |
| } |
| } |
| throw new IllegalArgumentException(reference+" is not a valid child of "+this+". Children="+children); |
| } |
| |
| /** |
| * Replaces an existing {@link NodeChild} with another one. |
| * |
| * @see #insertAfter(Dom, String, Dom) |
| */ |
| public synchronized void replaceChild(Dom reference, String name, Dom newNode) { |
| ListIterator<Child> itr = children.listIterator(); |
| while(itr.hasNext()) { |
| Child child = itr.next(); |
| if (child instanceof NodeChild) { |
| NodeChild nc = (NodeChild) child; |
| if(nc.dom==reference) { |
| reference.release(); |
| newNode.domDescriptor = addWithAlias(getHabitat(), newNode,newNode.getProxyType(), newNode.getKey()); |
| |
| itr.set(new NodeChild(name,newNode)); |
| return; |
| } |
| } |
| } |
| throw new IllegalArgumentException(reference+" is not a valid child of "+this+". Children="+children); |
| } |
| |
| /** |
| * Removes an existing {@link NodeChild} |
| * |
| */ |
| public synchronized void removeChild(final Dom reference) { |
| ListIterator<Child> itr = children.listIterator(); |
| while (itr.hasNext()) { |
| Child child = itr.next(); |
| if (child instanceof NodeChild) { |
| NodeChild nc = (NodeChild) child; |
| if (nc.dom == reference) { |
| nc.dom.removeNestedChildren(); |
| itr.remove(); |
| reference.release(); |
| return; |
| } |
| } |
| } |
| throw new IllegalArgumentException(reference + " is not a valid child of " + this + ". Children=" + children); |
| |
| } |
| |
| private synchronized void removeNestedChildren() { |
| ListIterator<Child> itr = children.listIterator(); |
| while (itr.hasNext()) { |
| Child child = itr.next(); |
| if (child instanceof NodeChild) { |
| NodeChild nc = (NodeChild) child; |
| nc.dom.removeNestedChildren(); |
| nc.dom.release(); |
| } |
| } |
| } |
| |
| public synchronized boolean addLeafElement(String xmlName, String value) { |
| if (children.size()==0) { |
| children = new ArrayList<Child>(); |
| } |
| return children.add(new LeafChild(xmlName, value)); |
| |
| } |
| |
| public synchronized boolean removeLeafElement(String xmlName, String element) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| for (Child child : children) { |
| if(child.name.equals(xmlName) && ((LeafChild) child).value.equals(element)) { |
| return children.remove(child); |
| } |
| } |
| return false; |
| |
| } |
| |
| public synchronized boolean changeLeafElement(String xmlName, String oldValue, String newValue) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| int len = children.size(); |
| for( int i=0; i<len; i++ ) { |
| Child child = children.get(i); |
| if(child.name.equals(xmlName) && ((LeafChild) child).value.equals(oldValue)) { |
| return (children.set(i, new LeafChild(xmlName, newValue))!=null); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Picks up one leaf-element value without variable expansion. |
| */ |
| public synchronized String rawLeafElement(String name) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| int len = children.size(); |
| for( int i=0; i<len; i++ ) { |
| Child child = children.get(i); |
| if(child.name.equals(name)) { |
| // error check on model guarantees that this works. |
| return ((LeafChild)child).value; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Given a master list and the new sub list, replace the items in the master list with the matching items |
| * from the new sub list. This process works even if the length of the new sublist is different. |
| * |
| * <p> |
| * For example, givn: |
| * |
| * <pre> |
| * replace A by A': |
| * M=[A,B,C], S=[A'] => [A',B,C] |
| * M=[A,B,A,B,C], S=[A',A'] => [A',B,A',B,C] |
| * |
| * when list length is different: |
| * M=[A,A,B,C], S=[] => [B,C] |
| * M=[A,B,C], S=[A',A'] => [A',A',B,C] |
| * M=[B,C], S=[A',A'] => [B,C,A',A'] |
| * </pre> |
| */ |
| private static List<Child> stitchList(List<Child> list, String name, List<? extends Child> newSubList) { |
| List<Child> removed = new LinkedList<Child>(); |
| // to preserve order, try to put new itesm where old items are found. |
| // if the new list is longer than the current list, we put all the extra |
| // after the last item in the sequence. That is, |
| // given [A,A,B,C] and [A',A',A'], we'll update the list to [A',A',A',B,C] |
| // The 'last' variable remembers the insertion position. |
| int last = list.size(); |
| |
| ListIterator<Child> itr = list.listIterator(); |
| ListIterator<? extends Child> jtr = newSubList.listIterator(); |
| while(itr.hasNext()) { |
| Child child = itr.next(); |
| if(child.name.equals(name)) { |
| if(jtr.hasNext()) { |
| itr.set(jtr.next()); // replace |
| last = itr.nextIndex(); |
| removed.add(child); |
| } else { |
| itr.remove(); // remove |
| removed.add(child); |
| } |
| |
| } |
| } |
| |
| // new list is longer than the current one |
| if(jtr.hasNext()) |
| list.addAll(last,newSubList.subList(jtr.nextIndex(),newSubList.size())); |
| |
| return removed; |
| } |
| |
| /** |
| * Updates leaf-element values. |
| * <p> |
| * Synchronized so that concurrenct modifications will work correctly. |
| */ |
| public synchronized void setLeafElements(final String name, String... values) { |
| List<Child> newChildren = new ArrayList<Child>(children); |
| |
| LeafChild[] leaves = new LeafChild[values.length]; |
| for (int i = 0; i < values.length; i++) |
| leaves[i] = new LeafChild(name,values[i]); |
| |
| stitchList(newChildren,name,Arrays.asList(leaves)); |
| children = newChildren; |
| |
| // see attribute(String,String) for the issue with this |
| getInjector().injectElement(this,name,get()); |
| } |
| |
| /** |
| * Picks up all leaf-element values of the given name. |
| * @return |
| * Can be empty but never null. |
| */ |
| public synchronized List<String> leafElements(String name) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| final List<String> r = new ArrayList<String>(); |
| for (Child child : children) { |
| if (child.name.equals(name)) { |
| // error check on model guarantees that this cast works. |
| r.add(t(((LeafChild) child).value)); |
| } |
| } |
| return r; |
| } |
| |
| |
| /** |
| * Picks up all leaf-element values of the given name, without variable expansion. |
| * |
| * @return |
| * can be empty, but never null (even if such element name is not defined in the model.) |
| */ |
| public synchronized List<String> rawLeafElements(String name) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| final List<String> r = new ArrayList<String>(); |
| for (Child child : children) { |
| if (child.name.equals(name)) { |
| // error check on model guarantees that this cast works. |
| r.add(((LeafChild) child).value); |
| } |
| } |
| return r; |
| } |
| |
| /** |
| * Picks up one node-element value. |
| */ |
| public synchronized Dom nodeElement(String name) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| int len = children.size(); |
| for( int i=0; i<len; i++ ) { |
| Child child = children.get(i); |
| if(child.name.equals(name)) { |
| // error check on model guarantees that this works. |
| return ((NodeChild)child).dom; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Updates node-element values. |
| * <p> |
| * Synchronized so that concurrenct modifications will work correctly. |
| */ |
| public synchronized void setNodeElements(final String name, Dom... values) { |
| List<Child> newChildren = new ArrayList<Child>(children); |
| |
| NodeChild[] leaves = new NodeChild[values.length]; |
| for (int i = 0; i < values.length; i++) |
| leaves[i] = new NodeChild(name,values[i]); |
| |
| List<Child> removed = stitchList(newChildren,name,Arrays.asList(leaves)); |
| children = newChildren; |
| |
| for (Child c : removed) { |
| ((NodeChild) c).dom.release(); |
| } |
| |
| // see attribute(String,String) for the issue with this |
| getInjector().injectElement(this,name,get()); |
| } |
| |
| /** |
| * Picks up all node-elements that have the given element name. |
| */ |
| public synchronized List<Dom> nodeElements(String elementName) { |
| List<Child> children = this.children; // fix the snapshot that we'll work with |
| |
| final List<Dom> r = new ArrayList<Dom>(); |
| int len = children.size(); |
| for( int i=0; i<len; i++ ) { |
| Child child = children.get(i); |
| if(child.name.equals(elementName)) { |
| // error check on model guarantees that this works. |
| r.add(((NodeChild)child).dom); |
| } |
| } |
| return r; |
| } |
| |
| /** |
| * Picks up all node elements that are assignable to the given type, |
| * except those who are matched by other named elements in the model. |
| * |
| * Used to implement {@code FromElement("*")}. |
| */ |
| public synchronized List<Dom> domNodeByTypeElements(Class baseType) { |
| List<Dom> r = new ArrayList<Dom>(); |
| |
| int len = children.size(); |
| for( int i=0; i<len; i++ ) { |
| Child child = children.get(i); |
| if (child instanceof NodeChild) { |
| NodeChild nc = (NodeChild) child; |
| if(model.elements.containsKey(nc.name)) |
| continue; // match with named |
| if(baseType.isAssignableFrom(nc.dom.getImplementationClass())) |
| r.add(nc.dom); |
| } |
| } |
| return r; |
| } |
| |
| public <T> List<T> nodeByTypeElements(final Class<T> baseType) { |
| final List<Dom> elements = domNodeByTypeElements(baseType); |
| return new AbstractList<T>() { |
| public T get(int index) { |
| return baseType.cast(elements.get(index).get()); |
| } |
| public int size() { |
| return elements.size(); |
| } |
| }; |
| } |
| |
| public synchronized <T> T nodeByTypeElement(Class<T> baseType) { |
| int len = children.size(); |
| for( int i=0; i<len; i++ ) { |
| Child child = children.get(i); |
| if (child instanceof NodeChild) { |
| NodeChild nc = (NodeChild) child; |
| if(model.elements.containsKey(nc.name)) |
| continue; // match with named |
| if(baseType.isAssignableFrom(nc.dom.getImplementationClass())) |
| return baseType.cast(nc.dom.get()); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Performs injection to the given object. |
| */ |
| public void inject(Object target) { |
| model.inject(this,target); |
| } |
| |
| /** |
| * Gets the {@link ConfigInjector} instance that can be used to inject |
| * this DOM to a bean. |
| */ |
| public ConfigInjector getInjector() { |
| return ServiceLocatorUtilities.getService(habitat, model.injector); |
| } |
| |
| /** |
| * Locates the DOM that serves as the symbol space root. |
| * |
| * @return always non-null. |
| */ |
| public Dom getSymbolSpaceRoot(String typeName) { |
| Dom dom = this; |
| while(!dom.model.symbolSpaces.contains(typeName)) { |
| Dom p = dom.parent(); |
| if(p==null) return dom; // root |
| dom = p; |
| } |
| return dom; |
| } |
| |
| /** |
| * Recursively decends the DOM tree and finds a DOM that has the given key |
| * and the type name. |
| * |
| * <p> |
| * TODO: the current algorithm does a full tree scan. Expand the model |
| * so that we can detect deadends that are statically known not to contain |
| * the kind we are looking for, and use that to cut the search space. |
| */ |
| public synchronized Dom resolveReference(String key, String typeName) { |
| String keyedAs = model.keyedAs; |
| if(keyedAs!=null && keyedAs.equals(typeName) && getKey().equals(key)) |
| return this; // found it |
| |
| for (Child child : children) { |
| if (child instanceof NodeChild) { |
| NodeChild n = (NodeChild) child; |
| Dom found = n.dom.resolveReference(key,typeName); |
| if(found!=null) return found; |
| } |
| } |
| |
| return null; |
| } |
| |
| private final WeakCARCache<Class<?>, ConfigBeanProxy> proxyCache = |
| CacheUtilities.createWeakCARCache(new DomProxyComputable(this), 200, false); |
| |
| /** |
| * Creates a strongly-typed proxy to access values in this {@link Dom} object, |
| * by using the specified interface type as the proxy type. |
| */ |
| public <T extends ConfigBeanProxy> T createProxy(final Class<T> proxyType) { |
| ConfigBeanProxy retVal = proxyCache.compute(proxyType); |
| return proxyType.cast(retVal); |
| } |
| |
| /** |
| * Creates a strongly-typed proxy to access values in this {@link Dom} object, |
| */ |
| public <T extends ConfigBeanProxy> T createProxy() { |
| return createProxy(this.<T>getProxyType()); |
| } |
| |
| /** |
| * Returns the proxy type for this configuration object |
| * @param <T> the proxy type |
| * @return the class object for the proxy type |
| */ |
| public <T extends ConfigBeanProxy> Class<T> getProxyType() { |
| return model.getProxyType(); |
| } |
| |
| /** |
| * This ensures no-one tried to reify this descriptor, which has an impl class |
| * the interface |
| * |
| * @return always true |
| */ |
| public boolean isReified() { |
| return true; |
| } |
| |
| public Class<?> getImplementationClass() { |
| Class<?> retVal = (Class<?>) model.getProxyType(); |
| return retVal; |
| } |
| |
| public Type getImplementationType() { |
| return getImplementationClass(); |
| } |
| |
| public void setImplementationType(Type t) { |
| throw new AssertionError("Can not set type of Dom descriptor"); |
| } |
| |
| public Set<Type> getContractTypes() { |
| HashSet<Type> retVal = new HashSet<Type>(); |
| |
| retVal.add(model.getProxyType()); |
| |
| return retVal; |
| } |
| |
| public Class<? extends Annotation> getScopeAnnotation() { |
| String scope = getScope(); |
| if (scope != null && scope.equals(Singleton.class.getName())) { |
| return Singleton.class; |
| } |
| |
| return PerLookup.class; |
| } |
| |
| public Set<Annotation> getQualifierAnnotations() { |
| return Collections.emptySet(); |
| } |
| |
| public List<Injectee> getInjectees() { |
| return Collections.emptyList(); |
| } |
| |
| public Long getFactoryServiceId() { |
| return null; |
| } |
| |
| public Long getFactoryLocatorId() { |
| return null; |
| } |
| |
| /** |
| * {@link InvocationHandler} implementation that allows strongly-typed access |
| * to the configuration. |
| * |
| * <p> |
| * TODO: it might be a great performance improvement to have APT generate |
| * code that does this during the development time by looking at the interface. |
| */ |
| public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
| // serve java.lang.Object methods by ourselves |
| Class<?> clazz = method.getDeclaringClass(); |
| if(clazz ==Object.class) { |
| try { |
| return method.invoke(this,args); |
| } catch (InvocationTargetException e) { |
| throw e.getTargetException(); |
| } |
| } |
| |
| if(method.getAnnotation(DuckTyped.class)!=null) { |
| return invokeDuckMethod(method,proxy,args); |
| } |
| if(method.getAnnotation(ConfigExtensionMethod.class) != null) { |
| ConfigExtensionMethod cem = method.getAnnotation(ConfigExtensionMethod.class); |
| ConfigExtensionHandler handler = (ConfigExtensionHandler) ((cem.value() != null) |
| ? getServiceLocator().getService(ConfigExtensionHandler.class, cem.value()) |
| : getServiceLocator().getService(ConfigExtensionHandler.class)); |
| return invokeConfigExtensionMethod(handler, this, model.getProxyType(), args); |
| } |
| |
| ConfigModel.Property p = model.toProperty(method); |
| if(p==null) |
| throw new IllegalArgumentException("No corresponding property found for method: "+method); |
| |
| if(args==null || args.length==0) { |
| // getter |
| return getter(p, method.getGenericReturnType()); |
| } else { |
| throw new PropertyVetoException("Instance of " + getImplementation() + " named '" + getKey() + |
| "' is not locked for writing when invoking method " + method.getName() |
| + " you must use transaction semantics to access it.", null); |
| } |
| } |
| |
| /** |
| * Another version of the {@link #invoke(Object, Method, Object[])}, |
| * but instead of {@link Method} object, it takes the method name and argument types. |
| */ |
| public Object invoke(Object proxy, String methodName, Class[] argTypes, Object[] args) throws Throwable { |
| return invoke( proxy, getProxyType().getMethod(methodName, argTypes), args ); |
| } |
| |
| /** |
| * Invoke the user defined static method in the nested "Duck" class so that |
| * the user can define convenience methods on the config beans. |
| */ |
| Object invokeDuckMethod(Method method, Object proxy, Object[] args) throws Exception { |
| Method duckMethod = model.getDuckMethod(method); |
| |
| Object[] duckArgs; |
| if(args==null) { |
| duckArgs = new Object[]{proxy}; |
| } else { |
| duckArgs = new Object[args.length+1]; |
| duckArgs[0] = proxy; |
| System.arraycopy(args,0,duckArgs,1,args.length); |
| } |
| |
| try { |
| return duckMethod.invoke(null,duckArgs); |
| } catch (InvocationTargetException e) { |
| Throwable t = e.getTargetException(); |
| if (t instanceof Exception) |
| throw (Exception) t; |
| if (t instanceof Error) |
| throw (Error) t; |
| throw e; |
| } |
| } |
| /** |
| * Invoke the user defined static method in the nested "Duck" class so that |
| * the user can define convenience methods on the config beans. |
| */ |
| <T extends ConfigBeanProxy> T invokeConfigExtensionMethod(ConfigExtensionHandler<T> handler, Dom dom, |
| Class<T> clazz, Object[] args) throws Exception { |
| |
| return handler.handleExtension(dom, clazz, args); |
| } |
| |
| protected Object getter(ConfigModel.Property target, Type t) { |
| return target.get(this, t); |
| } |
| |
| protected void setter(ConfigModel.Property target, Object value) throws Exception { |
| target.set(this, value); |
| } |
| |
| public static String convertName(String name) { |
| // first, trim off the prefix |
| for (String p : PROPERTY_PREFIX) { |
| if(name.startsWith(p)) { |
| name = name.substring(p.length()); |
| break; |
| } |
| } |
| |
| // tokenize by finding 'x|X' and 'X|Xx' then insert '-'. |
| StringBuilder buf = new StringBuilder(name.length()+5); |
| for(String t : TOKENIZER.split(name)) { |
| if(buf.length()>0) buf.append('-'); |
| buf.append(t.toLowerCase(Locale.ENGLISH)); |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Used to tokenize the property name into XML name. |
| */ |
| static final Pattern TOKENIZER; |
| private static String split(String lookback,String lookahead) { |
| return "((?<="+lookback+")(?="+lookahead+"))"; |
| } |
| private static String or(String... tokens) { |
| StringBuilder buf = new StringBuilder(); |
| for (String t : tokens) { |
| if(buf.length()>0) buf.append('|'); |
| buf.append(t); |
| } |
| return buf.toString(); |
| } |
| static { |
| String pattern = or( |
| split("x","X"), // AbcDef -> Abc|Def |
| split("X","Xx"), // USArmy -> US|Army |
| //split("\\D","\\d"), // SSL2 -> SSL|2 |
| split("\\d","\\D") // SSL2Connector -> SSL|2|Connector |
| ); |
| pattern = pattern.replace("x","\\p{Lower}").replace("X","\\p{Upper}"); |
| TOKENIZER = Pattern.compile(pattern); |
| } |
| |
| static final String[] PROPERTY_PREFIX = new String[]{"get","set","is","has"}; |
| |
| /** |
| * This is how we inject the configuration into the created object. |
| * <p> |
| * There are two kinds — one where @{@link Configured} is put on |
| * a bean and that is placedinto Habitat, and the other is |
| * where @{@link Configured} is on {@link ConfigBeanProxy} subtype, |
| * in which case the proxy to {@link Dom} will be placed into the habitat. |
| */ |
| @SuppressWarnings("unchecked") |
| protected Creator createCreator(Class c) { |
| Creator creator = new CreatorImpl(c, getServiceLocator()); |
| |
| return (ConfigBeanProxy.class.isAssignableFrom(c) ? |
| new DomProxyCreator(c, this) : |
| new ConfiguredCreator(creator,this)); |
| } |
| |
| public static <T extends Annotation> T digAnnotation(Class<?> target, Class<T> annotationType) { |
| return digAnnotation(target, annotationType, new ArrayList<Class<? extends Annotation>>()); |
| } |
| |
| public static <T extends Annotation> T digAnnotation(Class<?> target, Class<T> annotationType, List<Class<? extends Annotation>> visited) { |
| T result = target.getAnnotation(annotationType); |
| if (result==null) { |
| for (Annotation a : target.getAnnotations()) { |
| if (!visited.contains(a.annotationType())) { |
| visited.add(a.annotationType()); |
| result = digAnnotation(a.annotationType(), annotationType, visited); |
| if (result!=null) { |
| return result; |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Used by the parser to set a list of children. |
| */ |
| /*package*/ synchronized void setChildren(List<Child> children) { |
| this.children = children; |
| } |
| |
| /** |
| * Returns the map of attributes names and values for attributes which |
| * value is neither null or the default value. These attributes are |
| * considered having a non default value and must be written out. |
| * |
| * @return map of attributes indexed by name that must be persisted |
| */ |
| private Map<String, String> attributesToWrite() { |
| |
| Map<String, String> attributesToWrite = new HashMap<String, String>(); |
| Map<String, String> localAttr = new HashMap<String, String>(attributes); |
| for (Map.Entry<String, String> a : localAttr.entrySet()) { |
| ConfigModel.AttributeLeaf am = model.attributes.get(a.getKey()); |
| String dv = am.getDefaultValue(); |
| if (dv==null || !dv.equals(a.getValue())) { |
| attributesToWrite.put(a.getKey(), a.getValue()); |
| } |
| } |
| return attributesToWrite; |
| } |
| |
| /** |
| * Returns true if this element is empty |
| * meaning all their attributes have default values and it has |
| * no descendants. |
| * |
| * @return true if the element is empty, false otherwise |
| */ |
| private synchronized boolean isEmpty() { |
| Map<String, String> attributesToWrite = attributesToWrite(); |
| |
| if (!attributesToWrite.isEmpty()) { |
| return false; |
| } |
| |
| // if we have children, we are not empty. |
| return children.isEmpty(); |
| |
| |
| } |
| |
| /** |
| * Writes back this element. |
| * |
| * @param tagName |
| * The tag name of this element to be written. If null, this DOM node |
| * must be a global element and its tag name will be used. |
| * @param w |
| * Receives XML infoset stream. |
| */ |
| public void writeTo(String tagName, XMLStreamWriter w) throws XMLStreamException { |
| if(tagName==null) |
| tagName = model.tagName; |
| if(tagName==null) |
| throw new IllegalArgumentException("Trying t write a local element "+this+" w/o a tag name"); |
| |
| /** |
| * If someone has explicitly called the skipFromXml then dont write the element to |
| * domain.xml |
| */ |
| if (! writeToXml) { |
| return; |
| } |
| w.writeStartElement(tagName); |
| |
| for (Map.Entry<String, String> attributeToWrite : attributesToWrite().entrySet()) { |
| w.writeAttribute(attributeToWrite.getKey(), attributeToWrite.getValue()); |
| } |
| |
| List<Child> localChildren; |
| synchronized(this) { |
| localChildren = new ArrayList<Child>(children); |
| } |
| for (Child c : localChildren) |
| c.writeTo(w); |
| |
| w.writeEndElement(); |
| } |
| |
| public void release() { |
| if (domDescriptor != null) { // children added via createProxy are not registered in serviceLocator |
| ServiceLocatorUtilities.removeOneDescriptor(getHabitat(), domDescriptor, true); |
| } |
| listeners.clear(); |
| } |
| |
| Set<ConfigListener> listeners = new HashSet<ConfigListener>(); |
| |
| public void addListener(ConfigListener listener) { |
| if (listener==null) { |
| throw new IllegalArgumentException("Listener cannot be null"); |
| } |
| listeners.add(listener); |
| } |
| |
| public boolean removeListener(ConfigListener listener) { |
| return listeners.remove(listener); |
| } |
| |
| Collection<ConfigListener> getListeners() { |
| return listeners; |
| } |
| |
| private boolean isCacheSet = false; |
| private Object cache; |
| |
| /* (non-Javadoc) |
| * @see org.glassfish.hk2.api.SingleCache#getCache() |
| */ |
| @Override |
| public Object getCache() { |
| return cache; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.glassfish.hk2.api.SingleCache#isCacheSet() |
| */ |
| @Override |
| public boolean isCacheSet() { |
| return isCacheSet; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.glassfish.hk2.api.SingleCache#setCache(java.lang.Object) |
| */ |
| @Override |
| public void setCache(Object cacheMe) { |
| cache = cacheMe; |
| isCacheSet = true; |
| |
| } |
| |
| /* (non-Javadoc) |
| * @see org.glassfish.hk2.api.SingleCache#releaseCache() |
| */ |
| @Override |
| public void releaseCache() { |
| isCacheSet = false; |
| cache = null; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.glassfish.hk2.api.ActiveDescriptor#create(org.glassfish.hk2.api.ServiceHandle) |
| */ |
| @Override |
| public Object create(ServiceHandle root) { |
| return createProxy(); |
| } |
| |
| public Object get() { |
| if (serviceHandle == null) { |
| return null; |
| } |
| |
| Object result = serviceHandle.getService(); |
| |
| return result; |
| } |
| |
| public ServiceLocator getServiceLocator() { |
| return habitat; |
| } |
| |
| private static DescriptorImpl createDescriptor( |
| String typeName, |
| HK2Loader cl, |
| Map<String, List<String>> metadata) { |
| DescriptorImpl retVal = new DescriptorImpl(); |
| |
| retVal.setImplementation(typeName); |
| retVal.addAdvertisedContract(typeName); |
| retVal.setLoader(cl); |
| retVal.setMetadata(metadata); |
| |
| return retVal; |
| } |
| |
| public int hashCode() { |
| return System.identityHashCode(this); |
| } |
| |
| public boolean equals(Object o) { |
| return this == o; |
| } |
| |
| private static class DomProxyComputable implements Computable<Class<?>, ConfigBeanProxy> { |
| private final Dom dom; |
| |
| private DomProxyComputable(Dom dom) { |
| this.dom = dom; |
| } |
| |
| @Override |
| public ConfigBeanProxy compute(final Class<?> proxyType) |
| throws ComputationErrorException { |
| |
| ClassLoader cl; |
| if (System.getSecurityManager()!=null) { |
| cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { |
| @Override |
| public ClassLoader run() { |
| return proxyType.getClassLoader(); |
| } |
| }); |
| } else { |
| cl = proxyType.getClassLoader(); |
| } |
| |
| ConfigBeanProxy retVal = (ConfigBeanProxy) Proxy.newProxyInstance(cl,new Class[]{proxyType}, dom); |
| |
| return retVal; |
| } |
| |
| |
| } |
| } |