/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2017 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jpos.ee;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.hibernate.*;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.resource.transaction.spi.TransactionStatus;
import org.hibernate.stat.SessionStatistics;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.schema.TargetType;
import org.jpos.core.ConfigurationException;
import org.jpos.ee.support.ModuleUtils;
import org.jpos.space.Space;
import org.jpos.space.SpaceFactory;
import org.jpos.util.Log;
import org.jpos.util.LogEvent;
import org.jpos.util.Logger;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.util.*;
/**
* @author Alejandro P. Revilla
* @version $Revision: 1.5 $ $Date: 2004/12/09 00:50:14 $
* <p>
* DB encapsulate some housekeeping specific
* to Hibernate O/R mapping engine
*/
@SuppressWarnings({"UnusedDeclaration"})
public class DB implements Closeable
{
Session session;
Log log;
String configModifier;
private static volatile SessionFactory sessionFactory = null;
private static String propFile;
private static final String MODULES_CONFIG_PATH = "META-INF/org/jpos/ee/modules/";
/**
* Creates DB Object using default Hibernate instance
*/
public DB()
{
super();
}
/**
* Creates DB Object using a config <i>modifier</i>.
*
* The <i>configModifier</i> can take a number of different forms used to locate the <code>cfg/db.properties</code> file.
*
* <ul>
*
* <li>a filename prefix, i.e.: "mysql" used as modifier would pick the configuration from
* <code>cfg/mysql-db.properties</code> instead of the default <code>cfg/db.properties</code> </li>
*
* <li>if a colon and a second modifier is present (e.g.: "mysql:v1"), the metadata is picked from
* <code>META-INF/org/jpos/ee/modules/v1-*</code> instead of just
* <code>META-INF/org/jpos/ee/modules/</code> </li>
*
* <li>finally, if the modifier ends with <code>.hbm.xml</code> (case insensitive), then all configuration
* is picked from that config file.</li>
* </ul>
*
* @param configModifier modifier
*/
public DB (String configModifier) {
super();
this.configModifier = configModifier;
}
/**
* Creates DB Object using default Hibernate instance
*
* @param log Log object
*/
public DB(Log log)
{
super();
setLog(log);
}
/**
* Creates DB Object using default Hibernate instance
*
* @param log Log object
* @param configModifier modifier
*/
public DB(Log log, String configModifier) {
this(configModifier);
setLog(log);
}
/**
* @return Hibernate's session factory
*/
public SessionFactory getSessionFactory()
{
if (sessionFactory == null)
{
synchronized (DB.class)
{
if (sessionFactory == null)
{
try
{
sessionFactory = newSessionFactory();
}
catch (IOException | ConfigurationException | DocumentException e)
{
throw new RuntimeException("Could not configure session factory", e);
}
}
}
}
return sessionFactory;
}
public static synchronized void invalidateSessionFactory()
{
sessionFactory = null;
}
private SessionFactory newSessionFactory() throws IOException, ConfigurationException, DocumentException {
return getMetadata().buildSessionFactory();
}
private void configureProperties(Configuration cfg) throws IOException
{
String propFile = System.getProperty("DB_PROPERTIES", "cfg/db.properties");
Properties dbProps = loadProperties(propFile);
if (dbProps != null)
{
cfg.addProperties(dbProps);
}
}
private void configureMappings(Configuration cfg) throws ConfigurationException, IOException {
try {
List<String> moduleConfigs = ModuleUtils.getModuleEntries("META-INF/org/jpos/ee/modules/");
for (String moduleConfig : moduleConfigs) {
addMappings(cfg, moduleConfig);
}
} catch (DocumentException e) {
throw new ConfigurationException("Could not parse mappings document", e);
}
}
private void addMappings(Configuration cfg, String moduleConfig) throws ConfigurationException, DocumentException
{
Element module = readMappingElements(moduleConfig);
if (module != null)
{
for (Iterator l = module.elementIterator("mapping"); l.hasNext(); )
{
Element mapping = (Element) l.next();
parseMapping(cfg, mapping, moduleConfig);
}
}
}
private void parseMapping(Configuration cfg, Element mapping, String moduleName) throws ConfigurationException
{
final String resource = mapping.attributeValue("resource");
final String clazz = mapping.attributeValue("class");
if (resource != null)
{
cfg.addResource(resource);
}
else if (clazz != null)
{
try
{
cfg.addAnnotatedClass(ReflectHelper.classForName(clazz));
}
catch (ClassNotFoundException e)
{
throw new ConfigurationException("Class " + clazz + " specified in mapping for module " + moduleName + " cannot be found");
}
}
else
{
throw new ConfigurationException("<mapping> element in configuration specifies no known attributes at module " + moduleName);
}
}
private Element readMappingElements(String moduleConfig) throws DocumentException
{
SAXReader reader = new SAXReader();
final URL url = getClass().getClassLoader().getResource(moduleConfig);
assert url != null;
final Document doc = reader.read(url);
return doc.getRootElement().element("mappings");
}
private Properties loadProperties(String filename) throws IOException
{
Properties props = new Properties();
final String s = filename.replaceAll("/", "\\" + File.separator);
final File f = new File(s);
if (f.exists())
{
props.load(new FileReader(f));
}
return props;
}
/**
* Creates database schema
*
* @param outputFile optional output file (may be null)
* @param create true to actually issue the create statements
*/
public void createSchema(String outputFile, boolean create) throws HibernateException, DocumentException {
try {
// SchemaExport export = new SchemaExport(getMetadata());
SchemaExport export = new SchemaExport();
List<TargetType> targetTypes=new ArrayList<>();
if (outputFile != null) {
if(outputFile.trim().equals("-")) targetTypes.add(TargetType.STDOUT);
else {
export.setOutputFile(outputFile);
export.setDelimiter(";");
targetTypes.add(TargetType.SCRIPT);
}
}
if (create)
targetTypes.add(TargetType.DATABASE);
if(targetTypes.size()>0)
export.create(EnumSet.copyOf(targetTypes), getMetadata());
}
catch (IOException | ConfigurationException e)
{
throw new HibernateException("Could not create schema", e);
}
}
/**
* open a new HibernateSession if none exists
*
* @return HibernateSession associated with this DB object
* @throws HibernateException
*/
public synchronized Session open() throws HibernateException
{
if (session == null)
{
session = getSessionFactory().openSession();
}
return session;
}
/**
* close hibernate session
*
* @throws HibernateException
*/
public synchronized void close() throws HibernateException
{
if (session != null)
{
session.close();
session = null;
}
}
/**
* @return session hibernate Session
*/
public Session session()
{
return session;
}
/**
* handy method used to avoid having to call db.session().save (xxx)
*
* @param obj to save
*/
public void save(Object obj) throws HibernateException
{
session.save(obj);
}
/**
* handy method used to avoid having to call db.session().saveOrUpdate (xxx)
*
* @param obj to save or update
*/
public void saveOrUpdate(Object obj) throws HibernateException
{
session.saveOrUpdate(obj);
}
public void delete(Object obj)
{
session.delete(obj);
}
/**
* @return newly created Transaction
* @throws HibernateException
*/
public synchronized Transaction beginTransaction() throws HibernateException
{
return session.beginTransaction();
}
public synchronized void commit()
{
if (session() != null)
{
Transaction tx = session().getTransaction();
if (tx != null && tx.getStatus().isOneOf(TransactionStatus.ACTIVE))
{
tx.commit();
}
}
}
public synchronized void rollback()
{
if (session() != null)
{
Transaction tx = session().getTransaction();
if (tx != null && tx.getStatus().canRollback())
{
tx.rollback();
}
}
}
/**
* @param timeout in seconds
* @return newly created Transaction
* @throws HibernateException
*/
public synchronized Transaction beginTransaction(int timeout) throws HibernateException
{
Transaction tx = session.getTransaction();
if (timeout > 0)
tx.setTimeout(timeout);
tx.begin();
return tx;
}
public synchronized Log getLog()
{
if (log == null)
{
log = Log.getLog("Q2", "DB"); // Q2 standard Logger
}
return log;
}
public synchronized void setLog(Log log)
{
this.log = log;
}
public static Object exec(DBAction action) throws Exception
{
try (DB db = new DB())
{
db.open();
return action.exec(db);
}
}
public static Object execWithTransaction(DBAction action) throws Exception
{
try (DB db = new DB())
{
db.open();
db.beginTransaction();
Object obj = action.exec(db);
db.commit();
return obj;
}
}
@SuppressWarnings("unchecked")
public static <T> T unwrap (T proxy) {
Hibernate.getClass(proxy);
Hibernate.initialize(proxy);
return (proxy instanceof HibernateProxy) ?
(T) ((HibernateProxy) proxy).getHibernateLazyInitializer().getImplementation() : proxy;
}
@SuppressWarnings({"unchecked"})
public void printStats()
{
if (getLog() != null)
{
LogEvent info = getLog().createInfo();
if (session != null)
{
info.addMessage("==== STATISTICS ====");
SessionStatistics statistics = session().getStatistics();
info.addMessage("==== ENTITIES ====");
Set<EntityKey> entityKeys = statistics.getEntityKeys();
for (EntityKey ek : entityKeys)
{
info.addMessage(String.format("[%s] %s", ek.getIdentifier(), ek.getEntityName()));
}
info.addMessage("==== COLLECTIONS ====");
Set<CollectionKey> collectionKeys = statistics.getCollectionKeys();
for (CollectionKey ck : collectionKeys)
{
info.addMessage(String.format("[%s] %s", ck.getKey(), ck.getRole()));
}
info.addMessage("=====================");
}
else
{
info.addMessage("Session is not open");
}
Logger.log(info);
}
}
private MetadataImplementor getMetadata() throws IOException, ConfigurationException, DocumentException {
StandardServiceRegistryBuilder ssrb = new StandardServiceRegistryBuilder();
String propFile;
String dbPropertiesPrefix = "";
String metadataPrefix = "";
if (configModifier != null) {
String[] ss = configModifier.split(":");
if (ss.length > 0)
dbPropertiesPrefix = ss[0] + "-";
if (ss.length > 1)
metadataPrefix = ss[1] + "-";
}
String hibCfg = System.getProperty("HIBERNATE_CFG","/" + dbPropertiesPrefix + "hibernate.cfg.xml");
if (getClass().getClassLoader().getResource(hibCfg) == null)
hibCfg = null;
if (hibCfg == null)
hibCfg = System.getProperty("HIBERNATE_CFG","/hibernate.cfg.xml");
ssrb.configure(hibCfg);
propFile = System.getProperty(dbPropertiesPrefix + "DB_PROPERTIES", "cfg/" + dbPropertiesPrefix + "db.properties");
Properties dbProps = loadProperties(propFile);
if (dbProps != null) {
for (Map.Entry entry : dbProps.entrySet()) {
ssrb.applySetting((String) entry.getKey(), entry.getValue());
}
}
// if DBInstantiator has put db user name and/or password in Space, set Hibernate config accordingly
Space sp = SpaceFactory.getSpace("tspace:dbconfig");
String user = (String) sp.inp("connection.username");
String pass = (String) sp.inp("connection.password");
if (user != null)
ssrb.applySetting("hibernate.connection.username", user);
if (pass != null)
ssrb.applySetting("hibernate.connection.password", pass);
MetadataSources mds = new MetadataSources(ssrb.build());
List<String> moduleConfigs = ModuleUtils.getModuleEntries(MODULES_CONFIG_PATH);
for (String moduleConfig : moduleConfigs) {
if (metadataPrefix.length() == 0 || moduleConfig.substring(MODULES_CONFIG_PATH.length()).startsWith(metadataPrefix)) {
addMappings(mds, moduleConfig);
}
}
return (MetadataImplementor) mds.buildMetadata();
}
private void addMappings(MetadataSources mds, String moduleConfig) throws ConfigurationException, DocumentException
{
Element module = readMappingElements(moduleConfig);
if (module != null)
{
for (Iterator l = module.elementIterator("mapping"); l.hasNext(); )
{
Element mapping = (Element) l.next();
parseMapping(mds, mapping, moduleConfig);
}
}
}
private void parseMapping (MetadataSources mds, Element mapping, String moduleName) throws ConfigurationException
{
final String resource = mapping.attributeValue("resource");
final String clazz = mapping.attributeValue("class");
if (resource != null)
mds.addResource(resource);
else if (clazz != null)
mds.addAnnotatedClassName(clazz);
else
throw new ConfigurationException("<mapping> element in configuration specifies no known attributes at module " + moduleName);
}
}