Package org.freezedry.persistence.keyvalue.renderers

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

/*
* 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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.freezedry.persistence.annotations.PersistMap;
import org.freezedry.persistence.containers.Pair;
import org.freezedry.persistence.keyvalue.KeyValueBuilder;
import org.freezedry.persistence.keyvalue.renderers.decorators.StringDecorator;
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 Map}. {@link Map}s are rendered
* as the map's persistence name followed by the decorated key. For example, a {@code Map< String, Double >}
* would be persisted in the following format when using the default decorator and settings:
* <code><pre>
* Person.friends{"Polly"} = "bird"
* Person.friends{"Sparky"} = "dog"
* </pre></code>
* or for a more complicated map such as {@code Map&lt; String, Map&lt; String, String > >}:
* <code><pre>
* Person.groups{"numbers"}{"one"} = "ONE"
* Person.groups{"numbers"}{"two"} = "TWO"
* Person.groups{"numbers"}{"three"} = "THREE"
* Person.groups{"letters"}{"a"} = "AY"
* Person.groups{"letters"}{"b"} = "BEE"
* </pre></code>
*
* @author Robert Philipp
*/
public class MapRenderer extends AbstractPersistenceRenderer {

  private static final Logger LOGGER = LoggerFactory.getLogger( MapRenderer.class );

  public static final String OPEN = "{";
  public static final String CLOSE = "}";
 
  private String mapEntryName = PersistMap.ENTRY_PERSIST_NAME;
  private String mapKeyName = PersistMap.KEY_PERSIST_NAME;
  private String mapValueName = PersistMap.VALUE_PERSIST_NAME;
 
  private final StringDecorator keyDecorator;
  private final String decorationRegex;
  private final Pattern decorationPattern;
  private final String validationRegex;
  private final Pattern validationPattern;

  /**
   * Constructs a key-value {@link MapRenderer} for rendering the key-values for a {@link Map}
   * @param builder The {@link KeyValueBuilder} that makes calls to this renderer. Recall that this
   * is part of a recursive algorithm.
   * @param openKey The decorator for the key that represents the beginning of the key. For example, if
   *                the key is surrounded by "<code>{</code>" and "<code>}</code>", then the {@code openKey}
   *                would be "<code>{</code>".
   * @param closeKey The decorator for the key that represents the end of the key. For example, if
   *                the key is surrounded by "<code>{</code>" and "<code>}</code>", then the {@code openKey}
   *                would be "<code>}</code>".
   */
  public MapRenderer( final KeyValueBuilder builder, final String openKey, final String closeKey )
  {
    super( builder );
   
    // create the decorator for the key
    keyDecorator = new StringDecorator( openKey, closeKey );
   
    // 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( openKey );
    final String close = Pattern.quote( closeKey );

    // create and compile the regex pattern for the decoration
    decorationRegex = open + "[\\p{Graph}\\p{Space}]+" + close;
    decorationPattern = Pattern.compile( decorationRegex );

    // create and compile the regex pattern for validating the complete key
    validationRegex = "\\w+" + decorationRegex + "|^" + decorationRegex;
    validationPattern = Pattern.compile( validationRegex );
  }

  /**
   * Constructs a key-value {@link MapRenderer} for renderering the key-values for a {@link Map}.
   * Decorates the key with the default decorations.
   * @param builder The {@link KeyValueBuilder} that makes calls to this renderer. Recall that this
   * is part of a recursive algorithm.
   */
  public MapRenderer( final KeyValueBuilder builder )
  {
    this( builder, OPEN, CLOSE );
  }
 
  /**
   * Copy constructor
   * @param renderer The {@link MapRenderer} to copy
   */
  public MapRenderer( final MapRenderer renderer )
  {
    super( renderer );
   
    this.keyDecorator = renderer.keyDecorator.getCopy();
    this.decorationRegex = renderer.decorationRegex;
    this.decorationPattern = renderer.decorationPattern;
    this.validationRegex = renderer.validationRegex;
    this.validationPattern = renderer.validationPattern;
  }

  /**
   * @param name the persistence name in the {@link InfoNode}s representing {@link Map.Entry}
   * The default value is {@link PersistMap#ENTRY_PERSIST_NAME}
   */
  public void setMapEntryName( final String name )
  {
    this.mapEntryName = name;
  }
 
  /**
   * @return the persistence name in the {@link InfoNode}s representing {@link Map.Entry}
   */
  public String getMapEntryName()
  {
    return mapEntryName;
  }

  /**
   * @param name the persistence name in the {@link InfoNode}s representing {@link Map} keys
   * The default value is {@link PersistMap#KEY_PERSIST_NAME}
   */
  public void setMapKeyName( final String name )
  {
    this.mapKeyName = name;
  }

  /**
   * @return the persistence name in the {@link InfoNode}s representing {@link Map} keys
   */
  public String getMapKeyName()
  {
    return mapKeyName;
  }

  /**
   * @param name the persistence name in the {@link InfoNode}s representing {@link Map} values
   * The default value is {@link PersistMap#VALUE_PERSIST_NAME}
   */
  public void setMapValueName( final String name )
  {
    this.mapValueName = name;
  }

  /**
   * @return the persistence name in the {@link InfoNode}s representing {@link Map} values
   */
  public String getMapValueName()
  {
    return mapValueName;
  }

  /**
   * Builds a key-value pair and adds it to the list of key-value pairs. If the
   * {@link InfoNode} is compound, then the {@link PersistenceRenderer} may refer back to
   * the {@link KeyValueBuilder} to build out the compound node.
   * @param infoNode The current {@link InfoNode} in the semantic model.
   * @param key The current key. The key is constructed by appending persistence names, which
   * may or may not be decorated, to the current key. In this way it represents a flattening
   * of the semantic model.
   * @param keyValues The current list of key-value pairs
   * @param isWithholdPersistName true if the renderer implementation should not append the
   * {@link InfoNode}'s persistence name to the key.
   */
  @Override
  public void buildKeyValuePair( final InfoNode infoNode,
                   final String key,
                   final List< Pair< String, Object > > keyValues,
                   final boolean isWithholdPersistName )
  {
    // [Division:months{January}[0], 1]
    // [Division:months{January}[1], 2]
    // [Division:systems{ALM}, "Investments and Capital Markets Division"]
    // [Division:systems{SAP}, "Single Family Division"]
    // [Division.people[0].Person.groups{"numbers"}{"one"} = "ONE"]
    // [Division.personMap{"funny"}.Person.givenName = "Pryor"]
    for( InfoNode node : infoNode.getChildren() )
    {
      // the node should be a MapEntry class, if not, then we've got problems, which
      // we will not hesitate to report to the proper authorities.
      if( ReflectionUtils.isClassOrSuperclass( Map.Entry.class, node.getClazz() ) )
      {
        // there should be two nodes hanging off the MapEntry: the key and the value.
        // each of these may have their own subnodes.
        final List< InfoNode > entryNodes = node.getChildren();
        if( entryNodes.size() != 2 )
        {
          final StringBuilder message = new StringBuilder();
          message.append( "The MapRenderer expects MapEntry nodes to have exactly 2 subnodes." ).append( Constants.NEW_LINE );
          message.append( "  Number of subnodes: " ).append( entryNodes.size() ).append( Constants.NEW_LINE );
          message.append( "  Persist Name: " ).append( node.getPersistName() );
          LOGGER.error( message.toString() );
          throw new IllegalStateException( message.toString() );
        }
       
        // find the info node that holds the key, and the info node that holds the value
        InfoNode keyNode;
        InfoNode valueNode;
        final String name1 = entryNodes.get( 0 ).getPersistName();
        final String name2 = entryNodes.get( 1 ).getPersistName();
        if( name1.equals( mapKeyName ) && name2.equals( mapValueName ) )
        {
          keyNode = entryNodes.get( 0 );
          valueNode = entryNodes.get( 1 );
        }
        else if( name2.equals( mapKeyName ) && name1.equals( mapValueName ) )
        {
          keyNode = entryNodes.get( 1 );
          valueNode = entryNodes.get( 0 );
        }
        else
        {
          final String message = new StringBuilder()
              .append( "The MapRenderer expects MapEntry nodes to have a key subnode and a value subnode." ).append( Constants.NEW_LINE )
              .append( "  Required Key Sub-node Persist Name: " ).append( mapKeyName ).append( Constants.NEW_LINE )
              .append( "  Required Value Sub-node Persist Name: " ).append( mapValueName ).append( Constants.NEW_LINE )
              .append( "  First Node's Persist Name: " ).append( name1 ).append( Constants.NEW_LINE )
              .append( "  Second Node's Persist Name: " ).append( name2 )
              .toString();
          LOGGER.error( message );
          throw new IllegalStateException( message );
        }
       
        // no we can continue to parse the nodes.
        String newKey = createNodeKey( key, infoNode, isWithholdPersistName );
        if( keyNode.isLeafNode() )
        {
          final Object object = keyNode.getValue();
          final Class< ? > clazz = object.getClass();

          String value;
          if( containsDecorator( clazz ) )
          {
            value = getDecorator( clazz ).decorate( object );
          }
          else
          {
            value = object.toString();
          }
          newKey += keyDecorator.decorate( value );

          // create the key-value pair and return it. we know that we have value node, and that
          // the persistence name of the value is "Value" or something else set by the user. we
          // don't want to write that out, so we simply remove the value from the node.
          getPersistenceBuilder().createKeyValuePairs( valueNode, newKey, keyValues, true );
        }
        else
        {
          // TODO currently we have a slight problem here for compound keys.
          final StringBuilder message = new StringBuilder();
          message.append( "The MapRenderer doesn't allow compound (composite) keys at this point." ).append( Constants.NEW_LINE );
          message.append( "  Current Key: " ).append( newKey ).append( Constants.NEW_LINE );
          LOGGER.error( message.toString() );
          throw new IllegalStateException( message.toString() );
        }
       
        // mark the node as processed so that it doesn't get processed again
        node.setIsProcessed( true );
      }
      else
      {
        final StringBuilder message = new StringBuilder();
        message.append( "The MapRenderer expects the root node of the map to have only sub-nodes of type MapEntry." ).append( Constants.NEW_LINE );
        message.append( "  InfoNode Type: " ).append( (node.getClazz() == null ? "[null]" : node.getClazz().getName()) ).append( Constants.NEW_LINE );
        message.append( "  Persist Name: " ).append( node.getPersistName() );
        LOGGER.error( message.toString() );
        throw new IllegalStateException( message.toString() );
      }
    }
  }

  /**
   * Creates a new key based on the specified key and the persistence name found in the info node
   * @param key The current key
   * @param node The {@link InfoNode}
   * @param isHidePersistName set to {@code true} if the persistence name should be hidded; {@code false} to show it
   * @return a new key based on the specified key and the persistence name found in the info node
   */
  private String createNodeKey( final String key, final InfoNode node, final boolean isHidePersistName )
  {
    String newKey = key;
    if( node.getPersistName() != null && !node.getPersistName().isEmpty() && !isHidePersistName )
    {
      newKey += getPersistenceBuilder().getSeparator() + node.getPersistName();
    }
    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 map, and create the compound node
    // that holds the map entries of the map as child nodes, and add it
    // to the parent node
    final String group = getGroupName( keyValues.get( 0 ).getFirst() );
    final InfoNode mapNode = InfoNode.createCompoundNode( null, group, null );
    parentNode.addChild( mapNode );
   
    // 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 );
   
    // we want to have groups by the map key. the map key is the map key in the key-value pair that.
    // for example, in friends{"Polly"}, the map key is "Polly" (including the quotes). then we
    // can parse each group into its proper node.
    final Map< String, List< Pair< String, String > > > mapKeyGroups = getMapKeyGroups( keyValues );
    for( Map.Entry< String, List< Pair< String, String > > > entry : mapKeyGroups.entrySet() )
    {
      // for each group, i.e. each key, we need a map entry node attached to the map node.
      final InfoNode mapEntryNode = InfoNode.createCompoundNode( null, mapEntryName, null );
      mapNode.addChild( mapEntryNode );

      // grab the map key and the list of key-values associated with that map key
      final String mapKey = entry.getKey();
     
      // run through the list of key-values creating the child nodes for the map-entry node
      final List< Pair< String, String > > keyValueGroup = entry.getValue();
      final List< Pair< String, String > > copiedKeyValues = new ArrayList<>( keyValueGroup );
      for( Pair< String, String > keyValue : keyValueGroup )
      {
        // 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();
       
        // 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 );
        final String trimmedKey = key.replaceAll( " ", "" );
        if( leafMatcher.find() && !trimmedKey.contains( "\"}{\"" ) )
        {
          // its a leaf, create the key node
          final String rawMapKey = getDecorator( mapKey ).undecorate( mapKey );
          final InfoNode keyNode = InfoNode.createLeafNode( null, rawMapKey, mapKeyName, null );
          mapEntryNode.addChild( keyNode );
         
          // 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 String rawValue = getDecorator( value ).undecorate( value );
         
          // create the leaf info node and add it to the collection node
          final InfoNode valueNode = InfoNode.createLeafNode( null, rawValue, mapValueName, null );
          mapEntryNode.addChild( valueNode );
        }
        else if( compoundMatcher.find() )
        {
          // its a compound node, create the key node
          final String rawMapKey = getDecorator( mapKey ).undecorate( mapKey );
          final InfoNode keyNode = InfoNode.createLeafNode( null, rawMapKey, mapKeyName, null );
          mapEntryNode.addChild( keyNode );
         
          // 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 > > mapValueKeyValues = new ArrayList<>();
          for( Pair< String, String > copiedKeyValue : keyValues )
          {
            final String copiedKey = copiedKeyValue.getFirst();
            final String keyFirstElement = extractMapKeyPart( 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 months{"April"}[1] we remove all but the [1]
              // 2. for something like months{"April"}.Date we remove all but the Date
              // 3. for something like months{"April"}[1].Mood we remove all but [1].Mood
              // 4. for something like months{"April"}{"happiness"} we need to remove all but {"happiness"}
              // 5. for something like months{"April"}.Person.givenName we need to remove all but Person.giveName
              StringBuilder strippedKey = new StringBuilder( mapValueName );
              final String keyRemainder = stripFirstElement( copiedKey, separator );
              if( !keyRemainder.startsWith( "{" ) && !keyRemainder.startsWith( "[" ) )
              {
                strippedKey.append( separator );
              }
              strippedKey.append( keyRemainder );

              // add the key to the list of keys that belong to the compound node
              mapValueKeyValues.add( new Pair<>( strippedKey.toString(), copiedKeyValue.getSecond() ) );
             
              // and remove the element from the list of key values
              copiedKeyValues.remove( copiedKeyValue );
            }
          }
         
          // call the builder (which called this method) to build the compound node
          getPersistenceBuilder().createInfoNode( mapEntryNode, mapValueName, mapValueKeyValues );
        }
        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 keys and creates a {@link Map} that has as its keys these map keys. The values are the
   * key-value pairs associated with each of these map keys. For leaf nodes, the list of key-value pairs will
   * be of size one. For compound nodes, the list will have a size greater than or equal to one.
   * @param keyValues The list of key values for the group.
   * @return a {@link Map} whose keys are the map keys in the list of key-value pairs
   */
  private Map< String, List< Pair< String, String > > > getMapKeyGroups( final List< Pair< String, String > > keyValues )
  {
    final Map< String, List< Pair< String, String > > > keyGroups = new LinkedHashMap<>();
    for( Pair< String, String > keyValue : keyValues )
    {
      final String mapKey = extractGroupMapKey( keyValue.getFirst() );
      if( keyGroups.containsKey( mapKey ) )
      {
        keyGroups.get( mapKey ).add( keyValue );
      }
      else
      {
        final List< Pair< String, String > > keyValueList = new ArrayList<>();
        keyValueList.add( keyValue );
        keyGroups.put( mapKey, keyValueList );
      }
    }
    return keyGroups;
  }
 
  /**
   * Extracts the map key from the key. For example, if the key is <code>months{"January"}[0]</code>, then this method
   * will return the {@link String} <code>"January"</code> (including the quotes). Or, for example, if the key is <code>id{234}</code>
   * this method will return the {@link String} <code>234</code>.
   * @param key The key from which to extract the map key
   * @return the map key as a {@link String}.
   */
  private String extractGroupMapKey( final String key )
  {
    String mapKey = null;
    final Matcher matcher = decorationPattern.matcher( key );
    if( matcher.find() )
    {
      int end = matcher.end();
      if( key.replaceAll( " ", "" ).contains( "\"}{\"" ) )
      {
        end = key.indexOf( "}" ) + 1;
      }
      mapKey = keyDecorator.undecorate( key.substring( matcher.start(), end ) );
    }
    return mapKey;
  }
 
  /**
   * 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 extractMapKeyPart( final String key )
  {
    String mapKey = null;
    final Matcher matcher = decorationPattern.matcher( key );
    if( matcher.find() )
    {
      int end = matcher.end();
      if( key.replaceAll( " ", "" ).contains( "\"}{\"" ) )
      {
        end = key.indexOf( "}" ) + 1;
      }
      mapKey = key.substring( 0, 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 removeMapKeyPart( final String key )
  {
    String keyRemainder = null;
    final Matcher matcher = decorationPattern.matcher( key );
    if( matcher.find() )
    {
      int end = matcher.end();
      if( key.replaceAll( " ", "" ).contains( "\"}{\"" ) )
      {
        end = key.indexOf( "}" ) + 1;
      }
      keyRemainder = key.substring( end );
    }
    return keyRemainder;
  }

  /**
   * Strips the first element from a compound element. For example, if the key is {@code groups\{"numbers"\}\{"one"\}}
   * then strips off and returns {@code groups\{"numbers"\}}
   * @param key The key to process
   * @param separator The decorator (i.e. "{")
   * @return The first element
   */
  private String stripFirstElement( final String key, final String separator )
  {
    String remainder = removeMapKeyPart( key );
    if( remainder.startsWith( separator ) )
    {
      remainder = remainder.replaceAll( "^" + Pattern.quote( separator ), "" );
    }
    return remainder;
  }
 
  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer#isRenderer(java.lang.String)
   */
  @Override
  public boolean isRenderer( 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;
  }
 
  /*
   * (non-Javadoc)
   * @see org.freezedry.persistence.copyable.Copyable#getCopy()
   */
  @Override
  public MapRenderer getCopy()
  {
    return new MapRenderer( this );
  }
}
TOP

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

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.