/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. The ASF licenses this file to You
* 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. For additional information regarding
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/
package org.apache.roller.business.hibernate;
import java.io.StringBufferInputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.apache.roller.RollerException;
import org.apache.roller.pojos.Assoc;
import org.apache.roller.pojos.HierarchicalPersistentObject;
import org.apache.roller.pojos.PersistentObject;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.jdom.output.DOMOutputter;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
/**
* Base class for Hibernate persistence implementation.
*
* This class serves as a helper/util class for all of the Hibernate
* manager implementations by providing a set of basic persistence methods
* that can be easily reused.
*
*/
public class HibernatePersistenceStrategy {
static final long serialVersionUID = 2561090040518169098L;
protected static SessionFactory sessionFactory = null;
private static Log log = LogFactory.getLog(HibernatePersistenceStrategy.class);
/** No-op so XML parser doesn't hit the network looking for Hibernate DTDs */
private EntityResolver noOpEntityResolver = new EntityResolver() {
public InputSource resolveEntity(String publicId, String systemId) {
return new InputSource(new StringBufferInputStream(""));
}
};
public HibernatePersistenceStrategy() {
}
/**
* Construct self using Hibernate config resource and optional dialect.
* @param configResouce Classpath-based path to Hibernate config file (e.g. "/hibernate.cgf.xml")
* @parma dialect Classname of Hibernate dialect to be used (overriding any specified in the configResource)
*/
public HibernatePersistenceStrategy(
String configResource,
String dialect) throws Exception {
log.info("configResource: " + configResource);
log.info("dialect: " + dialect);
// read configResource into DOM form
SAXBuilder builder = new SAXBuilder();
builder.setEntityResolver(noOpEntityResolver);
Document configDoc = builder.build(
getClass().getResourceAsStream(configResource));
Element root = configDoc.getRootElement();
Element sessionFactoryElem = root.getChild("session-factory");
// remove any existing connection.datasource and dialect properties
List propertyElems = sessionFactoryElem.getChildren("property");
List removeList = new ArrayList();
for (Iterator it = propertyElems.iterator(); it.hasNext();) {
Element elem = (Element) it.next();
if (elem.getAttribute("name") != null
&& elem.getAttribute("name").getValue().equals("dialect")) {
removeList.add(elem);
}
}
for (Iterator it = removeList.iterator(); it.hasNext();) {
Element elem = (Element) it.next();
sessionFactoryElem.removeContent(elem);
}
// add Roller dialect property
Element prop = new Element("property").setAttribute(
new Attribute("name","dialect"));
prop.addContent(dialect);
sessionFactoryElem.addContent(prop);
Configuration config = new Configuration();
DOMOutputter outputter = new DOMOutputter();
config.configure(outputter.output(configDoc));
this.sessionFactory = config.buildSessionFactory();
}
/**
* Construct self using Hibernate config resource and optional dialect.
* @param configResouce Classpath-based path to Hibernate config file (e.g. "/hibernate.cgf.xml")
* @parma dialect Classname of Hibernate dialect to be used (or null to use one specified in configResource)
*/
public HibernatePersistenceStrategy(
String configResource,
String dialect,
String driverClass,
String connectionURL,
String username,
String password) throws Exception {
log.info("configResource: " + configResource);
log.info("dialect: " + dialect);
log.info("driverClass: " + driverClass);
log.info("connectionURL: " + connectionURL);
log.info("username: " + username);
// read configResource into DOM form
SAXBuilder builder = new SAXBuilder();
builder.setEntityResolver(noOpEntityResolver);
Document configDoc = builder.build(
getClass().getResourceAsStream(configResource));
Element root = configDoc.getRootElement();
Element sessionFactoryElem = root.getChild("session-factory");
// remove any existing connection.datasource and dialect properties
List propertyElems = sessionFactoryElem.getChildren("property");
List removeList = new ArrayList();
for (Iterator it = propertyElems.iterator(); it.hasNext();) {
Element elem = (Element) it.next();
if (elem.getAttribute("name") != null
&& elem.getAttribute("name").getValue().equals("connection.datasource")) {
removeList.add(elem);
}
if (elem.getAttribute("name") != null
&& elem.getAttribute("name").getValue().equals("dialect")) {
removeList.add(elem);
}
}
for (Iterator it = removeList.iterator(); it.hasNext();) {
Element elem = (Element) it.next();
sessionFactoryElem.removeContent(elem);
}
// add JDBC connection params instead
Element prop = new Element("property").setAttribute(
new Attribute("name","hibernate.connection.driver_class"));
prop.addContent(driverClass);
sessionFactoryElem.addContent(prop);
prop = new Element("property").setAttribute(
new Attribute("name","hibernate.connection.url"));
prop.addContent(connectionURL);
sessionFactoryElem.addContent(prop);
prop = new Element("property").setAttribute(
new Attribute("name","hibernate.connection.username"));
prop.addContent(username);
sessionFactoryElem.addContent(prop);
prop = new Element("property").setAttribute(
new Attribute("name","hibernate.connection.password"));
prop.addContent(password);
sessionFactoryElem.addContent(prop);
prop = new Element("property").setAttribute(
new Attribute("name","dialect"));
prop.addContent(dialect);
sessionFactoryElem.addContent(prop);
Configuration config = new Configuration();
DOMOutputter outputter = new DOMOutputter();
config.configure(outputter.output(configDoc));
this.sessionFactory = config.buildSessionFactory();
}
/**
* Get persistence session on current thread.
*
* This will open a new Session if one is not already open, otherwise
* it will return the already open Session.
*/
public Session getSession() {
log.debug("Opening Hibernate Session");
// get Hibernate Session and make sure we are in a transaction
// this will join existing Session/Transaction if they exist
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
return session;
}
public void flush() throws RollerException {
Session session = getSession();
try {
session.getTransaction().commit();
} catch(Throwable t) {
// uh oh ... failed persisting, gotta release
release();
// wrap and rethrow so caller knows something bad happened
throw new RollerException(t);
}
}
/**
* Release database session, rollback any uncommitted changes.
*
* IMPORTANT: we don't want to open a transaction and force the use of a
* jdbc connection just to close the session and do a rollback, so this
* method must be sensitive about how the release is triggered.
*
* In particular we don't want to use our custom getSession() method which
* automatically begins a transaction. Instead we get a Session and check
* if there is already an active transaction that needs to be rolled back.
* If not then we can close the Session without ever getting a jdbc
* connection, which is important for scalability.
*/
public void release() {
try {
Session session = sessionFactory.getCurrentSession();
if(session != null && session.isOpen()) {
log.debug("Closing Hibernate Session");
try {
Transaction tx = session.getTransaction();
if(tx != null && tx.isActive()) {
log.debug("Forcing rollback on active transaction");
tx.rollback();
}
} catch(Throwable t) {
log.error("ERROR doing Hibernate rollback", t);
} finally {
if(session.isOpen()) {
session.close();
}
}
}
} catch(Throwable t) {
log.error("ERROR closing Hibernate Session", t);
}
}
/**
* Retrieve object. We return null if the object is not found.
*/
public PersistentObject load(String id, Class clazz) throws RollerException {
if(id == null || clazz == null) {
throw new RollerException("Cannot load objects when value is null");
}
return (PersistentObject) getSession().get(clazz, id);
}
/**
* Store object.
*/
public void store(PersistentObject obj) throws HibernateException {
if(obj == null) {
throw new HibernateException("Cannot save null object");
}
Session session = getSession();
// TODO BACKEND: this is wacky, we should double check logic here
// TODO BACKEND: better to use session.saveOrUpdate() here, if possible
if ( obj.getId() == null || obj.getId().trim().equals("") ) {
// Object has never been written to database, so save it.
// This makes obj into a persistent instance.
session.save(obj);
}
/*
* technically we shouldn't have any reason to support the saving
* of detached objects, so at some point we should re-evaluate this.
*
* objects should be re-attached before being saved again. it would
* be more appropriate to reject these kinds of saves because they are
* not really safe.
*
* NOTE: this may be coming from the way we use formbeans on the UI.
* we very commonly repopulate all data in a pojo (including id) from
* form data rather than properly loading the object from a Session
* then modifying its properties.
*/
if ( !session.contains(obj) ) {
log.debug("storing detached object: "+obj.toString());
// Object has been written to database, but instance passed in
// is not a persistent instance, so must be loaded into session.
PersistentObject vo =
(PersistentObject)session.load(obj.getClass(),obj.getId());
vo.setData(obj);
obj = vo;
}
}
/**
* Remove object.
*
* TODO BACKEND: force the use of remove(Object) moving forward.
*/
public void remove(String id, Class clazz) throws HibernateException {
if(id == null || clazz == null) {
throw new HibernateException("Cannot remove object when values are null");
}
Session session = getSession();
PersistentObject obj = (PersistentObject) session.load(clazz,id);
session.delete(obj);
}
/**
* Remove object.
*/
public void remove(PersistentObject obj) throws HibernateException {
if(obj == null) {
throw new HibernateException("Cannot remove null object");
}
// TODO BACKEND: can hibernate take care of this check for us?
// what happens if object does not use id?
// can't remove transient objects
if (obj.getId() != null) {
getSession().delete(obj);
}
}
/**
* Store hierarchical object.
*
* NOTE: if the object has proper cascade setting then is all this necessary?
*/
public void store(HierarchicalPersistentObject obj)
throws HibernateException, RollerException {
if(obj == null) {
throw new HibernateException("Cannot save null object");
}
log.debug("Storing hierarchical object "+obj);
Session session = getSession();
HierarchicalPersistentObject mNewParent = obj.getNewParent();
boolean fresh = (obj.getId() == null || "".equals(obj.getId()));
if (fresh) {
// Object has never been written to database, so save it.
// This makes obj into a persistent instance.
session.save(obj);
}
if(!session.contains(obj)) {
// Object has been written to database, but instance passed in
// is not a persistent instance, so must be loaded into session.
HierarchicalPersistentObject vo =
(HierarchicalPersistentObject)session.load(obj.getClass(),obj.getId());
vo.setData(obj);
obj = vo;
}
if (fresh) {
// Every fresh cat needs a parent assoc
Assoc parentAssoc = obj.createAssoc(
obj, mNewParent, Assoc.PARENT);
this.store(parentAssoc);
} else if (null != mNewParent) {
// New parent must be added to parentAssoc
Assoc parentAssoc = obj.getParentAssoc();
if(parentAssoc == null)
log.error("parent assoc is null");
parentAssoc.setAncestor(mNewParent);
this.store(parentAssoc);
}
// Clear out existing grandparent associations
Iterator ancestors = obj.getAncestorAssocs().iterator();
while (ancestors.hasNext()) {
Assoc assoc = (Assoc)ancestors.next();
if (assoc.getRelation().equals(Assoc.GRANDPARENT)) {
this.remove(assoc);
}
}
// Walk parent assocations, creating new grandparent associations
int count = 0;
Assoc currentAssoc = obj.getParentAssoc();
while (null != currentAssoc.getAncestor()) {
if (count > 0) {
Assoc assoc = obj.createAssoc(obj,
currentAssoc.getAncestor(),
Assoc.GRANDPARENT);
this.store(assoc);
}
currentAssoc = currentAssoc.getAncestor().getParentAssoc();
count++;
}
Iterator children = obj.getChildAssocs().iterator();
while (children.hasNext()) {
Assoc assoc = (Assoc) children.next();
// resetting parent will cause reset of ancestors links
assoc.getObject().setParent(obj);
// recursively...
this.store(assoc.getObject());
}
// Clear new parent now that new parent has been saved
mNewParent = null;
}
/**
* Store assoc.
*/
public void store(Assoc assoc) throws HibernateException {
if(assoc == null) {
throw new HibernateException("Cannot save null object");
}
getSession().saveOrUpdate(assoc);
}
/**
* Remove hierarchical object.
*
* NOTE: if the object has proper cascade setting then is all this necessary?
*/
public void remove(HierarchicalPersistentObject obj) throws RollerException {
if(obj == null) {
throw new RollerException("Cannot remove null object");
}
log.debug("Removing hierarchical object "+obj.getId());
// loop to remove all descendents and associations
List toRemove = new LinkedList();
List assocs = obj.getAllDescendentAssocs();
for (int i=assocs.size()-1; i>=0; i--) {
Assoc assoc = (Assoc)assocs.get(i);
HierarchicalPersistentObject hpo = assoc.getObject();
// remove my descendent's parent and grandparent associations
Iterator ancestors = hpo.getAncestorAssocs().iterator();
while (ancestors.hasNext()) {
Assoc dassoc = (Assoc)ancestors.next();
this.remove(dassoc);
}
// remove decendent association and descendents
//assoc.remove();
toRemove.add(hpo);
}
Iterator removeIterator = toRemove.iterator();
while (removeIterator.hasNext()) {
PersistentObject po = (PersistentObject) removeIterator.next();
getSession().delete(po);
}
// loop to remove my own parent and grandparent associations
Iterator ancestors = obj.getAncestorAssocs().iterator();
while (ancestors.hasNext()) {
Assoc assoc = (Assoc)ancestors.next();
this.remove(assoc);
}
getSession().delete(obj);
}
/**
* Remove assoc.
*/
public void remove(Assoc assoc) throws HibernateException {
if(assoc == null) {
throw new HibernateException("Cannot save null object");
}
getSession().delete(assoc);
}
}