nz.co.gregs.dbvolution.reflection.DataModel Maven / Gradle / Ivy
/*
* Copyright 2015 gregory.graham.
*
* 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 nz.co.gregs.dbvolution.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javassist.Modifier;
import nz.co.gregs.dbvolution.databases.DBDatabase;
import nz.co.gregs.dbvolution.DBQuery;
import nz.co.gregs.dbvolution.DBRow;
import nz.co.gregs.dbvolution.exceptions.DBRuntimeException;
import nz.co.gregs.dbvolution.internal.properties.PropertyWrapper;
import nz.co.gregs.dbvolution.internal.properties.RowDefinitionInstanceWrapper;
import nz.co.gregs.dbvolution.internal.properties.RowDefinitionWrapperFactory;
import org.reflections.Reflections;
/**
* Provides convenient access to classes, instances, and methods that may be of
* use during reflection.
*
*
* This is very experimental at the moment, use with caution.
*
*
* The intent of this class is to provide access to methods that might define
* database connections, the schema of those databases, and methods to
* manipulate the schema objects in a generic way.
*
*
Support DBvolution at
* Patreon
*
* @author gregory.graham
*/
public class DataModel {
private static Set storedRequiredTables;
private DataModel() {
}
/**
* Scans all known classes and returns a set of all DBDatabase instances that
* could be found.
*
*
* Excludes the standard DBDatabases in nz.co.gregs.dbvolution.databases.
*
*
* The intent of this method is to help provide access to classes that might
* define database connections.
*
*
Support DBvolution at
* Patreon
*
* @return a set of {@link DBDatabase} classes.
*/
protected static Set> getUseableDBDatabaseClasses() {
Reflections reflections = new Reflections("");
final Set> allKnownDBDatabases = reflections.getSubTypesOf(DBDatabase.class);
final Set> usefulDBDatabases = new HashSet>();
for (Class extends DBDatabase> known : allKnownDBDatabases) {
if (!known.getPackage().getName().equals("nz.co.gregs.dbvolution.databases")) {
usefulDBDatabases.add(known);
}
}
return usefulDBDatabases;
}
/**
* Finds the constructors for
* {@link #getUseableDBDatabaseClasses() all known databases}.
*
*
* Support DBvolution at
* Patreon
*
* @return a set of all constructors for all the known databases.
*/
public static Set> getDBDatabaseConstructors() {
Set> constructors = new HashSet>();
final Set> allKnownDBDatabases = getUseableDBDatabaseClasses();
for (Class extends DBDatabase> aDatabase : allKnownDBDatabases) {
@SuppressWarnings("unchecked")
Constructor[] cons = (Constructor[]) aDatabase.getDeclaredConstructors();
constructors.addAll(Arrays.asList(cons));
}
return constructors;
}
/**
* Scans across {@link #getUseableDBDatabaseClasses() all known databases} and
* finds no-parameter methods that return a DBDatabase object.
*
*
* The intent of this method is to find DBDatabase subclasses that define
* standard DBDatabase factory methods.
*
*
* For instance a useful pattern for your databases during development might
* be something like this:
*
*
* private static class TestPostgreSQL extends PostgresDB {
*
* protected static PostgresDB getTestDatabaseOnHost(String hostname) {
* return new PostgresDB(hostname, 5432, "testdb", "developer", "developer
* password");
* }
*
* protected static PostgresDB getTestDatabase() {
* return new PostgresDB("testdb.mycompany.com", 5432, "testdb", "developer",
* "developer password");
* }
*
* protected static PostgresDB getLocalTestDatabase() {
* return new PostgresDB("localhost", 5432, "testdb", "developer", "developer
* password");
* }
* }
*
*
*
*
* This method aims to find the getTestDatabase and getLocalTestDatabase
* methods. The method getTestDatabaseOnHost(String) is excluded as it
* requires a parameter to be supplied.
*
*
Support DBvolution at
* Patreon
*
* @return a list of methods defined in DBDatabase classes that return a
* DBDatabase and require no parameters.
*/
public static List getDBDatabaseSimpleCreationMethods() {
List allMethods = new ArrayList();
List meths = getDBDatabaseCreationMethods();
for (Method meth : meths) {
// weed out the clone methods, as they copy not create DBDatabases
if (getMethodParameterCount(meth) == 0) {
allMethods.add(meth);
}
}
return allMethods;
}
/**
* Scans across {@link #getUseableDBDatabaseClasses() all known databases} and
* finds methods that return a DBDatabase object.
*
*
* The intent of this method is to find DBDatabase subclasses that define
* standard DBDatabase factory methods.
*
*
* For instance a useful pattern for your databases during development might
* be something like this:
*
*
* private static class TestPostgreSQL extends PostgresDB {
*
* protected static PostgresDB getTestDatabaseOnHost(String hostname) {
* return new PostgresDB(hostname, 5432, "testdb", "developer", "developer
* password");
* }
*
* protected PostgresDB getTestDatabase() {
* return new PostgresDB("testdb.mycompany.com", 5432, "testdb", "developer",
* "developer password");
* }
*
* protected static PostgresDB getLocalTestDatabase() {
* return new PostgresDB("localhost", 5432, "testdb", "developer", "developer
* password");
* }
* }
*
*
*
*
* This method aims to find the getTestDatabaseOnHost(String),
* getTestDatabase(), getLocalTestDatabase() methods.
*
*
Support DBvolution at
* Patreon
*
* @return a list of methods defined in DBDatabase classes that return a
* DBDatabase.
*/
public static List getDBDatabaseCreationMethods() {
List allMethods = new ArrayList();
final Set> allKnownDBDatabases = getUseableDBDatabaseClasses();
for (Class extends DBDatabase> aDatabase : allKnownDBDatabases) {
@SuppressWarnings("unchecked")
Method[] meths = aDatabase.getDeclaredMethods();
for (Method meth : meths) {
// weed out the clone methods, as they copy not create DBDatabases
if (!(meth.getName().equals("clone"))
&& DBDatabase.class.isAssignableFrom(meth.getReturnType())) {
allMethods.add(meth);
}
}
}
return allMethods;
}
private static int getMethodParameterCount(Method meth) {
return meth.getParameterTypes().length;
}
/**
* Scans the classpath for DBDatabase subclasses and returns all static
* parameter-less methods that produce DBDatabase objects.
*
*
* The intent of this method is to find the easiest to use DBDatabase creation
* methods. That is DBDatabase creating methods that require no state or
* parameters.
*
*
* For instance a useful pattern for your databases during development might
* be something like this:
*
*
* private static class TestPostgreSQL extends PostgresDB {
*
* protected static PostgresDB getTestDatabaseOnHost(String hostname) {
* return new PostgresDB(hostname, 5432, "testdb", "developer", "developer
* password");
* }
*
* protected PostgresDB getTestDatabase() {
* return new PostgresDB("testdb.mycompany.com", 5432, "testdb", "developer",
* "developer password");
* }
*
* protected static PostgresDB getLocalTestDatabase() {
* return new PostgresDB("localhost", 5432, "testdb", "developer", "developer
* password");
* }
* }
*
*
*
*
* This method aims to find the getLocalTestDatabase() method.
*
*
Support DBvolution at
* Patreon
*
* @return a list of static methods with no parameters defined in DBDatabase
* classes that return a DBDatabase.
*/
public static List getDBDatabaseCreationMethodsStaticWithoutParameters() {
List creationMethods = new ArrayList();
for (Method meth : getDBDatabaseSimpleCreationMethods()) {
if (DBDatabase.class.isAssignableFrom(meth.getReturnType())) {
if ((getMethodParameterCount(meth) == 0) && (Modifier.isStatic(meth.getModifiers()))) {
creationMethods.add(meth);
}
}
}
return creationMethods;
}
/**
* Scan the classpath for DBDatabase subclasses with public parameterless
* constructors and return those constructors.
*
*
* The intent of this method is to find easy to use constructors that MAY
* directly create a usable DBDatabase object.
*
*
Support DBvolution at
* Patreon
*
* @return a list of easy to invoke DBDatabase constructors
*/
public static Set> getDBDatabaseConstructorsPublicWithoutParameters() {
Set> constructors = getDBDatabaseConstructors();
Set> parameterlessConstructors = new HashSet>();
for (Constructor constructor : constructors) {
if (constructor.getParameterTypes().length == 0 && Modifier.isPublic(constructor.getModifiers())) {
parameterlessConstructors.add(constructor);
}
}
return parameterlessConstructors;
}
/**
* Using {@link #getDBDatabaseConstructorsPublicWithoutParameters() } creates
* instances of the accessible DBDatabases.
*
* Support DBvolution at
* Patreon
*
* @return all the instances that can be created from the constructors found
* by {@link #getDBDatabaseConstructorsPublicWithoutParameters() }.
*/
public static List getDBDatabaseInstancesWithoutParameters() {
ArrayList databaseInstances = new ArrayList();
Set> constructors = getDBDatabaseConstructorsPublicWithoutParameters();
for (Constructor constr : constructors) {
constr.setAccessible(true);
try {
DBDatabase newInstance = constr.newInstance();
databaseInstances.add(newInstance);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(DataModel.class.getName()).log(Level.SEVERE, null, ex);
}
}
return databaseInstances;
}
/**
* Find all DBRow subclasses on the current classpath.
*
* Support DBvolution at
* Patreon
*
* @return all the subclasses of DBRow in the current classpath.
*/
public static Set> getDBRowSubclasses() {
Reflections reflections = new Reflections("");
return reflections.getSubTypesOf(DBRow.class);
}
/**
* Find all DBRow subclasses on the current classpath.
*
* Support DBvolution at
* Patreon
*
* @return all the subclasses of DBRow in the current classpath.
*/
public static Set> getDBRowDirectSubclasses() {
Reflections reflections = new Reflections("");
Set> subTypesOf = reflections.getSubTypesOf(DBRow.class);
Set> result = new HashSet>();
for (Class extends DBRow> clzz : subTypesOf) {
try {
clzz.getConstructor();// checking that there an appropriate constructor
if (clzz.getGenericSuperclass().equals(DBRow.class)) {
result.add(clzz);
}
} catch (NoSuchMethodException | SecurityException ex) {
;// no constructor
}
}
return result;
}
/**
* Find all DBRow subclasses on the current classpath.
*
* Support DBvolution at
* Patreon
*
* @return all the subclasses of DBRow in the current classpath.
*/
public synchronized static Set< DBRow> getRequiredTables() {
if (storedRequiredTables == null || storedRequiredTables.isEmpty()) {
Set< DBRow> result = new HashSet<>(0);
Set> dbRowDirectSubclasses = getDBRowDirectSubclasses();
for (Class extends DBRow> clzz : dbRowDirectSubclasses) {
DBRow dbRow = DBRow.getDBRow(clzz);
if (dbRow.isRequiredTable()) {
result.add(dbRow);
}
}
storedRequiredTables = result;
}
return storedRequiredTables;
}
/**
* Find all DBRow subclasses on the current classpath, minus the example
* classes found in DBvolution.
*
* Support DBvolution at
* Patreon
*
* @return all the subclasses of DBRow in the current classpath, except DBV's
* examples.
*/
public static Set> getDBRowClassesExcludingDBvolutionExamples() {
Set> dbRowClasses = getDBRowSubclasses();
HashSet> returnSet = new HashSet>();
for (Class extends DBRow> dbrowClass : dbRowClasses) {
if (!dbrowClass.getPackage().getName().startsWith("nz.co.gregs.dbvolution")) {
returnSet.add(dbrowClass);
}
}
return returnSet;
}
/**
* Using the classes found by {@link #getDBRowSubclasses() }, creates an
* instance of as many classes as possible.
*
* Support DBvolution at
* Patreon
*
* @return an instance of every DBRow class that can be found and created
* easily.
*/
public static List getDBRowInstances() {
return getDBRowInstances(getDBRowSubclasses());
}
/**
* Using the classes supplied creates an instance of as many classes as
* possible.
*
* Support DBvolution at
* Patreon
*
* @param dbRowClasses
* @return an instance of every DBRow class that can be found and created
* easily.
*/
public static List getDBRowInstances(Collection> dbRowClasses) {
List dbrows = new ArrayList();
for (Class extends DBRow> dbRowClass : dbRowClasses) {
try {
DBRow newInstance = dbRowClass.newInstance();
dbrows.add(newInstance);
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(DataModel.class.getName()).log(Level.INFO, null, ex);
}
}
return dbrows;
}
/**
* Creates a query from a string of tables,fields, and values separated by 3
* different separators.
*
*
* Intended to create a DBQuery from a string like
* {@code carcompany.name=toyota&marque.name=toyota} using the separators
* "&", ".", and "=".
*
*
* The greater intent is an extensible mechanism for parsing web links and
* creating queries from them.
*
* @param db
* @param encodedTablesPropertiesAndValues
* @param interpreter
*
Support DBvolution at
* Patreon
* @return a DBQuery.
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws IllegalAccessException
*/
public static DBQuery createDBQueryFromEncodedTablesPropertiesAndValues(DBDatabase db, String encodedTablesPropertiesAndValues, EncodingInterpreter interpreter) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
final RowDefinitionWrapperFactory rowDefinitionWrapperFactory = new RowDefinitionWrapperFactory();
final Map foundAlready = new HashMap();
String[] parameters = interpreter.splitParameters(encodedTablesPropertiesAndValues);
for (String parameter : parameters) {
String table = interpreter.getDBRowClassName(parameter);
DBRow newInstance = foundAlready.get(table);
if (newInstance == null) {
Class> tableClass = Class.forName(table);
if (DBRow.class.isAssignableFrom(tableClass)) {
newInstance = (DBRow) tableClass.newInstance();
foundAlready.put(table, newInstance);
} else {
throw new DBRuntimeException("Class Specified Is Not A DBRow Sub-Class: expected " + tableClass + "(derived from " + table + ") to be a DBRow subclass but it was not. Please only use DBRows with this method.");
}
}
if (newInstance != null) {
String propertyName = interpreter.getPropertyName(parameter);
if (propertyName != null) {
String value = interpreter.getPropertyValue(parameter);
RowDefinitionInstanceWrapper instanceWrapper = rowDefinitionWrapperFactory.instanceWrapperFor(newInstance);
PropertyWrapper propertyWrapper = instanceWrapper.getPropertyByName(propertyName);
interpreter.setValue(propertyWrapper.getQueryableDatatype(), value);
}
}
}
final DBRow[] allTables = foundAlready.values().toArray(new DBRow[]{});
return db.getDBQuery(allTables);
}
}