/* * 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.*; import java.util.Map.Entry; import org.teiid.metadata.*; import org.teiid.translator.TranslatorException; import org.teiid.translator.mongodb.MergeDetails.Association; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBObject; import com.mongodb.DBRef; class MongoDocument { private RuntimeMetadata metadata; private Table table; private MergeDetails mergeKey; private List<MergeDetails> embeddedKeys = new ArrayList<MergeDetails>(); private LinkedHashMap<List<String>, MergeDetails> foreignKeys = new LinkedHashMap<List<String>, MergeDetails>(); private ArrayList<MergeDetails> copyto = new ArrayList<MergeDetails>(); private MongoDocument mergeDocument; private HashMap<String, MongoDocument> relatedDocs = new HashMap<String, MongoDocument>(); private String documentAlias; public MongoDocument(Table table, RuntimeMetadata metadata) throws TranslatorException { this.table = table; this.metadata = metadata; if (isEmbeddable() && isMerged()) { throw new TranslatorException(MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18013, table.getName())); } build(); } public Table getTable() { return this.table; } public Table getTargetTable() throws TranslatorException { if (isMerged()) { Table merge = getMergeTable(); MongoDocument mergeDoc = getDocument(merge.getName()); if (mergeDoc.isMerged()) { return mergeDoc.getTargetTable(); } return merge; } return getTable(); } public MongoDocument getTargetDocument() throws TranslatorException { if (isMerged()) { return getMergeDocument().getTargetDocument(); } return this; } public boolean isEmbeddable() { return isEmbeddable(this.table); } public static boolean isEmbeddable(Table tbl) { return Boolean.parseBoolean(tbl.getProperty(MongoDBMetadataProcessor.EMBEDDABLE, false)); } public boolean isMerged() { return this.table.getProperty(MongoDBMetadataProcessor.MERGE, false) != null; } public Table getMergeTable() throws TranslatorException { String tblName = this.table.getProperty(MongoDBMetadataProcessor.MERGE, false); if (tblName == null) { return null; } Table mergeTable = this.metadata.getTable(this.table.getParent().getName(), tblName); return mergeTable; } public MongoDocument getMergeDocument() throws TranslatorException { if (this.mergeDocument != null) { return this.mergeDocument; } Table mergeTable = getMergeTable(); if (mergeTable != null) { this.mergeDocument = new MongoDocument(mergeTable, this.metadata); } return this.mergeDocument; } public Association getMergeAssociation() { return this.mergeKey.getAssociation(); } public boolean hasEmbeddedDocuments() { return !this.embeddedKeys.isEmpty(); } public List<String> getEmbeddedDocumentNames(){ ArrayList<String> names = new ArrayList<String>(); for (MergeDetails ref:this.embeddedKeys) { names.add(ref.getName()); } return names; } private void build() throws TranslatorException { buildForeignKeyReferences(); buildEmbeddableIntoReferences(); buildEmbeddedReferences(); buildMergeKey(); } private void buildEmbeddableIntoReferences() { // if this table is marked as "embeddable", figure out all the tables it is // copied in. if (isEmbeddable()) { for (Table t:this.table.getParent().getTables().values()) { for (ForeignKey fk:t.getForeignKeys()) { if (fk.getReferenceKey().getParent().equals(this.table)){ MergeDetails key = new MergeDetails(this); key.setName(this.table.getName()); key.setParentTable(t.getName()); key.setEmbeddedTable(this.table.getName()); key.setColumns(MongoDBSelectVisitor.getColumnNames(fk.getColumns())); key.setReferenceColumns(fk.getReferenceColumns()); key.setAssociation(Association.ONE); this.copyto.add(key); } } } } } private void buildEmbeddedReferences() throws TranslatorException { for (ForeignKey fk:this.table.getForeignKeys()) { Table referenceTable = fk.getReferenceKey().getParent(); MongoDocument refereceDoc = new MongoDocument(referenceTable, this.metadata); if (refereceDoc.isEmbeddable()) { // if this table itself is merged into embedded; then skip it if (isMerged() && getMergeTable().getName().equals(referenceTable.getName())) { // avoid self inclusion continue; } MergeDetails key = new MergeDetails(this); key.setName(fk.getReferenceTableName()); key.setParentTable(this.table.getName()); key.setReferenceColumns(MongoDBSelectVisitor.getColumnNames(fk.getColumns())); key.setColumns(fk.getReferenceColumns()); key.setEmbeddedTable(fk.getReferenceTableName()); // if the primary is reference, then it needs to built as such during the fetch if (MongoDBSelectVisitor.isPartOfForeignKey(referenceTable, fk.getReferenceColumns().get(0))) { key.setIdReference(MongoDBSelectVisitor.getForeignKeyRefTable(referenceTable, fk.getReferenceColumns().get(0))); } this.embeddedKeys.add(key); } } } private void buildForeignKeyReferences() throws TranslatorException { for (ForeignKey fk:this.table.getForeignKeys()) { MergeDetails key = new MergeDetails(this); key.setParentTable(fk.getReferenceTableName()); key.setEmbeddedTable(this.table.getName()); key.setName(fk.getName()); key.setColumns(MongoDBSelectVisitor.getColumnNames(fk.getColumns())); key.setReferenceColumns(fk.getReferenceColumns()); this.foreignKeys.put(MongoDBSelectVisitor.getColumnNames(fk.getColumns()), key); } } private void buildMergeKey() throws TranslatorException { if (!isMerged()) { return; } Table mergeTable = getMergeTable(); for (ForeignKey fk:this.table.getForeignKeys()) { if (fk.getReferenceKey().getParent().equals(mergeTable)) { MergeDetails key = new MergeDetails(this); key.setName(this.table.getName()); key.setParentTable(mergeTable.getName()); key.setColumns(MongoDBSelectVisitor.getColumnNames(fk.getColumns())); key.setReferenceColumns(fk.getReferenceColumns()); key.setEmbeddedTable(this.table.getName()); key.setAssociation(Association.MANY); // check to see if the parent table has relation to this table, if yes // then it is one-to-one, other wise many-to-one for (ForeignKey fk1:mergeTable.getForeignKeys()) { if (fk1.getReferenceKey().getParent().equals(this.table)) { key.setAssociation(Association.ONE); break; } } // or for 1 to 1 to be true, fk columns are same as PK columns if (this.table.getPrimaryKey() != null && sameKeys(MongoDBSelectVisitor.getColumnNames(fk.getColumns()), MongoDBSelectVisitor.getColumnNames(this.table.getPrimaryKey().getColumns()))) { key.setAssociation(Association.ONE); } this.mergeKey = key; break; } } } private boolean sameKeys(List<String> columns1, List<String> columns2) { if (columns1.size() != columns2.size()) { return false; } for (String name : columns1) { if (!columns2.contains(name)) { return false; } } return true; } public void updateReferenceColumnValue(String tableName, String columnName, Object value ) { Iterator<Entry<List<String>, MergeDetails>> it = this.foreignKeys.entrySet().iterator(); while (it.hasNext()) { Map.Entry<List<String>, MergeDetails> pairs = it.next(); List<String> keys = pairs.getKey(); MergeDetails ref = pairs.getValue(); if (keys.contains(columnName) && ref.getEmbeddedTable().equals(tableName)) { ref.setId(columnName, value); } } // parent table selection query. if (this.mergeKey != null && this.mergeKey.getColumns().contains(columnName) && this.mergeKey.getEmbeddedTable().equals(tableName)) { for (int i = 0; i < this.mergeKey.getColumns().size(); i++) { String column = this.mergeKey.getColumns().get(i); if (column.equals(columnName)) { String referenceColumn = this.mergeKey.getReferenceColumns().get(i); this.mergeKey.setId(referenceColumn, value); } } } // child table selection query if (!this.embeddedKeys.isEmpty()) { for (MergeDetails ref:this.embeddedKeys) { if (ref.getReferenceColumns().contains(columnName) && ref.getParentTable().equals(tableName)) { for (int i = 0; i < ref.getReferenceColumns().size(); i++) { String column = ref.getReferenceColumns().get(i); if (column.equals(columnName)) { String referenceColumn = ref.getColumns().get(i); ref.setId(referenceColumn, value); } } } } } } public MergeDetails getFKReference(String columnName) { Iterator<Entry<List<String>, MergeDetails>> it = this.foreignKeys.entrySet().iterator(); while (it.hasNext()) { Map.Entry<List<String>, MergeDetails> pairs = it.next(); List<String> keys = pairs.getKey(); MergeDetails ref = pairs.getValue(); if (keys.contains(columnName)) { return ref; } } return null; } public DBObject getEmbeddedDocument(DB mongoDB, String docName) { for (MergeDetails ref:this.embeddedKeys) { if (ref.getName().equals(docName)) { DBRef dbRef = ref.getDBRef(mongoDB, false); if (dbRef != null) { return mongoDB.getCollection(dbRef.getRef()).findOne(new BasicDBObject("_id", dbRef.getId())); //$NON-NLS-1$ } } } return null; } public String getColumnName(String columnName) throws TranslatorException { String originalColumnName = columnName; boolean primaryKey = false; if (isPartOfPrimaryKey(originalColumnName)) { columnName = "_id"; //$NON-NLS-1$ if (hasCompositePrimaryKey()) { columnName = "_id."+originalColumnName; //$NON-NLS-1$ } primaryKey = true; } if (isMerged()) { if (primaryKey && pkExistsInParent(this)) { if (this.documentAlias != null) { return this.documentAlias+"."+columnName; //$NON-NLS-1$ } return columnName; } // even if the key is part of foreign key, then this // key does not exist in child document. if (isPartOfForeignKey(originalColumnName)) { return getMergeDocument().getColumnName(this.mergeKey.getParentColumnName(originalColumnName)); } return this.getDocumentName()+"."+columnName; //$NON-NLS-1$ } else if (isEmbeddable()) { } return columnName; } public String getDocumentName() throws TranslatorException { return this.documentAlias != null ? this.documentAlias:getQualifiedName(false); } // if a table is ONE-2-ONE all the way to target document, then PK exists only on top public boolean pkExistsInParent(MongoDocument document) throws TranslatorException { while(document.isMerged()) { if (document.getMergeKey().getAssociation() == Association.ONE) { document = document.getMergeDocument(); } else { return false; } } return true; } /** * References that are going OUT * @return */ public List<MergeDetails> getEmbeddedIntoReferences(){ return this.copyto; } MergeDetails getMergeKey() { return this.mergeKey; } /** * references that are coming IN * @return */ List<MergeDetails> getEmbeddedReferences(){ return this.embeddedKeys; } public boolean embeds(MongoDocument right) throws TranslatorException { if (equals(right)) { return false; } for (MergeDetails ref:this.embeddedKeys) { if (ref.getEmbeddedTable().equals(right.getTable().getName())) { return true; } } for (MergeDetails ref:right.getEmbeddedIntoReferences()) { if (ref.getParentTable().equals(getTable().getName())) { return true; } } return nestedEmbedded(right); } public boolean merges(MongoDocument right) throws TranslatorException { if (equals(right)) { return false; } if (right.isMerged()) { if (right.mergeKey.getParentTable().equals(getTable().getName())) { return true; } } return nestedMerge(right); } public boolean contains(MongoDocument right) throws TranslatorException { return (embeds(right) || merges(right)); } /** * Check if it is grand kids. Multiple nesting.. * @param right * @return */ private boolean nestedEmbedded(MongoDocument right) throws TranslatorException { for (MergeDetails ref:this.embeddedKeys) { MongoDocument parent = getDocument(ref.getEmbeddedTable()); if (parent.embeds(right)) { return true; } } for (MergeDetails ref:right.getEmbeddedIntoReferences()) { MongoDocument parent = getDocument(ref.getParentTable()); if (parent.embeds(right)) { return true; } } return false; } private boolean nestedMerge(MongoDocument right) throws TranslatorException { if (right.isMerged()) { MongoDocument parent = getDocument(right.mergeKey.getParentTable()); if (parent.merges(right)) { return true; } } return false; } public String getQualifiedName(boolean positional) throws TranslatorException { MongoDocument document = this; String tableName = getTable().getName(); // check if document nested merge, i.e parent is also merged document while (document.isMerged()) { document = document.getMergeDocument(); if (document.isMerged()) { if (positional) { if (document.mergeKey.getAssociation() == Association.ONE) { tableName = document.getTable().getName()+"."+tableName; //$NON-NLS-1$ } else { tableName = document.getTable().getName()+".$."+tableName; //$NON-NLS-1$ } } else { tableName = document.getTable().getName()+"."+tableName; //$NON-NLS-1$ } } } return tableName; } private MongoDocument getDocument(String tblName) throws TranslatorException { if (this.relatedDocs.get(tblName) != null) { return this.relatedDocs.get(tblName); } Table tbl = this.metadata.getTable(this.table.getParent().getName(), tblName); MongoDocument doc = new MongoDocument(tbl, this.metadata); this.relatedDocs.put(tblName, doc); return doc; } public MergeDetails getEmbeddedDocumentReferenceKey(MongoDocument right) throws TranslatorException { if (equals(right)) { return null; } for (MergeDetails ref:this.embeddedKeys) { if (ref.getEmbeddedTable().equals(right.getTable().getName())) { return ref.clone(); } } for (MergeDetails ref:right.getEmbeddedIntoReferences()) { if (ref.getParentTable().equals(getTable().getName())) { return ref.clone(); } } return getNestedEmbeddedDocumentReferenceKey(right); } private MergeDetails getNestedEmbeddedDocumentReferenceKey(MongoDocument right) throws TranslatorException { for (MergeDetails ref:this.embeddedKeys) { MongoDocument parent = getDocument(ref.getEmbeddedTable()); if (parent.contains(right)) { MergeDetails key = parent.getEmbeddedDocumentReferenceKey(right); key.setName(parent.getTable().getName()+"."+key.getName()); //$NON-NLS-1$ key.setNested(true); return key; } } for (MergeDetails ref:right.getEmbeddedIntoReferences()) { MongoDocument parent = getDocument(ref.getParentTable()); if (parent.contains(right)) { MergeDetails key = parent.getEmbeddedDocumentReferenceKey(right); key.setName(parent.getTable().getName()+"."+key.getName()); //$NON-NLS-1$ key.setNested(true); return key; } } return null; } public boolean isPartOfPrimaryKey(String columnName) { KeyRecord pk = this.table.getPrimaryKey(); if (pk != null) { for (Column column:pk.getColumns()) { if (column.getName().equals(columnName)) { return true; } } } return false; } boolean hasCompositePrimaryKey() { KeyRecord pk = this.table.getPrimaryKey(); return pk.getColumns().size() > 1; } boolean isPartOfForeignKey(String columnName) { for (ForeignKey fk : this.table.getForeignKeys()) { for (Column column : fk.getColumns()) { if (column.getName().equals(columnName)) { return true; } } } return false; } boolean isCompositeForeignKey(String columnName) { for (ForeignKey fk : this.table.getForeignKeys()) { for (Column column : fk.getColumns()) { if (column.getName().equals(columnName)) { return fk.getColumns().size() > 1; } } } return false; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((this.table == null) ? 0 : this.table.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof MongoDocument)) { return false; } MongoDocument other = (MongoDocument) obj; if (getTable().getName().equals(other.getTable().getName())) { return true; } return false; } @Override public String toString() { return getTable().getName(); } public void setAlias(String alias) { this.documentAlias = alias; } public String getAlias() { return this.documentAlias; } }