View Javadoc

    1   package org.codehaus.xfire.aegis.type;
    2
    3   import java.beans.PropertyDescriptor;
    4   import java.io.InputStream;
    5   import java.lang.reflect.Method;
    6   import java.util.HashMap;
    7   import java.util.Iterator;
    8   import java.util.List;
    9   import java.util.Map;
    10
    11  import javax.xml.namespace.QName;
    12  import javax.xml.stream.XMLStreamException;
    13
    14  import org.apache.commons.logging.Log;
    15  import org.apache.commons.logging.LogFactory;
    16  import org.codehaus.xfire.XFireRuntimeException;
    17  import org.codehaus.xfire.aegis.type.basic.BeanType;
    18  import org.codehaus.xfire.aegis.type.basic.XMLBeanTypeInfo;
    19  import org.codehaus.xfire.util.ClassLoaderUtils;
    20  import org.codehaus.yom.Document;
    21  import org.codehaus.yom.Element;
    22  import org.codehaus.yom.stax.StaxBuilder;
    23  import org.codehaus.yom.xpath.YOMXPath;
    24  import org.jaxen.JaxenException;
    25  import org.jaxen.XPath;
    26
    27  /***
    28   * Deduce mapping information from an xml file.
    29   * The xml file should be in the same packages as the class, with the name <code>className.aegis.xml</code>.
    30   * For example, given the following service interface:
    31   * <p/>
    32   * <pre>
    33   * public Collection getResultsForValues(String id, Collection values); //method 1
    34   * public Collection getResultsForValues(int id, Collection values); //method 2
    35   * public String getResultForValue(String value); //method 3
    36   * </pre>
    37   * An example of the type xml is:
    38   * <pre>
    39   * &lt;mappings&gt;
    40   *  &lt;mapping&gt;
    41   *    &lt;method name="getResultsForValues"&gt;
    42   *      &lt;return-type componentType="com.acme.ResultBean" /&gt;
    43   *      &lt;!-- no need to specify index 0, since it's a String --&gt;
    44   *      &lt;parameter index="1" componentType="java.lang.String" /&gt;
    45   *    &lt;/method&gt;
    46   *  &lt;/mapping&gt;
    47   * &lt;/mappings&gt;
    48   * </pre>
    49   * <p/>
    50   * Note that for values which can be easily deduced (such as the String parameter, or the second service method)
    51   * no mapping need be specified in the xml descriptor, which is why no mapping is specified for method 3.
    52   * <p/>
    53   * However, if you have overloaded methods with different semantics, then you will need to specify enough
    54   * parameters to disambiguate the method and uniquely identify it. So in the example above, the mapping
    55   * specifies will apply to both method 1 and method 2, since the parameter at index 0 is not specified.
    56   *
    57   * @author Hani Suleiman
    58   *         Date: Jun 14, 2005
    59   *         Time: 7:47:56 PM
    60   */
    61  public class XMLTypeCreator extends AbstractTypeCreator
    62  {
    63      private static final Log log = LogFactory.getLog(XMLTypeCreator.class);
    64      //cache of classes to documents
    65      private Map documents = new HashMap();
    66
    67      protected Document getDocument(Class clazz)
    68      {
    69          Document doc = (Document)documents.get(clazz.getName());
    70          if(doc != null)
    71          {
    72              return doc;
    73          }
    74          String path = '/' + clazz.getName().replace('.', '/') + ".aegis.xml";
    75          InputStream is = clazz.getResourceAsStream(path);
    76          if(is == null) return null;
    77          try
    78          {
    79              doc = new StaxBuilder().build(is);
    80              documents.put(clazz.getName(), doc);
    81              return doc;
    82          }
    83          catch(XMLStreamException e)
    84          {
    85              log.error("Error loading file " + path, e);
    86          }
    87          return null;
    88      }
    89
    90      public Type createCollectionType(TypeClassInfo info)
    91      {
    92          return super.createCollectionType(info, (Class)info.getGenericType());
    93      }
    94
    95      public TypeClassInfo createClassInfo(PropertyDescriptor pd)
    96      {
    97          Element mapping = findMapping(pd.getReadMethod().getDeclaringClass());
    98          if(mapping == null)
    99          {
    100             return nextCreator.createClassInfo(pd);
    101         }
    102
    103         Element propertyEl = getMatch(mapping, "./property[@name='" + pd.getName() + "']");
    104         if(propertyEl == null)
    105         {
    106             return nextCreator.createClassInfo(pd);
    107         }
    108
    109         TypeClassInfo info = new TypeClassInfo();
    110         info.setTypeClass(pd.getReadMethod().getReturnType());
    111         readMetadata(info, propertyEl);
    112
    113         return info;
    114     }
    115
    116     protected Element findMapping(Class clazz)
    117     {
    118         Document doc = getDocument(clazz);
    119         if(doc == null) return null;
    120
    121         Element mapping = getMatch(doc, "/mappings/mapping[@uri='" + getTypeMapping().getEncodingStyleURI() + "']");
    122         if (mapping == null)
    123         {
    124             mapping = getMatch(doc, "/mappings/mapping");
    125         }
    126
    127         return mapping;
    128     }
    129
    130     public Type createDefaultType(TypeClassInfo info)
    131     {
    132         Element mapping = findMapping(info.getTypeClass());
    133
    134         if (mapping != null)
    135         {
    136             XMLBeanTypeInfo btinfo = new XMLBeanTypeInfo(info.getTypeClass(), mapping);
    137             btinfo.setTypeMapping(getTypeMapping());
    138
    139             BeanType type = new BeanType(btinfo);
    140
    141             QName name = btinfo.getSchemaType();
    142             if (name == null) name = createQName(info.getTypeClass());
    143
    144             type.setSchemaType(name);
    145
    146             type.setTypeClass(info.getTypeClass());
    147             type.setTypeMapping(getTypeMapping());
    148
    149             return type;
    150         }
    151         else
    152         {
    153             return nextCreator.createDefaultType(info);
    154         }
    155     }
    156
    157     public TypeClassInfo createClassInfo(Method m, int index)
    158     {
    159         Element mapping = findMapping(m.getDeclaringClass());
    160
    161         if(mapping == null) return nextCreator.createClassInfo(m, index);
    162
    163         //find the elements that apply to the specified method
    164         TypeClassInfo info = new TypeClassInfo();
    165         if(index >= 0)
    166         {
    167             if(index >= m.getParameterTypes().length)
    168             {
    169                 throw new XFireRuntimeException("Method " + m + " does not have a parameter at index " + index);
    170             }
    171             //we don't want nodes for which the specified index is not specified
    172             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*");
    173             if(nodes.size() == 0)
    174             {
    175                 //no mapping for this method
    176                 return nextCreator.createClassInfo(m, index);
    177             }
    178             //pick the best matching node
    179             Element bestMatch = getBestMatch(mapping, m, nodes);
    180             if(bestMatch == null)
    181             {
    182                 //no mapping for this method
    183                 return nextCreator.createClassInfo(m, index);
    184             }
    185             info.setTypeClass(m.getParameterTypes()[index]);
    186             //info.setAnnotations(m.getParameterAnnotations()[index]);
    187             Element parameter = getMatch(bestMatch, "parameter[@index='" + index + "']");
    188             readMetadata(info, parameter);
    189         }
    190         else
    191         {
    192             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/return-type/parent::*");
    193             if(nodes.size() == 0) return nextCreator.createClassInfo(m, index);
    194             Element bestMatch = getBestMatch(mapping, m, nodes);
    195             if(bestMatch == null)
    196             {
    197                 //no mapping for this method
    198                 return nextCreator.createClassInfo(m, index);
    199             }
    200             info.setTypeClass(m.getReturnType());
    201             //info.setAnnotations(m.getAnnotations());
    202             Element rtElement = bestMatch.getFirstChildElement("return-type");
    203             readMetadata(info, rtElement);
    204         }
    205
    206         return info;
    207     }
    208
    209     protected void readMetadata(TypeClassInfo info, Element parameter)
    210     {
    211         info.setTypeName(createQName(parameter, parameter.getAttributeValue("typeName")));
    212         setComponentType(info, parameter);
    213         setKeyType(info, parameter);
    214         setType(info, parameter);
    215     }
    216
    217     protected void setComponentType(TypeClassInfo info, Element parameter)
    218     {
    219         String componentType = parameter.getAttributeValue("componentType");
    220         if(componentType != null)
    221         {
    222             try
    223             {
    224                 info.setGenericType(ClassLoaderUtils.loadClass(componentType, getClass()));
    225             }
    226             catch(ClassNotFoundException e)
    227             {
    228                 throw new XFireRuntimeException("Unable to load component type class " + componentType, e);
    229             }
    230         }
    231     }
    232
    233     protected void setType(TypeClassInfo info, Element parameter)
    234     {
    235         String type = parameter.getAttributeValue("type");
    236         if(type != null)
    237         {
    238             try
    239             {
    240                 info.setType(ClassLoaderUtils.loadClass(type, getClass()));
    241             }
    242             catch(ClassNotFoundException e)
    243             {
    244                 throw new XFireRuntimeException("Unable to load type class " + type, e);
    245             }
    246         }
    247     }
    248
    249     protected void setKeyType(TypeClassInfo info, Element parameter)
    250     {
    251         String componentType = parameter.getAttributeValue("keyType");
    252         if(componentType != null)
    253         {
    254             try
    255             {
    256                 info.setKeyType(ClassLoaderUtils.loadClass(componentType, getClass()));
    257             }
    258             catch(ClassNotFoundException e)
    259             {
    260                 log.error("Unable to load mapping class " + componentType);
    261             }
    262         }
    263     }
    264
    265     private Element getBestMatch(Element mapping, Method method, List availableNodes)
    266     {
    267         //first find all the matching method names
    268         List nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']");
    269         //remove the ones that aren't in our acceptable set, if one is specified
    270         if(availableNodes != null)
    271         {
    272             nodes.retainAll(availableNodes);
    273         }
    274         //no name found, so no matches
    275         if(nodes.size() == 0) return null;
    276         //if the method has no params, then more than one mapping is pointless
    277         Class[] parameterTypes = method.getParameterTypes();
    278         if(parameterTypes.length == 0) return (Element)nodes.get(0);
    279         //here's the fun part.
    280         //we go through the method parameters, ruling out matches
    281         for(int i = 0; i < parameterTypes.length; i++)
    282         {
    283             Class parameterType = parameterTypes[i];
    284             for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
    285             {
    286                 Element element = (Element)iterator.next();
    287                 //first we check if the parameter index is specified
    288                 Element match = getMatch(element, "parameter[@index='" + i + "']");
    289                 if(match != null)
    290                 {
    291                     //we check if the type is specified and matches
    292                     if(match.getAttributeValue("type") != null)
    293                     {
    294                         //if it doesn't match, then we can definitely rule out this result
    295                         if(!match.getAttributeValue("type").equals(parameterType.getName()))
    296                         {
    297                             iterator.remove();
    298                         }
    299                     }
    300                 }
    301             }
    302         }
    303         //if we have just one node left, then it has to be the best match
    304         if(nodes.size() == 1) return (Element)nodes.get(0);
    305         //all remaining definitions could apply, so we need to now pick the best one
    306         //the best one is the one with the most parameters specified
    307         Element bestCandidate = null;
    308         int highestSpecified = 0;
    309         for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
    310         {
    311             Element element = (Element)iterator.next();
    312             int availableParameters = element.getChildElements("parameter").size();
    313             if(availableParameters > highestSpecified)
    314             {
    315                 bestCandidate = element;
    316                 highestSpecified = availableParameters;
    317             }
    318         }
    319         return bestCandidate;
    320     }
    321
    322     private Element getMatch(Object doc, String xpath)
    323     {
    324         try
    325         {
    326             XPath path = new YOMXPath(xpath);
    327             return (Element)path.selectSingleNode(doc);
    328         }
    329         catch(JaxenException e)
    330         {
    331             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
    332         }
    333     }
    334
    335     private List getMatches(Object doc, String xpath)
    336     {
    337         try
    338         {
    339             XPath path = new YOMXPath(xpath);
    340             List result = path.selectNodes(doc);
    341             return result;
    342         }
    343         catch(JaxenException e)
    344         {
    345             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
    346         }
    347     }
    348
    349     /***
    350      * Creates a QName from a string, such as "ns:Element".
    351      */
    352     protected QName createQName(Element e, String value)
    353     {
    354         if (value == null || value.length() == 0) return null;
    355
    356         int index = value.indexOf(":");
    357
    358         if (index == -1)
    359         {
    360             return new QName(getTypeMapping().getEncodingStyleURI(), value);
    361         }
    362
    363         String prefix = value.substring(0, index);
    364         String localName = value.substring(index+1);
    365         String ns = e.getNamespaceURI(prefix);
    366
    367         if (ns == null || localName == null)
    368             throw new XFireRuntimeException("Invalid QName in mapping: " + value);
    369
    370         return new QName(ns, localName, prefix);
    371     }
    372 }