/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.core.property;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.PropertyData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.app.StructrApp;
import org.structr.core.converter.PropertyConverter;
import org.structr.core.entity.AbstractNode;
import org.structr.schema.ConfigurationProvider;
import org.structr.schema.SchemaHelper;
/**
* A container for properties and their values that is used for input/output and database
* conversion.
*
*
*/
public class PropertyMap {
private static final Logger logger = LoggerFactory.getLogger(PropertyMap.class.getName());
private static final Map<String, String> CMIS_PROPERTY_MAPPING = new LinkedHashMap<>();
static {
CMIS_PROPERTY_MAPPING.put(PropertyIds.OBJECT_ID, "id");
CMIS_PROPERTY_MAPPING.put(PropertyIds.NAME, "name");
CMIS_PROPERTY_MAPPING.put(PropertyIds.OBJECT_TYPE_ID, "type");
}
protected Map<PropertyKey, Object> properties = new LinkedHashMap<>();
public PropertyMap() {
}
public PropertyMap(final PropertyMap source) {
putAll(source);
}
public <T> PropertyMap(final PropertyKey<T> key, final T value) {
properties.put(key, value);
}
@Override
public String toString() {
return properties.toString();
}
public int size() {
return properties.size();
}
public boolean isEmpty() {
return properties.isEmpty();
}
public <T> boolean containsKey(PropertyKey<T> key) {
return properties.containsKey(key);
}
public boolean containsValue(Object value) {
return properties.containsValue(value);
}
public <T> T get(PropertyKey<T> key) {
return (T)properties.get(key);
}
public <T> T put(PropertyKey<T> key, T value) {
return (T)properties.put(key, value);
}
public final void putAll(PropertyMap source) {
if (source != null) {
for (Entry<PropertyKey, Object> entry : source.entrySet()) {
properties.put(entry.getKey(), entry.getValue());
}
}
}
public <T> T remove(PropertyKey<T> key) {
return (T)properties.remove(key);
}
public void clear() {
properties.clear();
}
public Set<PropertyKey> keySet() {
return properties.keySet();
}
public Collection<Object> values() {
return properties.values();
}
public Set<Entry<PropertyKey, Object>> entrySet() {
return properties.entrySet();
}
public Map<PropertyKey, Object> getRawMap() {
return properties;
}
/**
* Calculates a hash code for the contents of this PropertyMap.
*
* @param comparableKeys the set of property keys to use for hash code calculation, or null to use the whole keySet
* @param includeSystemProperties whether to include system properties in the calculatio9n
* @return hash code
*/
public int contentHashCode(Set<PropertyKey> comparableKeys, boolean includeSystemProperties) {
Map<PropertyKey, Object> sortedMap = new TreeMap<>(new PropertyKeyComparator());
int hashCode = 42;
sortedMap.putAll(properties);
if (comparableKeys == null) {
// calculate hash code for all properties in this map
for (Entry<PropertyKey, Object> entry : sortedMap.entrySet()) {
if (includeSystemProperties || !entry.getKey().isUnvalidated()) {
hashCode ^= entry.hashCode();
}
}
} else {
for (Entry<PropertyKey, Object> entry : sortedMap.entrySet()) {
PropertyKey key = entry.getKey();
if (comparableKeys.contains(key)) {
if (includeSystemProperties || !key.isUnvalidated()) {
hashCode ^= entry.hashCode();
}
}
}
}
return hashCode;
}
// ----- static methods -----
public static PropertyMap javaTypeToDatabaseType(SecurityContext securityContext, GraphObject entity, Map<String, Object> source) throws FrameworkException {
PropertyMap resultMap = new PropertyMap();
if (source != null) {
for (Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key != null) {
PropertyKey propertyKey = StructrApp.getConfiguration().getPropertyKeyForDatabaseName(entity.getClass(), key);
PropertyConverter converter = propertyKey.databaseConverter(securityContext, entity);
if (converter != null) {
try {
Object propertyValue = converter.convert(value);
resultMap.put(propertyKey, propertyValue);
} catch(ClassCastException cce) {
throw new FrameworkException(422, "Invalid JSON input for key " + propertyKey.jsonName() + ", expected a JSON " + propertyKey.typeName() + ".");
}
} else {
resultMap.put(propertyKey, value);
}
}
}
}
return resultMap;
}
public static PropertyMap databaseTypeToJavaType(SecurityContext securityContext, GraphObject entity, Map<String, Object> source) throws FrameworkException {
PropertyMap resultMap = new PropertyMap();
Class entityType = entity.getClass();
if (source != null) {
for (Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key != null) {
PropertyKey propertyKey = StructrApp.getConfiguration().getPropertyKeyForDatabaseName(entityType, key);
PropertyConverter converter = propertyKey.databaseConverter(securityContext, entity);
if (converter != null) {
try {
Object propertyValue = converter.revert(value);
resultMap.put(propertyKey, propertyValue);
} catch(ClassCastException cce) {
throw new FrameworkException(422, "Invalid JSON input for key " + propertyKey.jsonName() + ", expected a JSON " + propertyKey.typeName() + ".");
}
} else {
resultMap.put(propertyKey, value);
}
}
}
}
return resultMap;
}
public static PropertyMap databaseTypeToJavaType(SecurityContext securityContext, Class<? extends GraphObject> entityType, Map<String, Object> source) throws FrameworkException {
PropertyMap resultMap = new PropertyMap();
if (source != null) {
for (Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key != null) {
PropertyKey propertyKey = StructrApp.getConfiguration().getPropertyKeyForDatabaseName(entityType, key);
PropertyConverter converter = propertyKey.databaseConverter(securityContext);
if (converter != null) {
try {
Object propertyValue = converter.revert(value);
resultMap.put(propertyKey, propertyValue);
} catch(ClassCastException cce) {
throw new FrameworkException(422, "Invalid JSON input for key " + propertyKey.jsonName() + ", expected a JSON " + propertyKey.typeName() + ".");
}
} else {
resultMap.put(propertyKey, value);
}
}
}
}
return resultMap;
}
public static PropertyMap inputTypeToJavaType(SecurityContext securityContext, Map<String, Object> source) throws FrameworkException {
if (source != null) {
Object typeName = source.get(AbstractNode.type.jsonName());
if (typeName != null) {
Class<? extends GraphObject> type = SchemaHelper.getEntityClassForRawType(typeName.toString());
if (type != null) {
return inputTypeToJavaType(securityContext, type, source);
} else {
logger.warn("No entity type found for raw type {}", typeName);
}
} else {
logger.warn("No entity type found in source map: {}", source);
}
}
return fallbackPropertyMap(source);
}
public static PropertyMap inputTypeToJavaType(SecurityContext securityContext, Class<? extends GraphObject> entity, Map<String, Object> source) throws FrameworkException {
PropertyMap resultMap = new PropertyMap();
if (source != null) {
// caution, source can be null when an empty nested property group is encountered!
for (Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key != null) {
PropertyKey propertyKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(entity, key);
PropertyConverter converter = propertyKey.inputConverter(securityContext);
if (converter != null) {
try {
// test
converter.setContext(source);
Object propertyValue = converter.convert(value);
resultMap.put(propertyKey, propertyValue);
} catch (ClassCastException cce) {
logger.warn("", cce);
throw new FrameworkException(422, "Invalid JSON input for key " + propertyKey.jsonName() + ", expected a JSON " + propertyKey.typeName() + ".");
}
} else {
resultMap.put(propertyKey, value);
}
}
}
}
return resultMap;
}
public static Map<String, Object> javaTypeToDatabaseType(SecurityContext securityContext, Class<? extends GraphObject> entity, PropertyMap properties) throws FrameworkException {
Map<String, Object> databaseTypedProperties = new LinkedHashMap<>();
for(Entry<PropertyKey, Object> entry : properties.entrySet()) {
PropertyKey propertyKey = entry.getKey();
PropertyConverter converter = propertyKey.databaseConverter(securityContext);
if (converter != null) {
try {
Object propertyValue = converter.convert(entry.getValue());
databaseTypedProperties.put(propertyKey.jsonName(), propertyValue);
} catch(ClassCastException cce) {
throw new FrameworkException(422, "Invalid JSON input for key " + propertyKey.jsonName() + ", expected a JSON " + propertyKey.typeName() + ".");
}
} else {
databaseTypedProperties.put(propertyKey.jsonName(), entry.getValue());
}
}
return databaseTypedProperties;
}
public static Map<String, Object> javaTypeToInputType(SecurityContext securityContext, Class<? extends GraphObject> entity, PropertyMap properties) throws FrameworkException {
Map<String, Object> inputTypedProperties = new LinkedHashMap<>();
for(Entry<PropertyKey, Object> entry : properties.entrySet()) {
PropertyKey propertyKey = entry.getKey();
PropertyConverter converter = propertyKey.inputConverter(securityContext);
if (converter != null) {
try {
Object propertyValue = converter.revert(entry.getValue());
inputTypedProperties.put(propertyKey.jsonName(), propertyValue);
} catch(ClassCastException cce) {
throw new FrameworkException(422, "Invalid JSON input for key " + propertyKey.jsonName() + ", expected a JSON " + propertyKey.typeName() + ".");
}
} else {
inputTypedProperties.put(propertyKey.jsonName(), entry.getValue());
}
}
return inputTypedProperties;
}
public static PropertyMap cmisTypeToJavaType(final SecurityContext securityContext, final Class type, final Properties properties) throws FrameworkException {
final Map<String, PropertyData<?>> map = properties.getProperties();
final ConfigurationProvider config = StructrApp.getConfiguration();
final PropertyMap propertyMap = new PropertyMap();
for (final Entry<String, PropertyData<?>> entry : map.entrySet()) {
final PropertyData<?> propertyValue = entry.getValue();
Object value = propertyValue.getFirstValue();
String key = entry.getKey();
// convert CMIS properties to Structr properties
if (CMIS_PROPERTY_MAPPING.containsKey(key)) {
key = CMIS_PROPERTY_MAPPING.get(key);
}
final PropertyKey propertyKey = config.getPropertyKeyForJSONName(type, key, false);
if (propertyKey != null) {
final PropertyConverter converter = propertyKey.inputConverter(securityContext);
if (converter != null) {
value = converter.convert(value);
}
propertyMap.put(propertyKey, value);
} else {
throw new FrameworkException(500, "Invalid property key " + key + " for type " + type.getSimpleName() + " provided.");
}
}
return propertyMap;
}
private static PropertyMap fallbackPropertyMap(Map<String, Object> source) {
PropertyMap map = new PropertyMap();
logger.error("Using GenericProperty for input {}", source);
//Thread.dumpStack();
if (source != null) {
for (Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key != null && value != null) {
map.put(new GenericProperty(key), value);
}
}
}
return map;
}
private static class PropertyKeyComparator implements Comparator<PropertyKey> {
@Override
public int compare(PropertyKey o1, PropertyKey o2) {
return o1.jsonName().compareTo(o2.jsonName());
}
}
}