blob: b13edd615addbd28c538927759423bffb38188aa [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation
*
******************************************************************************/
package org.eclipse.persistence.tools.gen.nosql.mongo;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.ServerAddress;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.eclipse.persistence.tools.gen.nosql.mongo.meta.CollectionDescriptor;
import org.eclipse.persistence.tools.gen.nosql.mongo.meta.ColumnDescriptor;
import org.eclipse.persistence.tools.gen.nosql.mongo.meta.LeafColumnDescriptor;
import org.eclipse.persistence.tools.gen.nosql.mongo.meta.NestedColumnDescriptor;
import org.eclipse.persistence.tools.mapping.orm.AccessType;
import org.eclipse.persistence.tools.mapping.orm.DataFormatType;
import org.eclipse.persistence.tools.mapping.orm.ExternalColumnMapping;
import org.eclipse.persistence.tools.mapping.orm.ExternalElementCollectionMapping;
import org.eclipse.persistence.tools.mapping.orm.ExternalEmbeddable;
import org.eclipse.persistence.tools.mapping.orm.ExternalEmbeddedMapping;
import org.eclipse.persistence.tools.mapping.orm.ExternalEntity;
import org.eclipse.persistence.tools.mapping.orm.ExternalNamedQuery;
import org.eclipse.persistence.tools.mapping.orm.ExternalNoSql;
import org.eclipse.persistence.tools.mapping.orm.ExternalORMConfiguration;
import org.eclipse.persistence.tools.mapping.orm.ORMDocumentType;
import org.eclipse.persistence.tools.mapping.orm.dom.ORMRepository;
import org.eclipse.persistence.tools.utility.NameTools;
import org.eclipse.persistence.tools.utility.StringTools;
import org.eclipse.persistence.tools.utility.StringUtil;
import org.eclipse.persistence.tools.utility.collection.ListTools;
/**
* This class is an entry point for dynamic entity xml and source generation for
* MongoDB. It also provides API for discovering metadata (table names, etc) and generating
* EclipseLink-JPA NoSql, mapping files.
* <p>
* Provisional API: This interface is part of an interim API that is still under development and
* expected to change significantly before reaching stability. It is available at this early stage
* to solicit feedback from pioneering adopters on the understanding that any code that uses this
* API will almost certainly be broken (repeatedly) as the API evolves.<p>
*
* @author John Bracken
* @version 2.6
*/
@SuppressWarnings("nls")
public class MongoEntityGenerator {
/** Mongo database connection to use in generation. */
private Mongo connection;
/** Mongo database instance to use in generation. */
private DB database;
/** Number of rows in each collection to sample during generation. */
private int rowSampleSize;
/**
* Constructs a new {@link MongoEntityGenerator} based on the provided
* db connection information.
*
* @param host the MongoDB network host.
* @param port the port of the Mongo database.
* @param dbName the name of the Mongo database.
* @param rowSampleSize number of rows in each collection to sample to determine the
* table structure. Since not all rows may have all possible values explicitly
* defined, it's important to use a sufficient sample size.
*
* @throws MongoException
* @throws UnknownHostException
*/
public MongoEntityGenerator(String host,
int port,
String dbName,
int rowSampleSize) throws MongoException, UnknownHostException {
super();
this.rowSampleSize = rowSampleSize;
this.connection = new Mongo(new ServerAddress(host, port));
this.database = this.connection.getDB(dbName);
}
/**
* Builds a list of {@link CollectionDescriptor}s that describe the collection and associated
* column metadata of the Mongo collections named in <code>collectionNames</code>.
*
* @param collectionNames names of the Mongo collections to build descriptors for.
*
* @return {@link List} of {@link CollectionDescriptor}s.
*/
private List<CollectionDescriptor> buildCollectionDescriptors(Collection<String> collectionNames) {
List<CollectionDescriptor> collectionDescriptors = new LinkedList<CollectionDescriptor>();
for (String collectionName : collectionNames) {
CollectionDescriptor collectionDescriptor = new CollectionDescriptor(collectionName);
collectionDescriptors.add(collectionDescriptor);
updateColumnDescriptors(collectionDescriptor);
}
return collectionDescriptors;
}
/**
* Updates the {@link CollectionDescriptor}s associated with the provided
* {@link CollectionDescriptor}.
*
* @param collectionDescriptor the {@link CollectionDescriptor} to update.
*/
private void updateColumnDescriptors(CollectionDescriptor collectionDescriptor) {
// Read the collection from the database
DBCollection collection = this.database.getCollection(collectionDescriptor.getName());
// Read a sampling of the rows to determine the super set of column keys
// that are in the collection.
DBCursor cursor = collection.find().limit(this.rowSampleSize);
for ( ;cursor.hasNext();) {
DBObject row = cursor.next();
updateCollectionDescriptor(collectionDescriptor, row);
}
}
/**
* Generates a set of EclipseLink, java entity files mapped to the underlying Mongo
* database for the given <code>collectionNames</code>.
*
* @param collectionNames names of the Mongo collections to generate entities for.
* @param packageName java package to use for the generated code.
* @param baseDirectory the base directory to use for the generated code.
* @param type the access type to use (method versus field annotations).
* @param characterEncoding the type of character encoding to use (e.g. "UTF-8").
*
* @throws Exception
*/
public void generateSource(Collection<String> collectionNames, String packageName,
File baseDirectory, AccessType type,
String characterEncoding) throws Exception {
// Create metadata descriptors for the specified collections.
List<CollectionDescriptor> collectionDescriptors = buildCollectionDescriptors(collectionNames);
ExternalORMConfiguration config = generate(collectionDescriptors, packageName);
// Ensure that the provided gen directory and package folders exists
File packageDirectory = new File(baseDirectory, packageName.replace('.', '/') + "/");
packageDirectory.mkdirs();
// Build up the velocity generator
Properties vep = new Properties();
vep.setProperty("resource.loader", "class");
vep.setProperty("class.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
VelocityEngine ve = new VelocityEngine();
ve.init(vep);
// Generate source for entities
for (ExternalEntity entity : config.entities()) {
generateSource(entity, type, "entity.java.vm", packageName, packageDirectory, ve, characterEncoding);
}
// Generate source for embeddables
for (ExternalEmbeddable entity : config.embeddables()) {
generateSource(entity, type, "embeddable.java.vm", packageName, packageDirectory, ve, characterEncoding);
}
}
/**
* Generates the entity source file for the provided mapping metadata.
*
* @param entity the metadata representation of the entity.
* @param accessType the {@link AccessType} to use (method or field) for annotations.
* @param templateName the source gen template to use (embeddable, entity, etc).
* @param packageName the package name to use.
* @param packageDirectory the directory to generate the source file in.
* @param ve the velocity engine to use.
* @param characterEncoding the type of character encoding to use.
*
* @throws Exception
*/
private void generateSource(ExternalEmbeddable entity, AccessType accessType,
String templateName, String packageName,
File packageDirectory, VelocityEngine ve,
String characterEncoding) throws Exception {
VelocityContext context = new VelocityContext();
context.put("entity", entity);
context.put("mappings", ListTools.list(entity.mappings()));
context.put("packageName", packageName);
context.put("accessType", accessType);
StringWriter w = new StringWriter();
ve.mergeTemplate(templateName, context, w);
String fileContent = w.toString();
File javaFile = new File(packageDirectory, entity.getClassShortName() + ".java");
byte[] content = fileContent.getBytes(characterEncoding);
javaFile.createNewFile();
FileOutputStream writer = new FileOutputStream(javaFile);
writer.write(content);
writer.flush();
writer.close();
}
/**
* Generates and adds to the specified entity a mapping for the leaf mapping described by the
* {@link CollectionDescriptor}.
*
* @param exEntity entity to add the mapping to.
* @param column the column to generate the mapping from.
* @param columnName the name of the column.
* @param javaColumnName the java identifier name to use for the column.
*/
private void generateLeafMapping(ExternalEmbeddable exEntity, LeafColumnDescriptor column,
String columnName, String javaColumnName) {
ExternalColumnMapping mapping;
// If this is a list-type column, then a collection based mapping is required.
if (column.isList()) {
mapping = exEntity.addElementCollectionMapping(columnName);
((ExternalElementCollectionMapping)mapping).
setTargetClassName(column.getColumnType().getName());
mapping.setAttributeType(Vector.class.getName());
} else {
// special case where _id is reserved as a pk name in mongo.
// this is always intended to be an ID mapping.
if (columnName.equals("_id")) {
mapping = exEntity.addIdMapping(javaColumnName);
} else {
mapping = exEntity.addBasicMapping(javaColumnName);
}
mapping.setAttributeType(column.getColumnType().getName());
}
mapping.setNoSqlField(columnName);
}
/**
* Generates mappings for the provided mapped class ({@link ExternalEmbeddableEntity}) based on
* the columns enumerated in the provided {@link CollectionDescriptor}.
*
* @param exEntity the entity to generate mappings on.
* @param collection the Mongo DB collection descriptor to derive rows from.
* @param config the orm configuration.
* @param allEntityNames a {@link Set} of all of the entity names already in use.
*/
private void generateMappings(ExternalEmbeddable exEntity, CollectionDescriptor collection, String packageName,
ExternalORMConfiguration config, Set<String> allEntityNames) {
for (ColumnDescriptor column : collection.columns()) {
String columnName = column.getColumnName();
String javaColumnName = NameTools.javaNameFromDatabaseName(columnName);
if (column instanceof LeafColumnDescriptor) {
generateLeafMapping(exEntity, (LeafColumnDescriptor)column,
columnName, javaColumnName);
} else if (column instanceof NestedColumnDescriptor) {
generateNestedMapping(exEntity, config, packageName, allEntityNames,
(NestedColumnDescriptor)column, columnName, javaColumnName);
}
}
}
/**
* Generates a mapping on the provided mapped class that represents the nested mapping. This will
* also involved generating an {@link Embeddable} to represent the nested value.
*
* @param exEntity the entity to generate the mapping on.
* @param config the {@link ExternalORMConfiguration} that the entity is associated with.
* @param allEntityNames the existing used namespace for all mapped classes.
* @param column the nested column to generate the mapping for.
* @param columnName the name of the column.
* @param javaColumnName the java identifier name for the column.
*/
private void generateNestedMapping(ExternalEmbeddable exEntity,ExternalORMConfiguration config, String packageName,
Set<String> allEntityNames, NestedColumnDescriptor column, String columnName, String javaColumnName) {
// Create the embeddable for the nested value
String embeddableName = uniqueJavaClassName(columnName, packageName, allEntityNames);
ExternalEmbeddable embeddable = config.addEmbeddable(embeddableName);
embeddable.addNoSql().setDataFormat(DataFormatType.MAPPED);
embeddable.setAccessType(AccessType.VIRTUAL);
// generate mappings for the embeddable
generateMappings(embeddable, column.getColumnDescriptor(), packageName, config, allEntityNames);
// if a collection, generate an Element Collection
if (column.isList()) {
ExternalElementCollectionMapping mapping = exEntity.addElementCollectionMapping(javaColumnName);
mapping.setNoSqlField(columnName);
mapping.setAttributeType(Vector.class.getName());
mapping.setTargetClassName(embeddableName);
}
// otherwise, just an embedded mapping.
else {
ExternalEmbeddedMapping mapping = exEntity.addEmbeddedMapping(javaColumnName);
mapping.setNoSqlField(columnName);
mapping.setAttributeType(embeddableName);
}
}
/**
* Generates and adds a root level named query for each entity
* enumerate in the {@link ExternalORMConfiguration}.
*
* @param config the configuration to generate and add queries from.
*/
private void generateQueries(ExternalORMConfiguration config) {
for (ExternalEntity entity : config.entities()) {
String entityName = entity.getClassShortName();
ExternalNamedQuery query = config.addNamedQuery(entityName + ".findAll");
char identifier = Character.toLowerCase(entityName.charAt(0));
query.setQuery("select " + identifier + " from " + entityName + " " + identifier);
}
}
/**
* Generates a {@link String} representation of an eclipselink-orm.xml descriptor based
* on the provided MongoDB {@link CollectionDescriptor} definitions.
*
* @param collectionDescriptors the MongoDB collections to generate entities from.
* @param packageName the package name to qualify all generated entities for.
*
* @return a {@link String} representation of the eclipselink-orm.xml.
*/
private ExternalORMConfiguration generate(List<CollectionDescriptor> collectionDescriptors, String packageName) {
try {
// Create a new orm.xml metadata model.
ExternalORMConfiguration config = new ORMRepository().buildORMConfiguration(null, ORMDocumentType.ECLIPELINK_2_6);
// track all entity names being used.
Set<String> allEntityNames = new HashSet<String>();
// Iterate over all collection descriptors and create an entity and associated mapping for them.
for (CollectionDescriptor collection : collectionDescriptors) {
// add an entity with a unique name
String collectionName = collection.getName();
String entityName = uniqueJavaClassName(collectionName, packageName, allEntityNames);
ExternalEntity exEntity = config.addEntity(entityName);
// access type is virtual
exEntity.setAccessType(AccessType.VIRTUAL);
// Configure NoSql settings
ExternalNoSql noSqlDesc = exEntity.addNoSql();
noSqlDesc.setDataFormat(DataFormatType.MAPPED);
noSqlDesc.setDataType(collectionName);
// add mappings
generateMappings(exEntity, collection, packageName, config, allEntityNames);
}
// Generate a default, read all query for all entities
generateQueries(config);
return config;
}
catch (IOException e) {
// Never happens
return null;
}
}
/**
* Creates an eclipselink-orm.xml descriptor mapping the provided <code>collectionNames</code>
* as EclipseLink dynamic entities.
*
* @param collectionNames the names of the Mongo collections to generate for.
* @param packageName the qualifying package name to use for the generated entities.
* @return a {@link String} representation of the eclipselink-orm.xml.
*/
public String generateXML(Collection<String> collectionNames, String packageName) {
// Create metadata descriptors for the specified collections.
List<CollectionDescriptor> collectionDescriptors = buildCollectionDescriptors(collectionNames);
// Generate an eclpselink-orm.xml from the collection descriptors
return generate(collectionDescriptors, packageName).getXML();
}
/**
* Updates the give {@link CollectionDescriptor} with the leaf column information defined
* by row value.
*
* @param collectionDescriptor the {@link CollectionDescriptor} to update.
* @param columnName the name of the column.
* @param value the row value of the column.
*/
private void handleLeafColumn(CollectionDescriptor collectionDescriptor,
String columnName, Object value) {
LeafColumnDescriptor columnDescriptor = (LeafColumnDescriptor)collectionDescriptor.getColumn(columnName);
if (columnDescriptor == null) {
columnDescriptor = collectionDescriptor.addLeafColumn(columnName);
}
Class<?> valueClass = value.getClass();
// Special case for an id-type column. If one is not explicitly defined,
// Mongo auto-generates one and uses the noted class for the type. This
// should be considered a String in java.
valueClass = valueClass.getName().equals("org.bson.types.ObjectId") ? String.class : valueClass;
// if the column type isn't consistent, just use Object as the type.
if (columnDescriptor.getColumnType() == null) {
columnDescriptor.setColumnType(valueClass);
} else if (columnDescriptor.getColumnType() != valueClass) {
columnDescriptor.setColumnType(Object.class);
}
}
/**
* Updates the given {@link CollectionDescriptor} with the a column representing
* the list-type value.
*
* @param collectionDescriptor the {@link CollectionDescriptor} to update.
* @param columnName the name of the column to update.
* @param value the row value, or in this case the list value row.
*/
private void handleListColumn(CollectionDescriptor collectionDescriptor, String columnName,
Object value) {
ColumnDescriptor columnDescriptor = collectionDescriptor.getColumn(columnName);
BasicDBList listValue = (BasicDBList)value;
if (listValue.size() > 0) {
Iterator<?> listValues = listValue.listIterator();
Object valueFromList = listValues.next();
// Handle nested list
if (valueFromList instanceof BasicDBObject) {
NestedColumnDescriptor nestedColumnDesc;
if (columnDescriptor == null) {
nestedColumnDesc = collectionDescriptor.addNestedColumn(columnName);
} else {
nestedColumnDesc = (NestedColumnDescriptor)columnDescriptor;
}
nestedColumnDesc.setList(true);
updateCollectionDescriptor(nestedColumnDesc.getColumnDescriptor(), (DBObject)valueFromList);
// Iterate over subsequent nested values to ensure the superset of all columns keys
// are included
for (;listValues.hasNext();) {
valueFromList = listValues.next();
updateCollectionDescriptor(nestedColumnDesc.getColumnDescriptor(), (DBObject)valueFromList);
}
}
// Handle leaf list
else {
LeafColumnDescriptor leafColumnDescriptor;
if (columnDescriptor == null) {
leafColumnDescriptor = collectionDescriptor.addLeafColumn(columnName);
} else {
leafColumnDescriptor = (LeafColumnDescriptor)columnDescriptor;
}
leafColumnDescriptor.setList(true);
leafColumnDescriptor.setColumnType(valueFromList.getClass());
// Iterate over subsequent elements. If the element type isn't the same as the last, default to Object
// and break.
for (;listValues.hasNext();) {
valueFromList = listValues.next();
if (leafColumnDescriptor.getColumnType() != valueFromList.getClass()) {
leafColumnDescriptor.setColumnType(Object.class);
break;
}
}
}
}
}
/**
* Updates the given {@link CollectionDescriptor} with the nested column information
* derived from the provided row value.
*
* @param collectionDescriptor the {@link CollectionDescriptor} to update.
* @param columnName name of the column.
* @param value row value of the column, or in this case the nest row.
*/
private void handleNestedColumn(CollectionDescriptor collectionDescriptor,
String columnName, Object value) {
NestedColumnDescriptor columnDescriptor =
(NestedColumnDescriptor)collectionDescriptor.getColumn(columnName);
if (columnDescriptor == null) {
columnDescriptor = collectionDescriptor.addNestedColumn(columnName);
}
updateCollectionDescriptor(columnDescriptor.getColumnDescriptor(), (DBObject)value);
}
/**
* Returns the names of the Mongo collections on the specified
* database.
*
* @return the name of the Mongo collections.
*/
public Set<String> listCollectionNames() {
Set<String> collectionNames = this.database.getCollectionNames();
// Remove the internal system.indexes table name
collectionNames.remove("system.indexes");
return collectionNames;
}
/**
* Generates a unique, fully qualified and singularised entity name for the specified
* Mongo collection name.
*
* @param collectionName the name of the Mongo collection.
* @param packageName the package name to qualify the entity name with.
* @param allEntityNames all of the existing entity names in use.
*
* @return the unique, qualifed and singularised entity name.
*/
private String uniqueJavaClassName(String collectionName, String packageName, Set<String> allEntityNames) {
String entityName = StringUtil.singularise(NameTools.javaNameFromDatabaseName(collectionName, true));
if (!StringTools.isBlank(packageName)) {
entityName = packageName + "." + entityName;
}
entityName = NameTools.uniqueName(entityName, allEntityNames);
allEntityNames.add(entityName);
return entityName;
}
/**
* Updates the given {@link CollectionDescriptor} with columns implied by the provided row
* or {@link DBObject}.
*
* @param collectionDescriptor the {@link CollectionDescriptor} to update.
* @param dbObject the row to infer columns from.
*/
private void updateCollectionDescriptor(CollectionDescriptor collectionDescriptor, DBObject dbObject) {
// iterate over all of the available names in the row.
for (String columnName : dbObject.keySet()) {
Object value = dbObject.get(columnName);
// if a column does not yet exist in our descriptor, add one.
if (value != null) {
// List type
if (value instanceof BasicDBList) {
handleListColumn(collectionDescriptor, columnName, value);
}
// Single Nested
else if (value instanceof BasicDBObject) {
handleNestedColumn(collectionDescriptor, columnName, value);
}
// Leaf Value
else {
handleLeafColumn(collectionDescriptor, columnName, value);
}
}
}
}
}