From 49569253290f57a2f78ccf1cc15b52c7050fed83 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 8 Jun 2012 19:04:35 +0200 Subject: [PATCH] [New] Initial commit of the XML Persistence implementation This is a rudimentary implementation which works in that objects can be written to the DB, read back and helper methods like keySet, size are also implemented --- .gitignore | 3 + config/log4j.properties | 12 + src/ch/eitchnet/xmlpers/XmlDao.java | 66 +++ src/ch/eitchnet/xmlpers/XmlDaoFactory.java | 20 + src/ch/eitchnet/xmlpers/XmlFilePersister.java | 428 ++++++++++++++++++ .../xmlpers/XmlPersistenceExecption.java | 23 + .../xmlpers/XmlPersistenceHandler.java | 120 +++++ .../xmlpers/XmlPersistencePathBuilder.java | 144 ++++++ .../xmlpers/XmlPersistenceTransaction.java | 300 ++++++++++++ .../xmlpers/test/XmlPersistenceTest.java | 311 +++++++++++++ .../plugin/xmlpers/test/impl/MyClass.java | 106 +++++ .../plugin/xmlpers/test/impl/MyClassDao.java | 126 ++++++ .../xmlpers/test/impl/MyDaoFactory.java | 34 ++ 13 files changed, 1693 insertions(+) create mode 100644 config/log4j.properties create mode 100644 src/ch/eitchnet/xmlpers/XmlDao.java create mode 100644 src/ch/eitchnet/xmlpers/XmlDaoFactory.java create mode 100644 src/ch/eitchnet/xmlpers/XmlFilePersister.java create mode 100644 src/ch/eitchnet/xmlpers/XmlPersistenceExecption.java create mode 100644 src/ch/eitchnet/xmlpers/XmlPersistenceHandler.java create mode 100644 src/ch/eitchnet/xmlpers/XmlPersistencePathBuilder.java create mode 100644 src/ch/eitchnet/xmlpers/XmlPersistenceTransaction.java create mode 100644 test/ch/eitchnet/featherlite/plugin/xmlpers/test/XmlPersistenceTest.java create mode 100644 test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClass.java create mode 100644 test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClassDao.java create mode 100644 test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyDaoFactory.java diff --git a/.gitignore b/.gitignore index 0f182a034..c77d8a98c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ *.jar *.war *.ear + +# Project files +tmp diff --git a/config/log4j.properties b/config/log4j.properties new file mode 100644 index 000000000..a745f967b --- /dev/null +++ b/config/log4j.properties @@ -0,0 +1,12 @@ +log4j.rootLogger = info, stdout, file + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %5p [%t] %C{1} %M - %m%n + +log4j.appender.file=org.apache.log4j.RollingFileAppender +log4j.appender.file.File=${user.dir}/logs/app.log +log4j.appender.file.MaxFileSize=10000KB +log4j.appender.file.MaxBackupIndex=0 +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d %5p [%t] %C{1} %M - %m%n \ No newline at end of file diff --git a/src/ch/eitchnet/xmlpers/XmlDao.java b/src/ch/eitchnet/xmlpers/XmlDao.java new file mode 100644 index 000000000..d1dd3c456 --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlDao.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.xmlpers; + +import org.w3c.dom.DOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.ContentHandler; + +/** + * @author Robert von Burg + * + */ +public interface XmlDao { + + /** + * @param object + * @return + */ + public String getType(T object); + + /** + * @param object + * @return + */ + public String getSubType(T object); + + /** + * @param object + * @return + */ + public String getId(T object); + + /** + * + * @param object + * @param domImplementation + * @return + */ + // XXX think about returning a document, instead of an element, or use document as input + public Document serializeToDom(T object, DOMImplementation domImplementation); + + /** + * @param element + * @return + */ + public T parseFromDom(Element element); + + /** + * @param object + * @param contentHandler + */ + // XXX Use the XMLSerializer object for serializing to SAX... + public void serializeToSax(T object, ContentHandler contentHandler); + + // XXX parse from SAX is missing... + +} diff --git a/src/ch/eitchnet/xmlpers/XmlDaoFactory.java b/src/ch/eitchnet/xmlpers/XmlDaoFactory.java new file mode 100644 index 000000000..81f929572 --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlDaoFactory.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.xmlpers; + +/** + * @author Robert von Burg + * + */ +public interface XmlDaoFactory { + + public XmlDao getDao(String type); +} diff --git a/src/ch/eitchnet/xmlpers/XmlFilePersister.java b/src/ch/eitchnet/xmlpers/XmlFilePersister.java new file mode 100644 index 000000000..c02acfb25 --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlFilePersister.java @@ -0,0 +1,428 @@ +package ch.eitchnet.xmlpers; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; + +import org.apache.log4j.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import ch.eitchnet.utils.helper.FileHelper; + +import com.sun.org.apache.xml.internal.serialize.OutputFormat; +import com.sun.org.apache.xml.internal.serialize.XMLSerializer; + +/** + *@author Robert von Burg + * + */ +public class XmlFilePersister { + + // + private static final String XML_DEFAULT_ENCODING = "UTF-8"; + + private static final Logger logger = Logger.getLogger(XmlFilePersister.class); + + private boolean verbose; + private XmlPersistencePathBuilder xmlPathHelper; + + /** + * @param xmlPathHelper + * @param verbose + */ + public XmlFilePersister(XmlPersistencePathBuilder xmlPathHelper, boolean verbose) { + this.xmlPathHelper = xmlPathHelper; + this.verbose = verbose; + } + + /** + * @param type + * @param subType + * @param id + * @param document + */ + public void saveOrUpdate(String type, String subType, String id, Document document) { + + File pathF; + if (subType != null) + pathF = xmlPathHelper.getPathF(type, subType, id); + else + pathF = xmlPathHelper.getPathF(type, id); + + // if this is a new file, then check create parents, if the don't exist + if (!pathF.exists()) { + File parentFile = pathF.getParentFile(); + if (!parentFile.exists() && !parentFile.mkdirs()) { + throw new XmlPersistenceExecption("Could not create path for " + type + " / " + subType + " / " + id + + " at " + pathF.getAbsolutePath()); + } + } + + if (verbose) + logger.info("Persisting " + type + " / " + subType + " / " + id + " to " + pathF.getAbsolutePath() + "..."); + + BufferedOutputStream outStream = null; + try { + + outStream = new BufferedOutputStream(new FileOutputStream(pathF)); + + OutputFormat outputFormat = new OutputFormat("XML", XML_DEFAULT_ENCODING, true); + outputFormat.setIndent(1); + outputFormat.setIndenting(true); + //of.setDoctype(null, null); + + XMLSerializer serializer = new XMLSerializer(outStream, outputFormat); + serializer.asDOMSerializer(); + serializer.serialize(document); + outStream.flush(); + + } catch (Exception e) { + throw new XmlPersistenceExecption("Could not persist object " + type + " / " + subType + " / " + id + + " to " + pathF.getAbsolutePath(), e); + } finally { + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + logger.error(e, e); + } + } + } + + if (verbose) + logger.info("Done."); + } + + /** + * @param type + * @param subType + * @param id + */ + public void remove(String type, String subType, String id) { + + File pathF; + if (subType != null) + pathF = xmlPathHelper.getPathF(type, subType, id); + else + pathF = xmlPathHelper.getPathF(type, id); + + if (verbose) + logger.info("Remove persistence file for " + type + " / " + subType + " / " + id + " from " + + pathF.getAbsolutePath() + "..."); + + if (!pathF.exists()) { + logger.error("Persistence file for " + type + " / " + subType + " / " + id + " does not exist at " + + pathF.getAbsolutePath()); + } else if (!pathF.delete()) { + throw new XmlPersistenceExecption("Could not delete persistence file for " + type + " / " + subType + " / " + + id + " at " + pathF.getAbsolutePath()); + } + + if (verbose) + logger.info("Done."); + } + + /** + * @param type + * @param subType + */ + public void removeAll(String type, String subType) { + + File pathF; + if (subType == null) + pathF = xmlPathHelper.getPathF(type); + else + pathF = xmlPathHelper.getPathF(type, subType); + + if (!pathF.exists()) { + if (subType == null) + logger.error("Path for " + type + " at " + pathF.getAbsolutePath() + + " does not exist, so removing not possible!"); + else + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so removing not possible!"); + + } else { + + File[] filesToRemove = pathF.listFiles(); + boolean removed = FileHelper.deleteFiles(filesToRemove, verbose); + + if (!removed) { + if (subType == null) + throw new XmlPersistenceExecption("Could not delete persistence files for " + type + " at " + + pathF.getAbsolutePath()); + + throw new XmlPersistenceExecption("Could not delete persistence files for " + type + " / " + subType + + " at " + pathF.getAbsolutePath()); + } + } + } + + /** + * + * @param type + * @param subType + * + * @return + */ + public Set queryKeySet(String type, String subType) { + + // if a sub type is required, then it's simple: + if (subType != null) { + + File pathF = this.xmlPathHelper.getPathF(type, subType); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so no objects exist!"); + return Collections.emptySet(); + } + + Set keySet = new HashSet(); + for (File f : pathF.listFiles()) { + String name = f.getName(); + keySet.add(name.substring(0, name.length() - XmlPersistencePathBuilder.FILE_EXT.length())); + } + + if (verbose) + logger.info("Found " + keySet.size() + " elements for " + type + " / " + subType); + + return keySet; + } + + // otherwise we need to iterate any existing subTypes and create a combined key set + File pathF = this.xmlPathHelper.getPathF(type); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so no objects exist!"); + return Collections.emptySet(); + } + + Set keySet = new HashSet(); + + File[] subTypeFiles = pathF.listFiles(); + for (File subTypeFile : subTypeFiles) { + + if (subTypeFile.isFile()) { + keySet.add(xmlPathHelper.getId(subTypeFile.getName())); + } else { + + for (File f : subTypeFile.listFiles()) { + keySet.add(xmlPathHelper.getId(f.getName())); + } + } + } + + if (verbose) + logger.info("Found " + keySet.size() + " elements for " + type); + + return keySet; + } + + /** + * + * @param type + * @param subType + * + * @return + */ + public long querySize(String type, String subType) { + + // if a sub type is required, then it's simple: + if (subType != null) { + + File pathF = this.xmlPathHelper.getPathF(type, subType); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so no objects exist!"); + return 0l; + } + + int length = pathF.listFiles().length; + + if (verbose) + logger.info("Found " + length + " elements for " + type + " / " + subType); + + return length; + } + + // otherwise we need to iterate any existing sub types and + // return the size of the combined collection + + File pathF = this.xmlPathHelper.getPathF(type); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so no objects exist!"); + return 0l; + } + + long numberOfFiles = 0l; + + File[] subTypeFiles = pathF.listFiles(); + for (File subTypeFile : subTypeFiles) { + + if (subTypeFile.isFile()) { + numberOfFiles++; + } else { + numberOfFiles += subTypeFile.listFiles().length; + } + } + + if (verbose) + logger.info("Found " + numberOfFiles + " elements for " + type); + + return numberOfFiles; + } + +// XXX think about allowing paged loading... +// /** +// * @param type +// * @param subType +// * @param firstResult +// * @param maxResults +// * +// * @return +// */ +// public List queryFrom(String type, String subType, int firstResult, int maxResults) { +// +// File pathF = this.xmlPathHelper.getPathF(type, subType); +// if (!pathF.exists()) { +// logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() +// + " does not exist, so no objects exist!"); +// return Collections.emptyList(); +// } +// +// File[] listFiles = pathF.listFiles(); +// Arrays.sort(listFiles, new Comparator() { +// +// @Override +// public int compare(File file1, File file2) { +// return file1.getName().compareTo(file2.getName()); +// } +// }); +// +// // make sure positions are not illegal +// int size = listFiles.length; +// if (firstResult >= size) +// return Collections.emptyList(); +// +// if ((firstResult + maxResults) > size) +// maxResults = size - firstResult; +// +// File[] result = Arrays.copyOfRange(listFiles, firstResult, firstResult + maxResults); +// +// List list = new ArrayList(); +// for (File f : result) { +// list.add(loadObject(clazz, f)); +// } +// +// return list; +// } + + /** + * + * @param type + * @param subType + * + * @return + */ + public List queryAll(String type, String subType, DocumentBuilder docBuilder) { + + // if a sub type is required, then it's simple: + if (subType != null) { + + File pathF = this.xmlPathHelper.getPathF(type, subType); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so no objects exist!"); + return Collections.emptyList(); + } + + List list = new ArrayList(); + for (File subTypeF : pathF.listFiles()) { + list.add(parseFile(subTypeF, docBuilder)); + } + + if (verbose) + logger.info("Loaded " + list.size() + " elements for " + type + " / " + subType); + + return list; + } + + // otherwise we need to iterate any existing sub types and + // return those elements as well + + File pathF = this.xmlPathHelper.getPathF(type); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " at " + pathF.getAbsolutePath() + + " does not exist, so no objects exist!"); + return Collections.emptyList(); + } + + List list = new ArrayList(); + + File[] subTypeFiles = pathF.listFiles(); + for (File subTypeFile : subTypeFiles) { + + if (subTypeFile.isFile()) { + list.add(parseFile(subTypeFile, docBuilder)); + + } else { + for (File subTypeF : subTypeFile.listFiles()) { + list.add(parseFile(subTypeF, docBuilder)); + } + } + } + + if (verbose) + logger.info("Loaded " + list.size() + " elements for " + type); + + return list; + } + + /** + * + * @param type + * @param subType + * @param id + * + * @return + */ + public Element queryById(String type, String subType, String id, DocumentBuilder docBuilder) { + + File pathF = this.xmlPathHelper.getPathF(type, subType, id); + if (!pathF.exists()) { + logger.error("Path for " + type + " / " + subType + " / " + id + " at " + pathF.getAbsolutePath() + + " does not exist, so object does not exist!"); + return null; + } + + return parseFile(pathF, docBuilder); + } + + /** + * @param subTypeF + * @return + */ + private Element parseFile(File subTypeF, DocumentBuilder docBuilder) { + try { + + Document document = docBuilder.parse(subTypeF); + return document.getDocumentElement(); + + } catch (SAXException e) { + throw new XmlPersistenceExecption("Failed to parse file " + subTypeF.getAbsolutePath(), e); + } catch (IOException e) { + throw new XmlPersistenceExecption("Failed to read file " + subTypeF.getAbsolutePath(), e); + } + } +} diff --git a/src/ch/eitchnet/xmlpers/XmlPersistenceExecption.java b/src/ch/eitchnet/xmlpers/XmlPersistenceExecption.java new file mode 100644 index 000000000..868f7d83f --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlPersistenceExecption.java @@ -0,0 +1,23 @@ +package ch.eitchnet.xmlpers; + +/** + * @author Robert von Burg + */ +public class XmlPersistenceExecption extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * @param message + * @param cause + */ + public XmlPersistenceExecption(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param message + */ + public XmlPersistenceExecption(String message) { + super(message); + } +} diff --git a/src/ch/eitchnet/xmlpers/XmlPersistenceHandler.java b/src/ch/eitchnet/xmlpers/XmlPersistenceHandler.java new file mode 100644 index 000000000..0681a2c2a --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlPersistenceHandler.java @@ -0,0 +1,120 @@ +package ch.eitchnet.xmlpers; + +import org.apache.log4j.Logger; + +import ch.eitchnet.utils.helper.SystemHelper; +import ch.eitchnet.utils.objectfilter.ITransactionObject; +import ch.eitchnet.utils.objectfilter.ObjectFilter; + +/** + * @author Robert von Burg + * + */ +public class XmlPersistenceHandler { + + /** + * + */ + public static final String CONFIG_VERBOSE = "ch.eitchnet.xmlpers.config.verbose"; + + /** + * + */ + public static final String CONFIG_BASEPATH = "ch.eitchnet.xmlpers.config.basepath"; + + /** + * + */ + public static final String CONFIG_DAO_FACTORY_CLASS = "ch.eitchnet.xmlpers.config.daoFactoryClass"; + + protected static final Logger logger = Logger.getLogger(XmlPersistenceHandler.class); + + protected boolean verbose; + protected ThreadLocal xmlPersistenceTxThreadLocal; + protected XmlFilePersister persister; + protected XmlDaoFactory xmlDaoFactory; + + /** + * + */ + public void initialize() { + + String basePath = SystemHelper.getProperty(XmlPersistenceHandler.class.getSimpleName(), CONFIG_BASEPATH, null); + verbose = SystemHelper.getPropertyBool(XmlPersistenceHandler.class.getSimpleName(), CONFIG_VERBOSE, + Boolean.FALSE).booleanValue(); + + // get class to use as transaction + String daoFactoryClassName = SystemHelper.getProperty(XmlPersistenceHandler.class.getSimpleName(), + CONFIG_DAO_FACTORY_CLASS, null); + try { + @SuppressWarnings("unchecked") + Class xmlDaoFactoryClass = (Class) Class.forName(daoFactoryClassName); + + xmlDaoFactory = xmlDaoFactoryClass.newInstance(); + + } catch (ClassNotFoundException e) { + throw new XmlPersistenceExecption("XmlDaoFactory class does not exist " + daoFactoryClassName, e); + } catch (Exception e) { + throw new XmlPersistenceExecption("Failed to load class " + daoFactoryClassName, e); + } + + XmlPersistencePathBuilder pathBuilder = new XmlPersistencePathBuilder(basePath); + persister = new XmlFilePersister(pathBuilder, verbose); + + // initialize the Thread local object which is used per transaction + xmlPersistenceTxThreadLocal = new ThreadLocal(); + } + + /** + * + */ + public XmlPersistenceTransaction openTx() { + + if (verbose) + logger.info("Opening new transaction..."); + + // make sure no previous filter exists + XmlPersistenceTransaction xmlPersistenceTx = this.xmlPersistenceTxThreadLocal.get(); + if (xmlPersistenceTx != null) + throw new XmlPersistenceExecption("Previous transaction not properly closed"); + + // set a new persistence transaction object + ObjectFilter objectFilter = new ObjectFilter(); + xmlPersistenceTx = new XmlPersistenceTransaction(); + xmlPersistenceTx.initialize(persister, xmlDaoFactory, objectFilter, verbose); + + this.xmlPersistenceTxThreadLocal.set(xmlPersistenceTx); + + return xmlPersistenceTx; + } + + /** + * + */ + public XmlPersistenceTransaction getTx() { + XmlPersistenceTransaction xmlPersistenceTx = this.xmlPersistenceTxThreadLocal.get(); + if (xmlPersistenceTx == null) + throw new XmlPersistenceExecption("No transaction currently open!"); + + return xmlPersistenceTx; + } + + /** + * + */ + public void commitTx() { + + if (verbose) + logger.info("Committing transaction..."); + + try { + XmlPersistenceTransaction xmlPersistenceTx = this.xmlPersistenceTxThreadLocal.get(); + if (xmlPersistenceTx == null) + throw new XmlPersistenceExecption("No transaction currently open!"); + + xmlPersistenceTx.commitTx(); + } finally { + this.xmlPersistenceTxThreadLocal.set(null); + } + } +} diff --git a/src/ch/eitchnet/xmlpers/XmlPersistencePathBuilder.java b/src/ch/eitchnet/xmlpers/XmlPersistencePathBuilder.java new file mode 100644 index 000000000..a8ed1d638 --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlPersistencePathBuilder.java @@ -0,0 +1,144 @@ +package ch.eitchnet.xmlpers; + +import java.io.File; +import java.io.IOException; + +import org.apache.log4j.Logger; + +/** + * @author Robert von Burg + * + */ +public class XmlPersistencePathBuilder { + private static final Logger logger = Logger.getLogger(XmlPersistencePathBuilder.class); + + /** + * + */ + public static final String FILE_EXT = ".xml"; + + /** + * + */ + public static final int EXT_LENGTH = FILE_EXT.length(); + + private String basePath; + + /** + * @param basePath + */ + public XmlPersistencePathBuilder(String basePath) { + File basePathF = new File(basePath); + if (!basePathF.exists()) + throw new XmlPersistenceExecption("The database store path does not exist at " + + basePathF.getAbsolutePath()); + if (!basePathF.canWrite()) + throw new XmlPersistenceExecption("The database store path is not writeable at " + + basePathF.getAbsolutePath()); + + try { + this.basePath = basePathF.getCanonicalPath(); + } catch (IOException e) { + throw new XmlPersistenceExecption("Failed to build canonical path from " + basePath, e); + } + + logger.info("Using base path " + basePath); + } + + /** + * @param id + * @return + */ + public String getFilename(String id) { + return id.concat(FILE_EXT); + } + + /** + * @param filename + * @return + */ + public String getId(String filename) { + if (filename.charAt(filename.length() - EXT_LENGTH) != '.') + throw new XmlPersistenceExecption("The filename does not have a . at index " + + (filename.length() - EXT_LENGTH)); + + return filename.substring(0, filename.length() - EXT_LENGTH); + } + + /** + * @param type + * + * @return + */ + public String getPath(String type) { + + StringBuilder sb = new StringBuilder(basePath); + sb.append("/"); + sb.append(type); + + return sb.toString(); + } + + /** + * @param type + * + * @return + */ + public File getPathF(String type) { + return new File(getPath(type)); + } + + /** + * @param type + * @param subType + * @return + */ + public String getPath(String type, String subType) { + + StringBuilder sb = new StringBuilder(basePath); + sb.append("/"); + sb.append(type); + sb.append("/"); + sb.append(subType); + + return sb.toString(); + } + + /** + * @param type + * @param subType + * @return + */ + public File getPathF(String type, String subType) { + return new File(getPath(type, subType)); + } + + /** + * @param type + * @param subType + * @param id + * @return + */ + public String getPath(String type, String subType, String id) { + + StringBuilder sb = new StringBuilder(basePath); + sb.append("/"); + sb.append(type); + sb.append("/"); + sb.append(subType); + sb.append("/"); + sb.append(getFilename(id)); + + return sb.toString(); + } + + /** + * @param type + * @param subType + * @param id + * @return + */ + public File getPathF(String type, String subType, String id) { + return new File(getPath(type, subType, id)); + } +} diff --git a/src/ch/eitchnet/xmlpers/XmlPersistenceTransaction.java b/src/ch/eitchnet/xmlpers/XmlPersistenceTransaction.java new file mode 100644 index 000000000..1e3dfeba8 --- /dev/null +++ b/src/ch/eitchnet/xmlpers/XmlPersistenceTransaction.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.xmlpers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.log4j.Logger; +import org.w3c.dom.DOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import ch.eitchnet.utils.objectfilter.ITransactionObject; +import ch.eitchnet.utils.objectfilter.ObjectFilter; + +/** + * @author Robert von Burg + * + */ +public class XmlPersistenceTransaction { + + private static final Logger logger = Logger.getLogger(XmlPersistenceTransaction.class); + + private boolean verbose; + private XmlFilePersister persister; + private XmlDaoFactory xmlDaoFactory; + private ObjectFilter objectFilter; + private DocumentBuilder docBuilder; + private DOMImplementation domImplementation; + + /** + * @param persister + * @param xmlDaoFactory + * @param objectFilter + */ + public void initialize(XmlFilePersister persister, XmlDaoFactory xmlDaoFactory, + ObjectFilter objectFilter, boolean verbose) { + this.persister = persister; + this.xmlDaoFactory = xmlDaoFactory; + this.objectFilter = objectFilter; + this.verbose = verbose; + } + + private DocumentBuilder getDocBuilder() { + if (docBuilder == null) { + try { + docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new XmlPersistenceExecption("Failed to load document builder: " + e.getLocalizedMessage(), e); + } + } + return docBuilder; + } + + /** + * @return + */ + protected DOMImplementation getDomImpl() { + if (domImplementation == null) + domImplementation = getDocBuilder().getDOMImplementation(); + return domImplementation; + } + + /* + * modifying methods + */ + + /** + * @param object + */ + public void add(ITransactionObject object) { + this.objectFilter.add(object); + } + + /** + * @param objects + */ + public void addAll(List objects) { + this.objectFilter.addAll(objects); + } + + /** + * @param object + */ + public void update(ITransactionObject object) { + this.objectFilter.update(object); + } + + /** + * @param objects + */ + public void updateAll(List objects) { + this.objectFilter.updateAll(objects); + } + + /** + * @param object + */ + public void remove(ITransactionObject object) { + this.objectFilter.remove(object); + } + + /** + * @param objects + */ + public void removeAll(List objects) { + this.objectFilter.removeAll(objects); + } + + /* + * querying methods + */ + + /** + * @param type + * @return + */ + public Set queryKeySet(String type) { + return queryKeySet(type, null); + } + + /** + * @param type + * @param subType + * @return + */ + public Set queryKeySet(String type, String subType) { + return this.persister.queryKeySet(type, subType); + } + + /** + * @param type + * @return + */ + public long querySize(String type) { + return querySize(type, null); + } + + /** + * @param type + * @param subType + * @return + */ + public long querySize(String type, String subType) { + return this.persister.querySize(type, subType); + } + + /** + * @param type + * @return + */ + public List queryAll(String type) { + return queryAll(type, null); + } + + /** + * @param type + * @param subType + * @return + */ + public List queryAll(String type, String subType) { + + // XXX ok, this is very ugly, but for starters it will have to do + XmlDao dao = xmlDaoFactory.getDao(type); + + List elements = this.persister.queryAll(type, subType, getDocBuilder()); + List objects = new ArrayList(elements.size()); + + for (Element element : elements) { + @SuppressWarnings("unchecked") + T object = (T) dao.parseFromDom(element); + objects.add(object); + } + + return objects; + } + + /** + * @param type + * @param id + * @return + */ + public T queryById(String type, String id) { + return queryById(type, null, id); + } + + /** + * @param type + * @param subType + * @param id + * @return + */ + public T queryById(String type, String subType, String id) { + + XmlDao dao = xmlDaoFactory.getDao(type); + + Element element = this.persister.queryById(type, subType, id, getDocBuilder()); + if (element == null) + throw new XmlPersistenceExecption("No object exists for " + type + " / " + subType + " / " + id); + + @SuppressWarnings("unchecked") + T object = (T) dao.parseFromDom(element); + + return object; + } + + /* + * committing + */ + + /** + * + */ + void commitTx() { + + if (verbose) + logger.info("Committing..."); + + Set keySet = objectFilter.keySet(); + if (keySet.isEmpty()) + return; + + for (String key : keySet) { + + XmlDao dao = xmlDaoFactory.getDao(key); + + List removed = objectFilter.getRemoved(key); + if (removed.isEmpty()) { + if (verbose) + logger.info("No objects removed in this tx."); + } else { + if (verbose) + logger.info(removed.size() + " objects removed in this tx."); + + for (ITransactionObject object : removed) { + + String type = dao.getType(object); + String subType = dao.getSubType(object); + String id = dao.getId(object); + + persister.remove(type, subType, id); + } + } + + List updated = objectFilter.getUpdated(key); + if (updated.isEmpty()) { + if (verbose) + logger.info("No objects updated in this tx."); + } else { + if (verbose) + logger.info(updated.size() + " objects updated in this tx."); + + for (ITransactionObject object : updated) { + + String type = dao.getType(object); + String subType = dao.getSubType(object); + String id = dao.getId(object); + + Document asDom = dao.serializeToDom(object, getDomImpl()); + persister.saveOrUpdate(type, subType, id, asDom); + } + } + + List added = objectFilter.getAdded(key); + if (added.isEmpty()) { + if (verbose) + logger.info("No objects added in this tx."); + } else { + if (verbose) + logger.info(updated.size() + " objects added in this tx."); + + for (ITransactionObject object : added) { + + String type = dao.getType(object); + String subType = dao.getSubType(object); + String id = dao.getId(object); + + Document asDom = dao.serializeToDom(object, getDomImpl()); + persister.saveOrUpdate(type, subType, id, asDom); + } + } + } + + objectFilter.clearCache(); + logger.info("Completed TX"); + } +} diff --git a/test/ch/eitchnet/featherlite/plugin/xmlpers/test/XmlPersistenceTest.java b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/XmlPersistenceTest.java new file mode 100644 index 000000000..be194acbe --- /dev/null +++ b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/XmlPersistenceTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2010 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ + +/** + * + */ +package ch.eitchnet.featherlite.plugin.xmlpers.test; + +import java.io.File; +import java.util.List; +import java.util.Set; + +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import ch.eitchnet.featherlite.plugin.xmlpers.test.impl.MyClass; +import ch.eitchnet.featherlite.plugin.xmlpers.test.impl.MyDaoFactory; +import ch.eitchnet.utils.helper.Log4jConfigurator; +import ch.eitchnet.utils.objectfilter.ITransactionObject; +import ch.eitchnet.xmlpers.XmlPersistenceExecption; +import ch.eitchnet.xmlpers.XmlPersistenceHandler; +import ch.eitchnet.xmlpers.XmlPersistenceTransaction; + +/** + * @author Robert von Burg + * + */ +public class XmlPersistenceTest { + + private static final Logger logger = Logger.getLogger(XmlPersistenceTest.class.getName()); + + private static XmlPersistenceHandler persistenceHandler; + + /** + * @throws Exception + * if something goes wrong + */ + @BeforeClass + public static void init() throws Exception { + + try { + // set up log4j + Log4jConfigurator.configure(); + + String userDir = System.getProperty("user.dir"); + String basePath = userDir + "/tmp/testdb"; + File basePathF = new File(basePath); + if (!basePathF.exists() && !basePathF.mkdirs()) + Assert.fail("Could not create temporaray database store in " + basePathF.getAbsolutePath()); + + System.setProperty(XmlPersistenceHandler.CONFIG_BASEPATH, "tmp/testdb"); + System.setProperty(XmlPersistenceHandler.CONFIG_VERBOSE, "true"); + System.setProperty(XmlPersistenceHandler.CONFIG_DAO_FACTORY_CLASS, MyDaoFactory.class.getName()); + + persistenceHandler = new XmlPersistenceHandler(); + persistenceHandler.initialize(); + + logger.info("Initialized persistence handler."); + + } catch (Exception e) { + logger.error(e, e); + + throw new RuntimeException("Initialization failed: " + e.getLocalizedMessage(), e); + } + } + + /** + * + */ + @Test + public void testCreate() { + + try { + logger.info("Trying to create..."); + + // new instance + MyClass myClass = new MyClass("@id", "@name", "@subtype"); + + // persist instance + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + tx.add(myClass); + persistenceHandler.commitTx(); + + logger.info("Done creating."); + + } catch (Exception e) { + logger.error(e, e); + Assert.fail("Failed: " + e.getLocalizedMessage()); + } + } + + /** + * + */ + @Test + public void testRead() { + + try { + logger.info("Trying to read..."); + + // query MyClass with id @id + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + MyClass myClass = tx.queryById(MyClass.class.getName(), "@subtype", "@id"); + logger.info("Found MyClass: " + myClass); + persistenceHandler.commitTx(); + + logger.info("Done reading."); + + } catch (Exception e) { + logger.error(e, e); + Assert.fail("Failed: " + e.getLocalizedMessage()); + } + } + + /** + * + */ + @Test + public void testUpdate() { + + try { + logger.info("Trying to update an object..."); + + // query the instance + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + MyClass myClass = tx.queryById(MyClass.class.getName(), "@subtype", "@id"); + logger.info("Found MyClass: " + myClass); + + // modify the instance + myClass.setName("@name_modified"); + + // update the instance + tx.update(myClass); + persistenceHandler.commitTx(); + + logger.info("Done updating."); + + } catch (Exception e) { + logger.error(e, e); + Assert.fail("Failed: " + e.getLocalizedMessage()); + } + } + + /** + * + */ + @Test + public void testRemove() { + + logger.info("Trying to remove..."); + + // query the instance + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + MyClass myClass = tx.queryById(MyClass.class.getName(), "@subtype", "@id"); + logger.info("Found MyClass: " + myClass); + + tx.remove(myClass); + persistenceHandler.commitTx(); + + logger.info("Done removing."); + } + + /** + * + */ + @Test(expected = XmlPersistenceExecption.class) + public void testQueryFail() { + + try { + logger.info("Trying to query removed object..."); + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + MyClass myClass = tx.queryById(MyClass.class.getName(), "@subtype", "@id"); + logger.info("Found MyClass: " + myClass); + logger.info("Done querying removed object"); + } finally { + persistenceHandler.commitTx(); + } + } + + /** + * + */ + @Test + public void testReCreate() { + + try { + logger.info("Trying to recreate..."); + + // new instance + MyClass myClass = new MyClass("@id", "@name", "@subtype"); + + // persist instance + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + tx.add(myClass); + persistenceHandler.commitTx(); + + logger.info("Done creating."); + + } catch (Exception e) { + logger.error(e, e); + Assert.fail("Failed: " + e.getLocalizedMessage()); + } + } + +// /** +// * +// */ +// @Test +// public void testQueryFromTo() { +// Assert.fail("Not yet implemented"); +// } + + /** + * + */ + @Test + public void testQueryAll() { + + try { + + logger.info("Trying to query all..."); + + // query all + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + List list = tx.queryAll(MyClass.class.getName()); + Assert.assertTrue("Expected only one object, found " + list.size(), list.size() == 1); + + // also with subtype + list = tx.queryAll(MyClass.class.getName(), "@subtype"); + Assert.assertTrue("Expected only one object, found " + list.size(), list.size() == 1); + + // and now something useless + list = tx.queryAll(MyClass.class.getName(), "@inexistant"); + Assert.assertTrue("Expected no objects, found " + list.size(), list.size() == 0); + + logger.info("Done querying."); + + } finally { + persistenceHandler.commitTx(); + } + } + + /** + * + */ + @Test + public void testKeySet() { + + try { + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + + Set keySet = tx.queryKeySet(MyClass.class.getName()); + Assert.assertTrue("Expected one key, found " + keySet.size(), keySet.size() == 1); + + // also with subtype + keySet = tx.queryKeySet(MyClass.class.getName(), "@subtype"); + Assert.assertTrue("Expected one key, found " + keySet.size(), keySet.size() == 1); + + // and now something useless + keySet = tx.queryKeySet(MyClass.class.getName(), "@inexistant"); + Assert.assertTrue("Expected no keys, found " + keySet, keySet.size() == 0); + + } finally { + persistenceHandler.commitTx(); + } + } + + /** + * + */ + @Test + public void testRemoveAll() { + + try { + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + + List objects = tx.queryAll(MyClass.class.getName(), "@subType"); + tx.removeAll(objects); + + } finally { + persistenceHandler.commitTx(); + } + } + + /** + * + */ + @Test + public void testSize() { + + try { + XmlPersistenceTransaction tx = persistenceHandler.openTx(); + + long size = tx.querySize(MyClass.class.getName(), "@subType"); + Assert.assertTrue("Expected size = 0, found: " + size, size == 0); + + } finally { + persistenceHandler.commitTx(); + } + } +} diff --git a/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClass.java b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClass.java new file mode 100644 index 000000000..9b333a08e --- /dev/null +++ b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClass.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.featherlite.plugin.xmlpers.test.impl; + +import ch.eitchnet.utils.objectfilter.ITransactionObject; + +/** + * @author Robert von Burg + * + */ +public class MyClass implements ITransactionObject { + + private long txId; + private String id; + private String name; + private String type; + + /** + * @param id + * @param name + * @param type + */ + public MyClass(String id, String name, String type) { + super(); + this.id = id; + this.name = name; + this.type = type; + } + + /** + * @return the id + */ + public String getId() { + return this.id; + } + + /** + * @param id + * the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * @param name + * the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the type + */ + public String getType() { + return this.type; + } + + /** + * @param type + * the type to set + */ + public void setType(String type) { + this.type = type; + } + + /** + * @see ch.eitchnet.utils.objectfilter.ITransactionObject#setTransactionID(long) + */ + @Override + public void setTransactionID(long id) { + this.txId = id; + } + + /** + * @see ch.eitchnet.utils.objectfilter.ITransactionObject#getTransactionID() + */ + @Override + public long getTransactionID() { + return this.txId; + } + + /** + * @see ch.eitchnet.utils.objectfilter.ITransactionObject#resetTransactionID() + */ + @Override + public void resetTransactionID() { + this.txId = ITransactionObject.UNSET; + } +} diff --git a/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClassDao.java b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClassDao.java new file mode 100644 index 000000000..ec148642d --- /dev/null +++ b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyClassDao.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.featherlite.plugin.xmlpers.test.impl; + +import org.w3c.dom.DOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Text; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import ch.eitchnet.xmlpers.XmlDao; + +/** + * @author Robert von Burg + * + */ +public class MyClassDao implements XmlDao { + + /** + * @see ch.eitchnet.xmlpers.XmlDao#getType(java.lang.Object) + */ + @Override + public String getType(MyClass object) { + return MyClass.class.getName(); + } + + /** + * @see ch.eitchnet.xmlpers.XmlDao#getSubType(java.lang.Object) + */ + @Override + public String getSubType(MyClass object) { + return object.getType(); + } + + /** + * @see ch.eitchnet.xmlpers.XmlDao#getId(java.lang.Object) + */ + @Override + public String getId(MyClass object) { + return object.getId(); + } + + /** + * @see ch.eitchnet.xmlpers.XmlDao#serializeToDom(java.lang.Object, org.w3c.dom.DOMImplementation) + */ + @Override + public Document serializeToDom(MyClass object, DOMImplementation domImplementation) { + + Document document = domImplementation.createDocument(null, null, null); + Element element = document.createElement("MyClass"); + document.appendChild(element); + + element.setAttribute("id", object.getId()); + element.setAttribute("type", object.getType()); + + Element nameElement = document.createElement("Name"); + element.appendChild(nameElement); + Text textNode = document.createTextNode(object.getName()); + nameElement.appendChild(textNode); + + return document; + } + + /** + * @see ch.eitchnet.xmlpers.XmlDao#parseFromDom(org.w3c.dom.Element) + */ + @Override + public MyClass parseFromDom(Element element) { + + String id = element.getAttribute("id"); + String type = element.getAttribute("type"); + Element nameElement = (Element) element.getElementsByTagName("Name").item(0); + String name = nameElement.getTextContent(); + + MyClass myClass = new MyClass(id, name, type); + + return myClass; + } + + /** + * @see ch.eitchnet.xmlpers.XmlDao#serializeToSax(java.lang.Object, org.xml.sax.ContentHandler) + */ + @Override + public void serializeToSax(MyClass object, ContentHandler contentHandler) { + + try { + contentHandler.startDocument(); + + // MyClass element / root + { + AttributesImpl atts = new AttributesImpl(); + atts.addAttribute("", "", "id", "", object.getId()); + atts.addAttribute("", "", "type", "", object.getType()); + contentHandler.startElement("", "", "MyClass", atts); + + // name element + { + contentHandler.startElement("", "", "Name", null); + char[] nameArr = object.getName().toCharArray(); + contentHandler.characters(nameArr, 0, nameArr.length); + contentHandler.endElement("", "", "name"); + } + + // MyClass end + contentHandler.endElement("", "", "MyClass"); + } + + // end document + contentHandler.endDocument(); + + } catch (SAXException e) { + throw new RuntimeException("Failed to serialize " + object + " to SAX", e); + } + } + +} diff --git a/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyDaoFactory.java b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyDaoFactory.java new file mode 100644 index 000000000..1a2b4fcd7 --- /dev/null +++ b/test/ch/eitchnet/featherlite/plugin/xmlpers/test/impl/MyDaoFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.featherlite.plugin.xmlpers.test.impl; + +import ch.eitchnet.xmlpers.XmlDao; +import ch.eitchnet.xmlpers.XmlDaoFactory; + +/** + * @author Robert von Burg + * + */ +public class MyDaoFactory implements XmlDaoFactory { + + /** + * @see ch.eitchnet.xmlpers.XmlDaoFactory#getDao(java.lang.String) + */ + @SuppressWarnings("unchecked") + @Override + public XmlDao getDao(String type) { + if (type.equals(MyClass.class.getName())) + return (XmlDao) new MyClassDao(); + + throw new RuntimeException("Class with type " + type + " is unknown!"); + } + +}