Created
March 18, 2019 01:30
-
-
Save Manevolent/02f8d13571d25f742a0c93bc5ec70678 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.github.manevolent.jbot.database; | |
import com.google.common.collect.MapMaker; | |
import org.hibernate.*; | |
import org.hibernate.boot.model.naming.*; | |
import org.hibernate.cfg.Configuration; | |
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; | |
import org.hibernate.type.Type; | |
import javax.persistence.EntityManager; | |
import java.io.Serializable; | |
import java.util.*; | |
import java.util.function.Function; | |
import java.util.stream.Collectors; | |
public class HibernateManager implements DatabaseManager { | |
private static final String tableNamingFormat = "%s_%s"; | |
private final Properties properties; | |
private final Object entityLock = new Object(); | |
private final Map<String, EntityMapping> entityByName = new LinkedHashMap<>(); | |
private final Set<EntityMapping> entities = new LinkedHashSet<>(); | |
private final Map<String, com.github.manevolent.jbot.database.Database> databases = new LinkedHashMap<>(); | |
/** | |
* This naming strategy allows tables to be implicitly named via a globally-acceptable naming format | |
* (see tableNamingFormat) | |
* | |
* This way, tables can be named uniquely per database. | |
*/ | |
private final ImplicitNamingStrategy implicitNamingStrategy = new ImplicitNamingStrategyJpaCompliantImpl() { | |
public Identifier determinePrimaryTableName(ImplicitEntityNameSource source) { | |
Identifier identifier = super.determinePrimaryTableName(source); | |
EntityMapping entityMapping = entityByName.get(source.getEntityNaming().getClassName()); | |
if (entityMapping == null) throw new IllegalArgumentException(source.getEntityNaming().getClassName()); | |
// Build new identifier | |
return new Identifier( | |
String.format(tableNamingFormat, entityMapping.database.getName(), identifier.getText()), | |
identifier.isQuoted() | |
); | |
} | |
}; | |
/** | |
* Physical naming strategy is not controlled | |
*/ | |
private final PhysicalNamingStrategy physicalNamingStrategy = new PhysicalNamingStrategyStandardImpl() { | |
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { | |
return super.toPhysicalTableName(name, context); | |
} | |
}; | |
/** | |
* Interceptor provides global (cross-SessionFactory) caching mechanism for the system | |
*/ | |
private final Interceptor interceptor = new EmptyInterceptor() { | |
@Override | |
public boolean onLoad(Object entity, Serializable key, Object[] values, String[] properties, Type[] types) | |
throws CallbackException { | |
Class<?> clazz = entity.getClass(); | |
EntityMapping mapping = entityByName.get(clazz.getName()); | |
if (mapping == null) return false; | |
mapping.putInstance(key, entity); | |
return true; | |
} | |
@Override | |
public Object getEntity(String entityName, Serializable id) { | |
EntityMapping mapping = entityByName.get(entityName); | |
if (mapping == null) return null; | |
return entityByName.get(entityName).getInstance(id); | |
} | |
}; | |
public HibernateManager(Properties properties) { | |
this.properties = new Properties(); | |
for (String property : properties.stringPropertyNames()) | |
this.properties.setProperty(property, properties.getProperty(property)); | |
this.properties.setProperty("hibernate.enable_lazy_load_no_trans", "true"); | |
} | |
public Collection<Class<?>> getEntities() { | |
return Collections.unmodifiableCollection( | |
entities.stream() | |
.map(EntityMapping::getEntityClass) | |
.collect(Collectors.toList()) | |
); | |
} | |
private EntityMapping registerEntityClass(Database database, Class<?> clazz) { | |
synchronized (entityLock) { | |
EntityMapping mapping = new EntityMapping(clazz, database); | |
entities.add(mapping); | |
entityByName.put(clazz.getName(), mapping); | |
return mapping; | |
} | |
} | |
/** | |
* Builds a new SessionFactory given the specific graph objects. | |
* @return SessionFactory instance. | |
*/ | |
private SessionFactory buildFactory(Collection<Class<?>> classes) { | |
Configuration configuration = new Configuration(); | |
configuration.addProperties(properties); | |
configuration.setPhysicalNamingStrategy(physicalNamingStrategy); | |
configuration.setImplicitNamingStrategy(implicitNamingStrategy); | |
configuration.setInterceptor(interceptor); | |
classes.forEach(configuration::addAnnotatedClass); | |
return configuration.buildSessionFactory(); | |
} | |
@Override | |
public Collection<com.github.manevolent.jbot.database.Database> getDatabases() { | |
return Collections.unmodifiableCollection(databases.values()); | |
} | |
@Override | |
public com.github.manevolent.jbot.database.Database defineDatabase(String name, | |
Function<Database.ModelConstructor, | |
com.github.manevolent.jbot.database.Database> function) { | |
return databases.computeIfAbsent(name, key -> { | |
Database.ModelConstructor constructor = new com.github.manevolent.jbot.database.Database.ModelConstructor() | |
{ | |
private final Set<com.github.manevolent.jbot.database.Database> dependentDatabases | |
= new LinkedHashSet<>(); | |
/** | |
* All entities that must be accessible by this database, including self entities as defined below. | |
*/ | |
private final Set<Class<?>> allEntities = new LinkedHashSet<>(); | |
/** | |
* Entities specific to this database at the dependency node level | |
*/ | |
private final Set<Class<?>> selfEntities = new LinkedHashSet<>(); | |
@Override | |
public String getDatabaseName() { | |
return key; | |
} | |
/** | |
* Recursive function used to obtain all needed entity class definitions for this dependency tree | |
* @param database database to depend | |
*/ | |
private void dependIntl(com.github.manevolent.jbot.database.Database database) { | |
if (!(database instanceof Database)) | |
throw new IllegalArgumentException( | |
"database", | |
new ClassCastException( | |
databases.getClass().getName() + | |
" cannot be cast to " + | |
Database.class.getName())); | |
if (((Database) database).instance != HibernateManager.this) { | |
throw new IllegalArgumentException( | |
"database", | |
new IllegalAccessException( | |
database.getClass().getName() | |
+ " was created by a different instance of " + | |
HibernateManager.class.getName())); | |
} | |
// depend on database self entities in the respective tree for this database | |
((Database) database).selfEntities.forEach(this::registerDependentEntity); | |
// depend on this database's children, as well. | |
for (com.github.manevolent.jbot.database.Database child : | |
((Database) database).dependentDatabases) | |
dependIntl(child); | |
} | |
@Override | |
public Database.ModelConstructor depend(com.github.manevolent.jbot.database.Database database) { | |
dependIntl(database); | |
dependentDatabases.add(database); | |
return this; | |
} | |
private void registerDependentEntity(Class<?> aClass) { | |
// recognize in the large list | |
if (!allEntities.add(aClass)) | |
throw new IllegalStateException("entity class " + aClass.getName() + " already registered"); | |
} | |
private void registerSelfEntity(Class<?> aClass) { | |
// recognize in self list (used for future dependency graphing) | |
if (!selfEntities.add(aClass)) | |
throw new IllegalStateException("entity class " + aClass.getName() + " already registered"); | |
} | |
@Override | |
public Database.ModelConstructor registerEntity(Class<?> aClass) { | |
registerDependentEntity(aClass); // done for HashSet duplication checking | |
registerSelfEntity(aClass); // done for future dependency resolution | |
return this; | |
} | |
@Override | |
public Database define() { | |
return new Database(name, selfEntities, allEntities, dependentDatabases); | |
} | |
}; | |
return function.apply(constructor); | |
}); | |
} | |
private class Database implements com.github.manevolent.jbot.database.Database { | |
private final HibernateManager instance = HibernateManager.this; | |
private final String name; | |
private final Collection<EntityMapping> selfMappings = new LinkedHashSet<>(); | |
private final Collection<Class<?>> selfEntities; | |
private final Collection<Class<?>> allEntities; | |
private final Collection<com.github.manevolent.jbot.database.Database> dependentDatabases; | |
private final SessionFactory sessionFactory; | |
public Database(String name, | |
Collection<Class<?>> selfEntities, | |
Collection<Class<?>> allEntities, | |
Collection<com.github.manevolent.jbot.database.Database> dependentDatabases) { | |
this.name = name; | |
this.selfEntities = selfEntities; | |
this.allEntities = allEntities; | |
this.dependentDatabases = dependentDatabases; | |
this.sessionFactory = buildSessionFactory(); | |
} | |
private SessionFactory buildSessionFactory() { | |
// register own entities | |
selfEntities.forEach(clazz -> selfMappings.add(registerEntityClass(this, clazz))); | |
// build the SessionFactory used to interact with this model graph | |
return buildFactory(allEntities); | |
} | |
@Override | |
public String getName() { | |
return name; | |
} | |
@Override | |
public Collection<Class<?>> getEntities() { | |
return Collections.unmodifiableCollection(selfEntities); | |
} | |
@Override | |
public Collection<com.github.manevolent.jbot.database.Database> getDependentDatabases() { | |
return Collections.unmodifiableCollection(dependentDatabases); | |
} | |
@Override | |
public boolean isClosed() { | |
return sessionFactory.isClosed(); | |
} | |
@Override | |
public EntityManager openSession() { | |
return sessionFactory.openSession(); | |
} | |
@Override | |
public int hashCode() { | |
return getName().hashCode(); | |
} | |
@Override | |
public void close() { | |
sessionFactory.close(); | |
for (EntityMapping mapping : selfMappings) | |
mapping.clearPersistence(); | |
} | |
} | |
private class EntityMapping { | |
private final Class<?> clazz; | |
private final Database database; | |
private final Map<Serializable, Object> persistenceMap = new MapMaker().weakValues().makeMap(); | |
private EntityMapping(Class<?> clazz, Database database) { | |
this.clazz = clazz; | |
this.database = database; | |
} | |
public Class<?> getEntityClass() { | |
return clazz; | |
} | |
public Database getDatabase() { | |
return database; | |
} | |
public void clearPersistence() { | |
persistenceMap.clear(); | |
} | |
public Object getInstance(Serializable key) { | |
return persistenceMap.get(key); | |
} | |
public Object putInstance(Serializable key, Object instance) { | |
return persistenceMap.put(key, instance); | |
} | |
@Override | |
public int hashCode() { | |
return clazz.hashCode() ^ database.hashCode(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment