001    /*******************************************************************************
002     * Portions created by Sebastian Thomschke are copyright (c) 2005-2013 Sebastian
003     * Thomschke.
004     *
005     * All Rights Reserved. This program and the accompanying materials
006     * are made available under the terms of the Eclipse Public License v1.0
007     * which accompanies this distribution, and is available at
008     * http://www.eclipse.org/legal/epl-v10.html
009     *
010     * Contributors:
011     *     Sebastian Thomschke - initial implementation.
012     *******************************************************************************/
013    package net.sf.oval.ogn;
014    
015    import java.lang.reflect.AccessibleObject;
016    import java.util.Locale;
017    
018    import net.sf.oval.exception.InvalidConfigurationException;
019    import net.sf.oval.internal.util.Assert;
020    import net.sf.oval.internal.util.ReflectionUtils;
021    
022    import org.apache.commons.jxpath.JXPathBeanInfo;
023    import org.apache.commons.jxpath.JXPathContext;
024    import org.apache.commons.jxpath.JXPathIntrospector;
025    import org.apache.commons.jxpath.JXPathNotFoundException;
026    import org.apache.commons.jxpath.Pointer;
027    import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
028    import org.apache.commons.jxpath.ri.QName;
029    import org.apache.commons.jxpath.ri.model.NodePointer;
030    import org.apache.commons.jxpath.ri.model.beans.BeanPointer;
031    import org.apache.commons.jxpath.ri.model.beans.BeanPointerFactory;
032    import org.apache.commons.jxpath.ri.model.beans.NullPointer;
033    import org.apache.commons.jxpath.ri.model.beans.NullPropertyPointer;
034    import org.apache.commons.jxpath.ri.model.beans.PropertyPointer;
035    
036    /**
037     * JXPath {@link "http://commons.apache.org/jxpath/"} based object graph navigator implementation.
038     * @author Sebastian Thomschke
039     */
040    public class ObjectGraphNavigatorJXPathImpl implements ObjectGraphNavigator
041    {
042            protected static final class BeanPointerEx extends BeanPointer
043            {
044                    private static final long serialVersionUID = 1L;
045    
046                    private final JXPathBeanInfo beanInfo;
047    
048                    public BeanPointerEx(final NodePointer parent, final QName name, final Object bean, final JXPathBeanInfo beanInfo)
049                    {
050                            super(parent, name, bean, beanInfo);
051                            this.beanInfo = beanInfo;
052                    }
053    
054                    public BeanPointerEx(final QName name, final Object bean, final JXPathBeanInfo beanInfo, final Locale locale)
055                    {
056                            super(name, bean, beanInfo, locale);
057                            this.beanInfo = beanInfo;
058                    }
059    
060                    @Override
061                    public boolean equals(final Object obj)
062                    {
063                            if (this == obj) return true;
064                            if (!super.equals(obj)) return false;
065                            if (getClass() != obj.getClass()) return false;
066                            final BeanPointerEx other = (BeanPointerEx) obj;
067                            if (beanInfo == null)
068                            {
069                                    if (other.beanInfo != null) return false;
070                            }
071                            else if (!beanInfo.equals(other.beanInfo)) return false;
072                            return true;
073                    }
074    
075                    @Override
076                    public boolean isValidProperty(final QName name)
077                    {
078                            if (!super.isValidProperty(name)) return false;
079    
080                            // JXPath's default implementation returns true, even if the given property does not exit
081                            if (beanInfo.getPropertyDescriptor(name.getName()) == null)
082                                    throw new JXPathNotFoundException("No pointer for xpath: " + toString() + "/" + name);
083    
084                            return true;
085                    }
086            }
087    
088            protected static final class BeanPointerFactoryEx extends BeanPointerFactory
089            {
090                    @Override
091                    public NodePointer createNodePointer(final NodePointer parent, final QName name, final Object bean)
092                    {
093                            if (bean == null) return new NullPointer(parent, name);
094    
095                            final JXPathBeanInfo bi = JXPathIntrospector.getBeanInfo(bean.getClass());
096                            return new BeanPointerEx(parent, name, bean, bi);
097                    }
098    
099                    @Override
100                    public NodePointer createNodePointer(final QName name, final Object bean, final Locale locale)
101                    {
102                            final JXPathBeanInfo bi = JXPathIntrospector.getBeanInfo(bean.getClass());
103                            return new BeanPointerEx(name, bean, bi, locale);
104                    }
105    
106                    @Override
107                    public int getOrder()
108                    {
109                            return BeanPointerFactory.BEAN_POINTER_FACTORY_ORDER - 1;
110                    }
111            }
112    
113            static
114            {
115                    /*
116                     * JXPath currently does not distinguish between invalid object graph paths, e.g. by referencing a non-existing property on a Java Bean,
117                     * and incomplete object graph paths because of null-values.
118                     * In both cases a JXPathNotFoundException is thrown if JXPathContext.lenient is <code>false</code>, and in both cases a NullPropertyPointer is returned if
119                     * JXPathContext.lenient is <code>true</code>.
120                     *
121                     * Therefore we install a patched BeanPointerFactory that checks the existence of properties and throws a JXPathNotFoundException if it does not exist, no matter
122                     * to which setting JXPathContext.lenient is set.
123                     */
124                    JXPathContextReferenceImpl.addNodePointerFactory(new BeanPointerFactoryEx());
125            }
126    
127            public ObjectGraphNavigationResult navigateTo(final Object root, final String xpath) throws InvalidConfigurationException
128            {
129                    Assert.argumentNotNull("root", root);
130                    Assert.argumentNotNull("xpath", xpath);
131    
132                    try
133                    {
134                            final JXPathContext ctx = JXPathContext.newContext(root);
135                            ctx.setLenient(true); // do not throw an exception if object graph is incomplete, e.g. contains null-values
136                            final Pointer pointer = ctx.getPointer(xpath);
137    
138                            if (pointer instanceof NullPropertyPointer) return null;
139    
140                            if (pointer instanceof PropertyPointer)
141                            {
142                                    final PropertyPointer pp = (PropertyPointer) pointer;
143                                    final Class< ? > beanClass = pp.getBean().getClass();
144                                    AccessibleObject accessor = ReflectionUtils.getField(beanClass, pp.getPropertyName());
145                                    if (accessor == null) accessor = ReflectionUtils.getGetter(beanClass, pp.getPropertyName());
146                                    return new ObjectGraphNavigationResult(root, xpath, pp.getBean(), accessor, pointer.getValue());
147                            }
148    
149                            return new ObjectGraphNavigationResult(root, xpath, pointer.getNode(), null, pointer.getValue());
150                    }
151                    catch (final JXPathNotFoundException ex)
152                    {
153                            // thrown if the xpath is invalid
154                            throw new InvalidConfigurationException(ex);
155                    }
156            }
157    }