/*
* Copyright 2014 Effektif GmbH.
*
* 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 com.effektif.workflow.api.workflow;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/** Base class for extensible objects that can store user-defined properties.
*
* Note on serialization and deserialization:
* If serialization is used, the Jackson serializer will be able to
* serialize most types to Json. But deserialization does not create
* java beans since this class does not contain the information about which
* types are the property values are. Instead, Jackson will deserialize
* json objects to Maps and json arrays to Lists
*
* @author Tom Baeyens
*/
public abstract class Extensible {
private static Map<Class,Set<String>> invalidPropertyKeysByClass = new ConcurrentHashMap<>();
protected Map<String,Object> properties;
public void addProperties(Map<String,Object> additionalProperties) {
if (additionalProperties == null) {
return;
}
if (properties == null) {
properties = new HashMap<>();
}
properties.putAll(additionalProperties);
}
/** @see Extensible */
public Map<String,Object> getProperties() {
return this.properties;
}
/** @see Extensible */
public void setProperties(Map<String,Object> properties) {
if (properties!=null) {
for (String key: properties.keySet()) {
setProperty(key, properties.get(key));
}
}
this.properties = properties;
}
/** @see Extensible */
public Extensible property(String key,Object value) {
setProperty(key, value);
return this;
}
/** @see Extensible */
public Extensible propertyOpt(String key,Object value) {
if (value!=null) {
setProperty(key, value);
}
return this;
}
/** @see Extensible */
public Object getProperty(String key) {
return properties!=null ? properties.get(key) : null;
}
/** @see Extensible */
public void setProperty(String key,Object value) {
checkProperty(key, value);
if (properties==null) {
properties = new HashMap<>();
}
this.properties.put(key, value);
}
/** only sets the property if the value is not null
* @see Extensible */
public void setPropertyOpt(String key,Object value) {
if (value==null) {
return;
}
setProperty(key, value);
}
public Object removeProperty(String key) {
return properties!=null ? properties.remove(key) : null;
}
/** throws RuntimeException if a property is set with an invalid key.
* All the known fieldnames are invalid values because the properties are
* serialized inside the containing object json.
* @param value
* @see Extensible */
private void checkProperty(String key, Object value) {
Set<String> invalidPropertyKeys = getInvalidPropertyKeys(getClass());
if (key==null || invalidPropertyKeys.contains(key)) {
throw new RuntimeException("Invalid property '"+key+"'");
}
// TODO checkValue(key, value);
// checkValue still fails on the bpmn tests
}
private void checkValue(String key, Object value) {
if ( value==null
|| (value instanceof String)
|| (value instanceof Number)
|| (value instanceof Boolean) ) {
return;
}
if (value instanceof Map) {
checkValueMap(key, (Map)value);
return;
}
if (value instanceof Collection) {
checkValueCollection(key, (Collection)value);
return;
}
throw new RuntimeException("Invalid value in property '"+key+"': "+value+" ("+value.getClass()+") Allowed types: String,Number,Boolean,Collection,Map");
}
private void checkValueCollection(String key, Collection value) {
for (Object element: (Collection)value) {
checkValue(key, element);
}
}
private void checkValueMap(String key, Map value) {
for (Object mapKey: ((Map)value).keySet()) {
if (!(mapKey instanceof String)) {
throw new RuntimeException("Invalid key in map in '"+key+"': "+mapKey+" ("+mapKey.getClass()+") Only String's are allowed as map key types: String");
}
}
for (Object element: ((Map)value).values()) {
checkValue(key, element);
}
}
public static Set<String> getInvalidPropertyKeys(Class<?> clazz) {
Set<String> invalidPropertyKeys = invalidPropertyKeysByClass.get(clazz);
if (invalidPropertyKeys!=null) {
return invalidPropertyKeys;
}
invalidPropertyKeys = new HashSet<>();
collectInvalidPropertyKeys(clazz, invalidPropertyKeys);
invalidPropertyKeysByClass.put(clazz, invalidPropertyKeys);
return invalidPropertyKeys;
}
private static void collectInvalidPropertyKeys(Class<?> clazz, Set<String> invalidPropertyKeys) {
Field[] fields = clazz.getDeclaredFields();
if (fields!=null) {
for (Field field: fields) {
invalidPropertyKeys.add(field.getName());
}
}
Class<?> superclass = clazz.getSuperclass();
if (superclass!=Object.class) {
collectInvalidPropertyKeys(superclass, invalidPropertyKeys);
}
}
}