All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jvnet.hk2.config.WriteableView Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2007-2011 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.jvnet.hk2.config;

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.lang.annotation.ElementType;
import java.text.MessageFormat;
import java.util.*;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import javax.validation.TraversableResolver;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.Validator;
import javax.validation.ValidatorContext;
import javax.validation.metadata.ConstraintDescriptor;

/**
 * A WriteableView is a view of a ConfigBean object that allow access to the
 * setters of the ConfigBean.
 *
 * @author Jerome Dochez
 */
public class WriteableView implements InvocationHandler, Transactor, ConfigView {

    private final ConfigBean bean;
    private final ConfigBeanProxy defaultView;
    private final Map changedAttributes;
    private final Map changedCollections;
    Transaction currentTx;
    private static Validator beanValidator=null;

    private final static ResourceBundle i18n = ResourceBundle.getBundle("org.jvnet.hk2.config.LocalStrings");
    
    public Transaction getTransaction() { return currentTx; }
    
    public WriteableView(ConfigBeanProxy readView) {
        this.bean = (ConfigBean) ((ConfigView) Proxy.getInvocationHandler(readView)).getMasterView();
        this.defaultView = bean.createProxy();
        changedAttributes = new HashMap();
        changedCollections = new HashMap();
        if (beanValidator == null) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            try {
                Thread.currentThread().setContextClassLoader(null);
                TraversableResolver traversableResolver =
                    new TraversableResolver() {
                        public boolean isReachable(Object traversableObject,
                            Path.Node traversableProperty, Class rootBeanType,
                            Path pathToTraversableObject, ElementType elementType) {
                                return true;
                        }

                        public boolean isCascadable(Object traversableObject,
                            Path.Node traversableProperty, Class rootBeanType,
                            Path pathToTraversableObject, ElementType elementType) {
                                return true;
                        }
                    };

                ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
                ValidatorContext validatorContext = validatorFactory.usingContext();
                validatorContext.messageInterpolator(new MessageInterpolatorImpl());                
                    beanValidator = validatorContext.traversableResolver(
                            traversableResolver).getValidator();
            } finally {
                Thread.currentThread().setContextClassLoader(cl);
            }
        }
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getName().equals("hashCode"))
            return super.hashCode();
        
        if (method.getName().equals("equals"))
            return super.equals(args[0]);

        if(method.getAnnotation(DuckTyped.class)!=null) {
            return bean.invokeDuckMethod(method,proxy,args);
        }
        
        ConfigModel.Property property = bean.model.toProperty(method);

        if(property==null)
             throw new IllegalArgumentException(
                "No corresponding property found for method: "+method);

        if(args==null || args.length==0) {
            // getter, maybe one of our changed properties
            if (changedAttributes.containsKey(property.xmlName())) {
                // serve masked changes.
                Object changedValue = changedAttributes.get(property.xmlName()).getNewValue();
                if (changedValue instanceof Dom) {
                    return ((Dom) changedValue).createProxy();
                } else {
                    return changedValue;
                }
            } else {
                // pass through.
                return getter(property, method.getGenericReturnType());
            }
        } else {
            setter(property, args[0], method.getGenericParameterTypes()[0]);
            return null;
        }
    }

    public String getPropertyValue(String propertyName) {

        ConfigModel.Property prop = this.getProperty(propertyName);
        if (prop!=null) {
            if (changedAttributes.containsKey(prop.xmlName())) {
                // serve masked changes.
                return (String) changedAttributes.get(prop.xmlName()).getNewValue();
            } else {
                return (String) getter(prop, String.class);
            }
        }
        return null;
    }

    public Object getter(ConfigModel.Property property, java.lang.reflect.Type t) {
        Object value =  bean._getter(property, t);
        if (value instanceof List) {
            if (!changedCollections.containsKey(property.xmlName())) {
                // wrap collections so we can record events on that collection mutation.
                changedCollections.put(property.xmlName(),
                        new ProtectedList(List.class.cast(value), defaultView, property.xmlName()));
            }
            return changedCollections.get(property.xmlName());
        }
        return value;
    }

    public void setter(ConfigModel.Property property,
        Object newValue, java.lang.reflect.Type t)  {
        
        // are we still in a transaction
        if (currentTx==null) {
            throw new IllegalStateException("Not part of a transaction");
        }
        try {
            if (newValue != null)
                handleValidation(property, newValue);
        } catch(Exception v) {
            bean.getLock().unlock();
            throw new RuntimeException(v);
        }

        // Following is a check to avoid duplication of elements with same key
        // attribute values. See Issue 7956
        if (property instanceof ConfigModel.AttributeLeaf) {

            // First check if the key leaf attribute is being set
            ConfigModel.AttributeLeaf al =
                (ConfigModel.AttributeLeaf)property;

            ConfigBean master = getMasterView();
            String key = master.model.key;

            // A key attribute may not exist at all if none of the attribs of
            // an element are annotated with key=true. If one exists, make sure
            // that attribute is actually the one being set
            if ((key != null) && (key.substring(1).equals(property.xmlName))) {

                // remove leading @
                key = key.substring(1);
                // Extract the old key value
                String oldKeyValue = getPropertyValue(key);

                // Get the Parent Element which has the key attribute specified
                // through the input paramater 'property'. For e.g. in case of
                // TopLevel->Resources->ConnectorConnectionPool->name(key attrib)
                // thisview will equal ConnectorConnectionPool
                Dom thisview = Dom.unwrap(defaultView);

                // parent will equal Resources
                Dom parent = thisview.parent();

                // siblings will contain all ConnectorConnectionPools under
                // Resources
                List siblings = 
                    parent.domNodeByTypeElements(thisview.getProxyType());

                // Iterate through each sibling element and see if anyone has
                // same key. If true throw an exception after unlocking this
                // element
                for (Dom sibling : siblings) {
                    String siblingKey = sibling.getKey();
                    if (newValue.equals(siblingKey)) {
                        bean.getLock().unlock();
                        throw new IllegalArgumentException(
                            "Keys cannot be duplicate. Old value of this key " +
                            "property, " + oldKeyValue + "will be retained");
                    }
                }
            }
        }

        // setter
        Object oldValue = bean.getter(property, t);
        if (newValue instanceof ConfigBeanProxy) {
            ConfigView bean = (ConfigView)
                Proxy.getInvocationHandler((ConfigBeanProxy) newValue);
            newValue = bean.getMasterView();
        }
        PropertyChangeEvent evt = new PropertyChangeEvent(
            defaultView,property.xmlName(), oldValue, newValue);
        try {
            for (ConfigBeanInterceptor interceptor : bean.getOptionalFeatures()) {
                interceptor.beforeChange(evt);
            }
        } catch(PropertyVetoException e) {
            throw new RuntimeException(e);
        }

        changedAttributes.put(property.xmlName(), evt);
        for (ConfigBeanInterceptor interceptor : bean.getOptionalFeatures()) {
            interceptor.afterChange(evt, System.currentTimeMillis());
        }
    }

    public ConfigModel.Property getProperty(String xmlName) {
        return bean.model.findIgnoreCase(xmlName);
    }

    /**
     * Enter a new Transaction, this method should return false if this object
     * is already enlisted in another transaction, or cannot be enlisted with
     * the passed transaction. If the object returns true, the object
     * is enlisted in the passed transaction and cannot be enlisted in another
     * transaction until either commit or abort has been issued.
     *
     * @param t the transaction to enlist with
     * @return true if the enlisting with the passed transaction was accepted,
     *         false otherwise
     */
    public synchronized boolean join(Transaction t) {
        if (currentTx==null) {
            currentTx = t;
            t.addParticipant(this);
            return true;
        }
        return false;
    }

    /**
     * Returns true of this Transaction can be committed on this object
     *
     * @param t is the transaction to commit, should be the same as the
     *          one passed during the join(Transaction t) call.
     * @return true if the trsaction commiting would be successful
     */
    public synchronized boolean canCommit(Transaction t) throws TransactionFailure {
        Set constraintViolations =
            beanValidator.validate(this.getProxy(this.getProxyType()));

        try {
            handleValidationException(constraintViolations);
        } catch (ConstraintViolationException constraintViolationException) {
            throw new TransactionFailure(constraintViolationException.getMessage(), constraintViolationException);
        }

        return currentTx==t;
    }

    private void handleValidationException(Set constraintViolations) throws ConstraintViolationException {

        if (constraintViolations != null && !constraintViolations.isEmpty()) {
            Iterator> it = constraintViolations.iterator();

            StringBuilder sb = new StringBuilder();
            sb.append(MessageFormat.format(i18n.getString("bean.validation.failure"), this.getProxyType().getSimpleName()));
            String violationMsg = i18n.getString("bean.validation.constraintViolation");
            while (it.hasNext()) {
                ConstraintViolation cv = it.next();
                sb.append(" ");
                sb.append(MessageFormat.format(violationMsg, cv.getMessage(), cv.getPropertyPath()));
                if (it.hasNext()) {
                    sb.append(i18n.getString("bean.validation.separator"));
                }
            }
            bean.getLock().unlock();
            throw new ConstraintViolationException(sb.toString(), constraintViolations);
        }
    }
     
    /** remove @ or <> eg "@foo" => "foo" or "" => "foo" */
    public static String stripMarkers(final String s ) {
        if ( s.startsWith("@") ) {
            return s.substring(1);
        }
        else if ( s.startsWith("<") ) {
            return s.substring(1, s.length()-1);
        }
        return s;
    }
    
    /**
     * Commit this Transaction.
     *
     * @param t the transaction commiting.
     * @throws TransactionFailure
     *          if the transaction commit failed
     */
    public synchronized List commit(Transaction t) throws TransactionFailure {
        if (currentTx==t) {
            currentTx=null;
        }
        
        // a key attribute must be non-null and have length >= 1
        final ConfigBean master = getMasterView();
        final String keyStr = master.model.key;
        if ( keyStr != null) {
            final String key = stripMarkers(keyStr);
            final String value = getPropertyValue(key);
            if ( value == null ) {
                throw new TransactionFailure( "Key value cannot be null: " + key );
            }
            if ( value.length() == 0 ) {
                throw new TransactionFailure( "Key value cannot be empty string: " + key );
            }
        }


        try {
            List appliedChanges = new ArrayList();
            for (PropertyChangeEvent event : changedAttributes.values()) {
                ConfigModel.Property property = bean.model.findIgnoreCase(event.getPropertyName());
                ConfigBeanInterceptor interceptor  = bean.getOptionalFeature(ConfigBeanInterceptor.class);
                try {
                    if (interceptor!=null) {
                        interceptor.beforeChange(event);
                    }
                } catch (PropertyVetoException e) {
                    throw new TransactionFailure(e.getMessage(), e);
                }
                property.set(bean, event.getNewValue());
                if (interceptor!=null) {
                    interceptor.afterChange(event, System.currentTimeMillis());
                }
                appliedChanges.add(event);
            }
            for (ProtectedList entry :  changedCollections.values())  {
                List originalList = entry.readOnly;
                for (PropertyChangeEvent event : entry.changeEvents) {
                    if (event.getOldValue()==null) {
                        originalList.add(event.getNewValue());
                    } else {
                        final Object toBeRemovedObj = event.getOldValue();
                        if ( toBeRemovedObj instanceof ConfigBeanProxy ) {
                            final Dom toBeRemoved = Dom.unwrap((ConfigBeanProxy)toBeRemovedObj);
                            for (int index=0;index T allocateProxy(Class type) throws TransactionFailure {
        if (currentTx==null) {
            throw new TransactionFailure("Not part of a transaction", null);
        }
        ConfigBean newBean = bean.allocate(type);
        WriteableView writeableView = bean.getHabitat().getComponent(ConfigSupport.class).getWriteableView(newBean.getProxy(type), newBean);
        writeableView.join(currentTx);

        return writeableView.getProxy(type);
   }

    public ConfigBean getMasterView() {
        return bean;
    }

    public void setMasterView(ConfigView view) {

    }

    public  Class getProxyType() {
        return bean.getProxyType();
    }

    @SuppressWarnings("unchecked")    
    public  T getProxy(Class type) {
        final ConfigBean sourceBean = getMasterView();
        if (!(type.getName().equals(sourceBean.model.targetTypeName))) {
            throw new IllegalArgumentException("This config bean interface is " + sourceBean.model.targetTypeName
                    + " not "  + type.getName());
        }
        Class[] interfacesClasses = { type };
        return (T) Proxy.newProxyInstance(type.getClassLoader(), interfacesClasses, this);
    }

/**
 * A Protected List is a @Link java.util.List implementation which mutable
 * operations are constrained by the owner of the list.
 *
 * @author Jerome Dochez
 */
private class ProtectedList extends AbstractList {

    final ConfigBeanProxy readView;
    final List readOnly;
    final String id;
    final List changeEvents = new ArrayList();
    final List proxied;

    ProtectedList(List readOnly, ConfigBeanProxy parent, String id) {
        proxied = Collections.synchronizedList(new ArrayList(readOnly));
        this.readView = parent;
        this.readOnly = readOnly;
        this.id = id;
    }

    /**
     * Returns the number of elements in this collection.  If the collection
     * contains more than Integer.MAX_VALUE elements, returns
     * Integer.MAX_VALUE.
     *
     * @return the number of elements in this collection.
     */
    public int size() {
        return proxied.size();
    }

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of element to return.
     * @return the element at the specified position in this list.
     * @throws IndexOutOfBoundsException if the given index is out of range
     *                                   (index < 0 || index >= size()).
     */
    public Object get(int index) {
        return proxied.get(index);
    }

    @Override
    public synchronized boolean add(Object object) {
        Object param = object;
        Object handler = null;
        try {
            handler = Proxy.getInvocationHandler(object);
        } catch(IllegalArgumentException e) {
            // ignore, this is a leaf
        }
        if (handler!=null && handler instanceof WriteableView) {
            ConfigBean master = ((WriteableView) handler).getMasterView();
            String key = master.model.key;
            if (key!=null) {
                // remove leading @
                key = key.substring(1);
                // check that we are not adding a duplicate key element
                String keyValue = ((WriteableView) handler).getPropertyValue(key);
                for (Object o : proxied) {
                    // the proxied object can be a read-only or a writeable view, we need
                    // to be careful
                    // ToDo : we need to encasulate this test.
                    String value = null;
                    if (Proxy.getInvocationHandler(o) instanceof WriteableView) {
                        ConfigBean masterView = ((WriteableView) handler).getMasterView();
                        String masterViewKey = masterView.model.key;
                        if(masterViewKey != null && key.equals(masterViewKey.substring(1))){
                            value = ((WriteableView) Proxy.getInvocationHandler(o)).getPropertyValue(key);
                        }
                    }  else {
                        Dom cbo = Dom.unwrap((ConfigBeanProxy) o);
                        String cboKey = cbo.model.key;
                        if(cboKey != null && key.equals(cboKey.substring(1))){
                            value = cbo.attribute(key);
                        }
                    }
                    if (keyValue!=null && value != null && keyValue.equals(value)) {
                        Dom parent = Dom.unwrap(readView);
                        throw new IllegalArgumentException("A " + master.getProxyType().getSimpleName() +
                                " with the same key \"" + keyValue + "\" already exists in " +
                                parent.getProxyType().getSimpleName() + " " + parent.getKey()) ;

                    }
                }
            }
            param = ((WriteableView) handler).getMasterView().createProxy(master.type());

        }
        changeEvents.add(new PropertyChangeEvent(defaultView, id, null, param));
        return proxied.add(object);
    }

    @Override
    public synchronized void clear() {
        // make a temporary list, iterating while removing doesn't work
        final List allItems = new ArrayList( proxied );
        for( final Object item : allItems ) {
            remove( item );
        }
    }
    
    @Override
    public synchronized boolean retainAll( final Collection keepers ) {
        final List toRemoveList = new ArrayList();
        for( final Object iffy : proxied ) {
            if ( ! keepers.contains(iffy) ) {
                toRemoveList.add(iffy);
            }
        }
        final boolean changed = removeAll(toRemoveList);
        
        return changed;
    }
    
    @Override
    public synchronized boolean removeAll( final Collection goners ) {
        boolean listChanged = false;
        for( final Object goner : goners ) {
            if ( remove(goner) ) {
                listChanged = true;
            }
        }

        return listChanged;
    }

    @Override
    public synchronized boolean remove(Object object) {
        changeEvents.add(new PropertyChangeEvent(defaultView, id, object, null));

        try {
            ConfigView handler = ((ConfigView) Proxy.getInvocationHandler(object)).getMasterView();
            for (int index = 0 ; index> constraintViolations = new HashSet>();
        if (property instanceof ConfigModel.AttributeLeaf) {
            ConfigModel.AttributeLeaf al = (ConfigModel.AttributeLeaf)property;
            ConstraintViolation cv = validateDataType(al, value.toString());
            if (cv!=null) {
                constraintViolations.add(cv);
            }
        }

        constraintViolations.addAll(
            beanValidator.validateValue(
                bean.getProxyType(), toCamelCase(property.xmlName()), value));

        handleValidationException(constraintViolations);
    }

    private ConstraintViolation validateDataType(final ConfigModel.AttributeLeaf al, final String value)
    {
        if (value.startsWith("${") && value.endsWith("}"))
          return null;

        boolean isValid = String.class.getName().equals(al.dataType);
        if ("int".equals(al.dataType) ||
            "java.lang.Integer".equals(al.dataType))
            isValid = representsInteger(value);
        else if ("boolean".equals(al.dataType) ||
                 "java.lang.Boolean".endsWith(al.dataType))
            isValid = representsBoolean(value);
        else if ("char".equals(al.dataType) ||
                 "java.lang.Character".equals(al.dataType))
            isValid = representsChar(value);        
        if (!isValid) {            
            return new ConstraintViolation() {
                @Override
                public String getMessage() {
                    return i18n.getString("bean.validation.dataType.failure") + al.dataType;
                }

                @Override
                public String getMessageTemplate() {
                    return null; 
                }

                @Override
                public Object getRootBean() {
                    return WriteableView.this;
                }

                @Override
                public Class getRootBeanClass() {
                    return WriteableView.this.getProxyType();
                }

                @Override
                public Object getLeafBean() {
                    return null;
                }

                @Override
                public Path getPropertyPath() {
                    final Set nodes = new HashSet();
                    nodes.add(new Path.Node() {
                        @Override
                        public String getName() {
                            return al.xmlName;
                        }

                        @Override
                        public boolean isInIterable() {
                            return false;
                        }

                        @Override
                        public Integer getIndex() {
                            return null;
                        }

                        @Override
                        public Object getKey() {
                            return null;
                        }
                    });
                    return new javax.validation.Path() {
                        @Override
                        public Iterator iterator() {
                            return nodes.iterator();
                        }
                    };
                }

                @Override
                public Object getInvalidValue() {
                    return value;
                }

                @Override
                public ConstraintDescriptor getConstraintDescriptor() {
                    return null;
                }
            };
        }
        return null;
    }
    
    private boolean representsBoolean(String value) {
        boolean isBoolean = 
           "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value);
        return (isBoolean);
    }

    private boolean representsChar(String value) {
            if (value.length() == 1)
                return true;
            return false;
    }

    private boolean representsInteger(String value) {
        try {
            Integer.parseInt(value);
            return true;
        } catch(NumberFormatException ne) {
            return false;
        }
    }

}