/*
* Copyright 2014 - 2017 Blazebit.
*
* 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 com.blazebit.persistence.impl.datanucleus.function;
import com.blazebit.apt.service.ServiceProvider;
import com.blazebit.persistence.impl.datanucleus.DataNucleusJpaProvider;
import com.blazebit.persistence.impl.jpa.function.CountStarFunction;
import com.blazebit.persistence.spi.EntityManagerFactoryIntegrator;
import com.blazebit.persistence.spi.JpaProvider;
import com.blazebit.persistence.spi.JpaProviderFactory;
import com.blazebit.persistence.spi.JpqlFunction;
import com.blazebit.persistence.spi.JpqlFunctionGroup;
import org.datanucleus.NucleusContext;
import org.datanucleus.store.StoreManager;
import org.datanucleus.store.rdbms.RDBMSStoreManager;
import org.datanucleus.store.rdbms.identifier.DatastoreIdentifier;
import org.datanucleus.store.rdbms.query.QueryGenerator;
import org.datanucleus.store.rdbms.sql.SQLStatement;
import org.datanucleus.store.rdbms.sql.expression.SQLExpressionFactory;
import org.datanucleus.store.rdbms.sql.method.SQLMethod;
import org.datanucleus.store.rdbms.table.Table;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;
/**
*
* @author Christian
*/
@ServiceProvider(EntityManagerFactoryIntegrator.class)
public class DataNucleusEntityManagerFactoryIntegrator implements EntityManagerFactoryIntegrator {
private static final Logger LOG = Logger.getLogger(DataNucleusEntityManagerFactoryIntegrator.class.getName());
private static final Map<String, String> VENDOR_TO_DBMS_MAPPING = new HashMap<String, String>();
private static final String VERSION;
private static final int MAJOR;
private static final int MINOR;
private static final int FIX;
static {
VENDOR_TO_DBMS_MAPPING.put("h2", "h2");
VENDOR_TO_DBMS_MAPPING.put("mysql", "mysql");
VENDOR_TO_DBMS_MAPPING.put("db2", "db2");
VENDOR_TO_DBMS_MAPPING.put("firebird", "firebird");
VENDOR_TO_DBMS_MAPPING.put("postgresql", "postgresql");
VENDOR_TO_DBMS_MAPPING.put("oracle", "oracle");
VENDOR_TO_DBMS_MAPPING.put("sqlite", "sqlite");
VENDOR_TO_DBMS_MAPPING.put("sqlserver", "microsoft");
VENDOR_TO_DBMS_MAPPING.put("sybase", "sybase");
// VENDOR_TO_DBMS_MAPPING.put("", "cubrid");
VENDOR_TO_DBMS_MAPPING.put("hsql", "hsql");
VENDOR_TO_DBMS_MAPPING.put("informix", "informix");
// VENDOR_TO_DBMS_MAPPING.put("", "ingres");
// VENDOR_TO_DBMS_MAPPING.put("", "interbase");
VERSION = readMavenPropertiesVersion("META-INF/maven/org.datanucleus/datanucleus-core/pom.properties");
String[] versionParts = VERSION.split("[\\.-]");
MAJOR = Integer.parseInt(versionParts[0]);
MINOR = Integer.parseInt(versionParts[1]);
FIX = Integer.parseInt(versionParts[2]);
}
@Override
public String getDbms(EntityManagerFactory entityManagerFactory) {
RDBMSStoreManager storeMgr = (RDBMSStoreManager) entityManagerFactory.unwrap(StoreManager.class);
return VENDOR_TO_DBMS_MAPPING.get(storeMgr.getDatastoreAdapter().getVendorID());
}
@Override
public JpaProviderFactory getJpaProviderFactory(EntityManagerFactory entityManagerFactory) {
return new JpaProviderFactory() {
@Override
public JpaProvider createJpaProvider(EntityManager em) {
return new DataNucleusJpaProvider(em, MAJOR, MINOR, FIX);
}
};
}
@SuppressWarnings("unchecked")
@Override
public EntityManagerFactory registerFunctions(EntityManagerFactory entityManagerFactory, Map<String, JpqlFunctionGroup> dbmsFunctions) {
RDBMSStoreManager storeMgr = (RDBMSStoreManager) entityManagerFactory.unwrap(StoreManager.class);
SQLExpressionFactory exprFactory = storeMgr.getSQLExpressionFactory();
String dbms = VENDOR_TO_DBMS_MAPPING.get(storeMgr.getDatastoreAdapter().getVendorID());
// Register compatibility functions
if (!exprFactory.isMethodRegistered(null, "COUNT_STAR")) {
exprFactory.registerMethod(null, "COUNT_STAR", new DataNucleusJpqlFunctionAdapter(new CountStarFunction(), true), true);
}
// DataNucleus4 uses a month function that is 0 based which conflicts with ANSI EXTRACT(MONTH)
if (MAJOR < 5 && !(exprFactory.getMethod("java.util.Date", "getMonth", null) instanceof DataNucleusJpqlFunctionAdapter)) {
LOG.warning("Overriding DataNucleus native 'MONTH' function to return months 1-based like ANSI EXTRACT instead of 0-based!");
JpqlFunctionGroup dbmsFunctionGroup = dbmsFunctions.get("month");
JpqlFunction function = dbmsFunctionGroup.get(dbms);
if (function == null && !dbmsFunctionGroup.contains(dbms)) {
function = dbmsFunctionGroup.get(null);
}
SQLMethod method = new DataNucleusJpqlFunctionAdapter(function, dbmsFunctionGroup.isAggregate());
String version = readMavenPropertiesVersion("META-INF/maven/org.datanucleus/datanucleus-core/pom.properties");
Set<Object> methodKeys = fieldGet("methodNamesSupported", exprFactory, version);
for (Object methodKey : methodKeys) {
if ("getMonth".equals((String) fieldGet("methodName", methodKey, version)) && "java.util.Date".equals((String) fieldGet("clsName", methodKey, version))) {
((Map<Object, Object>) fieldGet("methodByClassMethodName", exprFactory, version)).put(methodKey, method);
}
}
}
for (Map.Entry<String, JpqlFunctionGroup> functionEntry : dbmsFunctions.entrySet()) {
String functionName = functionEntry.getKey().toUpperCase();
JpqlFunctionGroup dbmsFunctionGroup = functionEntry.getValue();
JpqlFunction function = dbmsFunctionGroup.get(dbms);
if (function == null && !dbmsFunctionGroup.contains(dbms)) {
function = dbmsFunctionGroup.get(null);
}
if (function == null) {
LOG.warning("Could not register the function '" + functionName + "' because there is neither an implementation for the dbms '" + dbms + "' nor a default implementation!");
} else if (!exprFactory.isMethodRegistered(null, functionName)) {
exprFactory.registerMethod(null, functionName, new DataNucleusJpqlFunctionAdapter(function, dbmsFunctionGroup.isAggregate()), true);
}
}
return entityManagerFactory;
}
@Override
public Map<String, JpqlFunction> getRegisteredFunctions(EntityManagerFactory entityManagerFactory) {
NucleusContext context = entityManagerFactory.unwrap(NucleusContext.class);
RDBMSStoreManager storeMgr = (RDBMSStoreManager) entityManagerFactory.unwrap(StoreManager.class);
String storeName = storeMgr.getDatastoreAdapter().getVendorID();
SQLExpressionFactory exprFactory = storeMgr.getSQLExpressionFactory();
Set<Object> methodKeys = fieldGet("methodNamesSupported", exprFactory, VERSION);
if (methodKeys.isEmpty()) {
return new HashMap<>();
}
Map<String, JpqlFunction> functions = new HashMap<>();
// We need to construct a statement object and how this is done changed between 4 and 5 so we have to do a little reflection hack
// We need this because the function methods retrieve the expression factory through it
Class<?>[] parameterTypes = {RDBMSStoreManager.class, Table.class, DatastoreIdentifier.class, String.class};
SQLStatement stmt;
try {
Constructor c = Class.forName("org.datanucleus.store.rdbms.sql.SelectStatement").getConstructor(parameterTypes);
stmt = (SQLStatement) c.newInstance(storeMgr, null, null, null);
} catch (Exception e) {
try {
Constructor c = Class.forName("org.datanucleus.store.rdbms.sql.SQLStatement").getConstructor(parameterTypes);
stmt = (SQLStatement) c.newInstance(storeMgr, null, null, null);
} catch (Exception e2) {
throw new RuntimeException("Could not access the required methods to dynamically retrieve registered functions. Please report this version of datanucleus(" + VERSION + ") so we can provide support for it!", e2);
}
}
// Well apparently expressions get their class loader resolver by asking the statement they are part of
// which in turn asks the query generator that is responsible for it
// So this is the most non-hackish way to get this to work...
QueryGenerator noopGenerator = (QueryGenerator) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{ QueryGenerator.class }, new QueryGeneratorInvocationHandler(context));
stmt.setQueryGenerator(noopGenerator);
for (Object methodKey : methodKeys) {
String className = fieldGet("clsName", methodKey, VERSION);
String datastoreName = fieldGet("datastoreName", methodKey, VERSION);
String name = fieldGet("methodName", methodKey, VERSION);
if (className.isEmpty()
&& name.indexOf('.') == -1
&& ("ALL".equals(datastoreName) || storeName.equals(datastoreName))) {
// Only consider normal functions
SQLMethod method = exprFactory.getMethod(null, name, Collections.emptyList());
if (method instanceof DataNucleusJpqlFunctionAdapter) {
functions.put(name.toLowerCase(), ((DataNucleusJpqlFunctionAdapter) method).unwrap());
} else {
functions.put(name.toLowerCase(), new JpqlFunctionSQLMethod(stmt, method));
}
}
}
// The length function is the single exception to all functions that is based on a class
SQLMethod method = exprFactory.getMethod("java.lang.String", "length", Collections.emptyList());
functions.put("length", new JpqlFunctionInstanceSQLMethod(stmt, method));
return functions;
}
private static String readMavenPropertiesVersion(String name) {
InputStream is = null;
try {
is = NucleusContext.class.getClassLoader().getResourceAsStream(name);
Properties p = new Properties();
p.load(is);
return p.getProperty("version");
} catch (IOException e) {
throw new RuntimeException("Could not access the maven version properties of datanucleus!", e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignore
}
}
}
}
@SuppressWarnings("unchecked")
private <T> T fieldGet(String fieldName, Object value, String version) {
Exception ex;
Field field = null;
boolean madeAccessible = false;
try {
field = value.getClass().getDeclaredField(fieldName);
madeAccessible = !field.isAccessible();
if (madeAccessible) {
field.setAccessible(true);
}
return (T) field.get(value);
} catch (NoSuchFieldException e) {
ex = e;
} catch (SecurityException e) {
ex = e;
} catch (IllegalArgumentException e) {
ex = e;
} catch (IllegalAccessException e) {
ex = e;
} finally {
if (madeAccessible) {
field.setAccessible(false);
}
}
throw new RuntimeException("Could not access the required methods to dynamically retrieve registered functions. Please report this version of datanucleus(" + version + ") so we can provide support for it!", ex);
}
}