blob: 53c4635b871b2a61b3e87486aae97c1d06edcf12 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. 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.
*/
package org.apache.geode.connectors.jdbc.internal.cli;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;
import com.healthmarketscience.rmiio.RemoteInputStream;
import com.healthmarketscience.rmiio.RemoteInputStreamClient;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.geode.SerializationException;
import org.apache.geode.annotations.Experimental;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.connectors.jdbc.JdbcConnectorException;
import org.apache.geode.connectors.jdbc.internal.JdbcConnectorService;
import org.apache.geode.connectors.jdbc.internal.TableMetaDataManager;
import org.apache.geode.connectors.jdbc.internal.TableMetaDataView;
import org.apache.geode.connectors.jdbc.internal.configuration.FieldMapping;
import org.apache.geode.connectors.jdbc.internal.configuration.RegionMapping;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.classloader.ClassPathLoader;
import org.apache.geode.internal.jndi.JNDIInvoker;
import org.apache.geode.management.cli.CliFunction;
import org.apache.geode.management.internal.functions.CliFunctionResult;
import org.apache.geode.pdx.PdxWriter;
import org.apache.geode.pdx.ReflectionBasedAutoSerializer;
import org.apache.geode.pdx.internal.PdxOutputStream;
import org.apache.geode.pdx.internal.PdxType;
import org.apache.geode.pdx.internal.PdxWriterImpl;
import org.apache.geode.pdx.internal.TypeRegistry;
@Experimental
public class CreateMappingPreconditionCheckFunction extends CliFunction<Object[]> {
@Override
public CliFunctionResult executeFunction(FunctionContext<Object[]> context) {
Object[] args = context.getArguments();
RegionMapping regionMapping = (RegionMapping) args[0];
String remoteInputStreamName = (String) args[1];
RemoteInputStream remoteInputStream = (RemoteInputStream) args[2];
String dataSourceName = regionMapping.getDataSourceName();
DataSource dataSource = getDataSource(dataSourceName);
if (dataSource == null) {
throw new JdbcConnectorException("JDBC data-source named \"" + dataSourceName
+ "\" not found. Create it with gfsh 'create data-source --pooled --name="
+ dataSourceName + "'.");
}
InternalCache cache = (InternalCache) context.getCache();
TypeRegistry typeRegistry = cache.getPdxRegistry();
PdxType pdxType = getPdxTypeForClass(cache, typeRegistry, regionMapping.getPdxName(),
remoteInputStreamName, remoteInputStream);
try (Connection connection = dataSource.getConnection()) {
TableMetaDataView tableMetaData =
getTableMetaDataManager().getTableMetaDataView(connection, regionMapping);
JdbcConnectorService service = cache.getService(JdbcConnectorService.class);
List<FieldMapping> fieldMappings = service.createFieldMappingUsingPdx(pdxType, tableMetaData);
Object[] output = new Object[2];
output[1] = fieldMappings;
if (regionMapping.getIds() == null || regionMapping.getIds().isEmpty()) {
List<String> keyColumnNames = tableMetaData.getKeyColumnNames();
output[0] = String.join(",", keyColumnNames);
}
String member = context.getMemberName();
return new CliFunctionResult(member, output);
} catch (SQLException e) {
throw JdbcConnectorException.createException(e);
}
}
private PdxType getPdxTypeForClass(InternalCache cache, TypeRegistry typeRegistry,
String className, String remoteInputStreamName, RemoteInputStream remoteInputStream) {
Class<?> clazz = loadPdxClass(className, remoteInputStreamName, remoteInputStream);
PdxType result = typeRegistry.getExistingTypeForClass(clazz);
if (result != null) {
return result;
}
return generatePdxTypeForClass(cache, typeRegistry, clazz);
}
/**
* Generates and returns a PdxType for the given class.
* The generated PdxType is also stored in the TypeRegistry.
*
* @param cache used to generate pdx type
* @param clazz the class to generate a PdxType for
* @return the generated PdxType
* @throws JdbcConnectorException if a PdxType can not be generated
*/
private PdxType generatePdxTypeForClass(InternalCache cache, TypeRegistry typeRegistry,
Class<?> clazz) {
Object object = createInstance(clazz);
try {
cache.registerPdxMetaData(object);
} catch (SerializationException ex) {
String className = clazz.getName();
ReflectionBasedAutoSerializer serializer =
getReflectionBasedAutoSerializer("\\Q" + className + "\\E");
PdxWriter writer = createPdxWriter(typeRegistry, object);
boolean result = serializer.toData(object, writer);
if (!result) {
throw new JdbcConnectorException(
"Could not generate a PdxType using the ReflectionBasedAutoSerializer for the class "
+ clazz.getName() + " after failing to register pdx metadata due to "
+ ex.getMessage() + ". Check the server log for details.");
}
}
// serialization will leave the type in the registry
return typeRegistry.getExistingTypeForClass(clazz);
}
private Object createInstance(Class<?> clazz) {
try {
Constructor<?> ctor = clazz.getConstructor();
return ctor.newInstance();
} catch (NoSuchMethodException | SecurityException | InstantiationException
| IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new JdbcConnectorException(
"Could not generate a PdxType for the class " + clazz.getName()
+ " because it did not have a public zero arg constructor. Details: " + ex);
}
}
private Class<?> loadPdxClass(String className, String remoteInputStreamName,
RemoteInputStream remoteInputStream) {
try {
if (remoteInputStream != null) {
return loadPdxClassFromRemoteStream(className, remoteInputStreamName, remoteInputStream);
} else {
return loadClass(className);
}
} catch (ClassNotFoundException ex) {
throw new JdbcConnectorException(
"The pdx class \"" + className + "\" could not be loaded because: " + ex);
}
}
private Class<?> loadPdxClassFromRemoteStream(String className, String remoteInputStreamName,
RemoteInputStream remoteInputStream) throws ClassNotFoundException {
Path tempDir = createTemporaryDirectory("pdx-class-dir-");
try {
File file =
copyRemoteInputStreamToTempFile(className, remoteInputStreamName, remoteInputStream,
tempDir);
return loadClass(className, createURL(file, tempDir));
} finally {
deleteDirectory(tempDir);
}
}
Path createTemporaryDirectory(String prefix) {
try {
return createTempDirectory(prefix);
} catch (IOException ex) {
throw new JdbcConnectorException(
"Could not create a temporary directory with the prefix \"" + prefix + "\" because: "
+ ex);
}
}
void deleteDirectory(Path tempDir) {
try {
FileUtils.deleteDirectory(tempDir.toFile());
} catch (IOException ioe) {
// ignore
}
}
private URL createURL(File file, Path tempDir) {
URI uri;
if (isJar(file.getName())) {
uri = file.toURI();
} else {
uri = tempDir.toUri();
}
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new JdbcConnectorException(
"Could not convert \"" + uri + "\" to a URL, because: " + e);
}
}
private boolean isJar(String fileName) {
String fileExtension = FilenameUtils.getExtension(fileName);
return fileExtension.equalsIgnoreCase("jar");
}
private File copyRemoteInputStreamToTempFile(String className, String remoteInputStreamName,
RemoteInputStream remoteInputStream, Path tempDir) {
if (!isJar(remoteInputStreamName) && className.contains(".")) {
File packageDir = new File(tempDir.toFile(), className.replace(".", "/")).getParentFile();
packageDir.mkdirs();
tempDir = packageDir.toPath();
}
try {
Path tempPdxClassFile = Paths.get(tempDir.toString(), remoteInputStreamName);
try (InputStream input = RemoteInputStreamClient.wrap(remoteInputStream);
FileOutputStream output = new FileOutputStream(tempPdxClassFile.toString())) {
copyFile(input, output);
}
return tempPdxClassFile.toFile();
} catch (IOException iox) {
throw new JdbcConnectorException(
"The pdx class file \"" + remoteInputStreamName
+ "\" could not be copied to a temporary file, because: " + iox);
}
}
// unit test mocks this method
DataSource getDataSource(String dataSourceName) {
return JNDIInvoker.getDataSource(dataSourceName);
}
// unit test mocks this method
Class<?> loadClass(String className) throws ClassNotFoundException {
return ClassPathLoader.getLatest().forName(className);
}
// unit test mocks this method
Class<?> loadClass(String className, URL url) throws ClassNotFoundException {
return URLClassLoader.newInstance(new URL[] {url}).loadClass(className);
}
// unit test mocks this method
ReflectionBasedAutoSerializer getReflectionBasedAutoSerializer(String className) {
return new ReflectionBasedAutoSerializer(className);
}
// unit test mocks this method
PdxWriter createPdxWriter(TypeRegistry typeRegistry, Object object) {
return new PdxWriterImpl(typeRegistry, object, new PdxOutputStream());
}
// unit test mocks this method
TableMetaDataManager getTableMetaDataManager() {
return new TableMetaDataManager();
}
// unit test mocks this method
Path createTempDirectory(String prefix) throws IOException {
return Files.createTempDirectory(prefix);
}
// unit test mocks this method
void copyFile(InputStream input, FileOutputStream output) throws IOException {
IOUtils.copyLarge(input, output);
}
}