/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.translator.mongodb;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import java.util.regex.Pattern;
import org.bson.types.Binary;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.metadata.*;
import org.teiid.metadata.Column.SearchType;
import org.teiid.mongodb.MongoDBConnection;
import org.teiid.translator.MetadataProcessor;
import org.teiid.translator.TranslatorException;
import org.teiid.translator.TranslatorProperty;
import org.teiid.translator.TypeFacility;
import org.teiid.translator.TranslatorProperty.PropertyType;
import com.mongodb.*;
public class MongoDBMetadataProcessor implements MetadataProcessor<MongoDBConnection> {
@ExtensionMetadataProperty(applicable=Table.class, datatype=String.class, display="Merge Into Table", description="Declare the name of table that this table needs to be merged into. No separate copy maintained")
public static final String MERGE = MetadataFactory.MONGO_URI+"MERGE"; //$NON-NLS-1$
@ExtensionMetadataProperty(applicable=Table.class, datatype=String.class, display="Embedded Into Table", description="Declare the name of table that this table needs to be embedded into. A separate copy is also maintained")
public static final String EMBEDDABLE = MetadataFactory.MONGO_URI+"EMBEDDABLE"; //$NON-NLS-1$
private static final String ID = "_id"; //$NON-NLS-1$
private static final String TOP_LEVEL_DOC = "TOP_LEVEL_DOC"; //$NON-NLS-1$
private static final String ASSOSIATION = "ASSOSIATION"; //$NON-NLS-1$
private Pattern excludeTables;
private Pattern includeTables;
@Override
public void process(MetadataFactory metadataFactory, MongoDBConnection connection) throws TranslatorException {
DB db = connection.getDatabase();
for (String tableName:db.getCollectionNames()) {
if (getExcludeTables() != null && shouldExclude(tableName)) {
continue;
}
if (getIncludeTables() != null && !shouldInclude(tableName)) {
continue;
}
try {
DBCollection collection = db.getCollection(tableName);
DBCursor cursor = collection.find();
while(cursor.hasNext()) {
BasicDBObject row = (BasicDBObject)cursor.next();
if (row == null) {
continue;
}
Table table = addTable(metadataFactory, tableName, row);
if (table != null) {
// top level documents can not be seen as merged
table.setProperty(TOP_LEVEL_DOC, String.valueOf(Boolean.TRUE));
break;
}
}
cursor.close();
} catch (MongoException e) {
LogManager.logWarning(LogConstants.CTX_CONNECTOR, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18037, e));
}
}
for (Table table:metadataFactory.getSchema().getTables().values()) {
String merge = table.getProperty(MERGE, false);
if (merge != null) {
addForeignKey(metadataFactory, table, metadataFactory.getSchema().getTable(merge));
}
}
for (Table table:metadataFactory.getSchema().getTables().values()) {
String top = table.getProperty(TOP_LEVEL_DOC, false);
String merge = table.getProperty(MERGE, false);
if ( top != null) {
table.setProperty(TOP_LEVEL_DOC, null);
if (merge != null) {
table.setProperty(MERGE, null);
table.setProperty(EMBEDDABLE, "true"); //$NON-NLS-1$
}
}
}
}
private Table addTable(MetadataFactory metadataFactory, String tableName, BasicDBObject row) {
if (metadataFactory.getSchema().getTable(tableName) != null) {
Table t = metadataFactory.getSchema().getTable(tableName);
return t;
}
Set<String> keys = row.keySet();
if (keys != null && !keys.isEmpty()) {
Table table = metadataFactory.addTable(tableName);
table.setSupportsUpdate(true);
for (String columnKey:keys) {
Object value = row.get(columnKey);
Column column = addColumn(metadataFactory, table, columnKey, value);
if (column != null) {
column.setUpdatable(true);
}
}
return table;
}
return null;
}
private Column addColumn(MetadataFactory metadataFactory, Table table, String columnKey, Object value) {
Column column = null;
if (columnKey.equals(ID)) {
if (value instanceof BasicDBObject) {
BasicDBObject compositeKey = (BasicDBObject)value;
for (String key:compositeKey.keySet()) {
column = addColumn(metadataFactory, table, key, compositeKey.get(key));
column.setUpdatable(true);
}
}
}
if (!columnKey.equals(ID) && value instanceof BasicDBObject) {
// embedded doc - one to one
Table childTable = addTable(metadataFactory, columnKey, (BasicDBObject)value);
if (childTable != null) {
childTable.setProperty(MERGE, table.getName());
childTable.setProperty(ASSOSIATION, MergeDetails.Association.ONE.name());
}
}
else if (value instanceof BasicDBList) {
// embedded doc, list one to many
if (((BasicDBList)value).get(0) instanceof BasicDBObject) {
Table childTable = addTable(metadataFactory, columnKey, (BasicDBObject)((BasicDBList)value).get(0));
if (childTable != null) {
childTable.setProperty(MERGE, table.getName());
childTable.setProperty(ASSOSIATION, MergeDetails.Association.MANY.name());
}
}
else {
column = metadataFactory.addColumn(columnKey, TypeFacility.RUNTIME_NAMES.OBJECT+"[]", table); //$NON-NLS-1$
column.setSearchType(SearchType.Unsearchable);
}
}
else if (value instanceof DBRef) {
Object obj = ((DBRef)value).getId();
column = addColumn(metadataFactory, table, columnKey, obj);
String ref = ((DBRef)value).getRef();
metadataFactory.addForiegnKey("FK_"+columnKey, Arrays.asList(columnKey), ref, table); //$NON-NLS-1$
}
else {
column = metadataFactory.addColumn(columnKey, getDataType(value), table);
setNativeType(column, value);
}
// create a PK out of _id
if (columnKey.equals(ID)) {
if (value instanceof BasicDBObject) {
BasicDBObject compositeKey = (BasicDBObject)value;
ArrayList<String> columns = new ArrayList<String>();
for (String key:compositeKey.keySet()) {
columns.add(key);
}
metadataFactory.addPrimaryKey("PK0", columns, table); //$NON-NLS-1$
}
else {
metadataFactory.addPrimaryKey("PK0", Arrays.asList(ID), table); //$NON-NLS-1$
}
}
return column;
}
private void addForeignKey(MetadataFactory metadataFactory, Table childTable, Table table) {
MergeDetails.Association association = MergeDetails.Association.valueOf(childTable.getProperty(ASSOSIATION, false));
childTable.setProperty(ASSOSIATION, null);
if (association == MergeDetails.Association.ONE) {
KeyRecord record = table.getPrimaryKey();
if (record != null) {
ArrayList<String> pkColumns = new ArrayList<String>();
for (Column column:record.getColumns()) {
Column c = metadataFactory.getSchema().getTable(childTable.getName()).getColumnByName(column.getName());
if (c == null) {
c = metadataFactory.addColumn(column.getName(), column.getRuntimeType(), childTable);
}
pkColumns.add(c.getName());
}
metadataFactory.addPrimaryKey("PK0", pkColumns, childTable); //$NON-NLS-1$
metadataFactory.addForiegnKey("FK0", pkColumns, table.getName(), childTable); //$NON-NLS-1$
}
}
else {
KeyRecord record = table.getPrimaryKey();
if (record != null) {
ArrayList<String> pkColumns = new ArrayList<String>();
for (Column column:record.getColumns()) {
Column c = metadataFactory.getSchema().getTable(childTable.getName()).getColumnByName(table.getName()+"_"+column.getName()); //$NON-NLS-1$
if (c == null) {
c = metadataFactory.addColumn(table.getName()+"_"+column.getName(), column.getRuntimeType(), childTable); //$NON-NLS-1$
}
pkColumns.add(c.getName());
}
metadataFactory.addForiegnKey("FK0", pkColumns, table.getName(), childTable); //$NON-NLS-1$
}
}
}
private String getDataType(Object value) {
if (value instanceof Integer) {
return TypeFacility.RUNTIME_NAMES.INTEGER;
}
else if (value instanceof Double) {
return TypeFacility.RUNTIME_NAMES.DOUBLE;
}
else if (value instanceof Boolean) {
return TypeFacility.RUNTIME_NAMES.BOOLEAN;
}
else if (value instanceof Long) {
return TypeFacility.RUNTIME_NAMES.LONG;
}
else if (value instanceof String) {
return TypeFacility.RUNTIME_NAMES.STRING;
}
else if (value instanceof Date) {
return TypeFacility.RUNTIME_NAMES.TIMESTAMP;
}
else if (value instanceof Binary || value instanceof byte[]) {
return TypeFacility.RUNTIME_NAMES.VARBINARY;
}
else if (value instanceof org.bson.types.ObjectId ) {
return TypeFacility.RUNTIME_NAMES.STRING;
}
else {
return TypeFacility.RUNTIME_NAMES.OBJECT;
}
}
private void setNativeType(Column column, Object value) {
if (value instanceof Binary ) {
column.setNativeType(Binary.class.getName());
}
else if (column.getName().equals("_id") && value instanceof org.bson.types.ObjectId ) {
column.setNativeType(org.bson.types.ObjectId.class.getName());
column.setAutoIncremented(true);
}
}
@TranslatorProperty(display="Exclude Tables", category=PropertyType.IMPORT, description="A case-insensitive regular expression that when matched against a fully qualified Teiid table name will exclude it from import. Applied after table names are retrieved. Use a negative look-ahead (?!<inclusion pattern>).* to act as an inclusion filter.")
public String getExcludeTables() {
if (this.excludeTables == null) {
return null;
}
return this.excludeTables.pattern();
}
protected boolean shouldExclude(String fullName) {
return excludeTables != null && excludeTables.matcher(fullName).matches();
}
public void setExcludeTables(String excludeTables) {
this.excludeTables = Pattern.compile(excludeTables, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
}
@TranslatorProperty(display="Include Tables", category=PropertyType.IMPORT, description="A case-insensitive regular expression that when matched against a fully qualified Teiid table name will include it from import. Applied after table names are retrieved. Use a negative look-ahead (?!<inclusion pattern>).* to act as an exclusion filter")
public String getIncludeTables() {
if (this.includeTables == null) {
return null;
}
return this.includeTables.pattern();
}
protected boolean shouldInclude(String fullName) {
return includeTables != null && includeTables.matcher(fullName).matches();
}
public void setIncludeTables(String tableNamePattern) {
this.includeTables = Pattern.compile(tableNamePattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);;
}
}