/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.usergrid.persistence.index.impl;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.usergrid.persistence.model.entity.EntityMap;
/**
* Our parser that will parse our entity map data, and return a collection of all field objects
*
* TODO: Decide if we're really getting rid of the Entity field object. If not, this can be much faster using a visitor
* pattern on the Entity
*/
public class EntityMappingParser implements FieldParser {
private static final Logger logger = LoggerFactory.getLogger( EntityMappingParser.class );
/**
* Our stack for fields
*/
private Stack<String> fieldStack = new Stack();
/**
* Keeps track fo our last field type. Used for nested objects and nested collections
*/
private Stack<Object> lastCollection = new Stack();
/**
* List of all field tuples to return
*/
private Set<EntityField> fields = new HashSet<>();
/**
* Visit al the primitive values
*/
private void visit( final String value ) {
fields.add( EntityField.create( fieldStack.peek(), value.toLowerCase() ) );
}
/**
* Visit al the primitive values
*/
private void visit( final UUID value ) {
visit(value.toString());
}
private void visit( final boolean value ) {
fields.add( EntityField.create( fieldStack.peek(), value ) );
}
private void visit( final int value ) {
fields.add( EntityField.create( fieldStack.peek(), value ) );
}
private void visit( final long value ) {
fields.add( EntityField.create( fieldStack.peek(), value ) );
}
private void visit( final double value ) {
fields.add( EntityField.create( fieldStack.peek(), value ) );
}
private void visitNull( ) {
fields.add( EntityField.create( fieldStack.peek()) );
}
private void visit( final float value ) {
fields.add( EntityField.create( fieldStack.peek(), value ) );
}
/**
* Iterate over a collection
*/
private void iterate( final Collection value ) {
//we don't support indexing 2 dimensional arrays. Short circuit with a warning so we can track operationally
if(!lastCollection.isEmpty() && lastCollection.peek() instanceof Collection){
logger.warn( "Encountered 2 collections consecutively. N+1 dimensional arrays are unsupported, only arrays of depth 1 are supported" );
return;
}
lastCollection.push( value );
//fisit all the object element
for ( final Object element : value ) {
visitValue( element );
}
lastCollection.pop();
}
/**
* visit a value
*/
private void visitValue( final Object value ) {
if ( value instanceof Map ) {
//if it's a location, then create a location field.
if ( EntityMap.isLocationField( (Map)value ) ) {
Map<String,Object> map = ( Map ) value;
Map<String,Object> location = new HashMap<>(2);
//normalize location field to use lat/lon for es
location.put("lat",map.get("latitude"));
location.put("lon",map.get("longitude"));
fields.add( EntityField.create( fieldStack.peek(), location) );
return;
}
iterate( ( Map<String, ?> ) value );
}
//TODO figure out our nested array structure
else if ( value instanceof Collection) {
iterate( ( Collection ) value );
}
else {
visitPrimitive( value );
}
}
/**
* Construct the correct primitive
*/
private void visitPrimitive( final Object object ) {
if ( object instanceof String ) {
visit( ( String ) object );
return;
}
if(object instanceof UUID){
visit((UUID) object);
return;
}
if ( object instanceof Boolean ) {
visit( ( Boolean ) object );
return;
}
if ( object instanceof Integer ) {
visit( ( Integer ) object );
return;
}
if ( object instanceof Long ) {
visit( ( Long ) object );
return;
}
if ( object instanceof Float ) {
visit( ( Float ) object );
return;
}
if ( object instanceof Double ) {
visit( ( Double ) object );
return;
}
if ( object == null ) {
visitNull();
return;
}
}
/**
* Iterate all entries in a map and map them
* @param map
*/
private void iterate( final Map<String, ?> map ) {
lastCollection.push( map );
for ( final Map.Entry<String, ?> jsonField : map.entrySet() ) {
pushField( jsonField.getKey() );
visitValue( jsonField.getValue() );
popField();
}
lastCollection.pop();
}
/**
* Push a new fieldname on to the stack
*/
private void pushField( final String fieldName ) {
if ( fieldStack.isEmpty() ) {
fieldStack.push( fieldName );
return;
}
final String newFieldName = fieldStack.peek() + "." + fieldName;
fieldStack.push( newFieldName );
}
/**
* Pop a field name off the stack
*/
private void popField() {
fieldStack.pop();
}
/**
* Parse the map field
*/
public Set<EntityField> parse(final Map<String, ?> map ) {
iterate( map );
return fields;
}
}