| /******************************************************************************* |
| * 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; |
| } |
| } |