blob: f2af96584648d30b03db3f0768e318944db256a1 [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.text.MessageFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.commons.ComparatorUtils;
import org.eclipse.skalli.core.storage.FileStorageComponent;
import org.eclipse.skalli.model.EntityBase;
import org.eclipse.skalli.model.EntityFilter;
import org.eclipse.skalli.services.BundleProperties;
import org.eclipse.skalli.services.entity.EntityService;
import org.eclipse.skalli.services.entity.EntityServices;
import org.eclipse.skalli.services.extension.MigrationException;
import org.eclipse.skalli.services.persistence.PersistenceService;
import org.eclipse.skalli.services.persistence.StorageService;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link PersistenceService} based on XStream.
*/
public class XStreamPersistenceComponent extends PersistenceServiceBase implements PersistenceService {
private static final Logger LOG = LoggerFactory.getLogger(XStreamPersistenceComponent.class);
private static final Logger AUDIT_LOG = LoggerFactory.getLogger("audit"); //$NON-NLS-1$
private final EntityCache cache = new EntityCache();
private final EntityCache deleted = new EntityCache();
private XStreamPersistence xstreamPersistence;
private String storageServiceClassName;
protected void activate(ComponentContext context) {
LOG.info(MessageFormat.format("[PersistenceService][xstream] {0} : activated",
(String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
}
protected void deactivate(ComponentContext context) {
xstreamPersistence = null;
cache.clearAll();
deleted.clearAll();
LOG.info(MessageFormat.format("[PersistenceService][xstream] {0} : deactivated",
(String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
}
protected void bindStorageService(StorageService storageService) {
if (storageServiceClassName.equals(storageService.getClass().getName())) {
xstreamPersistence = new XStreamPersistence(storageService);
cache.clearAll();
deleted.clearAll();
LOG.info(MessageFormat.format("bindStorageService({0})", storageService)); //$NON-NLS-1$
}
}
protected void unbindStorageService(StorageService storageService) {
if (storageServiceClassName.equals(storageService.getClass().getName())) {
LOG.info(MessageFormat.format("unbindStorageService({0})", storageService)); //$NON-NLS-1$
xstreamPersistence = null;
cache.clearAll();
deleted.clearAll();
}
}
/**
* Creates a <code>XStreamPersistenceComponent</code> for the storage service specified
* with the {@link BundleProperties#PROPERTY_STORAGE_SERVICE} bundle property.
*/
public XStreamPersistenceComponent() {
storageServiceClassName = BundleProperties.getProperty(BundleProperties.PROPERTY_STORAGE_SERVICE,
FileStorageComponent.class.getName());
}
/**
* Creates a <code>XStreamPersistenceComponent</code> for a dedicated storage service.
* <p>
* This constructor is package protected for testing purposes.
*/
XStreamPersistenceComponent(StorageService storageService) {
xstreamPersistence = new XStreamPersistence(storageService);
}
@Override
public synchronized <T extends EntityBase> void persist(Class<T> entityClass, EntityBase entity, String userId) {
if (entity == null) {
throw new IllegalArgumentException("argument 'entity' must not be null");
}
if (StringUtils.isBlank(userId)) {
throw new IllegalArgumentException("argument 'userId' must not be null or an empty string");
}
if (xstreamPersistence == null) {
LOG.warn(MessageFormat.format("Cannot persist entity {0}: StorageService not available", entity));
return;
}
// load all project models
loadModel(entityClass);
// generate unique id
UUID entityId = entity.getUuid();
if (entityId == null) {
entity.setUuid(UUID.randomUUID());
}
// verify parent is known
// TODO should be in EntitySeriviceImpl#validate
if (entity.getParentEntityId() != null) {
UUID parentUUID = entity.getParentEntityId();
EntityBase parent = getParentEntity(entityClass, entity);
if (parent == null) {
throw new RuntimeException(MessageFormat.format("Parent entity {0} does not exist", parentUUID));
}
}
EntityService<?> entityService = EntityServices.getByEntityClass(entityClass);
if (entityService == null) {
LOG.warn(MessageFormat.format(
"Cannot persist entity {0}: No entity service registered for entities of type {1}",
entity.getUuid(), entityClass.getName()));
return;
}
EntityBase oldEntity = getCachedEntity(entityClass, entityId);
try {
xstreamPersistence.saveEntity(entityService, entity, userId,
getAliases(entityClass), getConverters(entityClass));
} catch (IOException e) {
throw new RuntimeException(e);
} catch (MigrationException e) {
throw new RuntimeException(e);
}
// reload the entity to proof that is has been persisted successfully;
// if so, adjust the parent/child relations of entity and put it into the cache.
if (loadEntity(entityClass, entityId) != null) {
adjustEntityRelations(entityClass, oldEntity, entity);
updateCache(entity);
if (entity.isDeleted()) {
AUDIT_LOG.info(MessageFormat.format("Entity {0} of type ''{1}'' has been deleted by user ''{2}''",
entityId, entityClass.getSimpleName(), userId));
} else {
AUDIT_LOG.info(MessageFormat.format("Entity {0} of type ''{1}'' has been changed by user ''{2}''",
entityId, entityClass.getSimpleName(), userId));
}
} else {
throw new RuntimeException(MessageFormat.format("Failed to save entity {0} of type {1}",
entity, entityClass.getName()));
}
}
@Override
public <T extends EntityBase> T loadEntity(Class<T> entityClass, UUID uuid) {
if (xstreamPersistence == null) {
LOG.warn(MessageFormat.format("Cannot load entity {0}/{1}: StorageService not available", entityClass, uuid));
return null;
}
EntityService<T> entityService = EntityServices.getByEntityClass(entityClass);
if (entityService == null) {
LOG.warn(MessageFormat.format("No entity service registered for entities of type {0}", entityClass.getName()));
return null;
}
T entity = null;
try {
entity = xstreamPersistence.loadEntity(entityService, uuid.toString(), getClassLoaders(entityClass),
getMigrations(entityClass), getAliases(entityClass), getConverters(entityClass));
} catch (IOException e) {
LOG.warn(MessageFormat.format("Cannot load entity {0}/{1} of type {2}):",
entityClass, uuid, entityClass.getName()), e);
}
// resolve the parent chain; if necessary, load missing entities from storage
if (entity != null) {
T parentEntity = getParentChain(entityClass, entity);
if (parentEntity != null) {
entity.setParentEntity(parentEntity);
}
}
return entity;
}
@Override
public <T extends EntityBase> T getEntity(Class<T> entityClass, UUID uuid) {
loadModel(entityClass);
return cache.getEntity(entityClass, uuid);
}
@Override
public <T extends EntityBase> List<T> getEntities(Class<T> entityClass) {
loadModel(entityClass);
return cache.getEntities(entityClass);
}
@Override
public <T extends EntityBase> List<T> getEntities(Class<T> entityClass, EntityFilter<T> filter) {
loadModel(entityClass);
return cache.getEntities(entityClass, filter);
}
@Override
public <T extends EntityBase> int size(Class<T> entityClass) {
loadModel(entityClass);
return cache.size(entityClass);
}
@Override
public <T extends EntityBase> Set<UUID> keySet(Class<T> entityClass) {
loadModel(entityClass);
return cache.keySet(entityClass);
}
@Override
public <T extends EntityBase> T getEntity(Class<T> entityClass, EntityFilter<T> filter) {
loadModel(entityClass);
return cache.getEntity(entityClass, filter);
}
@Override
public <T extends EntityBase> T getDeletedEntity(Class<T> entityClass, UUID uuid) {
loadModel(entityClass);
return deleted.getEntity(entityClass, uuid);
}
@Override
public <T extends EntityBase> List<T> getDeletedEntities(Class<T> entityClass) {
loadModel(entityClass);
return deleted.getEntities(entityClass);
}
@Override
public <T extends EntityBase> Set<UUID> deletedSet(Class<T> entityClass) {
loadModel(entityClass);
return deleted.keySet(entityClass);
}
@Override
public <T extends EntityBase> void refresh(Class<T> entityClass) {
cache.clearAll(entityClass);
deleted.clearAll(entityClass);
loadModel(entityClass);
}
@Override
public void refreshAll() {
Set<Class<? extends EntityBase>> entityClasses = new HashSet<Class<? extends EntityBase>>();
entityClasses.addAll(cache.getEntityTypes());
entityClasses.addAll(deleted.getEntityTypes());
cache.clearAll();
deleted.clearAll();
for (Class<? extends EntityBase> entityClass : entityClasses) {
loadModel(entityClass);
}
}
/**
* Loads all entities of a given class from storage.
*
* Resolves the parent/child hierarchy of the loaded entities and stores the
* result in the model caches (deleted entities in {@link #deleted},
* all others in {@link #cache}).
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the class of the entities to load.
*/
synchronized <T extends EntityBase> void loadModel(Class<T> entityClass) {
if (cache.size(entityClass) > 0) {
//nothing to do, all entities are already loaded in the cache :-)
return;
}
if (xstreamPersistence == null) {
LOG.warn(MessageFormat.format("Cannot load entities of type {0}: StorageService not available", entityClass));
return;
}
EntityService<T> entityService = EntityServices.getByEntityClass(entityClass);
if (entityService == null) {
LOG.warn(MessageFormat.format("No entity service registered for entities of type {0}", entityClass.getName()));
return;
}
registerEntityClass(entityClass);
List<T> loadedEntities;
try {
loadedEntities = xstreamPersistence.loadEntities(entityService,
getClassLoaders(entityClass), getMigrations(entityClass),
getAliases(entityClass), getConverters(entityClass));
} catch (IOException e) {
throw new RuntimeException(e);
}
for (EntityBase loadedEntity : loadedEntities) {
updateCache(loadedEntity);
}
resolveEntityRelations(entityClass);
}
<T extends EntityBase> T getCachedEntity(Class<T> entityClass, UUID uuid) {
T entity = cache.getEntity(entityClass, uuid);
if (entity == null) {
entity = deleted.getEntity(entityClass, uuid);
}
return entity;
}
/**
* Registers the given entity class with the caches. An entity class must be
* registered prior to adding an instance of the entity class to the caches.
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the entity class to register.
*/
<T extends EntityBase> void registerEntityClass(Class<T> entityClass) {
if (!cache.isRegistered(entityClass)) {
cache.registerEntityClass(entityClass);
}
if (!deleted.isRegistered(entityClass)) {
deleted.registerEntityClass(entityClass);
}
}
/**
* Adds the given entity to the cache (deleted entities in {@link #deleted},
* all other in {@link #cache}).
* <p>
* This method is package protected for testing purposes.
*
* @param entity the entity to add.
*/
void updateCache(EntityBase entity) {
if (entity.isDeleted()) {
cache.removeEntity(entity);
deleted.putEntity(entity);
} else {
deleted.removeEntity(entity);
cache.putEntity(entity);
}
}
/**
* Resolves all parent/child relations between entities of the
* given class (separately for deleted and non-deleted entities!).
* This method assumes that the caches have already been filled from
* storage so that all referenced parents/children/siblings of any
* entity in the cache can be resolved without loading additional
* data from storage.
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the class of entities to resolve relations for.
*/
<T extends EntityBase> void resolveEntityRelations(Class<T> entityClass) {
resolveEntityRelations(entityClass, cache.getEntities(entityClass));
resolveEntityRelations(entityClass, deleted.getEntities(entityClass));
}
/**
* Resolves all parent/child relations between entities in a
* given collection. This method assumes that the collection of entities
* is "self contained", i.e. all referenced parents/children/siblings
* are also contained in the given list.
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the class of entities to resolve relations for.
* @param entities the entities to resolve relations for.
*/
<T extends EntityBase> void resolveEntityRelations(Class<T> entityClass, Collection<T> entities) {
for (T entity : entities) {
resolveEntityRelations(entityClass, entity);
}
}
/**
* Determines the parent of the given entity and inserts the entity as
* child of that parent. This method assumes that the parent is already in the cache.
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the class of the entity.
* @param entity the entity to resolve.
*/
<T extends EntityBase> void resolveEntityRelations(Class<T> entityClass, EntityBase entity) {
T parentEntity = getParentEntity(entityClass, entity);
entity.setParentEntity(parentEntity);
if (parentEntity != null) {
insertChildEntity(parentEntity, entity);
}
}
/**
* Adjusts the parent/child relations of an entity after it has been changed
* or created. If the parent changed or the deleted flag has been switched,
* the entity is removed from the old parent (if any) and assigned to
* the new parent (if any). Furthermore, the children of the old entity
* are assigned to the new entity.
*
* @param entityClass the class of the entity.
* @param oldEntity the old entity instance, or <code>null</code> if the entity
* did not exist before.
* @param newEntity the new entity instance, never <code>null</code>.
*/
<T extends EntityBase> void adjustEntityRelations(Class<T> entityClass,
EntityBase oldEntity, EntityBase newEntity) {
T newParent = getParentEntity(entityClass, newEntity);
if (oldEntity != null) {
reassignChildren(oldEntity, newEntity);
T oldParent = getParentEntity(entityClass, oldEntity);
if (!ComparatorUtils.equals(oldParent, newParent)
|| oldEntity.isDeleted() != newEntity.isDeleted()) {
removeChildEntity(oldParent, oldEntity);
}
}
insertChildEntity(newParent, newEntity);
}
/**
* Re-assigns the children of the oldEntity to the newEntity, i.e.
* iterates through the children of oldEntity and sets the parent
* pointer to newEntity. Furthermore, the firstChild pointer of
* newEntity is assigned from oldEntity.
*
* @param oldEntity the old entity.
* @param newEntity the new entity.
*/
void reassignChildren(EntityBase oldEntity, EntityBase newEntity) {
EntityBase next = oldEntity.getFirstChild();
newEntity.setFirstChild(next);
while (next != null) {
next.setParentEntity(newEntity);
next = next.getNextSibling();
}
}
/**
* Assigns a child entity to a given parent entity.
* <p>
* If there was no child yet, insert the entity as first child.
* If an entity with the same uuid was already in the list of children,
* replace the entity with the new value. Otherwise append the entity
* to the end of the siblings chain. If the parent entity is <code>null</code>,
* or parent and child have different deleted flags, the method does nothing.
* <p>
* This method is package protected for testing purposes.
*
* @param parentEntity the parent entity, or <code>null</code>.
* @param entity the entity to replace or append.
*/
void insertChildEntity(EntityBase parentEntity, EntityBase entity) {
if (parentEntity == null) {
return;
}
entity.setParentEntity(parentEntity);
if (parentEntity.isDeleted() != entity.isDeleted()) {
return;
}
EntityBase next = parentEntity.getFirstChild();
if (next == null) {
parentEntity.setFirstChild(entity);
return;
}
EntityBase prev = null;
while (next != null) {
if (next.equals(entity)) {
if (prev != null) {
prev.setNextSibling(entity);
} else {
parentEntity.setFirstChild(entity);
}
entity.setNextSibling(next.getNextSibling());
return;
}
prev = next;
next = next.getNextSibling();
}
prev.setNextSibling(entity);
}
/**
* Removes a child entity from a given parent entity.
* <p>
* If the parent entity has no children, or there is no entity
* with the same uuid among the children, this method does nothing.
* Otherwise the child entity matching the uuid is removed and the
* siblings chain is adjusted. If the parent entity is <code>null</code>,
* the method does nothing.
* <p>
* This method is package protected for testing purposes.
*
* @param parentEntity the parent entity, or <code>null</code>.
* @param entity the entity to remove.
*/
void removeChildEntity(EntityBase parentEntity, EntityBase entity) {
if (parentEntity == null) {
return;
}
EntityBase next = parentEntity.getFirstChild();
if (next == null) {
return;
}
EntityBase prev = null;
while (next != null) {
if (next.equals(entity)) {
if (prev != null) {
prev.setNextSibling(next.getNextSibling());
} else {
parentEntity.setFirstChild(next.getNextSibling());
}
next.setNextSibling(null);
return;
}
prev = next;
next = next.getNextSibling();
}
}
/**
* Loads the whole chain of parent entities from storage (if neccessary),
* and returns the parent entity of the given entity.
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the class of the entity.
* @param entity the entity for which to lookup the parent.
*
* @return the parent entity, or <code>null</code> if the entity has no parent, the parent
* is deleted but the entity is not, or the parent entity could not be read from storage.
*/
<T extends EntityBase> T getParentChain(Class<T> entityClass, EntityBase entity) {
T parentEntity = null;
UUID parentId = entity.getParentEntityId();
if (parentId != null) {
parentEntity = getParentEntity(entityClass, entity);
if (parentEntity == null) {
parentEntity = loadEntity(entityClass, parentId);
if (parentEntity == null) {
LOG.warn(MessageFormat.format(
"Entity {0} references entity {1} as parent entity but there is no such entity",
entity.getUuid(), parentId));
return null;
}
}
if (parentEntity.isDeleted() && !entity.isDeleted()) {
LOG.warn(MessageFormat.format(
"Entity {0} cannot reference deleted entity {1} as parent entity",
entity.getUuid(), parentId));
return null;
}
}
return parentEntity;
}
/**
* Returns the parent entity of the given entity from the cache: If the given entity is
* {@link EntityBase#isDeleted() deleted} the lookup is performed in the cache of
* deleted entities. For all other entities the lookup is performed first in
* the cache of non-deleted entities. If there is no match, the lookup is repeated
* in the cache of deleted entities.
* <p>
* This method is package protected for testing purposes.
*
* @param entityClass the class of the entity.
* @param entity the entity for which to lookup the parent.
*
* @return the parent entity, or <code>null</code> if the parent entity is not in the caches
* or the entity has no parent.
*/
<T extends EntityBase> T getParentEntity(Class<T> entityClass, EntityBase entity) {
T parentEntity = null;
UUID parentId = entity.getParentEntityId();
if (parentId != null) {
if (!entity.isDeleted()) {
// undeleted entities can only reference undeleted entities
parentEntity = cache.getEntity(entityClass, parentId);
}
else {
// deleted entities can reference deleted & undeleted entities
parentEntity = cache.getEntity(entityClass, parentId);
if (parentEntity == null) {
parentEntity = deleted.getEntity(entityClass, parentId);
}
}
}
return parentEntity;
}
}