Package org.freezedry.persistence

Source Code of org.freezedry.persistence.PersistenceEngine

/*
* Copyright 2012 Robert Philipp
*
*  Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
*  You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing, software
*  distributed under the License is distributed on an "AS IS" BASIS,
*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*  See the License for the specific language governing permissions and
*  limitations under the License.
*/
package org.freezedry.persistence;

import org.freezedry.persistence.annotations.Persist;
import org.freezedry.persistence.builders.*;
import org.freezedry.persistence.readers.PersistenceReader;
import org.freezedry.persistence.tree.InfoNode;
import org.freezedry.persistence.utils.Constants;
import org.freezedry.persistence.utils.ReflectionUtils;
import org.freezedry.persistence.writers.PersistenceWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.*;
import java.util.*;

/**
* Creates a semantic model of the specified {@link Object}, and creates a specified {@link Object} form
* a specified semantic model. The semantic model is a tree that holds the information about each element
* (field) in a {@link InfoNode}. The {@link InfoNode} information is a complete set of information needed
* to persist and reconstruct the specified {@link Object}.<p>
*
* The {@link PersistenceEngine} allows you to specify {@link NodeBuilder}s that are responsible for converting
* an object into the semantic model, and a semantic model back into an object. By default, a set of node builders
* are created and added to the {@link PersistenceEngine} at construction. However, through the
* {@link #addNodeBuilder(Class, NodeBuilder)} method, you can specify which node builder to use for which class.
* For example, for and instance of the {@link PersistenceEngine}, {@code engine} you can map a {@link Map} to a
* {@link MapNodeBuilder}, so that all {@link Map} objects use the {@link MapNodeBuilder} to convert between the
* semantic model and an object, by<p>
* {@code engine.addNodeBuilder( Map.class, new MapNodeBuilder() );}<p>
* Importantly, in this case, the {@link PersistenceEngine} will use the {@link MapNodeBuilder} for all objects that
* implement {@link Map}. That means that a {@link LinkedHashMap} will also use {@link MapNodeBuilder}. Suppose you
* want to have the {@link LinkedHashMap} used a different {@link NodeBuilder}, say {@code LinkedMapNodeBuilder} (which
* doesn't exist, but you could write one), then you would perform the mapping as above:<p>
* {@code engine.addNodeBuilder( LinkedHashMap.class, new LinkedMapNodeBuilder() );}<p>
* And now, {@link HashMap} would use the {@link MapNodeBuilder}, but {@link LinkedHashMap} would use the
* {@code LinkedMapNodeBuilder}.<p>
*
* To customize the behavior of any {@link NodeBuilder} you write, you may also define annotations that works in
* conjunction with your {@link NodeBuilder}. For example, the {@link org.freezedry.persistence.annotations.PersistCollection} annotation works in
* conjunction with the {@link CollectionNodeBuilder}. The field annotation argument {@code elementPersistName}
* allows you to define the name that is used to persist each element, and the {@code elementType} allows you to define
* the type ({@link Class}) of each element. Or as another example, the {@link org.freezedry.persistence.annotations.PersistDateAs} annotation allows
* you to define the format with which a date is persisted.
*
* By default, class constants (i.e. static final fields) are not persisted. Use the {@link #setPersistClassConstants(boolean)}
* to change the default behavior. Note that class constants are not set when reconstructing the class from the persisted
* form. In other words, the class constants always take their value from the class source code value.
*
* When parsing the semantic model, this class will attempt to instantiate an object of the specified class by calling the
* constructor with the smallest number of arguments. If a no-argument constructor exists, then that is what will be called.
* Constructors that have arguments will be passed null objects, and then the fields will be set reflectively through the
* {@link Field#set(Object, Object)} method. This means that if the constructor performs checks against null, you will have
* a problem. (TODO allow passing default values to the constructor so that the instantiation passes its checks).
*
* @see InfoNode
* @see NodeBuilder
* @see PersistenceWriter
* @see PersistenceReader
* @see Persist
*
* @author Robert Philipp
*/
public class PersistenceEngine {
 
  private static final Logger LOGGER = LoggerFactory.getLogger( PersistenceEngine.class );
 
  private static final Map< Class< ? >, Object > PRIMITIVE_TYPES = createPrimitives();
  private static final Set< Class< ? > > NON_ROOT_OBJECTS = nonRootObjects();
 
  private final Map< Class< ? >, NodeBuilder > nodeBuilders;
  private ArrayNodeBuilder genaralArrayNodeBuilder;
  private boolean isPersistClassConstants = false;
  private boolean isPersistNullValues = false;
 
  private final Map< Class< ? >, Object > defaultInstances;
 
  /**
   * Constructs a {@link PersistenceEngine} with the default {@link InfoNode} info node builders
   */
  public PersistenceEngine()
  {
    this.nodeBuilders = createDefaultNodeBuilders();
    this.genaralArrayNodeBuilder = new ArrayNodeBuilder( this );
    this.defaultInstances = createDefaultInstances();
  }
 
  /*
   * @return a {@link Map} containing the {@link Class}es and their associated {@link InfoNode} {@link Generator}
   */
  private Map< Class< ? >, NodeBuilder > createDefaultNodeBuilders()
  {
    // the map holding the class-to-info node builder mapping
    final Map< Class< ? >, NodeBuilder > builders = new LinkedHashMap<>();
   
    // primitive node info node builders (these are intended to create leaf nodes)
    builders.put( Integer.TYPE, new IntegerNodeBuilder( this ) );
    builders.put( Double.TYPE, new DoubleNodeBuilder( this ) );
    builders.put( Float.TYPE, new FloatNodeBuilder( this ) );
    builders.put( Long.TYPE, new LongNodeBuilder( this ) );
    builders.put( Short.TYPE, new ShortNodeBuilder( this ) );
    builders.put( Boolean.TYPE, new BooleanNodeBuilder( this ) );
    builders.put( Character.TYPE, new CharacterNodeBuilder( this ) );
    builders.put( Byte.TYPE, new ByteNodeBuilder( this ) );
   
    builders.put( Integer.class, new IntegerNodeBuilder( this ) );
    builders.put( Double.class, new DoubleNodeBuilder( this ) );
    builders.put( Float.class, new FloatNodeBuilder( this ) );
    builders.put( Long.class, new LongNodeBuilder( this ) );
    builders.put( Short.class, new ShortNodeBuilder( this ) );
    builders.put( Boolean.class, new BooleanNodeBuilder( this ) );
    builders.put( Character.class, new CharacterNodeBuilder( this ) );
    builders.put( Byte.class, new ByteNodeBuilder( this ) );
    builders.put( String.class, new StringNodeBuilder( this ) );
   
    // other leaf nodes
    builders.put( Calendar.class, new DateNodeBuilder( this ) );
   
    // collection info node builder (specific info node builder)
    builders.put( Collection.class, new CollectionNodeBuilder( this ) );
   
    // map info node builder  (specific info node builder)
    builders.put( Map.class, new MapNodeBuilder( this ) );
   
    //
    return builders;
  }
 
  /**
   * @return a map containing a primitive type and the wrapper object used to represent that primitive type
   */
  private static Map< Class< ? >, Object > createPrimitives()
  {
    final Map< Class< ? >, Object > primitives = new HashMap<>();
    primitives.put( Integer.TYPE, 0 );
    primitives.put( Long.TYPE, (long) 0 );
    primitives.put( Short.TYPE, Short.MAX_VALUE );
    primitives.put( Double.TYPE, 0.0 );
    primitives.put( Float.TYPE, new Float( 0.0 ) );
    primitives.put( Boolean.TYPE, true );
    primitives.put( Byte.TYPE, Byte.MAX_VALUE );
    primitives.put( Character.TYPE, '0' );
    return primitives;
  }
 
  /**
   * @return a {@link Set} containing the primitive type wrapper objects and {@link String}. These
   * are the objects that can't be root objects
   */
  private static Set< Class< ? > > nonRootObjects()
  {
    return new HashSet<>();
  }
 
  /**
   * @return a map containing a primitive type and the wrapper object used to represent that primitive type
   */
  private static Map< Class< ? >, Object > createDefaultInstances()
  {
    final Map< Class< ? >, Object > defaults = new HashMap<>();
    defaults.putAll( createPrimitives() );
    defaults.put( Integer.class, 0 );
    defaults.put( Long.class, (long) 0 );
    defaults.put( Short.class, Short.MAX_VALUE );
    defaults.put( Double.class, 0.0 );
    defaults.put( Float.class, new Float( 0.0 ) );
    defaults.put( Boolean.class, true );
    defaults.put( Byte.class, Byte.MAX_VALUE );
    defaults.put( Character.class, '0' );
    return defaults;
  }
 
  /**
   * When set to {@link #isPersistClassConstants} is set true then class constants (i.e. static final fields)
   * are persisted. Otherwise, no node is created for class constants
   * @param isPersistClassConstants True to persist class constants; false otherwise
   */
  public void setPersistClassConstants( final boolean isPersistClassConstants )
  {
    this.isPersistClassConstants = isPersistClassConstants;
  }

  /**
   * When set to {@code true} null values will be persisted. The default behaviour is not to persist null values.
   * @param isPersistNullValues whether or not to persist null values.
   */
  public void setPersistNullValues( final boolean isPersistNullValues )
  {
    this.isPersistNullValues = isPersistNullValues;
  }

  /**
   * Tells the persistence engine to persist null values
   * @return This {@link org.freezedry.persistence.PersistenceEngine}
   */
  public PersistenceEngine withPersistNullValues()
  {
    this.isPersistNullValues = true;
    return this;
  }

  /**
   * Tells the persistence engine to persist class constants
   * @return This {@link org.freezedry.persistence.PersistenceEngine}
   */
  public PersistenceEngine withPersistClassConstants()
  {
    this.isPersistClassConstants = true;
    return this;
  }

  /**
   * Adds a {@link NodeBuilder} to be used for generating {@link InfoNode}s for the specified {@link Class}
   * @param clazz The {@link Class} of the object to persist and, therefore, for which to generate a node
   * @param builder The {@link NodeBuilder} used to generate {@link InfoNode}s for the specified {@link Class}
   * @return The {@link NodeBuilder} that used to be associated with the specified {@link Class}; or null if
   * no {@link NodeBuilder} was previously associated with the specified {@link Class}
   */
  public NodeBuilder addNodeBuilder( final Class< ? > clazz, final NodeBuilder builder )
  {
    return nodeBuilders.put( clazz, builder );
  }
 
  /**
   * Finds the {@link NodeBuilder} associated with the class. If the specified class
   * doesn't have a info node builder, then it searches for the closest parent class (inheritance)
   * and returns true. In this case, it adds an entry to the info node builders map for the
   * specified class associating it with the returned info node builder (performance speed-up for
   * subsequent calls).
   * @param clazz The class for which to find a info node builder
   * @return the true if a info node builder was found; false otherwise
   */
  public boolean containsNodeBuilder( final Class< ? > clazz )
  {
    return ( getNodeBuilder( clazz ) != null );
  }
 
  /**
   * Returns true if the specified field name of the specified {@link Class} has a {@link NodeBuilder} annotation;
   * false otherwise.
   * @param clazz The specified {@link Class}
   * @param fieldName The field name to check for a {@link NodeBuilder} annotation.
   * @return true if the specified field name of the specified {@link Class} has a {@link NodeBuilder} annotation;
   * false otherwise.
   */
  public boolean containsAnnotatedNodeBuilder( final Class< ? > clazz, final String fieldName )
  {
    boolean contains = false;
    try
    {
      if( clazz != null )
      {
        contains = ReflectionUtils.hasNodeBuilderAnnotation( clazz, fieldName );
      }
    }
    catch( IllegalArgumentException e ) { /* swallow the exception */ }
   
    return contains;
  }

  /**
   * Finds the {@link NodeBuilder} associated with the class. If the specified class
   * doesn't have a info node builder, then it searches for the closest parent class (inheritance)
   * and returns that. In this case, it adds an entry to the info node builders map for the
   * specified class associating it with the returned info node builder (performance speed-up for
   * subsequent calls).
   * @param clazz The class for which to find a info node builder
   * @return the {@link NodeBuilder} associated with the class
   */
  public NodeBuilder getNodeBuilder( final Class< ? > clazz )
  {
    return ReflectionUtils.getItemOrAncestorCopyable( clazz, nodeBuilders );
  }
 
  /**
   * Returns true if the specified {@link Class} is in the {@link #NON_ROOT_OBJECTS} {@link Set}
   * @param clazz The {@link Class} to find in the {@link #NON_ROOT_OBJECTS} {@link Set}
   * @return true if the {@link Class} is a non-root object (i.e. an object that shouldn't be a
   * root element in the semantic model.
   */
  public boolean isForbiddenRootObject( final Class< ? > clazz )
  {
    boolean isNonRootObject = false;
    if( NON_ROOT_OBJECTS.contains( clazz ) )
    {
      isNonRootObject = true;
    }
    else
    {
      for( Class< ? > element : NON_ROOT_OBJECTS )
      {
        if( ReflectionUtils.calculateClassDistance( clazz, element ) > -1 )
        {
          isNonRootObject = true;
         
          // add the value for faster look-up next time
          NON_ROOT_OBJECTS.add( clazz );
         
          break;
        }
      }
    }
    return isNonRootObject;
  }
 
  /**
   * Returns true if the specified {@link Class} is an allowed root object (i.e. the root node in
   * the semantic model); false otherwise
   * @param clazz The {@link Class} to test
   * @return true if the specified {@link Class} is an allowed root object (i.e. the root node in
   * the semantic model); false otherwise
   */
  public boolean isAllowedRootObject( final Class< ? > clazz )
  {
    return !isForbiddenRootObject( clazz );
  }
 
  /**
   * Returns a default instance of the type specified by the class
   * @param clazz The class for which to return an instance based on the default instances defined in the
   *              default instances map
   * @return The default instance
   */
  private Object getDefaultInstance( final Class< ? > clazz )
  {
    return ReflectionUtils.getItemOrAncestor( clazz, defaultInstances );
  }

  /**
   * Removes the {@link NodeBuilder} associated with the specified {@link InfoNode}
   * @param clazz The {@link Class} of the object to persist for which the associated {@link NodeBuilder}
   * will be removed.
   * @return The removed {@link NodeBuilder} associated with the specified {@link Class}; if the specified
   * {@link Class} was not found in the {@link Map}, then returns null.
   */
  public NodeBuilder removeNodeBuilder( final Class< ? > clazz )
  {
    return nodeBuilders.remove( clazz );
  }
 
  /**
   * Sets the default {@link NodeBuilder} used for arrays ({@code String[]}, {@code int[]}, {@code Object[]}, etc)
   * for objects for which a specific {@link NodeBuilder} hasn't been specified.
   * You can add a specific array {@link NodeBuilder} to using {@link #addNodeBuilder(Class, NodeBuilder)} method.
   * For example:<p>
   * {@code builder.addNodeBuilder( String[].class, new ArrayNodeBuilder() )}<p>
   * maps all {@code String[]} objects to use the {@link ArrayNodeBuilder}.
   * @param builder The default {@link NodeBuilder} to use for arrays for which a specific {@link NodeBuilder}
   * hasn't been specified.
   */
  public void setGeneralArrayNodeBuilder( final ArrayNodeBuilder builder )
  {
    this.genaralArrayNodeBuilder = builder;
  }

  /**
   * Searches through the existing node builders to see if they are of the specified {@link Class}.
   * If it doesn't find one, then it instantiates a new {@link NodeBuilder} and sets its {@link PersistenceEngine}
   * to this object.
   * @param clazz The {@link NodeBuilder} {@link Class} to be used for instantiating the object
   * @param fieldName The name of the field for which to get the node builder
   * @return a {@link NodeBuilder} object associated with the specified node builder {@link Class}
   */
  private NodeBuilder getAnnotatedNodeBuilder( final Class< ? > clazz, final String fieldName )
  {
    // grab the node builder class from the field annotation
    final Class< ? > nodeBuilderClass = ReflectionUtils.getNodeBuilderClass( clazz, fieldName );

    // first we check to see if the node builder has already been constructed, in which case
    // we use it.
    NodeBuilder nodeBuilder = null;
    for( Map.Entry< Class< ? >, NodeBuilder > entry : nodeBuilders.entrySet() )
    {
      if( entry.getValue().getClass().equals( nodeBuilderClass ) )
      {
        nodeBuilder = entry.getValue();
        break;
      }
    }
   
    // if the node builder is still null at this point, then we need to instantiate one.
    if( nodeBuilder == null )
    {
      try
      {
        nodeBuilder = (NodeBuilder)nodeBuilderClass.newInstance();
        nodeBuilder.setPersistenceEngine( this );
      }
      catch( InstantiationException | IllegalAccessException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Unable to instantiate the NodeBuilder class." ).append( Constants.NEW_LINE );
        message.append( "  NodeBuilder Class: " ).append( nodeBuilderClass.getName() );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    return nodeBuilder;
  }
 
  /**
   * Creates a semantic model of the specified {@link Object}. The semantic model is a tree
   * that holds the information about each element (field) in a {@link InfoNode}. The {@link InfoNode}
   * information is a complete set of information needed to persist and reconstruct the
   * specified {@link Object}.
   * @param object The object which to convert into a semantic model
   * @return The root {@link InfoNode} of the tree representing the semantic model
   * @see InfoNode
   */
  public final InfoNode createSemanticModel( final Object object )
  {
    // create the root node of the tree, which holds the information about the
    // object we are being asked to persist.
    final Class< ? > clazz = object.getClass();
   
    InfoNode rootNode;
   
    // if the object is an array of one or more dimensions, then we need to replace the
    // "[]" which the array suffix (by default = "Array"). Then we use the array node
    // builder to create the semantic model.
    if( clazz.isArray() )
    {
      final String persistName = clazz.getSimpleName().replaceAll( "\\[\\]", genaralArrayNodeBuilder.getCompoundArrayNameSuffix() );
      try
      {
        rootNode = genaralArrayNodeBuilder.createInfoNode( object, persistName );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Error building the root node" ).append( Constants.NEW_LINE );
        message.append( "  Class: " ).append( clazz.getName() );
        LOGGER.error( message.toString(), e );
        throw new IllegalArgumentException( message.toString(), e );
      }
    }
    // if the override node builder map contains a node builder for this specific class, and
    // the class is allowed to be a root object, then we'll use it
    else if( containsNodeBuilder( clazz ) && isAllowedRootObject( clazz ) )
    {
      try
      {
        rootNode = getNodeBuilder( clazz ).createInfoNode( object, clazz.getName() );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Error building the root node" ).append( Constants.NEW_LINE );
        message.append( "  Class: " ).append( clazz.getName() );
        LOGGER.error( message.toString(), e );
        throw new IllegalArgumentException( message.toString(), e );
      }
    }
    else
    {
      rootNode = InfoNode.createRootNode( clazz.getSimpleName(), clazz );
     
      // run through the methods building up the semantic model, which is a tree
      // that holds the information about each element in a node. the node information
      // is a complete set of information needed to persist and reconstruct an object
      addNodes( rootNode, object );
    }
   
    // return the root node of the tree
    return rootNode;
  }
 
  /*
   * Recurses its way down through the objects creating the semantic model. Uses reflection
   * and the persistence annotations to construct the model.
   * @param currentNode The current {@link InfoNode} in the semantic tree
   * @param object The object to convert into a node
   * @return The added {@link InfoNode}, which may have child {@link InfoNode}s
   */
  private InfoNode addNodes( final InfoNode currentNode, final Object object )
  {
    // grab the object's type
    final Class< ? > clazz = object.getClass();
   
    // run through the fields associated with object's Class< ? >, create the nodes for
    // each field, and add that node to the current node. recall that this is a recursive
    // algorithm where createNode(...) may call this method recursively.
    final List< Field > fields = ReflectionUtils.getAllDeclaredFields( clazz );
    for( final Field field : fields )
    {
      // if the field has a @Persist annotation, and the ignore is true, then
      // simply ignore the field
      final Persist persistAnnotation = field.getAnnotation( Persist.class );
      if( persistAnnotation != null && persistAnnotation.ignore() )
      {
        continue;
      }
     
      // if the field is a class constant (i.e. static final) then don't add the node
      final int modifiers = field.getModifiers();
      if( Modifier.isFinal( modifiers ) && Modifier.isStatic( modifiers ) && !isPersistClassConstants )
      {
        continue;
      }
     
      try
      {
        // make sure that we can access the field
        field.setAccessible( true );
       
        // create and add the node representing this object to the current node, unless the
        // node has a null value.
        final Object fieldObject = field.get( object );
        if( fieldObject != null || isPersistNullValues )
        {
          currentNode.addChild( createNode( clazz, fieldObject, field.getName() ) );
        }
      }
      catch( IllegalAccessException e )
      {
        LOGGER.error( "Attempted to access a field that doesn't exist." + Constants.NEW_LINE + "  Object " + clazz.getName() + Constants.NEW_LINE + "  Field Name: " + field.getName() + Constants.NEW_LINE, e );
      }
    }
    return currentNode;
  }
 
  /**
   * Creates a {@link InfoNode} for the specified {@link Object}, recursively, calling either {@link #addNodes(InfoNode, Object)}
   * or itself to construct {@link InfoNode}s and add them to the tree.
   * @param object The object for which to create the node
   * @param fieldName The name of the field for which to create the node
   * @return the top-level {@link InfoNode} for the object
   */
  public final InfoNode createNode( final Class< ? > containingClass, final Object object, final String fieldName )
  {
    InfoNode node;
    if( object == null )
    {
      return InfoNode.createLeafNode( fieldName, null, fieldName, Object.class );
    }

    // create the InfoNode object (we first have to determine the node type, down the road, we'll check the
    // factories for registered node info node builders for the Class< ? > of the object)
    final Class< ? > clazz = object.getClass();
   
    // construct the node. There are several cases to consider:
    // 0. the field is annotated with a specified node builder in mind
    // 1. the object is intended to be a leaf node: create a leaf InfoNode object
    //    with the appropriately populated values, and we're done.
    // 2. the object is of a special type: use the appropriate node factory
    //    to construct the appropriate node
    // 3. neither of the first two conditions are met: simply call the addNodes(...)
    //    method recursively to construct the nodes.
    // 4. the object is an array, and the array class hasn't been specified in the node
    //    builder map, then we must treat it as a generic array.
    // the first two cases are handled by the info node builders in the info node builders map. even
    // the leaf node info node builders may need to be overridden. the third case is handled
    // by the compound node
    if( containsAnnotatedNodeBuilder( containingClass, fieldName ) )
    {
      final NodeBuilder builder = getAnnotatedNodeBuilder( containingClass, fieldName );
      try
      {
        node = builder.createInfoNode( containingClass, object, fieldName );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Node Builder failed to create InfoNode:" ).append( Constants.NEW_LINE );
        message.append( "  Builder: " ).append( builder.getClass().getName() ).append( Constants.NEW_LINE );
        message.append( "  Containing Class Name: " ).append( containingClass.getName() ).append( Constants.NEW_LINE );
        message.append( "  Object: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
        message.append( "  Field Name: " ).append( fieldName ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    else if( containsNodeBuilder( clazz ) )
    {
      final NodeBuilder builder = getNodeBuilder( clazz );
      try
      {
        // if the containing class is null, then this is a root object and we
        // must use the root object version of the createInfoNode method
        if( containingClass == null )
        {
          node = builder.createInfoNode( object, fieldName );
        }
        else
        {
          node = builder.createInfoNode( containingClass, object, fieldName );
        }
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Node Builder failed to create InfoNode:" ).append( Constants.NEW_LINE );
        message.append( "  Builder: " ).append( builder.getClass().getName() ).append( Constants.NEW_LINE );
        message.append( "  Containing Class Name: " ).append( containingClass == null ? "[null]" : containingClass.getName() ).append( Constants.NEW_LINE );
        message.append( "  Object: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
        message.append( "  Field Name: " ).append( fieldName ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    else if( clazz.isArray() )
    {
      try
      {
        node = genaralArrayNodeBuilder.createInfoNode( containingClass, object, fieldName );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Default Array Node Builder failed to create InfoNode:" ).append( Constants.NEW_LINE );
        message.append( "  Builder: " ).append( genaralArrayNodeBuilder.getClass().getName() ).append( Constants.NEW_LINE );
        message.append( "  Containing Class Name: " ).append( containingClass.getName() ).append( Constants.NEW_LINE );
        message.append( "  Object: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
        message.append( "  Field Name: " ).append( fieldName ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    else
    {
      // create a new compound node to holds this, since it isn't a leaf node, and
      // call (recursively) the addNodes(...) method to add the nodes representing the
      // fields of this object to the newly created compound node.
      final InfoNode compoundNode = InfoNode.createCompoundNode( fieldName, fieldName, clazz );
      node = addNodes( compoundNode, object );
    }
   
    // then call addNodes(...) with the newly created node
    return node;
  }
 
  /**
   * Parses the semantic model (a.k.a. content tree) represented by the {@link InfoNode} tree hanging
   * off the specified root {@link InfoNode}. The {@link Class} of the returned object is the more specific
   * of the specified {@link Class} and the {@link Class} that may be held in the root {@link InfoNode}.
   * The {@link InfoNode}s don't require all the information in each node to be complete. At a minimum
   * it needs the field name (or the persist name if that is the same as the field name) and for leaf nodes,
   * the value of the node. The {@link PersistenceEngine} will use reflection to fill in missing information.
   * However there are limits to what can be obtained by reflection. For example, if the field is a {@link List},
   * then the actual concrete {@link Class} type must be available somehow.
   * @param clazz The specified {@link Class} of the object to create
   * @param rootNode The root {@link InfoNode} representing the semantic model
   * @return the object represented by the semantic model
   */
  public Object parseSemanticModel( final Class< ? > clazz, final InfoNode rootNode )
  {
    Object object;
   
    if( clazz.isArray() )
    {
      try
      {
        object = genaralArrayNodeBuilder.createObject( null, clazz, rootNode );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Error creating object" ).append( Constants.NEW_LINE );
        message.append( "  Class: " ).append( clazz.getName() );
        LOGGER.error( message.toString(), e );
        throw new IllegalArgumentException( message.toString(), e );
      }
    }
    // if the override node builders contains a node builder for this specific class, then we'll use it
    else if( containsNodeBuilder( clazz ) && isAllowedRootObject( clazz ) )
    {
      try
      {
        object = getNodeBuilder( clazz ).createObject( clazz, rootNode );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Error creating object" ).append( Constants.NEW_LINE );
        message.append( "  Class: " ).append( clazz.getName() );
        LOGGER.error( message.toString(), e );
        throw new IllegalArgumentException( message.toString(), e );
      }
    }
    else
    {
      // instantiate the object and build it recursively
      object = instantiate( clazz, rootNode );
      buildObject( object, rootNode );
    }
   
    return object;
  }
 
  /**
   * Instantiates an object of the specified class by calling the constructor with the smallest
   * number of arguments. If a no-arg constructor exists, then that is what will be called. Constructors
   * that have arguments will be passed null objects, and then the fields will be set reflectively
   * through the {@link Field#set(Object, Object)} method. This means that if the constructor performs
   * checks against null, you will have a problem.
   * @param clazz The {@link Class} to instantiate
   * @param rootNode The root {@link InfoNode} of the semantic tree containing the information about the
   * class to instantiate
   * @return The instantiated object.
   */
  private Object instantiate( final Class< ? > clazz, final InfoNode rootNode )
  {
    // grab the Class for the root node based on the specified class and the root node
    final Class< ? > rootClass = ReflectionUtils.getMostSpecificClass( clazz, rootNode );
   
    // instantiate the object represented by the root node
    Object object;
    try
    {
      // find the constructor, and if it exists, then use it to create a new instance
      // grab the constructors for the class. find the constructor with the smallest
      // number of arguments, and create dummy arguments, and call the newInstance method
      // on that constructor (the fields will get set with the appropriate values)
      final Constructor< ? >[] constructors = rootClass.getConstructors();
      Object instance;
      if( ( instance = getDefaultInstance( clazz ) ) != null )
      {
        object = instance;
      }
      else if( constructors.length > 0 )
      {
        // set the minimum number of constructor params to the largest possible
        int minNumParams = Integer.MAX_VALUE;
       
        // create the constructor reference and the list of arguments associated
        // with the constructor that has the smallest number of parameters
        Constructor< ? > minParamConstructor = null;
        Class< ? >[] minParamTypes = null;
       
        // search for the constructor with the smallest number of parameters and store
        // it and the parameter types
        for( Constructor< ? > constructor : constructors )
        {
          final Class< ? >[] paramTypes = constructor.getParameterTypes();
          if( paramTypes.length < minNumParams )
          {
            minNumParams = paramTypes.length;
            minParamConstructor = constructor;
            minParamTypes = paramTypes;
          }
        }

        // if no constructor was found, then throw an exception
        if( minParamConstructor == null )
        {
          final StringBuilder message = new StringBuilder();
          message.append( "Could not find an appropriate constructor to instantiate the Class." ).append( Constants.NEW_LINE )
              .append( "  Specified Class Name: " ).append( clazz.getName() ).append( Constants.NEW_LINE )
              .append( "  Number of Constructors Searched: " ).append( constructors.length );
          LOGGER.error( message.toString() );
          throw new IllegalArgumentException( message.toString() );
        }

        // create the parameter array (use default values that will get overriden
        // during the object creation anyway)
        final Object[] params = createConstructorParameters( minParamTypes );
       
        // create the new instance using the constructor and its default parameters
        object = minParamConstructor.newInstance( params );
      }
      else if( rootClass.isArray() )
      {
        final Class< ? > componentType = rootClass.getComponentType();
        object = Array.newInstance( componentType, rootNode.getChildCount() );
      }
      else
      {
        object = rootClass.newInstance();
      }
    }
    catch( IllegalAccessException e )
    {
      final StringBuilder message = new StringBuilder();
      message.append( "Failed to instantiate object from Class. Either the Class or the" ).append( Constants.NEW_LINE );
      message.append( "nullary constructor were not available." ).append( Constants.NEW_LINE );
      message.append( "  Specified Class Name: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
      message.append( "  Most Specific Class Name: " ).append( rootClass.getName() ).append( Constants.NEW_LINE );
      message.append( "  InfoNode: " ).append( rootNode.getClass().getName() ).append( Constants.NEW_LINE );
      LOGGER.error( message.toString(), e );
      throw new IllegalStateException( message.toString(), e );
    }
    catch( InstantiationException | InvocationTargetException e )
    {
      final StringBuilder message = new StringBuilder();
      message.append( "Failed to instantiate object from Class. This Class represents an " ).append( Constants.NEW_LINE );
      message.append( "abstract class, an interface, an array class, a primitive type, or " ).append( Constants.NEW_LINE );
      message.append( "void; or the class has no nullary constructor; or the instantiation " ).append( Constants.NEW_LINE );
      message.append( "failed for some other reason." ).append( Constants.NEW_LINE );
      message.append( "  Specified Class Name: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
      message.append( "  Most Specific Class Name: " ).append( rootClass.getName() ).append( Constants.NEW_LINE );
      message.append( "  InfoNode: " ).append( rootNode.getClass().getName() ).append( Constants.NEW_LINE );
      LOGGER.error( message.toString(), e );
      throw new IllegalStateException( message.toString(), e );
    }
   
    return object;
  }
 
  /*
   * Creates an array of default parameters for the constructor. If the arguments
   * are primitives, the creates the object of the correct type, otherwise, simply
   * type casts the null.
   * @param paramTypes The {@link Class} for constructor parameters 
   * @return an {@link Object} array containing the default parameters for the constructor
   */
  private Object[] createConstructorParameters( final Class< ? >[] paramTypes )
  {
    final Object[] params = new Object[ paramTypes.length ];
    for( int i = 0; i < paramTypes.length; ++i )
    {
      final Class< ? > type = paramTypes[ i ];
      if( type.isPrimitive() )
      {
        params[ i ] = PRIMITIVE_TYPES.get( type );
      }
      else
      {
        params[ i ] = type.cast( null );
      }
    }
   
    return params;
  }
 
  /*
   * Builds the specified {@link Object} by setting the fields it contains, using the information in
   * the specified {@link InfoNode}. The fields in the specified {@link Object} are objects that must be
   * instantiated before being set. This method is part of the recursive algorithm that walks down the
   * semantic model (a.k.a. content tree) and builds the object.
   * @param object The containing object whose fields to build into objects
   * @param currentNode The current {@link InfoNode} in the semantic model.
   * @return The specified {@link Object} with all its fields set.
   */
  private Object buildObject( final Object object, final InfoNode currentNode )
  {
    // 1. create the object for the specified clazz
    // 2. create the objects for the fields recursively
    final Class< ? > clazz = object.getClass();
   
    for( InfoNode node : currentNode.getChildren() )
    {
      // find the name of the field from the info node
      final String persistName = node.getPersistName();
      String fieldName = node.getFieldName();
      if( fieldName == null || fieldName.isEmpty() )
      {
        final Field field = ReflectionUtils.getFieldForPersistenceName( clazz, persistName );
        if( field != null )
        {
          fieldName = field.getName();
        }
        else
        {
          fieldName = persistName;
        }
      }

      // grab the class' field
      try
      {
        final Field field = ReflectionUtils.getDeclaredField( clazz, fieldName );
       
        // grab the generic parameter type of the field and add it to the info node
        final Type type = field.getGenericType();
        if( type instanceof ParameterizedType )
        {
          final List< Type > types = Arrays.asList( ((ParameterizedType)type).getActualTypeArguments() );
          node.setGenericParameterTypes( types );
        }
       
        // see if the field has a @Persist( instantiateAs = XXXX.class ) annotation
        final Persist annotation = field.getAnnotation( Persist.class );
        if( annotation != null )
        {
          // if no class information is stored in the node, or if the class stored in the
          // node is a super class of the instantiate type, then use the instantiate type
          final Class< ? > instantiateType = annotation.instantiateAs();
          if( !instantiateType.equals( Persist.Null.class ) )
          {
            final Class< ? > nodeClazz = node.getClazz();
            if( nodeClazz == null || ReflectionUtils.isClassOrSuperclass( nodeClazz, instantiateType ) )
            {
              node.setClazz( instantiateType );
            }
          }
        }
 
        // create the object
        final Class< ? > newClass = ReflectionUtils.getMostSpecificClass( field.getType(), node );
        final Class< ? > containingClass = field.getDeclaringClass();
        final Object newObject = createObject( containingClass, newClass, node );
 
        // set the field to be accessible (override the accessor)
        field.setAccessible( true );
 
        // if the field has a "static final" modifier, then we don't set the field
        // because it is a class constant
        final int modifiers = field.getModifiers();
        try
        {
          if( Modifier.isStatic( modifiers ) && Modifier.isFinal( modifiers ) )
          {
            if( LOGGER.isInfoEnabled() )
            {
              final String message = "Ignoring field because it has \"static final\" modifiers:" + Constants.NEW_LINE +
                  "  Containing Class: " + object.getClass().getName() + Constants.NEW_LINE +
                  "  Field Name: " + fieldName + Constants.NEW_LINE +
                  "  Field Modifiers: " + Modifier.toString( modifiers ) + Constants.NEW_LINE;
              LOGGER.info( message );
            }
          }
          else
          {
            // set the fields value
            field.set( object, newObject );
          }
        }
        catch( IllegalAccessException e )
        {
          final StringBuilder message = new StringBuilder();
          message.append( "Attempted to perform an invalid operation on field:" ).append( Constants.NEW_LINE );
          message.append( "  Field Name: " ).append( fieldName ).append( Constants.NEW_LINE );
          message.append( "  Field Modifiers: " ).append( Modifier.toString( field.getModifiers() ) ).append( Constants.NEW_LINE );
          message.append( "  Containing Class: " ).append( object.getClass().getName() ).append( Constants.NEW_LINE );
          LOGGER.error( message.toString(), e );
          throw new IllegalStateException( message.toString(), e );
        }

      }
      catch( NoSuchFieldException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Attempted to retrieve field for an invalid field name:" ).append( Constants.NEW_LINE );
        message.append( "  Field Name: " ).append( fieldName ).append( Constants.NEW_LINE );
        message.append( "  Containing Class: " ).append( object.getClass().getName() ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
   
    return object;
  }
 
  /**
   * Creates and returns the object represented by the specified {@link InfoNode} and {@link Class}. The method
   * chooses the most specific type (i.e. farthest down on the inheritance hierarchy) found in the {@link InfoNode}
   * or the {@link Class}. The specified {@link InfoNode} doesn't need to hold the type information, if such
   * information is not available.
   * @param clazz The {@link Class} type of the object to create (unless the one found in the {@link InfoNode} is
   * more specific).
   * @param currentNode The {@link InfoNode} containing information about the object to be created
   * @return The newly minted object
   */
  public Object createObject( final Class< ? > containingClass, final Class< ? > clazz, final InfoNode currentNode )
  {
    // grab the field name from the node
    String fieldName = currentNode.getFieldName();
    if( fieldName == null )
    {
      final String persistName = currentNode.getPersistName();
      if( containingClass != null )
      {
        fieldName = ReflectionUtils.getFieldNameForPersistenceName( containingClass, persistName );
      }
      if( fieldName == null )
      {
        fieldName = persistName;
      }
    }
   
    // find the node builder need to create the object. if there is
    // no node builder, then instantiate the object and make a recursive call
    // to the buildObject(...) method.
    Object object;
    if( containsAnnotatedNodeBuilder( containingClass, fieldName ) )
    {
      final NodeBuilder builder = getAnnotatedNodeBuilder( containingClass, fieldName );
      try
      {
        object = builder.createObject( containingClass, clazz, currentNode );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Node Builder failed to create object from Class and InfoNode:" ).append( Constants.NEW_LINE );
        message.append( "  Class Name: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
        message.append( "  Builder: " ).append( builder.getClass().getName() ).append( Constants.NEW_LINE );
        message.append( "  InfoNode: " ).append( currentNode.toString() ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString(), e );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    else if( containsNodeBuilder( clazz ) )
    {
      final NodeBuilder builder = getNodeBuilder( clazz );
      try
      {
        // if the containing class is null, then this is a root node, and so we must use
        // the root node version of the create object class
        if( containingClass == null )
        {
          object = builder.createObject( clazz, currentNode );
        }
        else
        {
          object = builder.createObject( containingClass, clazz, currentNode );
        }
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Node Builder failed to create object from Class and InfoNode:" ).append( Constants.NEW_LINE );
        message.append( "  Class Name: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
        message.append( "  Builder: " ).append( builder.getClass().getName() ).append( Constants.NEW_LINE );
        message.append( "  InfoNode: " ).append( currentNode.toString() ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    else if( clazz.isArray() )
    {
      try
      {
        object = genaralArrayNodeBuilder.createObject( containingClass, clazz, currentNode );
      }
      catch( ReflectiveOperationException e )
      {
        final StringBuilder message = new StringBuilder();
        message.append( "Default Array Node Builder failed to create object from Class and InfoNode:" ).append( Constants.NEW_LINE );
        message.append( "  Class Name: " ).append( clazz.getName() ).append( Constants.NEW_LINE );
        message.append( "  Builder: " ).append( genaralArrayNodeBuilder.getClass().getName() ).append( Constants.NEW_LINE );
        message.append( "  InfoNode: " ).append( currentNode.toString() ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString(), e );
      }
    }
    else
    {
      // create the new object and then build it recursively
      final Object newObject = instantiate( clazz, currentNode );
      object = buildObject( newObject, currentNode );
    }
    return object;
  }
}
TOP

Related Classes of org.freezedry.persistence.PersistenceEngine

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.