/* * Hibernate OGM, Domain model persistence for NoSQL datastores * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.ogm.datastore.document.impl; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.hibernate.ogm.datastore.document.association.impl.DocumentHelpers; import org.hibernate.ogm.model.key.spi.AssociationKey; import org.hibernate.ogm.model.key.spi.AssociationType; /** * Helpers to transform associative data into association rows. * @author Mark Paluch */ public class MapAssociationRowsHelpers { private static final String EMBEDDABLE_COLUMN_PREFIX = ".value."; private static final String PATH_SEPARATOR = "."; /** * Transform {@code toManyValue} into association rows * @param toManyValue an object, {@link Collection} or a {@link Map}, can be {@literal null} * @param associationKey the association key * @return collection of association rows. Empty list if {@code toManyValue} is null */ public static Collection<?> getRows( Object toManyValue, AssociationKey associationKey) { Collection<?> rows = null; if ( associationKey.getMetadata().getAssociationType() == AssociationType.ONE_TO_ONE ) { Object oneToOneValue = toManyValue; if ( oneToOneValue != null ) { rows = Collections.singletonList( oneToOneValue ); } } // list of rows if ( toManyValue instanceof Collection ) { rows = (Collection<?>) toManyValue; } // a map-typed association, rows are organized by row key else if ( toManyValue instanceof Map ) { rows = getRowsFromMapAssociation( associationKey, (Map) toManyValue ); } return rows != null ? rows : Collections.emptyList(); } /** * Restores the list representation of the given map-typed association. E.g. { 'home' : 123, 'work' : 456 } will be * transformed into [{ 'addressType='home', 'address_id'=123}, { 'addressType='work', 'address_id'=456} ]) as * expected by the row accessor. */ private static Collection<Map<String, Object>> getRowsFromMapAssociation( AssociationKey associationKey, Map<String, Object> value) { String rowKeyIndexColumn = associationKey.getMetadata().getRowKeyIndexColumnNames()[0]; List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>(); String[] associationKeyColumns = associationKey.getMetadata() .getAssociatedEntityKeyMetadata() .getAssociationKeyColumns(); // Omit shared prefix of compound ids, will be handled in the row accessor String prefix = DocumentHelpers.getColumnSharedPrefix( associationKeyColumns ); prefix = prefix == null ? "" : prefix + "."; String embeddedValueColumnPrefix = associationKey.getMetadata().getCollectionRole() + EMBEDDABLE_COLUMN_PREFIX; // restore the list representation for ( String rowKey : value.keySet() ) { Object mapRow = value.get( rowKey ); // include the row key index column Map<String, Object> row = new HashMap<>(); set( row, rowKeyIndexColumn, rowKey ); // several value columns, copy them all if ( mapRow instanceof Map ) { for ( String column : associationKey.getMetadata() .getAssociatedEntityKeyMetadata() .getAssociationKeyColumns() ) { // The column is part of an element collection; Restore the "value" node in the hierarchy if ( column.startsWith( embeddedValueColumnPrefix ) ) { set( row, column.substring( associationKey.getMetadata().getCollectionRole().length() + 1 ), ( (Map) mapRow ).get( column.substring( embeddedValueColumnPrefix.length() ) ) ); } else { set( row, column.substring( prefix.length() ), ( (Map) mapRow ).get( column.substring( prefix.length() ) ) ); } } } else { // single value column set( row, associationKey.getMetadata().getAssociatedEntityKeyMetadata().getAssociationKeyColumns()[0], mapRow ); } rows.add( row ); } return rows; } /** * Set a key/value within the {@code target} map. Nested maps will be converted into single properties using the path separator. * * @param name the transport map * @param name the property name * @param value the property value */ @SuppressWarnings("unchecked") public static void set(Map<String, Object> target, String name, Object value) { if ( value instanceof Map ) { setMapValue( target, name, (Map<String, Object>) value ); } else { target.put( name, value ); } } /** * Saves each entry of the map as a single property using the path separator. * <p> * For example { k1 = { k11 = v11, k12 = v12 } } becomes { k1.k11=v11, k1.k12=v12 }. */ private static void setMapValue(Map<String, Object> target, String name, Map<String, Object> value) { for ( Map.Entry<String, Object> entry : value.entrySet() ) { set( target, name + PATH_SEPARATOR + entry.getKey(), entry.getValue() ); } } }