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         setComponentType(info, propertyEl);
    112         setKeyType(info, propertyEl);
    113         info.setName(createQName(propertyEl, propertyEl.getAttributeValue("mappedName")));
    114
    115         return info;
    116     }
    117
    118     protected Element findMapping(Class clazz)
    119     {
    120         Document doc = getDocument(clazz);
    121         if(doc == null) return null;
    122
    123         Element mapping = getMatch(doc, "/mappings/mapping[@uri='" + getTypeMapping().getEncodingStyleURI() + "']");
    124         if (mapping == null)
    125         {
    126             mapping = getMatch(doc, "/mappings/mapping");
    127         }
    128
    129         return mapping;
    130     }
    131
    132     public Type createDefaultType(TypeClassInfo info)
    133     {
    134         Element mapping = findMapping(info.getTypeClass());
    135
    136         if (mapping != null)
    137         {
    138             XMLBeanTypeInfo btinfo = new XMLBeanTypeInfo(getTypeMapping(),
    139                                                  info.getTypeClass(),
    140                                                  mapping);
    141             btinfo.setTypeMapping(getTypeMapping());
    142
    143             BeanType type = new BeanType(btinfo);
    144
    145             QName name = btinfo.getSchemaType();
    146             if (name == null) name = createQName(info.getTypeClass());
    147
    148             type.setSchemaType(name);
    149
    150             type.setTypeClass(info.getTypeClass());
    151             type.setTypeMapping(getTypeMapping());
    152
    153             return type;
    154         }
    155         else
    156         {
    157             return nextCreator.createDefaultType(info);
    158         }
    159     }
    160
    161     public TypeClassInfo createClassInfo(Method m, int index)
    162     {
    163         Element mapping = findMapping(m.getDeclaringClass());
    164
    165         if(mapping == null) return nextCreator.createClassInfo(m, index);
    166
    167         //find the elements that apply to the specified method
    168         TypeClassInfo info = new TypeClassInfo();
    169         if(index >= 0)
    170         {
    171             if(index >= m.getParameterTypes().length)
    172             {
    173                 throw new XFireRuntimeException("Method " + m + " does not have a parameter at index " + index);
    174             }
    175             //we don't want nodes for which the specified index is not specified
    176             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*");
    177             if(nodes.size() == 0)
    178             {
    179                 //no mapping for this method
    180                 return nextCreator.createClassInfo(m, index);
    181             }
    182             //pick the best matching node
    183             Element bestMatch = getBestMatch(mapping, m, nodes);
    184             if(bestMatch == null)
    185             {
    186                 //no mapping for this method
    187                 return nextCreator.createClassInfo(m, index);
    188             }
    189             info.setTypeClass(m.getParameterTypes()[index]);
    190             //info.setAnnotations(m.getParameterAnnotations()[index]);
    191             Element parameter = getMatch(bestMatch, "parameter[@index='" + index + "']");
    192
    193             setComponentType(info, parameter);
    194             setKeyType(info, parameter);
    195
    196             info.setName(createQName(parameter, parameter.getAttributeValue("mappedName")));
    197         }
    198         else
    199         {
    200             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/return-type/parent::*");
    201             if(nodes.size() == 0) return nextCreator.createClassInfo(m, index);
    202             Element bestMatch = getBestMatch(mapping, m, nodes);
    203             if(bestMatch == null)
    204             {
    205                 //no mapping for this method
    206                 return nextCreator.createClassInfo(m, index);
    207             }
    208             info.setTypeClass(m.getReturnType());
    209             //info.setAnnotations(m.getAnnotations());
    210             Element rtElement = bestMatch.getFirstChildElement("return-type");
    211             String componentType = rtElement.getAttributeValue("componentType");
    212             if(componentType != null)
    213             {
    214                 try
    215                 {
    216                     info.setGenericType(ClassLoaderUtils.loadClass(componentType, getClass()));
    217                 }
    218                 catch(ClassNotFoundException e)
    219                 {
    220                     log.error("Unable to load mapping class " + componentType);
    221                 }
    222             }
    223
    224             info.setName(createQName(rtElement, rtElement.getAttributeValue("mappedName")));
    225         }
    226
    227         return info;
    228     }
    229
    230     protected void setComponentType(TypeClassInfo info, Element parameter)
    231     {
    232         String componentType = parameter.getAttributeValue("componentType");
    233         if(componentType != null)
    234         {
    235             try
    236             {
    237                 info.setGenericType(ClassLoaderUtils.loadClass(componentType, getClass()));
    238             }
    239             catch(ClassNotFoundException e)
    240             {
    241                 log.error("Unable to load mapping class " + componentType);
    242             }
    243         }
    244     }
    245
    246     protected void setKeyType(TypeClassInfo info, Element parameter)
    247     {
    248         String componentType = parameter.getAttributeValue("keyType");
    249         if(componentType != null)
    250         {
    251             try
    252             {
    253                 info.setKeyType(ClassLoaderUtils.loadClass(componentType, getClass()));
    254             }
    255             catch(ClassNotFoundException e)
    256             {
    257                 log.error("Unable to load mapping class " + componentType);
    258             }
    259         }
    260     }
    261
    262     private Element getBestMatch(Element mapping, Method method, List availableNodes)
    263     {
    264         //first find all the matching method names
    265         List nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']");
    266         //remove the ones that aren't in our acceptable set, if one is specified
    267         if(availableNodes != null)
    268         {
    269             nodes.retainAll(availableNodes);
    270         }
    271         //no name found, so no matches
    272         if(nodes.size() == 0) return null;
    273         //if the method has no params, then more than one mapping is pointless
    274         Class[] parameterTypes = method.getParameterTypes();
    275         if(parameterTypes.length == 0) return (Element)nodes.get(0);
    276         //here's the fun part.
    277         //we go through the method parameters, ruling out matches
    278         for(int i = 0; i < parameterTypes.length; i++)
    279         {
    280             Class parameterType = parameterTypes[i];
    281             for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
    282             {
    283                 Element element = (Element)iterator.next();
    284                 //first we check if the parameter index is specified
    285                 Element match = getMatch(element, "parameter[@index='" + i + "']");
    286                 if(match != null)
    287                 {
    288                     //we check if the type is specified and matches
    289                     if(match.getAttributeValue("type") != null)
    290                     {
    291                         //if it doesn't match, then we can definitely rule out this result
    292                         if(!match.getAttributeValue("type").equals(parameterType.getName()))
    293                         {
    294                             iterator.remove();
    295                         }
    296                     }
    297                 }
    298             }
    299         }
    300         //if we have just one node left, then it has to be the best match
    301         if(nodes.size() == 1) return (Element)nodes.get(0);
    302         //all remaining definitions could apply, so we need to now pick the best one
    303         //the best one is the one with the most parameters specified
    304         Element bestCandidate = null;
    305         int highestSpecified = 0;
    306         for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
    307         {
    308             Element element = (Element)iterator.next();
    309             int availableParameters = element.getChildElements("parameter").size();
    310             if(availableParameters > highestSpecified)
    311             {
    312                 bestCandidate = element;
    313                 highestSpecified = availableParameters;
    314             }
    315         }
    316         return bestCandidate;
    317     }
    318
    319     private Element getMatch(Object doc, String xpath)
    320     {
    321         try
    322         {
    323             XPath path = new YOMXPath(xpath);
    324             return (Element)path.selectSingleNode(doc);
    325         }
    326         catch(JaxenException e)
    327         {
    328             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
    329         }
    330     }
    331
    332     private List getMatches(Object doc, String xpath)
    333     {
    334         try
    335         {
    336             XPath path = new YOMXPath(xpath);
    337             List result = path.selectNodes(doc);
    338             return result;
    339         }
    340         catch(JaxenException e)
    341         {
    342             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
    343         }
    344     }
    345
    346     /***
    347      * Creates a QName from a string, such as "ns:Element".
    348      */
    349     protected QName createQName(Element e, String value)
    350     {
    351         if (value == null || value.length() == 0) return null;
    352
    353         int index = value.indexOf(":");
    354
    355         if (index == -1)
    356         {
    357             return new QName(getTypeMapping().getEncodingStyleURI(), value);
    358         }
    359
    360         String prefix = value.substring(0, index);
    361         String localName = value.substring(index+1);
    362         String ns = e.getNamespaceURI(prefix);
    363
    364         if (ns == null || localName == null)
    365             throw new XFireRuntimeException("Invalid QName in mapping: " + value);
    366
    367         return new QName(ns, localName, prefix);
    368     }
    369 }