/** * Licensed to the Austrian Association for Software Tool Integration (AASTI) * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. The AASTI 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.openengsb.core.ekb.common; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.File; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.reflect.FieldUtils; import org.openengsb.core.api.context.ContextHolder; import org.openengsb.core.api.model.FileWrapper; import org.openengsb.core.api.model.ModelWrapper; import org.openengsb.core.api.model.OpenEngSBModel; import org.openengsb.core.api.model.OpenEngSBModelEntry; import org.openengsb.core.api.model.annotation.OpenEngSBForeignKey; import org.openengsb.core.edb.api.EDBConstants; import org.openengsb.core.edb.api.EDBObject; import org.openengsb.core.edb.api.EDBObjectEntry; import org.openengsb.core.edb.api.EngineeringDatabaseService; import org.openengsb.core.ekb.api.ConnectorInformation; import org.openengsb.core.ekb.api.EKBCommit; import org.openengsb.core.ekb.api.EKBException; import org.openengsb.core.util.ModelUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; /** * The EDBConverter class responsibility is the converting between EDBObjects and models and the vice-versa. */ public class EDBConverter { private static final Logger LOGGER = LoggerFactory.getLogger(EDBConverter.class); public static final String FILEWRAPPER_FILENAME_SUFFIX = ".filename"; public static final String REFERENCE_PREFIX = "refersTo_"; private EngineeringDatabaseService edbService; public EDBConverter(EngineeringDatabaseService edbService) { this.edbService = edbService; } /** * Converts an EDBObject to a model of the given model type. */ @SuppressWarnings("unchecked") public <T> T convertEDBObjectToModel(Class<T> model, EDBObject object) { return (T) convertEDBObjectToUncheckedModel(model, object); } /** * Converts a list of EDBObjects to a list of models of the given model type. */ public <T> List<T> convertEDBObjectsToModelObjects(Class<T> model, List<EDBObject> objects) { List<T> models = new ArrayList<>(); for (EDBObject object : objects) { T instance = convertEDBObjectToModel(model, object); if (instance != null) { models.add(instance); } } return models; } /** * Tests if an EDBObject has the correct model class in which it should be converted. Returns false if the model * type is not fitting, returns true if the model type is fitting or model type is unknown. */ private boolean checkEDBObjectModelType(EDBObject object, Class<?> model) { String modelClass = object.getString(EDBConstants.MODEL_TYPE); if (modelClass == null) { LOGGER.warn(String.format("The EDBObject with the oid %s has no model type information." + "The resulting model may be a different model type than expected.", object.getOID())); } if (modelClass != null && !modelClass.equals(model.getName())) { return false; } return true; } /** * Converts an EDBObject to a model by analyzing the object and trying to call the corresponding setters of the * model. */ private Object convertEDBObjectToUncheckedModel(Class<?> model, EDBObject object) { if (!checkEDBObjectModelType(object, model)) { return null; } filterEngineeringObjectInformation(object, model); List<OpenEngSBModelEntry> entries = new ArrayList<>(); for (PropertyDescriptor propertyDescriptor : getPropertyDescriptorsForClass(model)) { if (propertyDescriptor.getWriteMethod() == null || propertyDescriptor.getName().equals(ModelUtils.MODEL_TAIL_FIELD_NAME)) { continue; } Object value = getValueForProperty(propertyDescriptor, object); Class<?> propertyClass = propertyDescriptor.getPropertyType(); if (propertyClass.isPrimitive()) { entries.add(new OpenEngSBModelEntry(propertyDescriptor.getName(), value, ClassUtils .primitiveToWrapper(propertyClass))); } else { entries.add(new OpenEngSBModelEntry(propertyDescriptor.getName(), value, propertyClass)); } } for (Map.Entry<String, EDBObjectEntry> objectEntry : object.entrySet()) { EDBObjectEntry entry = objectEntry.getValue(); Class<?> entryType; try { entryType = model.getClassLoader().loadClass(entry.getType()); entries.add(new OpenEngSBModelEntry(entry.getKey(), entry.getValue(), entryType)); } catch (ClassNotFoundException e) { LOGGER.error("Unable to load class {} of the model tail", entry.getType()); } } return ModelUtils.createModel(model, entries); } /** * Returns all property descriptors for a given class. */ private List<PropertyDescriptor> getPropertyDescriptorsForClass(Class<?> clasz) { try { BeanInfo beanInfo = Introspector.getBeanInfo(clasz); return Arrays.asList(beanInfo.getPropertyDescriptors()); } catch (IntrospectionException e) { LOGGER.error("instantiation exception while trying to create instance of class {}", clasz.getName()); } return Lists.newArrayList(); } /** * Generate the value for a specific property of a model out of an EDBObject. */ private Object getValueForProperty(PropertyDescriptor propertyDescriptor, EDBObject object) { Method setterMethod = propertyDescriptor.getWriteMethod(); String propertyName = propertyDescriptor.getName(); Object value = object.getObject(propertyName); Class<?> parameterType = setterMethod.getParameterTypes()[0]; // TODO: OPENENGSB-2719 do that in a better way than just an if-else series if (Map.class.isAssignableFrom(parameterType)) { List<Class<?>> classes = getGenericMapParameterClasses(setterMethod); value = getMapValue(classes.get(0), classes.get(1), propertyName, object); } else if (List.class.isAssignableFrom(parameterType)) { Class<?> clazz = getGenericListParameterClass(setterMethod); value = getListValue(clazz, propertyName, object); } else if (parameterType.isArray()) { Class<?> clazz = parameterType.getComponentType(); value = getArrayValue(clazz, propertyName, object); } else if (value == null) { return null; } else if (OpenEngSBModel.class.isAssignableFrom(parameterType)) { Object timestamp = object.getObject(EDBConstants.MODEL_TIMESTAMP); Long time = System.currentTimeMillis(); if (timestamp != null) { try { time = Long.parseLong(timestamp.toString()); } catch (NumberFormatException e) { LOGGER.warn("The model with the oid {} has an invalid timestamp.", object.getOID()); } } EDBObject obj = edbService.getObject((String) value, time); value = convertEDBObjectToUncheckedModel(parameterType, obj); object.remove(propertyName); } else if (parameterType.equals(FileWrapper.class)) { FileWrapper wrapper = new FileWrapper(); String filename = object.getString(propertyName + FILEWRAPPER_FILENAME_SUFFIX); String content = (String) value; wrapper.setFilename(filename); wrapper.setContent(Base64.decodeBase64(content)); value = wrapper; object.remove(propertyName + FILEWRAPPER_FILENAME_SUFFIX); } else if (parameterType.equals(File.class)) { return null; } else if (object.containsKey(propertyName)) { if (parameterType.isEnum()) { value = getEnumValue(parameterType, value); } } object.remove(propertyName); return value; } /** * Get the type of the list parameter of a setter. */ private Class<?> getGenericListParameterClass(Method setterMethod) { return getGenericParameterClasses(setterMethod, 1).get(0); } /** * Get the type of the map parameter of a setter */ private List<Class<?>> getGenericMapParameterClasses(Method setterMethod) { return getGenericParameterClasses(setterMethod, 2); } /** * Loads the generic parameter classes up to the given depth (1 for lists, 2 for maps) */ private List<Class<?>> getGenericParameterClasses(Method setterMethod, int depth) { Type t = setterMethod.getGenericParameterTypes()[0]; ParameterizedType pType = (ParameterizedType) t; List<Class<?>> classes = new ArrayList<>(); for (int i = 0; i < depth; i++) { classes.add((Class<?>) pType.getActualTypeArguments()[i]); } return classes; } /** * Gets a list object out of an EDBObject. */ @SuppressWarnings("unchecked") private <T> List<T> getListValue(Class<T> type, String propertyName, EDBObject object) { List<T> temp = new ArrayList<>(); for (int i = 0;; i++) { String property = getEntryNameForList(propertyName, i); Object obj = object.getObject(property); if (obj == null) { break; } if (OpenEngSBModel.class.isAssignableFrom(type)) { obj = convertEDBObjectToUncheckedModel(type, edbService.getObject(object.getString(property))); } temp.add((T) obj); object.remove(property); } return temp; } /** * Gets an array object out of an EDBObject. */ @SuppressWarnings("unchecked") private <T> T[] getArrayValue(Class<T> type, String propertyName, EDBObject object) { List<T> elements = getListValue(type, propertyName, object); T[] ar = (T[]) Array.newInstance(type, elements.size()); return elements.toArray(ar); } /** * Gets a map object out of an EDBObject. */ private Object getMapValue(Class<?> keyType, Class<?> valueType, String propertyName, EDBObject object) { Map<Object, Object> temp = new HashMap<>(); for (int i = 0;; i++) { String keyProperty = getEntryNameForMapKey(propertyName, i); String valueProperty = getEntryNameForMapValue(propertyName, i); if (!object.containsKey(keyProperty)) { break; } Object key = object.getObject(keyProperty); Object value = object.getObject(valueProperty); if (OpenEngSBModel.class.isAssignableFrom(keyType)) { key = convertEDBObjectToUncheckedModel(keyType, edbService.getObject(key.toString())); } if (OpenEngSBModel.class.isAssignableFrom(valueType)) { value = convertEDBObjectToUncheckedModel(valueType, edbService.getObject(value.toString())); } temp.put(key, value); object.remove(keyProperty); object.remove(valueProperty); } return temp; } /** * Gets an enum value out of an object. */ private Object getEnumValue(Class<?> type, Object value) { Object[] enumValues = type.getEnumConstants(); for (Object enumValue : enumValues) { if (enumValue.toString().equals(value.toString())) { value = enumValue; break; } } return value; } /** * Converts the models of an EKBCommit to EDBObjects and return an object which contains the three corresponding * lists */ public ConvertedCommit convertEKBCommit(EKBCommit commit) { ConvertedCommit result = new ConvertedCommit(); ConnectorInformation information = commit.getConnectorInformation(); result.setInserts(convertModelsToEDBObjects(commit.getInserts(), information)); result.setUpdates(convertModelsToEDBObjects(commit.getUpdates(), information)); result.setDeletes(convertModelsToEDBObjects(commit.getDeletes(), information)); return result; } /** * Convert a list of models to a list of EDBObjects (the version retrieving is not considered here. This is done in * the EDB directly). */ public List<EDBObject> convertModelsToEDBObjects(List<OpenEngSBModel> models, ConnectorInformation info) { List<EDBObject> result = new ArrayList<>(); if (models != null) { for (Object model : models) { result.addAll(convertModelToEDBObject(model, info)); } } return result; } /** * Converts an OpenEngSBModel object to an EDBObject (the version retrieving is not considered here. This is done in * the EDB directly). */ public List<EDBObject> convertModelToEDBObject(Object model, ConnectorInformation info) { if (!OpenEngSBModel.class.isAssignableFrom(model.getClass())) { throw new IllegalArgumentException("This function need to get a model passed"); } List<EDBObject> objects = new ArrayList<>(); if (model != null) { convertSubModel((OpenEngSBModel) model, objects, info); } return objects; } /** * Recursive function to generate a list of EDBObjects out of a model object. */ private String convertSubModel(OpenEngSBModel model, List<EDBObject> objects, ConnectorInformation info) { String contextId = ContextHolder.get().getCurrentContextId(); String oid = ModelWrapper.wrap(model).getCompleteModelOID(); EDBObject object = new EDBObject(oid); try { fillEDBObjectWithEngineeringObjectInformation(object, model); } catch (IllegalAccessException e) { LOGGER.warn("Unable to fill completely the EngineeringObjectInformation into the EDBObject", e); throw new EKBException("Unable to fill completely the EngineeringObjectInformation into the EDBObject", e); } for (OpenEngSBModelEntry entry : model.toOpenEngSBModelEntries()) { if (entry.getValue() == null) { continue; } else if (entry.getType().equals(FileWrapper.class)) { try { FileWrapper wrapper = (FileWrapper) entry.getValue(); String content = Base64.encodeBase64String(wrapper.getContent()); object.putEDBObjectEntry(entry.getKey(), content, String.class); object.putEDBObjectEntry(entry.getKey() + FILEWRAPPER_FILENAME_SUFFIX, wrapper.getFilename(), String.class); } catch (IOException e) { LOGGER.error(e.getMessage()); } } else if (OpenEngSBModel.class.isAssignableFrom(entry.getType())) { OpenEngSBModel temp = (OpenEngSBModel) entry.getValue(); String subOid = convertSubModel(temp, objects, info); object.putEDBObjectEntry(entry.getKey(), subOid, String.class); } else if (List.class.isAssignableFrom(entry.getType())) { List<?> list = (List<?>) entry.getValue(); if (list == null || list.size() == 0) { continue; } Boolean modelItems = null; for (int i = 0; i < list.size(); i++) { Object item = list.get(i); if (modelItems == null) { modelItems = OpenEngSBModel.class.isAssignableFrom(item.getClass()); } if (modelItems) { item = convertSubModel((OpenEngSBModel) item, objects, info); } String entryName = getEntryNameForList(entry.getKey(), i); object.putEDBObjectEntry(entryName, item, item.getClass()); } } else if (entry.getType().isArray()) { Object[] array = (Object[]) entry.getValue(); if (array == null || array.length == 0) { continue; } Boolean modelItems = null; for (int i = 0; i < array.length; i++) { Object item = array[i]; if (modelItems == null) { modelItems = OpenEngSBModel.class.isAssignableFrom(item.getClass()); } if (modelItems) { item = convertSubModel((OpenEngSBModel) item, objects, info); } String entryName = getEntryNameForList(entry.getKey(), i); object.putEDBObjectEntry(entryName, item, item.getClass()); } } else if (Map.class.isAssignableFrom(entry.getType())) { Map<?, ?> map = (Map<?, ?>) entry.getValue(); if (map == null || map.size() == 0) { continue; } Boolean keyIsModel = null; Boolean valueIsModel = null; int i = 0; for (Map.Entry<?, ?> ent : map.entrySet()) { if (keyIsModel == null) { keyIsModel = OpenEngSBModel.class.isAssignableFrom(ent.getKey().getClass()); } if (valueIsModel == null) { valueIsModel = OpenEngSBModel.class.isAssignableFrom(ent.getValue().getClass()); } Object key = ent.getKey(); Object value = ent.getValue(); if (keyIsModel) { key = convertSubModel((OpenEngSBModel) key, objects, info); } if (valueIsModel) { value = convertSubModel((OpenEngSBModel) value, objects, info); } object.putEDBObjectEntry(getEntryNameForMapKey(entry.getKey(), i), key); object.putEDBObjectEntry(getEntryNameForMapValue(entry.getKey(), i), value); i++; } } else { object.putEDBObjectEntry(entry.getKey(), entry.getValue(), entry.getType()); } } object.putEDBObjectEntry(EDBConstants.MODEL_TYPE, model.retrieveModelName()); object.putEDBObjectEntry(EDBConstants.MODEL_TYPE_VERSION, model.retrieveModelVersion()); object.putEDBObjectEntry("domainId", info.getDomainId()); object.putEDBObjectEntry("connectorId", info.getConnectorId()); object.putEDBObjectEntry("instanceId", info.getInstanceId()); object.putEDBObjectEntry("contextId", contextId); objects.add(object); return oid; } /** * Adds to the EDBObject special entries which mark that a model is referring to other models through * OpenEngSBForeignKey annotations */ private void fillEDBObjectWithEngineeringObjectInformation(EDBObject object, OpenEngSBModel model) throws IllegalAccessException { if (!new AdvancedModelWrapper(model).isEngineeringObject()) { return; } for (Field field : model.getClass().getDeclaredFields()) { OpenEngSBForeignKey annotation = field.getAnnotation(OpenEngSBForeignKey.class); if (annotation == null) { continue; } String value = (String) FieldUtils.readField(field, model, true); if (value == null) { continue; } value = String.format("%s/%s", ContextHolder.get().getCurrentContextId(), value); String key = getEOReferenceStringFromAnnotation(annotation); object.put(key, new EDBObjectEntry(key, value, String.class)); } } /** * Filters the reference prefix values added in the model to EDBObject conversion out of the EDBObject */ private void filterEngineeringObjectInformation(EDBObject object, Class<?> model) { if (!AdvancedModelWrapper.isEngineeringObjectClass(model)) { return; } Iterator<String> keys = object.keySet().iterator(); while (keys.hasNext()) { if (keys.next().startsWith(REFERENCE_PREFIX)) { keys.remove(); } } } /** * Returns the entry name for a map key in the EDB format. E.g. the map key for the property "map" with the index 0 * would be "map.0.key". */ public static String getEntryNameForMapKey(String property, Integer index) { return getEntryNameForMap(property, true, index); } /** * Returns the entry name for a map value in the EDB format. E.g. the map value for the property "map" with the * index 0 would be "map.0.value". */ public static String getEntryNameForMapValue(String property, Integer index) { return getEntryNameForMap(property, false, index); } /** * Returns the entry name for a map element in the EDB format. The key parameter defines if the entry name should be * generated for the key or the value of the map. E.g. the map key for the property "map" with the index 0 would be * "map.0.key". */ private static String getEntryNameForMap(String property, Boolean key, Integer index) { return String.format("%s.%d.%s", property, index, key ? "key" : "value"); } /** * Returns the entry name for a list element in the EDB format. E.g. the list element for the property "list" with * the index 0 would be "list.0". */ public static String getEntryNameForList(String property, Integer index) { return String.format("%s.%d", property, index); } /** * Converts an OpenEngSBForeignKey annotation to the fitting format which will be added to an EDBObject. */ public static String getEOReferenceStringFromAnnotation(OpenEngSBForeignKey key) { return String.format("%s%s:%s", REFERENCE_PREFIX, key.modelType(), key.modelVersion().toString()); } }