Package org.freezedry.persistence.keyvalue.renderers

Source Code of org.freezedry.persistence.keyvalue.renderers.CollectionRenderer

/*
* 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.keyvalue.renderers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.freezedry.persistence.containers.Pair;
import org.freezedry.persistence.keyvalue.KeyValueBuilder;
import org.freezedry.persistence.keyvalue.renderers.decorators.Decorator;
import org.freezedry.persistence.keyvalue.renderers.decorators.StringDecorator;
import org.freezedry.persistence.keyvalue.utils.KeyValueUtils;
import org.freezedry.persistence.tree.InfoNode;
import org.freezedry.persistence.utils.Constants;
import org.freezedry.persistence.utils.ReflectionUtils;

/**
* Renders the subtree of the semantic model that represents a {@link Collection}. {@link Collection}s are rendered
* as the collection's persistence name followed by the decorated index. For example, a {@code List< String >}
* would be persisted in the following format when using the default decorator and settings:
* <code><pre>
* Division.carNames[0] = "civic"
* Division.carNames[1] = "tsx"
* Division.carNames[2] = "accord"
* </pre></code>
*
* @author rob
*/
public class CollectionRenderer extends AbstractPersistenceRenderer {
 
  private static final Logger LOGGER = LoggerFactory.getLogger( CollectionRenderer.class );

  protected static final String OPEN = "[";
  protected static final String CLOSE = "]";
 
  private final String openIndex;
  private final String closeIndex;
  private final StringDecorator indexDecorator;
  private final String decorationRegex;
  private final Pattern decorationPattern;
  private final String validationRegex;
  private final Pattern validationPattern;
 
  private final Set< Class< ? > > withholdPeristName;
  private boolean withholdArrayPersistName = true;

  /**
   * Constructs a {@link CollectionRenderer} that is used to render {@link InfoNode} representing
   * {@link Collection}s into key value pairs.
   * @param builder The {@link KeyValueBuilder} used to flatten the semantic model. The builder calls
   * this class' {@link #buildKeyValuePair(InfoNode, String, List, boolean)} as part of the recursive
   * algorithm to flatten the semantic model
   * @param openIndex The {@link Decorator} for the opening symbol for the index.
   * @param closeIndex The {@link Decorator} for the closing symbol for the index.
   */
  public CollectionRenderer( final KeyValueBuilder builder, final String openIndex, final String closeIndex )
  {
    super( builder );
   
    this.openIndex = openIndex;
    this.closeIndex = closeIndex;
    this.indexDecorator = new StringDecorator( openIndex, closeIndex );
   
    // create the regular expression that determines if a string is renderered by this class
    // people[0] is a collection, but people{"test"}[0] is a map< string, list< integer > >
    // so we want to check that only word characters precede the "["
    final String open = Pattern.quote( openIndex );
    final String close = Pattern.quote( closeIndex );
   
    // create and compile the regex pattern for the decoration
    decorationRegex = open + "[0-9]" + close ;
    decorationPattern = Pattern.compile( decorationRegex );
   
    // create and compile the regex pattern for validating the complete key
    validationRegex = "\\w+" + decorationRegex + "|^" + decorationRegex;
    validationPattern = Pattern.compile( validationRegex );
   
    // create the set of classes for with to withhold the persistence name for compound collection elements
    // for example, List< List< Double > > called matrix gets rendered as matrix[i][j] instead of
    // matrix[i].ArrayList[j], or List< Map< String, String > > listOfMap gets renderered as listOfMap[i]{key}
    withholdPeristName = createDefaultPersistenceWithholding();
  }
 
  /**
   * Constructs a {@link CollectionRenderer} that is used to render {@link InfoNode} representing
   * {@link Collection}s into key value pairs. Uses the default the index decorator which prepends
   * a "{@code [}" onto the index and appends a "{@code ]}" to the end of the index. For example,
   * if the {@code index=1} then the index would be decorated to look like {@code [1]}.
   * @param builder The builder for converting into and out of {@link org.freezedry.persistence.tree.InfoNode} objects
   */
  public CollectionRenderer( final KeyValueBuilder builder )
  {
    this( builder, OPEN, CLOSE );
  }
 
  /**
   * Copy constructor
   * @param renderer The {@link CollectionRenderer} to copy
   */
  public CollectionRenderer( final CollectionRenderer renderer )
  {
    super( renderer );
   
    this.openIndex = renderer.openIndex;
    this.closeIndex = renderer.closeIndex;
    this.indexDecorator = renderer.indexDecorator.getCopy();
    this.decorationRegex = renderer.decorationRegex;
    this.decorationPattern = renderer.decorationPattern;
    this.validationRegex = renderer.validationRegex;
    this.validationPattern = renderer.validationPattern;
    this.withholdPeristName = renderer.withholdPeristName;
    this.withholdArrayPersistName = renderer.withholdArrayPersistName;
  }

  /*
   * Creates the set of classes for with to withhold the persistence name for compound collection elements
   * for example, List< List< Double > > called matrix gets rendered as matrix[i][j] instead of
   * matrix[i].ArrayList[j], or List< Map< String, String > > listOfMap gets renderered as listOfMap[i]{key}
   * @return
   */
  private static Set< Class< ? > > createDefaultPersistenceWithholding()
  {
    final Set< Class< ? > > withhold = new HashSet<>();
    withhold.add( Collection.class );
    withhold.add( Map.class );
    return withhold;
  }
 
  /**
   * Tells the renderer to withhold the persistence name for the specified {@link Class} in
   * instances where the {@link Collection} element is compound, and that compound element
   * is the specified {@link Class} or a subclass of the specified {@link Class}. For example,
   * when we have a {@code List&lt; Map&lt; String, String > >} called {@code listOfMaps}, then for each
   * list element, we have a {@link Map}, and so we would want to render it as <code>listOfMap[i]{key}</code>.
   * Similarly, if we have a {@code List&lt; List&lt; Double > >} called {@code matrix}, we would want to
   * render it as {@code matrix[i][j]}.
   * <p>For arrays, see the {@link #withholdArrayPersistName} method.
   * @param clazz The specified {@link Class} for which to withhold the persistence name
   * @return true if the {@link Class} was added; false otherwise
   * @see #withholdArrayPersistName
   */
  public boolean withholdCompoundPersistNameFor( final Class< ? > clazz )
  {
    return withholdPeristName.add( clazz );
  }
 
  /**
   * Tells the renderer that it should no longer withhold the persistence name for the specified {@link Class} or
   * its subclasses.
   * @param clazz The specified {@link Class} for which to no longer withhold the persistence name
   * @return true if the {@link Class} was removed; false otherwise
   * @see #withholdPeristName
   * @see #withholdArrayPersistName
   */
  public boolean removePersistNameWithholding( final Class< ? > clazz )
  {
    return withholdPeristName.remove( clazz );
  }
 
  /**
   * Tells the renderer to withhold the persistence name for arrays in instances where the {@link Collection}
   * element is compound, and that compound element is an array. For example, if we have a {@code List< int[] > >}
   * called {@code matrix}, we would want to render it as {@code matrix[i][j]}.
   * @param isWithhold true if peristence names are to be withheld; false otherwise
   * @see #withholdPeristName
   */
  public void setWithholdCompoundPersistNameForArrays( final boolean isWithhold )
  {
    withholdArrayPersistName = isWithhold;
  }

  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.renderers.PersistenceRenderer#createKeyValuePair(org.freezedry.persistence.tree.InfoNode, java.lang.String)
   */
  @Override
  public void buildKeyValuePair( final InfoNode infoNode, final String key, final List< Pair< String, Object > > keyValues, final boolean isWithholdPersistName )
  {
    int index = 0;
    for( InfoNode node : infoNode.getChildren() )
    {
      if( node.isLeafNode() )
      {
        // create the key-value pair and return it
        final String newKey = createNodeKey( key, infoNode, index, isWithholdPersistName );
        getPersistenceBuilder().createKeyValuePairs( node, newKey, keyValues, true );
      }
      else
      {
        // create the key-value pair and return it
        final String newKey = createNodeKey( key, infoNode, index, isWithholdPersistName );
        boolean hidePersistName = false;
       
        // next we need to check whether the group name for the key is empty. for example,
        // when we have a List< Map< String, String > > called listOfMaps, then for each
        // list element, we have a map, and so we would want to render it as listOfMap[i]{key}.
        // similarly, if we have a List< List< Double > > called matrix, we would want to
        // render it as matrix[i][j].
        if( isZeroOutPersistName( node ) )
        {
          node.setPersistName( "" );
          hidePersistName = true;
        }
       
        // have the key-value builder that called this method create a new node. back
        // into the recursive algorithm.
        getPersistenceBuilder().createKeyValuePairs( node, newKey, keyValues, hidePersistName );
      }

      // increment the index count
      ++index;
     
      // mark the node as processed so that it doesn't get processed again
      node.setIsProcessed( true );
    }
  }

  /**
   * Returns true if we have specified that when the collection element is a compound
   * element of the certain types, then we zero out the node's persistence name. For example,
   * when we have a {@code List&lt; Map&lt; String, String > >} called {@code listOfMaps}, then for each
   * list element, we have a {@link Map}, and so we would want to render it as <code>listOfMap[i]{key}</code>.
   * Similarly, if we have a {@code List&lt; List&lt; Double > >} called {@code matrix}, we would want to
   * render it as {@code matrix[i][j]}.
   * @param node The node containing the compound collection element
   * @return true if the persistence name of the node should be zeroed out.
   */
  protected boolean isZeroOutPersistName( final InfoNode node )
  {
    boolean isZeroOut = false;
   
    // grab the class represented by the node
    final Class< ? > nodeClazz = node.getClazz();
   
    // first we check to see if we are to withhold the perist name for
    // compound collection elements that are arrays
    if( withholdArrayPersistName && nodeClazz.isArray() )
    {
      isZeroOut = true;
    }
    else
    {
      // run through the list of the classes for which to withhold the
      // persist name if the compound elements are those classes or
      // subclasses of those classes
      for( Class< ? > clazz : withholdPeristName )
      {
        if( ReflectionUtils.isClassOrSuperclass( clazz, nodeClazz ) )
        {
          isZeroOut = true;
          break;
        }
      }
    }
   
    return isZeroOut;
  }

  /**
   * Creates a key for a node collection. For example, if the persist name for a {@link List} is
   * {@code people}, which is a <code>{@link List}< {@code Person} ></code>, then the key will be {@code people[i].Person}
   * where the {@code i} is the index of the list.
   * @param key The current key to which to append the persisted name and decorated index
   * @param parentNode The parent node, which holds the name of the field (in this example, "{@code people}")
   * @param index The index of the element in the {@link List}
   * @param isHidePersistName set to {@code true} if the persistence name should be hidded; {@code false} to show it
   * @return The key
   */
  protected final String createNodeKey( final String key, final InfoNode parentNode, final int index, final boolean isHidePersistName )
  {
    // grab the key-element separator
    final String separator = getPersistenceBuilder().getSeparator();

    // if the parent node has a persistence name then add it
    String newKey = key;
    if( parentNode.getPersistName() != null &&
      !parentNode.getPersistName().isEmpty() &&
      !isHidePersistName )
    {
      newKey += separator + parentNode.getPersistName();
    }
   
    // decorate the index. for example prepend a "[" and append a "]"
    newKey += indexDecorator.decorate( index );

    return newKey;
  }
 
  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer#buildInfoNode(org.freezedry.persistence.tree.InfoNode, java.util.List)
   */
  @Override
  public void buildInfoNode( final InfoNode parentNode, final List< Pair< String, String > > keyValues )
  {
    // nothing to do
    if( keyValues == null || keyValues.isEmpty() )
    {
      return;
    }
   
    // grab the group name for the collection, and create the compound node
    // that holds the elements of the collection as child nodes, and add it
    // to the parent node
    final String group = getGroupName( keyValues.get( 0 ).getFirst() );
    final InfoNode collectionNode = InfoNode.createCompoundNode( null, group, null );
    parentNode.addChild( collectionNode );
   
    // construct the patterns to determine if the node should be a compound node,
    // in which case we recurse back to the builder, or a leaf node, in which case
    // we simply create it here and add it to the collection node
    final String compoundRegex = "^" + group + decorationRegex;
    final Pattern compoundPattern = Pattern.compile( compoundRegex );
   
    final String leafRegex = compoundRegex + "$";
    final Pattern leafPattern = Pattern.compile( leafRegex );
   
    // run through the list of key-values creating the child nodes for the collection node
    final List< Pair< String, String > > copiedKeyValues = new ArrayList<>( keyValues );
    for( Pair< String, String > keyValue : keyValues )
    {
      // check to see if any items have been removed from the list. this could happen
      // when there is a compound node that we have combined, and removed all the entries
      // from this list
      if( !copiedKeyValues.contains( keyValue ) )
      {
        continue;
      }

      // grab the key
      final String key = keyValue.getFirst();
     
      // then we must figure out whether this is a compound node or a leaf node
      final Matcher compoundMatcher = compoundPattern.matcher( key );
      final Matcher leafMatcher = leafPattern.matcher( key );
      if( leafMatcher.find() )
      {
        // its a leaf, so now we need to figure out what the value is. we know that
        // it must be a number (integer, double) or a string.
        final String value = keyValue.getSecond();
        final Decorator decorator = getDecorator( value );
        final String rawValue = decorator.undecorate( value );
        final String persistName = decorator.representedClass().getSimpleName();
       
        // create the leaf info node and add it to the collection node
        final InfoNode elementNode = InfoNode.createLeafNode( null, rawValue, persistName, null );
        collectionNode.addChild( elementNode );
      }
      else if( compoundMatcher.find() )
      {
        // in this case, we'll have several entries that have the same index, so
        // we'll need to pull those out and put them into a new key-value list
        final String separator = getPersistenceBuilder().getSeparator();
        final List< Pair< String, String > > elementKeyValues = new ArrayList<>();
        for( Pair< String, String > copiedKeyValue : keyValues )
        {
          final String copiedKey = copiedKeyValue.getFirst();
          final String keyFirstElement = extractElementKeyPart( key.split( Pattern.quote( separator ) )[ 0 ] );
          if( copiedKey.startsWith( keyFirstElement ) )
          {
            // strip the first element off the key. this could mean one of three things:
            // 1. for something like matrix[0][1] we remove all but the [1]
            // 2. for something like matrix[0].Date we remove all but the Date
            // 3. for something like matrix[0]{"April"}.Mood we remove all but [1].Mood
            final String strippedKey = stripFirstElement( copiedKey, separator );
           
            // add the key to the list of keys that belong to the compound node
            elementKeyValues.add( new Pair<>( strippedKey, copiedKeyValue.getSecond() ) );
           
            // and remove the element from the list of key values
            copiedKeyValues.remove( copiedKeyValue );
          }
        }
       
        // create the node that holds the compound object and add it to the collection node
        final String persistName = KeyValueUtils.getFirstKeyElement( elementKeyValues.get( 0 ).getFirst(), separator );
       
        // call the builder (which called this method) to build the compound node
        getPersistenceBuilder().createInfoNode( collectionNode, persistName, elementKeyValues );
      }
      else
      {
        // error
        final StringBuilder message = new StringBuilder();
        message.append( "The key neither represents a leaf node nor a compound node. This is a real problem!" ).append( Constants.NEW_LINE );
        message.append( "  Parent Node Persistence Name: " ).append( parentNode.getPersistName() ).append( Constants.NEW_LINE );
        message.append( "  Key: " ).append( key ).append( Constants.NEW_LINE );
        LOGGER.error( message.toString() );
        throw new IllegalArgumentException( message.toString() );
      }
    }
  }
 
  /*
   * Extracts the map key part from the key. For example, if the key is <code>months{"January"}[0]</code>
   * this method will return <code>months{"January"}</code>.
   * @param key The key from which to extract the map-key part
   * @return the map key part from the key
   */
  private String extractElementKeyPart( final String key )
  {
    String mapKey = null;
    final Matcher matcher = decorationPattern.matcher( key );
    if( matcher.find() )
    {
      mapKey = key.substring( 0, matcher.end() );
    }
    return mapKey;
  }
 
  /*
   * Removes the map key part from the key. For example, if the key is <code>months{"January"}[0]</code>
   * this method will return <code>[0]</code>.
   * @param key The key from which to strip the map-key part
   * @return the key, stripped of the map key part
   */
  private String removeElementKeyPart( final String key )
  {
    String keyRemainder = null;
    final Matcher matcher = decorationPattern.matcher( key );
    if( matcher.find() )
    {
      keyRemainder = key.substring( matcher.end() );
    }
    return keyRemainder;
  }
 
  private String stripFirstElement( final String key, final String separator )
  {
    String remainder = removeElementKeyPart( key );
    if( remainder.startsWith( separator ) )
    {
      remainder = remainder.substring( 1 );
    }
    return remainder;
  }
 
  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer#isRenderer(java.lang.String)
   */
  @Override
  public boolean isRenderer( final String keyElement )
  {
    return validationPattern.matcher( keyElement ).find();
  }

  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer#getGroupName(java.lang.String)
   */
  @Override
  public String getGroupName( final String key )
  {
    final Matcher matcher = decorationPattern.matcher( key );
    String group = null;
    if( matcher.find() )
    {
      group = key.substring( 0, matcher.start() );
    }
    return group;
  }
 
  protected String getOpenIndex()
  {
    return openIndex;
  }
 
  protected String getCloseIndex()
  {
    return closeIndex;
  }
 
  /**
   * @return The regular expression string that is used to determine whether an index
   * is decorated by this renderer
   */
  protected final String getDecorationRegex()
  {
    return decorationRegex;
  }

  /**
   * @return The regular expression string that is used to validate that the a string
   * conforms to the this renderer.
   */
  protected final String getValidationRegex()
  {
    return validationRegex;
  }

  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.copyable.Copyable#getCopy()
   */
  @Override
  public CollectionRenderer getCopy()
  {
    return new CollectionRenderer( this );
  }

}
TOP

Related Classes of org.freezedry.persistence.keyvalue.renderers.CollectionRenderer

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.