blob: b6f575e73667754dcf0e5b61ef53f734e1b923f7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.core.persistence;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeMap;
import javax.xml.bind.DatatypeConverter;
import javax.xml.transform.TransformerException;
import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.commons.XMLUtils;
import org.eclipse.skalli.core.xstream.IgnoreUnknownElementsXStream;
import org.eclipse.skalli.model.EntityBase;
import org.eclipse.skalli.model.ExtensibleEntityBase;
import org.eclipse.skalli.model.ExtensionEntityBase;
import org.eclipse.skalli.model.Historized;
import org.eclipse.skalli.model.Issuer;
import org.eclipse.skalli.services.entity.EntityService;
import org.eclipse.skalli.services.extension.DataMigration;
import org.eclipse.skalli.services.extension.MigrationException;
import org.eclipse.skalli.services.extension.MigrationUtils;
import org.eclipse.skalli.services.persistence.StorageConsumer;
import org.eclipse.skalli.services.persistence.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.Converter;
/**
* Helper class for the conversion of entities to XML documents.
* Requires a {@link StorageService storage service}.
*/
public class XStreamPersistence implements Issuer {
private static final String TAG_VERSION = "version"; //$NON-NLS-1$
private static final String TAG_MODIFIED_BY = "modifiedBy"; //$NON-NLS-1$
private static final String TAG_LAST_MODIFIED = "lastModified"; //$NON-NLS-1$
private static final String TAG_INHERITED_EXTENSIONS = "inheritedExtensions"; //$NON-NLS-1$
private static final Logger LOG = LoggerFactory.getLogger(XStreamPersistence.class);
private static class XStreamConsumer<T extends EntityBase> implements StorageConsumer {
private final EntityService<T> entityService;
private final Set<ClassLoader> classLoaders;
private final Set<DataMigration> migrations;
private final Map<String, Class<?>> aliases;
private final Set<Converter> converters;
private List<T> loadEntities = new ArrayList<T>();
public XStreamConsumer(EntityService<T> entityService, Set<ClassLoader> classLoaders,
Set<DataMigration> migrations, Map<String, Class<?>> aliases, Set<Converter> converters) {
this.entityService = entityService;
this.classLoaders = classLoaders;
this.migrations = migrations;
this.aliases = aliases;
this.converters = converters;
}
public List<T> getLoadedEntities() {
return loadEntities;
}
@Override
public void consume(String category, String key, long lastModified, InputStream stream)
throws IOException {
try {
Document doc = XMLUtils.documentFromStream(stream);
preProcessXML(doc, migrations, aliases, entityService.getModelVersion());
mapInheritedExtensions(doc, byAlias(aliases));
EntityBase entity = domToEntity(classLoaders, aliases, converters, doc);
if (entity != null) {
postProcessEntity(doc, entity, aliases);
loadEntities.add(entityService.getEntityClass().cast(entity));
LOG.info(MessageFormat.format("Loaded entity {0}", entity.getUuid()));
}
} catch (Exception e) {
throw new IOException(MessageFormat.format(
"Failed to process entity {0} of type {1}", key, category), e);
}
}
}
StorageService storageService;
public XStreamPersistence(StorageService storageService) {
this.storageService = storageService;
}
public <T extends EntityBase> T loadEntity(EntityService<T> entityService, String key, Set<ClassLoader> classLoaders,
Set<DataMigration> migrations, Map<String, Class<?>> aliases, Set<Converter> converters)
throws IOException {
if (entityService == null) {
throw new IOException(MessageFormat.format(
"Could not load entity {0}: No corresponding entity service available", key));
}
XStreamConsumer<T> consumer = new XStreamConsumer<T>(entityService, classLoaders, migrations, aliases, converters);
storageService.read(entityService.getEntityClass().getSimpleName(), key, consumer);
List<T> loadedEntities = consumer.getLoadedEntities();
if (loadedEntities.isEmpty()) {
return null;
}
return entityService.getEntityClass().cast(loadedEntities.get(0));
}
public <T extends EntityBase> List<T> loadEntities(EntityService<T> entityService, Set<ClassLoader> classLoaders,
Set<DataMigration> migrations, Map<String, Class<?>> aliases, Set<Converter> converters)
throws IOException {
// Read all entities with one request to the storage service
// instead of one by one. This will improve the startup time
// considerably.
XStreamConsumer<T> consumer = new XStreamConsumer<T>(entityService, classLoaders, migrations, aliases, converters);
storageService.readAll(entityService.getEntityClass().getSimpleName(), consumer);
return consumer.getLoadedEntities();
}
public void saveEntity(EntityService<?> entityService, EntityBase entity, String userId,
Map<String, Class<?>> aliases, Set<Converter> converters) throws MigrationException, IOException {
Class<? extends EntityBase> entityClass = entity.getClass();
String category = entityClass.getSimpleName();
String key = entity.getUuid().toString();
if (entityClass.isAnnotationPresent(Historized.class)) {
storageService.archive(category, key);
}
Document newDoc = entityToDom(entity, aliases, converters);
mapInheritedExtensions(newDoc, byClassNames(aliases));
Document oldDoc = entityToDom(entityClass, entity.getUuid().toString());
postProcessXML(newDoc, oldDoc, aliases, userId, entityService.getModelVersion());
InputStream is;
try {
is = XMLUtils.documentToStream(newDoc);
} catch (TransformerException e) {
throw new IOException(MessageFormat.format("Failed to transform entity {0} to XML", entity), e);
}
storageService.write(category, key, is);
}
static void preProcessXML(Document doc, Set<DataMigration> migrations, Map<String, Class<?>> aliases, int modelVersion)
throws MigrationException {
int version = getVersionAttribute(doc);
if (migrations != null) {
DataMigrator migrator = new DataMigrator(migrations, aliases);
migrator.migrate(doc, version, modelVersion);
}
}
static void postProcessXML(Document newDoc, Document oldDoc, Map<String, Class<?>> aliases, String userId, int modelVersion)
throws MigrationException {
Element newDocElement = newDoc.getDocumentElement();
Element oldDocElement = oldDoc != null ? oldDoc.getDocumentElement() : null;
boolean identical = XMLDiff.identical(newDocElement, oldDocElement);
setLastModifiedAttributes(newDocElement, identical? oldDocElement : null, userId);
SortedMap<String, Element> newExts = getExtensionsByAlias(newDoc, aliases);
SortedMap<String, Element> oldExts = oldDoc != null ? getExtensionsByAlias(oldDoc, aliases) : null;
for (String alias : newExts.keySet()) {
Element newExt = newExts.get(alias);
Element oldExt = oldExts != null ? oldExts.get(alias) : null;
setLastModifiedAttributes(newExt, (identical || XMLDiff.identical(newExt, oldExt))? oldExt : null, userId);
}
setVersionAttribute(newDoc, modelVersion);
}
static void postProcessEntity(Document doc, EntityBase entity, Map<String, Class<?>> aliases)
throws MigrationException {
EntityHelper.normalize(entity);
Element docElement = doc.getDocumentElement();
entity.setLastModified(getLastModifiedAttribute(docElement));
entity.setLastModifiedBy(getLastModifiedByAttribute(docElement));
if (entity instanceof ExtensibleEntityBase) {
ExtensibleEntityBase extensible = (ExtensibleEntityBase) entity;
Map<String, Element> extensionElements = getExtensionsByClassName(doc, aliases);
SortedSet<ExtensionEntityBase> extensions = extensible.getAllExtensions();
for (ExtensionEntityBase extension : extensions) {
String extensionClassName = extension.getClass().getName();
Element extensionElement = extensionElements.get(extensionClassName);
if (extensionElement != null) {
extension.setLastModified(getLastModifiedAttribute(extensionElement));
extension.setLastModifiedBy(getLastModifiedByAttribute(extensionElement));
}
}
}
}
static int getVersionAttribute(Document doc) {
int version = 0;
String versionAttr = doc.getDocumentElement().getAttribute(TAG_VERSION);
if (StringUtils.isNotBlank(versionAttr)) {
version = Integer.parseInt(versionAttr);
}
return version;
}
static void setVersionAttribute(Document doc, int version) {
Element documentElement = doc.getDocumentElement();
if (doc.getDocumentElement().hasAttribute(TAG_VERSION)) {
throw new RuntimeException(MessageFormat.format("<{0}> element already has a ''{1}'' attribute",
documentElement.getNodeName(), TAG_VERSION));
}
documentElement.setAttribute(TAG_VERSION, Integer.toString(version));
}
static String getLastModifiedAttribute(Element element) {
String value = element.getAttribute(TAG_LAST_MODIFIED);
return StringUtils.isNotBlank(value) ? value : null;
}
static String getLastModifiedByAttribute(Element element) {
String value = element.getAttribute(TAG_MODIFIED_BY);
return StringUtils.isNotBlank(value) ? value : null;
}
static void setLastModifiedAttribute(Element element) {
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ENGLISH); //$NON-NLS-1$
String lastModified = DatatypeConverter.printDateTime(now);
setLastModifiedAttribute(element, lastModified);
}
static void setLastModifiedAttribute(Element element, String lastModified) {
if (StringUtils.isNotBlank(lastModified)) {
element.setAttribute(TAG_LAST_MODIFIED, lastModified);
}
}
static void setLastModifiedByAttribute(Element element, String userId) {
if (StringUtils.isNotBlank(userId)) {
element.setAttribute(TAG_MODIFIED_BY, userId);
}
}
static void setLastModifiedAttributes(Element newElement, Element oldElement, String userId) {
String lastModified = oldElement != null? getLastModifiedAttribute(oldElement) : null;
if (lastModified != null) {
setLastModifiedAttribute(newElement, lastModified);
} else {
setLastModifiedAttribute(newElement);
}
String lastModifiedBy = oldElement != null? getLastModifiedByAttribute(oldElement) : null;
if (lastModifiedBy != null) {
setLastModifiedByAttribute(newElement, lastModifiedBy);
} else{
setLastModifiedByAttribute(newElement, userId);
}
}
static SortedMap<String, Element> getExtensions(Document doc, Map<String, Class<?>> aliases, boolean byClassName)
throws MigrationException {
TreeMap<String, Element> result = new TreeMap<String, Element>();
if (aliases != null && aliases.size() > 0) {
List<Element> extensionElements = MigrationUtils.getExtensions(doc, aliases.keySet());
for (Element extensionElement : extensionElements) {
String name = extensionElement.getNodeName();
if (byClassName) {
Class<?> extensionClass = aliases.get(name);
if (extensionClass != null) {
result.put(extensionClass.getName(), extensionElement);
}
} else {
result.put(name, extensionElement);
}
}
}
return result;
}
static SortedMap<String, Element> getExtensionsByAlias(Document doc, Map<String, Class<?>> aliases)
throws MigrationException {
return getExtensions(doc, aliases, false);
}
static SortedMap<String, Element> getExtensionsByClassName(Document doc, Map<String, Class<?>> aliases)
throws MigrationException {
return getExtensions(doc, aliases, true);
}
static void mapInheritedExtensions(Document doc, Map<String, String> aliases) throws MigrationException {
if (aliases == null || aliases.isEmpty()) {
return;
}
Element inheritedElement = MigrationUtils.getElementOfEntity(doc, TAG_INHERITED_EXTENSIONS);
if (inheritedElement != null) {
NodeList nodes = inheritedElement.getChildNodes();
if (nodes != null) {
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if ("string".equals(node.getNodeName())) { //$NON-NLS-1$
mapTextContent((Element)node, aliases);
}
}
}
}
}
static void mapTextContent(Element stringElement, Map<String, String> aliases) {
String alias = stringElement.getTextContent();
if (StringUtils.isNotBlank(alias)) {
String mapped = aliases.get(alias);
if (mapped != null) {
stringElement.setTextContent(mapped);
}
}
}
static Map<String, String> byAlias(Map<String, Class<?>> aliases) {
Map<String, String> map = new HashMap<String, String>(aliases.size());
for (Map.Entry<String, Class<?>> entry: aliases.entrySet()) {
map.put(entry.getKey(), entry.getValue().getName());
}
return map;
}
static Map<String, String> byClassNames(Map<String, Class<?>> aliases) {
Map<String, String> map = new HashMap<String, String>(aliases.size());
for (Map.Entry<String, Class<?>> entry: aliases.entrySet()) {
map.put(entry.getValue().getName(), entry.getKey());
}
return map;
}
private static EntityBase domToEntity(Set<ClassLoader> entityClassLoaders, Map<String, Class<?>> aliases,
Set<Converter> converters, Document doc) throws IOException {
String xml = null;
try {
xml = XMLUtils.documentToString(doc);
} catch (TransformerException e) {
throw new IOException("Failed to transform XML to entity", e);
}
XStream xstream = IgnoreUnknownElementsXStream.getXStreamInstance(converters, entityClassLoaders, aliases);
EntityBase entity = null;
try {
entity = (EntityBase) xstream.fromXML(xml);
} catch (XStreamException e) {
LOG.warn(MessageFormat.format("Failed to convert XML document to entity: {0}", xml), e);
}
return entity;
}
private static Document entityToDom(EntityBase entity, Map<String, Class<?>> aliases, Set<Converter> converters)
throws IOException {
Document newDoc = null;
try {
XStream xstream = IgnoreUnknownElementsXStream.getXStreamInstance(converters, null, aliases);
String xml = xstream.toXML(entity);
newDoc = XMLUtils.documentFromString(xml);
} catch (Exception e) {
throw new IOException(MessageFormat.format("Failed to transform entity {0} to XML", entity), e);
}
return newDoc;
}
private Document entityToDom(Class<? extends EntityBase> entityClass, String key) throws IOException {
InputStream stream = storageService.read(entityClass.getSimpleName(), key);
if (stream == null) {
LOG.warn(MessageFormat.format("Storage services has no entity with key {0}", key));
return null;
}
Document doc;
try {
doc = XMLUtils.documentFromStream(stream);
} catch (Exception e) {
throw new IOException(MessageFormat.format(
"Failed to convert stream to dom for entity {0} of type {1}", key, entityClass), e);
}
return doc;
}
}