Coverage Report - org.simpleframework.xml.core.MethodScanner
 
Classes in this File Line Coverage Branch Coverage Complexity
MethodScanner
90%
133/147
80%
66/82
3.045
MethodScanner$1
N/A
N/A
3.045
MethodScanner$PartMap
100%
3/3
N/A
3.045
 
 1  
 /*
 2  
  * MethodScanner.java April 2007
 3  
  *
 4  
  * Copyright (C) 2007, Niall Gallagher <niallg@users.sf.net>
 5  
  *
 6  
  * Licensed under the Apache License, Version 2.0 (the "License");
 7  
  * you may not use this file except in compliance with the License.
 8  
  * You may obtain a copy of the License at
 9  
  *
 10  
  *     http://www.apache.org/licenses/LICENSE-2.0
 11  
  *
 12  
  * Unless required by applicable law or agreed to in writing, software
 13  
  * distributed under the License is distributed on an "AS IS" BASIS,
 14  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
 15  
  * implied. See the License for the specific language governing 
 16  
  * permissions and limitations under the License.
 17  
  */
 18  
 
 19  
 package org.simpleframework.xml.core;
 20  
 
 21  
 import static org.simpleframework.xml.DefaultType.PROPERTY;
 22  
 
 23  
 import java.lang.annotation.Annotation;
 24  
 import java.lang.reflect.Method;
 25  
 import java.util.Iterator;
 26  
 import java.util.LinkedHashMap;
 27  
 import java.util.List;
 28  
 
 29  
 import org.simpleframework.xml.Attribute;
 30  
 import org.simpleframework.xml.DefaultType;
 31  
 import org.simpleframework.xml.Element;
 32  
 import org.simpleframework.xml.ElementArray;
 33  
 import org.simpleframework.xml.ElementList;
 34  
 import org.simpleframework.xml.ElementListUnion;
 35  
 import org.simpleframework.xml.ElementMap;
 36  
 import org.simpleframework.xml.ElementMapUnion;
 37  
 import org.simpleframework.xml.ElementUnion;
 38  
 import org.simpleframework.xml.Text;
 39  
 import org.simpleframework.xml.Transient;
 40  
 import org.simpleframework.xml.Version;
 41  
 
 42  
 /**
 43  
  * The <code>MethodScanner</code> object is used to scan an object 
 44  
  * for matching get and set methods for an XML annotation. This will
 45  
  * scan for annotated methods starting with the most specialized
 46  
  * class up the class hierarchy. Thus, annotated methods can be 
 47  
  * overridden in a type specialization.
 48  
  * <p>
 49  
  * The annotated methods must be either a getter or setter method
 50  
  * following the Java Beans naming conventions. This convention is
 51  
  * such that a method must begin with "get", "set", or "is". A pair
 52  
  * of set and get methods for an annotation must make use of the
 53  
  * same type. For instance if the return type for the get method
 54  
  * was <code>String</code> then the set method must have a single
 55  
  * argument parameter that takes a <code>String</code> type.
 56  
  * <p>
 57  
  * For a method to be considered there must be both the get and set
 58  
  * methods. If either method is missing then the scanner fails with
 59  
  * an exception. Also, if an annotation marks a method which does
 60  
  * not follow Java Bean naming conventions an exception is thrown.
 61  
  *    
 62  
  * @author Niall Gallagher
 63  
  */
 64  
 class MethodScanner extends ContactList {
 65  
    
 66  
    /**
 67  
     * This is a factory used for creating property method parts.
 68  
     */
 69  
    private final MethodPartFactory factory;
 70  
    
 71  
    /**
 72  
     * This object contains various support functions for the class.
 73  
     */
 74  
    private final Support support;
 75  
    
 76  
    /**
 77  
     * This is used to collect all the set methods from the object.
 78  
     */
 79  
    private final PartMap write;
 80  
    
 81  
    /**
 82  
     * This is used to collect all the get methods from the object.
 83  
     */
 84  
    private final PartMap read;
 85  
 
 86  
    /**
 87  
     * This contains the details for the class that is being scanned.
 88  
     */
 89  
    private final Detail detail;
 90  
       
 91  
    /**
 92  
     * Constructor for the <code>MethodScanner</code> object. This is
 93  
     * used to create an object that will scan the specified class
 94  
     * such that all bean property methods can be paired under the
 95  
     * XML annotation specified within the class.
 96  
     * 
 97  
     * @param detail this contains the details for the class scanned
 98  
     * @param support this contains various support functions
 99  
     */
 100  3406
    public MethodScanner(Detail detail, Support support) throws Exception {
 101  3406
       this.factory = new MethodPartFactory(detail);
 102  3406
       this.write = new PartMap();
 103  3406
       this.read = new PartMap();
 104  3406
       this.support = support;
 105  3406
       this.detail = detail;
 106  3406
       this.scan(detail);
 107  3399
    }
 108  
    
 109  
    /**
 110  
     * This method is used to scan the class hierarchy for each class
 111  
     * in order to extract methods that contain XML annotations. If
 112  
     * a method is annotated it is converted to a contact so that
 113  
     * it can be used during serialization and deserialization.
 114  
     * 
 115  
     * @param detail this contains the details for the class scanned
 116  
     */
 117  
    private void scan(Detail detail) throws Exception {
 118  3406
       DefaultType access = detail.getAccess();
 119  3406
       Class base = detail.getSuper();
 120  
 
 121  3406
       if(base != null) {
 122  1018
          extend(base);
 123  
       }
 124  3406
       extract(detail, access);
 125  3406
       extract(detail);
 126  3403
       build();
 127  3400
       validate();
 128  3399
    }
 129  
    
 130  
    /**
 131  
     * This method is used to extend the provided class. Extending a
 132  
     * class in this way basically means that the fields that have
 133  
     * been scanned in the specific class will be added to this. Doing
 134  
     * this improves the performance of classes within a hierarchy.
 135  
     * 
 136  
     * @param base the class to inherit scanned fields from
 137  
     */
 138  
    private void extend(Class base) throws Exception {
 139  1018
       ContactList list = support.getMethods(base);
 140  
       
 141  1018
       for(Contact contact : list) {
 142  107
          process((MethodContact)contact);
 143  
       }
 144  1018
    }
 145  
    
 146  
    /**
 147  
     * This is used to scan the declared methods within the specified
 148  
     * class. Each method will be checked to determine if it contains
 149  
     * an XML element and can be used as a <code>Contact</code> for
 150  
     * an entity within the object.
 151  
     * 
 152  
     * @param detail this is one of the super classes for the object
 153  
     */
 154  
    private void extract(Detail detail) throws Exception {
 155  3406
       List<MethodDetail> methods = detail.getMethods();
 156  
 
 157  3406
       for(MethodDetail entry: methods) {
 158  33150
          Annotation[] list = entry.getAnnotations();
 159  33150
          Method method = entry.getMethod();
 160  
          
 161  33632
          for(Annotation label : list) {
 162  485
             scan(method, label, list);             
 163  
          }
 164  33147
       }     
 165  3403
    }
 166  
    
 167  
    /**
 168  
     * This is used to scan all the methods of the class in order to
 169  
     * determine if it should have a default annotation. If the method
 170  
     * should have a default XML annotation then it is added to the
 171  
     * list of contacts to be used to form the class schema.
 172  
     * 
 173  
     * @param detail this is the detail to have its methods scanned
 174  
     * @param access this is the default access type for the class
 175  
     */
 176  
    private void extract(Detail detail, DefaultType access) throws Exception {
 177  3406
       List<MethodDetail> methods = detail.getMethods();
 178  
 
 179  3406
       if(access == PROPERTY) {
 180  18
          for(MethodDetail entry : methods) {
 181  115
             Annotation[] list = entry.getAnnotations();
 182  115
             Method method = entry.getMethod();
 183  115
             Class value = factory.getType(method);
 184  
             
 185  115
             if(value != null) {
 186  99
                process(method, list);
 187  
             }
 188  115
          }  
 189  
       }
 190  3406
    }
 191  
    
 192  
    /**
 193  
     * This reflectively checks the annotation to determine the type 
 194  
     * of annotation it represents. If it represents an XML schema
 195  
     * annotation it is used to create a <code>Contact</code> which 
 196  
     * can be used to represent the method within the source object.
 197  
     * 
 198  
     * @param method the method that the annotation comes from
 199  
     * @param label the annotation used to model the XML schema
 200  
     * @param list this is the list of annotations on the method
 201  
     */ 
 202  
    private void scan(Method method, Annotation label, Annotation[] list) throws Exception {
 203  485
       if(label instanceof Attribute) {
 204  79
          process(method, label, list);
 205  
       }
 206  485
       if(label instanceof ElementUnion) {
 207  6
          process(method, label, list);
 208  
       }
 209  485
       if(label instanceof ElementListUnion) {
 210  5
          process(method, label, list);
 211  
       }
 212  485
       if(label instanceof ElementMapUnion) {
 213  0
          process(method, label, list);
 214  
       }
 215  485
       if(label instanceof ElementList) {
 216  22
          process(method, label, list);
 217  
       }
 218  485
       if(label instanceof ElementArray) {
 219  0
          process(method, label, list);
 220  
       }
 221  485
       if(label instanceof ElementMap) {
 222  0
          process(method, label, list);
 223  
       }
 224  485
       if(label instanceof Element) {
 225  293
          process(method, label, list);
 226  
       }    
 227  483
       if(label instanceof Version) {
 228  0
          process(method, label, list);
 229  
       }
 230  483
       if(label instanceof Text) {
 231  18
          process(method, label, list);
 232  
       }
 233  483
       if(label instanceof Transient) {
 234  7
          remove(method, label, list);
 235  
       }
 236  482
    }
 237  
   
 238  
    /**
 239  
     * This is used to classify the specified method into either a get
 240  
     * or set method. If the method is neither then an exception is
 241  
     * thrown to indicate that the XML annotations can only be used
 242  
     * with methods following the Java Bean naming conventions. Once
 243  
     * the method is classified is is added to either the read or 
 244  
     * write map so that it can be paired after scanning is complete.
 245  
     * 
 246  
     * @param method this is the method that is to be classified
 247  
     * @param label this is the annotation applied to the method
 248  
     * @param list this is the list of annotations on the method
 249  
     */  
 250  
    private void process(Method method, Annotation label, Annotation[] list) throws Exception {
 251  423
       MethodPart part = factory.getInstance(method, label, list);
 252  421
       MethodType type = part.getMethodType();     
 253  
       
 254  421
       if(type == MethodType.GET) {
 255  221
          process(part, read);
 256  
       }
 257  421
       if(type == MethodType.IS) {
 258  0
          process(part, read);
 259  
       }
 260  421
       if(type == MethodType.SET) {
 261  200
          process(part, write);
 262  
       }
 263  421
    } 
 264  
    
 265  
    /**
 266  
     * This is used to classify the specified method into either a get
 267  
     * or set method. If the method is neither then an exception is
 268  
     * thrown to indicate that the XML annotations can only be used
 269  
     * with methods following the Java Bean naming conventions. Once
 270  
     * the method is classified is is added to either the read or 
 271  
     * write map so that it can be paired after scanning is complete.
 272  
     * 
 273  
     * @param method this is the method that is to be classified
 274  
     * @param list this is the list of annotations on the method
 275  
     */  
 276  
    private void process(Method method, Annotation[] list) throws Exception {
 277  99
       MethodPart part = factory.getInstance(method, list);
 278  99
       MethodType type = part.getMethodType();     
 279  
       
 280  99
       if(type == MethodType.GET) {
 281  47
          process(part, read);
 282  
       }
 283  99
       if(type == MethodType.IS) {
 284  3
          process(part, read);
 285  
       }
 286  99
       if(type == MethodType.SET) {
 287  49
          process(part, write);
 288  
       }      
 289  99
    }
 290  
    
 291  
    /**
 292  
     * This is used to determine whether the specified method can be
 293  
     * inserted into the given <code>PartMap</code>. This ensures 
 294  
     * that only the most specialized method is considered, which 
 295  
     * enables annotated methods to be overridden in subclasses.
 296  
     * 
 297  
     * @param method this is the method part that is to be inserted
 298  
     * @param map this is the part map used to contain the method
 299  
     */
 300  
    private void process(MethodPart method, PartMap map) {
 301  520
       String name = method.getName();
 302  
       
 303  520
       if(name != null) {
 304  520
          map.put(name, method);
 305  
       }
 306  520
    }
 307  
    
 308  
    /**
 309  
     * This is used to process a method from a super class. Processing
 310  
     * the inherited method involves extracting out the individual
 311  
     * parts of the method an initializing the internal state of this
 312  
     * scanner. If method is overridden it overwrites the parts.
 313  
     * 
 314  
     * @param contact this is a method inherited from a super class
 315  
     */
 316  
    private void process(MethodContact contact) {
 317  107
       MethodPart get = contact.getRead();
 318  107
       MethodPart set = contact.getWrite();
 319  
       
 320  107
       if(set != null) {
 321  94
          insert(set, write);
 322  
       }
 323  107
       insert(get, read);
 324  107
    }
 325  
    
 326  
    /**
 327  
     * This is used to insert a contact to this contact list. Here if
 328  
     * a <code>Text</code> annotation is declared on a method that
 329  
     * already has an annotation then the other annotation is given
 330  
     * the priority, this is to so text can be processes separately.
 331  
     * 
 332  
     * @param method this is the part that is to be inserted
 333  
     * @param map this is the map that the part is to be inserted in
 334  
     */
 335  
    private void insert(MethodPart method, PartMap map) {
 336  201
       String name = method.getName();
 337  201
       MethodPart existing = map.remove(name);
 338  
       
 339  201
       if(existing != null) {
 340  0
          if(isText(method)) {
 341  0
             method = existing;
 342  
          }
 343  
       }
 344  201
       map.put(name, method);
 345  201
    }
 346  
       
 347  
    /**
 348  
     * This is used to determine if the <code>Text</code> annotation
 349  
     * has been declared on the method. If this annotation is used
 350  
     * then this will return true, otherwise this returns false.
 351  
     * 
 352  
     * @param contact the contact to check for the text annotation
 353  
     * 
 354  
     * @return true if the text annotation was declared on the method
 355  
     */
 356  
    private boolean isText(MethodPart method) {
 357  0
       Annotation label = method.getAnnotation();
 358  
       
 359  0
       if(label instanceof Text) {
 360  0
          return true;
 361  
       }
 362  0
       return false;
 363  
    }
 364  
    
 365  
    /**
 366  
     * This method is used to remove a particular method from the list
 367  
     * of contacts. If the <code>Transient</code> annotation is used
 368  
     * by any method then this method must be removed from the schema.
 369  
     * In particular it is important to remove methods if there are
 370  
     * defaults applied to the class.
 371  
     * 
 372  
     * @param method this is the method that is to be removed
 373  
     * @param label this is the label associated with the method
 374  
     * @param list this is the list of annotations on the method
 375  
     */
 376  
    private void remove(Method method, Annotation label, Annotation[] list) throws Exception {
 377  7
       MethodPart part = factory.getInstance(method, label, list);
 378  6
       MethodType type = part.getMethodType();     
 379  
       
 380  6
       if(type == MethodType.GET) {
 381  3
          remove(part, read);
 382  
       }
 383  6
       if(type == MethodType.IS) {
 384  0
          remove(part, read);
 385  
       }
 386  6
       if(type == MethodType.SET) {
 387  3
          remove(part, write);
 388  
       }
 389  6
    } 
 390  
    
 391  
    /**
 392  
     * This is used to remove the method part from the specified map.
 393  
     * Removal is performed using the name of the method part. If it
 394  
     * has been scanned and added to the map then it will be removed
 395  
     * and will not form part of the class schema.
 396  
     * 
 397  
     * @param part this is the part to be removed from the map 
 398  
     * @param map this is the map to removed the method part from
 399  
     */
 400  
    private void remove(MethodPart part, PartMap map) throws Exception {
 401  6
       String name = part.getName();
 402  
       
 403  6
       if(name != null) {
 404  6
          map.remove(name);
 405  
       }
 406  6
    }
 407  
    
 408  
    /**
 409  
     * This method is used to pair the get methods with a matching set
 410  
     * method. This pairs methods using the Java Bean method name, the
 411  
     * names must match exactly, meaning that the case and value of
 412  
     * the strings must be identical. Also in order for this to succeed
 413  
     * the types for the methods and the annotation must also match.
 414  
     */
 415  
    private void build() throws Exception {
 416  3403
       for(String name : read) {
 417  326
          MethodPart part = read.get(name);
 418  
          
 419  326
          if(part != null) {
 420  326
             build(part, name);
 421  
          }
 422  323
       }
 423  3400
    }
 424  
    
 425  
    /**
 426  
     * This method is used to pair the get methods with a matching set
 427  
     * method. This pairs methods using the Java Bean method name, the
 428  
     * names must match exactly, meaning that the case and value of
 429  
     * the strings must be identical. Also in order for this to succeed
 430  
     * the types for the methods and the annotation must also match.
 431  
     * 
 432  
     * @param read this is a get method that has been extracted
 433  
     * @param name this is the Java Bean methods name to be matched
 434  
     */
 435  
    private void build(MethodPart read, String name) throws Exception {      
 436  326
       MethodPart match = write.take(name);
 437  
 
 438  326
       if(match != null) {
 439  286
          build(read, match);
 440  
       } else {
 441  40
          build(read); 
 442  
       }
 443  323
    }   
 444  
    
 445  
    /**
 446  
     * This method is used to create a read only contact. A read only
 447  
     * contact object is used when there is constructor injection used
 448  
     * by the class schema. So, read only methods can be used in a 
 449  
     * fully serializable and deserializable object.
 450  
     * 
 451  
     * @param read this is the part to add as a read only contact
 452  
     */
 453  
    private void build(MethodPart read) throws Exception {
 454  40
       add(new MethodContact(read));
 455  40
    }
 456  
    
 457  
    /**
 458  
     * This method is used to pair the get methods with a matching set
 459  
     * method. This pairs methods using the Java Bean method name, the
 460  
     * names must match exactly, meaning that the case and value of
 461  
     * the strings must be identical. Also in order for this to succeed
 462  
     * the types for the methods and the annotation must also match.
 463  
     * 
 464  
     * @param read this is a get method that has been extracted
 465  
     * @param write this is the write method to compare details with    
 466  
     */
 467  
    private void build(MethodPart read, MethodPart write) throws Exception {
 468  286
       Annotation label = read.getAnnotation();
 469  286
       String name = read.getName();
 470  
       
 471  286
       if(!write.getAnnotation().equals(label)) {
 472  2
          throw new MethodException("Annotations do not match for '%s' in %s", name, detail);
 473  
       }
 474  284
       Class type = read.getType();
 475  
       
 476  284
       if(type != write.getType()) {
 477  1
          throw new MethodException("Method types do not match for %s in %s", name, type);
 478  
       }
 479  283
       add(new MethodContact(read, write));
 480  283
    }
 481  
    
 482  
    /**
 483  
     * This is used to validate the object once all the get methods
 484  
     * have been matched with a set method. This ensures that there
 485  
     * is not a set method within the object that does not have a
 486  
     * match, therefore violating the contract of a property.
 487  
     */
 488  
    private void validate() throws Exception {
 489  3400
       for(String name : write) {
 490  1
          MethodPart part = write.get(name);
 491  
          
 492  1
          if(part != null) {
 493  1
             validate(part, name);
 494  
          }
 495  0
       }
 496  3399
    }
 497  
    
 498  
    /**
 499  
     * This is used to validate the object once all the get methods
 500  
     * have been matched with a set method. This ensures that there
 501  
     * is not a set method within the object that does not have a
 502  
     * match, therefore violating the contract of a property.
 503  
     * 
 504  
     * @param write this is a get method that has been extracted
 505  
     * @param name this is the Java Bean methods name to be matched 
 506  
     */
 507  
    private void validate(MethodPart write, String name) throws Exception {      
 508  1
       MethodPart match = read.take(name);     
 509  1
       Method method = write.getMethod();      
 510  
          
 511  1
       if(match == null) {
 512  1
          throw new MethodException("No matching get method for %s in %s", method, detail);
 513  
       }      
 514  0
    }
 515  
    
 516  
    /**
 517  
     * The <code>PartMap</code> is used to contain method parts using
 518  
     * the Java Bean method name for the part. This ensures that the
 519  
     * scanned and extracted methods can be acquired using a common 
 520  
     * name, which should be the parsed Java Bean method name.
 521  
     * 
 522  
     * @see org.simpleframework.xml.core.MethodPart
 523  
     */
 524  13624
    private static class PartMap extends LinkedHashMap<String, MethodPart> implements Iterable<String>{
 525  
       
 526  
       /**
 527  
        * This returns an iterator for the Java Bean method names for
 528  
        * the <code>MethodPart</code> objects that are stored in the
 529  
        * map. This allows names to be iterated easily in a for loop.
 530  
        * 
 531  
        * @return this returns an iterator for the method name keys
 532  
        */
 533  
       public Iterator<String> iterator() {
 534  6803
          return keySet().iterator();
 535  
       }
 536  
       
 537  
       /**
 538  
        * This is used to acquire the method part for the specified
 539  
        * method name. This will remove the method part from this map
 540  
        * so that it can be checked later to ensure what remains.
 541  
        * 
 542  
        * @param name this is the method name to get the method with       
 543  
        * 
 544  
        * @return this returns the method part for the given key
 545  
        */
 546  
       public MethodPart take(String name) {
 547  327
          return remove(name);
 548  
       }
 549  
    }
 550  
 }