/* * 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.model.entity; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.guava.GuavaModule; import org.apache.usergrid.persistence.model.collection.SchemaManager; import org.apache.usergrid.persistence.model.field.*; import org.apache.usergrid.persistence.model.field.value.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.util.*; public class MapToEntityConverter{ public static final Logger logger = LoggerFactory.getLogger(MapToEntityConverter.class); private final JsonFactory jsonFactory = new JsonFactory(); private final ObjectMapper objectMapper = new ObjectMapper(jsonFactory).registerModule(new GuavaModule()); static final String locationKey = "location"; static final String lat = "latitude"; static final String lon = "longitude"; public Entity fromMap( Map<String, Object> map, boolean topLevel ) { Entity entity = new Entity(); return fromMap( entity, map, null, null, topLevel ); } public Entity fromMap(final Entity entity,final Map<String, Object> map,final SchemaManager schemaManager, final String entityType, boolean topLevel) { for ( String fieldName : map.keySet() ) { Object value = map.get( fieldName ); boolean unique = schemaManager == null ? topLevel : topLevel && schemaManager.isPropertyUnique(entityType, fieldName); entity.setField( processField( fieldName, value, unique)); } return entity; } private ListField processListValue(String fieldName, List list ) { if (list.isEmpty()) { return new ArrayField( fieldName ); } final List<Object> returnList = new ArrayList<>(); list.forEach( sample -> { if ( sample instanceof Map ) { returnList.add( fromMap( (Map<String, Object>) sample, false ) ); } else if ( sample instanceof List ) { returnList.add( processListForListField( fieldName, (List) sample ) ); } else { returnList.add( sample ); } }); return new ArrayField<>( fieldName, returnList); } private List processListForListField(String fieldName, List list ) { if ( list.isEmpty() ) { return list; } List<Object> newList = new ArrayList<>(); list.forEach( sample -> { if ( sample instanceof Map ) { newList.add( processMapValue( sample, fieldName) ); } else if ( sample instanceof List ) { for (Object o : list) { newList.add(o); } } else { newList.add( sample ); } }); return newList; } private Field processMapValue( Object value, String fieldName) { // check to see if the map is truly a location object if ( locationKey.equalsIgnoreCase(fieldName) ) { return processLocationField((Map<String, Object>) value, fieldName); } else { // not a location element, process it as a normal map return processMapField( value, fieldName); } } private Field processMapField ( Object value, String fieldName) { return new EntityObjectField( fieldName, fromMap( (Map<String, Object>)value, false)); } /** * for location we need to parse two formats potentially and convert to a typed field */ private Field processLocationField(Map<String, Object> value, String fieldName) { // get the object to inspect Map<String, Object> origMap = value; Map<String, Object> m = new HashMap<String, Object>(); // Tests expect us to treat "Longitude" the same as "longitude" for (String key : origMap.keySet()) { m.put(key.toLowerCase(), origMap.get(key)); } // Expect at least two fields in a Location object and must have lat lon if (m.size() >= 2 && ( (m.containsKey(lat) && m.containsKey(lon) ) || (m.containsKey("lat") && m.containsKey("lon") ) )) { Double latVal, lonVal; // check the properties to make sure they are set and are doubles if (m.containsKey(lat) && m.containsKey(lon)) { try { latVal = Double.parseDouble(m.get(lat).toString()); lonVal = Double.parseDouble(m.get(lon).toString()); } catch (NumberFormatException ignored) { throw new IllegalArgumentException( "Latitude and longitude must be doubles (e.g. 32.1234)."); } } else if (m.containsKey("lat") && m.containsKey("lon")) { if(logger.isDebugEnabled()){ logger.debug("Entity contains latitude and longitude in old format location{lat,long}"); } try { latVal = Double.parseDouble(m.get("lat").toString()); lonVal = Double.parseDouble(m.get("lon").toString()); } catch (NumberFormatException ignored) { throw new IllegalArgumentException("" + "Latitude and longitude must be doubles (e.g. 32.1234)."); } } else { throw new IllegalArgumentException("Location properties require two fields - " + "latitude and longitude, or lat and lon"); } return new LocationField(fieldName, new Location(latVal, lonVal)); } else { if(logger.isDebugEnabled()){ logger.debug( "entity cannot process location values that don't have valid " + "location{latitude,longitude} values, changing to generic object"); } return new EntityObjectField(fieldName,fromMap( value, false)); // recursion } } private Field processField(final String fieldName, final Object value, final boolean unique) { Field processedField; if ( value instanceof String ) { String stringValue =(String)value; processedField = new StringField(fieldName, stringValue, unique); } else if ( value instanceof Boolean ) { processedField = new BooleanField( fieldName, (Boolean)value, unique ); } else if ( value instanceof Integer ) { processedField = new IntegerField( fieldName, (Integer)value, unique ); } else if ( value instanceof Double ) { processedField = new DoubleField( fieldName, (Double)value, unique ); } else if ( value instanceof Float ) { processedField = new FloatField( fieldName, (Float)value, unique ); } else if ( value instanceof Long ) { processedField = new LongField( fieldName, (Long)value, unique ); } else if ( value instanceof List) { processedField = processListValue( fieldName, (List)value ); } else if ( value instanceof UUID) { processedField = new UUIDField( fieldName, (UUID)value, unique ); } else if ( value instanceof Map ) { processedField = processMapValue( value, fieldName); } else if ( value instanceof Enum ) { processedField = new StringField( fieldName, value.toString(), unique ); } else if ( value == null ){ // not supported from outside API yet, but let's keep it in serialization it's a handled in this logic processedField = new NullField( fieldName, unique ); } else { byte[] valueSerialized; try { valueSerialized = objectMapper.writeValueAsBytes( value ); } catch ( JsonProcessingException e ) { throw new RuntimeException( "Can't serialize object ",e ); } ByteBuffer byteBuffer = ByteBuffer.wrap(valueSerialized); processedField = new ByteArrayField( fieldName, byteBuffer.array(), value.getClass() ); } return processedField; } }