Java tutorial
/* * Copyright 2013 Sigurd Randoll. * * 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 de.digiway.rapidbreeze.server.infrastructure.objectstorage; import de.digiway.rapidbreeze.shared.util.ObjectHelper; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang3.ObjectUtils; /** * A very simple object storage to persist and retrieve annotated objects. It * relies on simple plain text files storing information about object * properties. It reads annotation similar to JPA for the entities to * manage.<p/> * * TODO: Annotation usage example * * @author Sigurd */ public class ObjectStorage { private FileSystem fileSystem; private Path root; private PlainStorageMap classMap; private static final String CLASSMAP_FILENAME = "classes.idx"; private static final Logger LOG = Logger.getLogger(ObjectStorage.class.getName()); /** * Creates a new instance of the object storage using the given location as * storage point. * * @param storageLocation */ public ObjectStorage(Path storageLocation) { if (storageLocation == null) { throw new IllegalArgumentException( "Cannot use path:" + storageLocation + " as storage location for objects."); } try { if (!Files.exists(storageLocation)) { Files.createDirectories(storageLocation); } this.root = storageLocation; this.fileSystem = FileSystems.getDefault(); this.classMap = getStorageMap(CLASSMAP_FILENAME); } catch (IOException ex) { throw new IllegalStateException(ex); } } /** * Closes this object storage. After the storage is closed, objects cannot * be persisted or retrieved anymore. */ public void close() { try { classMap.commit(); if (!FileSystems.getDefault().equals(fileSystem)) { fileSystem.close(); } } catch (IOException ex) { throw new IllegalStateException(ex); } } /** * Persists the given object. The object must be annotated correctly. See * class description. * * @param obj */ public void persist(Object obj) { if (!ObjectStorageHelper.isEntity(obj)) { throw new IllegalArgumentException("The given object: " + obj + " is not a valid entity."); } if (!ObjectStorageHelper.hasValidIdField(obj)) { throw new IllegalArgumentException( "The given object: " + obj + " does not have a valid identifier field."); } // Check if entity has a id. If not, assign a unique identifier: if (!ObjectStorageHelper.hasValidId(obj)) { ObjectStorageHelper.generateId(obj); } PlainStorageMap objectMap = null; PlainStorageMap classIndexMap = null; try { String tablename = ObjectStorageHelper.getEntityTable(obj); if (!classMap.containsKey(tablename)) { classMap.put(tablename, tablename + ".idx"); } String classIndexFilename = classMap.get(tablename); classIndexMap = getStorageMap(classIndexFilename); String id = ObjectStorageHelper.getId(obj); if (!classIndexMap.containsKey(id)) { classIndexMap.put(id, tablename + "_" + id); } String objectMapFilename = classIndexMap.get(id); objectMap = getStorageMap(objectMapFilename); Map<String, String> fields = getPersistedFields(obj); objectMap.putAll(fields); objectMap.commit(); classIndexMap.commit(); classMap.commit(); } catch (Exception ex) { if (objectMap != null) { objectMap.rollback(); } if (classIndexMap != null) { classIndexMap.rollback(); } classMap.rollback(); throw ex; } } /** * Loads all existing entities of the given class. * * @param <T> * @param clazz * @return List of entities which might be empty. * @throws IllegalArgumentException if the given class is not a valid entity * class. */ public <T> List<T> load(Class<T> clazz) { if (!ObjectStorageHelper.isEntity(clazz)) { throw new IllegalArgumentException("The given class: " + clazz + " is not a valid entity class."); } List<T> entities = new ArrayList<>(); String tablename = ObjectStorageHelper.getEntityTable(clazz); if (classMap.containsKey(tablename)) { String classIndexFilename = classMap.get(tablename); PlainStorageMap classIndexMap = getStorageMap(classIndexFilename); for (Map.Entry<String, String> entrySet : classIndexMap.entrySet()) { PlainStorageMap storageMap = getStorageMap(entrySet.getValue()); entities.add(loadInstance(clazz, storageMap)); } } return entities; } /** * Loads the entity of the given class with the given identifier. * * @param <T> * @param clazz * @param identifier * @return entity * @throws IllegalArgumentException if the class and the identifier do not * exist or if the given class is not a valid entity class. * @throws IllegalStateException if there is a configuration issue of the * entity class. */ public <T> T load(Class<T> clazz, String identifier) { if (!ObjectStorageHelper.isEntity(clazz)) { throw new IllegalArgumentException("The given class: " + clazz + " is not a valid entity class."); } String tablename = ObjectStorageHelper.getEntityTable(clazz); if (classMap.containsKey(tablename)) { String classIndexFilename = classMap.get(tablename); PlainStorageMap classIndexMap = getStorageMap(classIndexFilename); if (classIndexMap.containsKey(identifier)) { PlainStorageMap objectMap = getStorageMap(classIndexMap.get(identifier)); return loadInstance(clazz, objectMap); } } throw new IllegalArgumentException( "Cannot retrieve object of class " + clazz + " with identifier " + identifier); } /** * Returns a new {@linkplain ObjectStorageFilter} instance to perform filter * operations on the given class. * * @param <T> * @param clazz * @return */ public <T> ObjectStorageFilter<T> createFilter(Class<T> clazz) { return new ObjectStorageFilter<>(clazz); } /** * Loads all instances which apply to the given filter. * * @param <T> * @param filter * @return */ public <T> List<T> load(final ObjectStorageFilter filter) { List<T> list = load(filter.getClazz()); // Apply order by filter criteria: if (filter.getOrderByProperty() != null) { Collections.sort(list, new Comparator<T>() { @Override public int compare(T o1, T o2) { Comparable value1 = ObjectStorageHelper.getProperty(o1, filter.getOrderByProperty()); Comparable value2 = ObjectStorageHelper.getProperty(o2, filter.getOrderByProperty()); return ObjectUtils.compare(value1, value2); } }); if (!filter.isOrderByAscending()) { Collections.reverse(list); } } return list; } private <T> T loadInstance(Class<T> clazz, Map<String, String> properties) { try { // Create new object of class using default constructor: Constructor<T> constructor = clazz.getDeclaredConstructor(); if (!constructor.isAccessible()) { constructor.setAccessible(true); } T instance = constructor.newInstance(); // Iterate over all existing properties and fill fields of new object: for (Map.Entry<String, String> entrySet : properties.entrySet()) { for (Field field : clazz.getDeclaredFields()) { if (field.getName().equals(entrySet.getKey())) { if (!field.isAccessible()) { field.setAccessible(true); } field.set(instance, stringToObject(field.getType(), entrySet.getValue())); break; } } } return instance; } catch (NoSuchMethodException ex) { throw new IllegalStateException("Cannot find default constructor of class " + clazz, ex); } catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { throw new IllegalStateException("Error during value assignment.", ex); } } /** * Removes the given entity of the storagea. * * @param obj * @throws IllegalArgumentException if the given object is not a valid * entity or does not exist in the storage. */ public void remove(Object obj) { if (!ObjectStorageHelper.isEntity(obj)) { throw new IllegalArgumentException("The given object: " + obj + " is not a valid entity."); } if (!ObjectStorageHelper.hasValidId(obj)) { throw new IllegalArgumentException("The given object: " + obj + " does not have a valid identifier."); } String tablename = ObjectStorageHelper.getEntityTable(obj); String identifier = ObjectStorageHelper.getId(obj); if (classMap.containsKey(tablename)) { String classIndexFilename = classMap.get(tablename); PlainStorageMap classIndexMap = getStorageMap(classIndexFilename); if (classIndexMap.containsKey(identifier)) { try { String objectMapFilename = classIndexMap.remove(identifier); Path objectMapPath = fileSystem.getPath(root.toString(), objectMapFilename); classIndexMap.commit(); Files.deleteIfExists(objectMapPath); } catch (IOException ex) { LOG.log(Level.WARNING, "Cannot remove object property file. Leaving it there.", ex); } catch (Exception ex) { classIndexMap.rollback(); throw ex; } return; } } throw new IllegalArgumentException("Cannot find object: " + obj + " in storage."); } /** * Removes all entities of the given class. If there is no entity of the * given class existing in the storage, the method returns without any * action. * * @param clazz */ public void removeAll(Class<?> clazz) { if (!ObjectStorageHelper.isEntity(clazz)) { throw new IllegalArgumentException("The given class: " + clazz + " is not a valid entity class."); } String tablename = ObjectStorageHelper.getEntityTable(clazz); if (classMap.containsKey(tablename)) { String classIndexFilename = classMap.get(tablename); PlainStorageMap classIndexMap = getStorageMap(classIndexFilename); Iterator<Map.Entry<String, String>> it = classIndexMap.entrySet().iterator(); try { while (it.hasNext()) { String filename = it.next().getValue(); it.remove(); Path path = fileSystem.getPath(root.toString(), filename); Files.deleteIfExists(path); } classIndexMap.commit(); } catch (IOException ex) { LOG.log(Level.WARNING, "Cannot remove object property file. Leaving it there.", ex); } catch (Exception ex) { classIndexMap.rollback(); throw ex; } } } /** * Checks if the entity of the given class with the given identifier exists. * * @param clazz * @param identifier * @return */ public boolean exists(Class<? extends Object> clazz, String identifier) { String tablename = ObjectStorageHelper.getEntityTable(clazz); if (classMap.containsKey(tablename)) { String classIndexFilename = classMap.get(tablename); PlainStorageMap classIndexMap = getStorageMap(classIndexFilename); if (classIndexMap.containsKey(identifier)) { return true; } } return false; } /** * Checks if the given object is existing in the storage. * * @param obj * @return */ public boolean exists(Object obj) { String id = ObjectStorageHelper.getId(obj); return exists(obj.getClass(), id); } private PlainStorageMap getStorageMap(String filename) { return new PlainStorageMap(fileSystem.getPath(root.toString(), filename)); } private Map<String, String> getPersistedFields(Object obj) { Map<String, String> fields = new HashMap<>(); for (Field field : obj.getClass().getDeclaredFields()) { if (isFieldPersistable(field)) { try { if (!field.isAccessible()) { field.setAccessible(true); } Object value = field.get(obj); fields.put(field.getName(), objectToString(value)); } catch (IllegalArgumentException | IllegalAccessException ex) { throw new IllegalArgumentException("Cannot retrieve fields to persist.", ex); } } } return fields; } private boolean isFieldPersistable(Field field) { if (field.isAnnotationPresent(Id.class) || field.isAnnotationPresent(Column.class)) { return true; } return false; } private String objectToString(Object obj) { if (ObjectStorageHelper.isEntity(obj)) { persist(obj); String referenceId = ObjectStorageHelper.getId(obj); return referenceId; } else { return ObjectHelper.objectToString(obj); } } private Object stringToObject(Class<?> clazz, String value) { if (ObjectStorageHelper.isEntity(clazz)) { if (exists(clazz, value)) { return load(clazz, value); } return null; } else { return ObjectHelper.stringToObject(clazz, value); } } }