Java tutorial
/* * Copyright 2012-2013 inBloom, Inc. and its affiliates. * * 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 org.slc.sli.dal.convert; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.mongodb.BasicDBObject; import com.mongodb.CommandResult; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.WriteConcern; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.slc.sli.common.domain.EmbeddedDocumentRelations; import org.slc.sli.common.util.tenantdb.TenantContext; import org.slc.sli.common.util.uuid.UUIDGeneratorStrategy; import org.slc.sli.domain.Entity; import org.slc.sli.domain.MongoEntity; import org.slc.sli.validation.schema.INaturalKeyExtractor; /** * Utility for accessing subdocuments that have been collapsed into a super-doc * * @author nbrown */ public class SubDocAccessor { private static final Logger LOG = LoggerFactory.getLogger(SubDocAccessor.class); private final Map<String, Location> locations = new HashMap<String, SubDocAccessor.Location>(); private final MongoTemplate template; private final UUIDGeneratorStrategy didGenerator; private final INaturalKeyExtractor naturalKeyExtractor; public SubDocAccessor(MongoTemplate template, UUIDGeneratorStrategy didGenerator, INaturalKeyExtractor naturalKeyExtractor) { this.template = template; this.didGenerator = didGenerator; this.naturalKeyExtractor = naturalKeyExtractor; for (String entityType : EmbeddedDocumentRelations.getSubDocuments()) { String parent = EmbeddedDocumentRelations.getParentEntityType(entityType); String parentKey = EmbeddedDocumentRelations.getParentFieldReference(entityType); if (parent != null && parentKey != null) { store(entityType).within(parent).as(entityType).mapping(parentKey, "_id").register(); } } } /** * Start a location for a given sub doc type * * @param type * @return */ public LocationBuilder store(String type) { return new LocationBuilder(type); } public Location createLocation(String type, String parentColl, Map<String, String> lookup, String subField) { return new LocationBuilder(type).as(subField).within(parentColl).mapping(lookup).build(); } private class LocationBuilder { private Map<String, String> lookup = new HashMap<String, String>(); private String collection; private String subField; private final String type; public LocationBuilder(String type) { super(); this.type = type; } /** * Store the subdoc within the given super doc collection * * @param collection the collection the subdoc gets stored in * @return */ public LocationBuilder within(String collection) { this.collection = collection; return this; } /** * The field the subdocs show up in * * @param subField The field the subdocs show up in * @return */ public LocationBuilder as(String subField) { this.subField = subField; return this; } /** * Map a field in the sub doc to the super doc. This will be used when resolving parenthood * * @param subDocField * @param superDocField * @return */ public LocationBuilder mapping(String subDocField, String superDocField) { lookup.put(subDocField, superDocField); return this; } /** * Register it as a sub resource location */ public void register() { locations.put(type, new Location(collection, lookup, subField)); } public Location build() { return new Location(collection, lookup, subField); } public LocationBuilder mapping(Map<String, String> lookupMap) { lookup.putAll(lookupMap); return this; } } /** * THe location of the subDoc * * @author nbrown */ public class Location { private final String collection; private final Map<String, String> lookup; private final String subField; /** * Create a new location to store subdocs * * @param collection the collection the superdoc is in * @param key the field in the subdoc that refers to the super doc's id * @param subField the place to put the sub doc */ public Location(String collection, Map<String, String> lookup, String subField) { super(); this.collection = collection; this.lookup = lookup; this.subField = subField; } private DBObject getParentQuery(Map<String, Object> body) { Query parentQuery = new Query(); for (Entry<String, String> entry : lookup.entrySet()) { parentQuery.addCriteria(new Criteria(entry.getValue()).is(body.get(entry.getKey()))); } return parentQuery.getQueryObject(); } // this method is for supporting patch sub doc public boolean doUpdate(Query query, Update update) { DBObject queryDBObject = toSubDocQuery(query, true); DBObject elementMatch = new BasicDBObject("$elemMatch", query.getQueryObject()); queryDBObject.put(subField, elementMatch); DBObject patchUpdate = toSubDocUpdate(update); String updateCommand = "{findAndModify:\"" + collection + "\",query:" + queryDBObject.toString() + ",update:" + patchUpdate.toString() + "}"; LOG.debug("the update date mongo command is: {}", updateCommand); TenantContext.setIsSystemCall(false); CommandResult result = template.executeCommand(updateCommand); return result.get("value") != null; } // transform original update to sub doc update with positional operator $ @SuppressWarnings("unchecked") private DBObject toSubDocUpdate(Update originalUpdate) { DBObject updateDBObject = new BasicDBObject(); for (String key : originalUpdate.getUpdateObject().keySet()) { if (key.startsWith("$")) { Map<String, Object> fieldAndValue = (Map<String, Object>) originalUpdate.getUpdateObject() .get(key); Map<String, Object> newFieldAndValue = new HashMap<String, Object>(); for (Entry<String, Object> entry : fieldAndValue.entrySet()) { String field = entry.getKey(); if (!field.startsWith("$")) { newFieldAndValue.put(subField + ".$." + field, entry.getValue()); } } updateDBObject.put(key, newFieldAndValue); } } return updateDBObject; } private boolean doUpdate(DBObject parentQuery, List<Entity> subEntities) { boolean result = true; TenantContext.setIsSystemCall(false); result &= template.getCollection(collection) .update(parentQuery, buildPullObject(subEntities), true, false, WriteConcern.SAFE) .getLastError().ok(); result &= doPush(parentQuery, subEntities); return result; } private boolean doPush(DBObject parentQuery, List<Entity> subEntities) { DBObject query = new BasicDBObject(parentQuery.toMap()); query.putAll( new Query(Criteria.where(subField + "._id").nin(getSubDocDids(subEntities))).getQueryObject()); if (template.getCollection(collection) .update(query, buildPushObject(subEntities), false, false, WriteConcern.SAFE).getN() == 1) { return true; } else { if (subEntities.size() > 1) { // try each subentity on its own before failing for (Entity entity : subEntities) { doPush(parentQuery, Arrays.asList(entity)); } } return false; } } private DBObject buildPullObject(List<Entity> subEntities) { Set<String> existingIds = new HashSet<String>(getSubDocDids(subEntities)); Query pullQuery = new Query(Criteria.where("_id").in(existingIds)); Update update = new Update(); update.pull(subField, pullQuery.getQueryObject()); return update.getUpdateObject(); } private DBObject buildPushObject(List<Entity> subEntities) { List<DBObject> subDocs = new ArrayList<DBObject>(); for (Entity entity : subEntities) { subDocs.add(subDocToDBObject(entity)); } Update update = new Update(); update.set("type", collection).pushAll(subField, subDocs.toArray()); return update.getUpdateObject(); } private List<String> getSubDocDids(List<Entity> subEntities) { List<String> subDocDids = new ArrayList<String>(); for (Entity entity : subEntities) { subDocDids.add((String) subDocToDBObject(entity).get("_id")); } return subDocDids; } private DBObject subDocToDBObject(Entity entity) { MongoEntity mongoEntity; if (entity instanceof MongoEntity) { mongoEntity = (MongoEntity) entity; } else { mongoEntity = new MongoEntity(entity.getType(), entity.getEntityId(), entity.getBody(), entity.getMetaData()); } DBObject dbObject = mongoEntity.toDBObject(didGenerator, naturalKeyExtractor); return dbObject; } private Entity convertDBObjectToSubDoc(DBObject dbObject) { return MongoEntity.fromDBObject(dbObject); } public boolean create(Entity entity) { // return update(makeEntityId(entity), entity.getBody()); DBObject parentQuery = getParentQuery(entity.getBody()); List<Entity> subEntities = new ArrayList<Entity>(); subEntities.add(entity); return doUpdate(parentQuery, subEntities); } public boolean delete(Entity entity) { if (entity == null) { return false; } DBObject parentQuery = getParentQuery(entity.getBody()); List<Entity> subEntities = new ArrayList<Entity>(); subEntities.add(entity); TenantContext.setIsSystemCall(false); boolean updateResult = template.getCollection(collection) .update(parentQuery, buildPullObject(subEntities), false, false, WriteConcern.SAFE) .getLastError().ok(); if (!updateResult) { return updateResult; } // Now retrieve the full doc and if it has gone completely empty, remove it from the data store. // This is useful to clean out data consisting solely of IDs that appear in the "lookup" fields: // the subdoc data may be getting deleted as part of a larger, cascading delete that also deletes // the parent objects these IDs refer to. In these cases it is necessary to remove the entire superdoc // to avoid having dangling IDs anywhere in the data store. DBCursor subDocs = template.getCollection(collection).find(parentQuery); if (null == subDocs) { LOG.warn("Failed to retreive superdoc after item deletion using criteria: " + parentQuery.toString()); return updateResult; } // Should really only have one object in this cursor. However the empty-check is highly conservative // so that the effect on multiple matches would be to clean them all, which would be desirable. while (subDocs.hasNext()) { DBObject subDoc = subDocs.next(); Map<String, Object> map = subDoc.toMap(); map.remove("_id"); map.remove("type"); // Remove "lookup" keys present in body Map<String, Object> body = (Map<String, Object>) map.get("body"); if (body != null) { // Remove keys from body that match exactly keys in "lookup" of the form { "foo": "body.foo" } for (String lkey : lookup.keySet()) { String lval = lookup.get(lkey); if (!lval.startsWith("body.")) { continue; } lval = lval.substring(5); // Remove leading "body." if (null != body.get(lval)) { body.remove(lval); } } } // Remove entire document if it is empty (net of any extra "lookup" fields added for performance) if (isEffectivelyEmpty(map)) { boolean deleteResult = template.getCollection(collection).remove(parentQuery, WriteConcern.SAFE) .getLastError().ok(); if (!deleteResult) { return deleteResult; } } } return updateResult; } public boolean insert(List<Entity> entities) { ConcurrentMap<DBObject, List<Entity>> parentEntityMap = new ConcurrentHashMap<DBObject, List<Entity>>(); for (Entity entity : entities) { DBObject parentQuery = getParentQuery(entity.getBody()); parentEntityMap.putIfAbsent(parentQuery, new ArrayList<Entity>()); parentEntityMap.get(parentQuery).add(entity); } boolean result = true; for (Entry<DBObject, List<Entity>> entry : parentEntityMap.entrySet()) { result &= doUpdate(entry.getKey(), entry.getValue()); } return result; } public Entity findById(String id) { LOG.debug("the subDoc id is: {}", id); Query subDocQuery = new Query(Criteria.where("_id").is(id)); DBObject parentQueryDBObject = toSubDocQuery(subDocQuery, true); List<Entity> entities = findSubDocs(parentQueryDBObject, new Query().getQueryObject()); if (entities != null && entities.size() == 1) { return entities.get(0); } return null; } public List<Entity> findAll(Query originalQuery) { DBObject parentQueryDBObject = toSubDocQuery(originalQuery, true); List<Entity> entities = findSubDocs(parentQueryDBObject, getLimitQuery(originalQuery)); return entities; } // convert original query match criteria to match embeded subDocs protected DBObject toSubDocQuery(Query originalQuery, boolean isParentQuery) { return toSubDocQuery(originalQuery.getQueryObject(), isParentQuery); } @SuppressWarnings("unchecked") private DBObject toSubDocQuery(DBObject originalQueryDBObject, boolean isParentQuery) { DBObject queryDBObject = appendSubField(originalQueryDBObject, isParentQuery); for (String key : originalQueryDBObject.keySet()) { if (key.equals("$or") || key.equals("$and")) { List<DBObject> originalOrQueryDBObjects = (List<DBObject>) originalQueryDBObject.get(key); List<DBObject> orQueryDBObjects = new ArrayList<DBObject>(); for (DBObject originalOrQueryDBObject : originalOrQueryDBObjects) { DBObject orQueryDBObject = appendSubField(originalOrQueryDBObject, isParentQuery); if (orQueryDBObject.get("_id") != null) { addId(queryDBObject, orQueryDBObject.get("_id")); orQueryDBObject.removeField("_id"); } orQueryDBObjects.add(orQueryDBObject); } queryDBObject.put(key, orQueryDBObjects); } } return queryDBObject; } // retrieve limit/offset/sort info from the original query and make them applicable to // subDocs private DBObject getLimitQuery(Query originalQuery) { DBObject limitQueryDBObject = new Query().getQueryObject(); DBObject originalSortDBObject = originalQuery.getSortObject(); if (originalSortDBObject != null && originalSortDBObject.keySet().size() > 0) { limitQueryDBObject.put("$sort", appendSubField(originalSortDBObject, false)); } if (originalQuery.getSkip() > 0) { limitQueryDBObject.put("$skip", originalQuery.getSkip()); } if (originalQuery.getLimit() > 0) { limitQueryDBObject.put("$limit", originalQuery.getLimit()); } return limitQueryDBObject; } // append subField to original query key, so it can query in subDocs @SuppressWarnings("unchecked") private DBObject appendSubField(DBObject originalDBObject, boolean isParentQuery) { DBObject newDBObject = new Query().getQueryObject(); for (String key : originalDBObject.keySet()) { if (!key.startsWith("$")) { String newKey = key; Object newValue = originalDBObject.get(key); String updatedKey = key.replace("body.", ""); if (key.equals("_id") && getIds(newValue).size() != 0) { // use parent id for id query try { Set<String> parentIds = getParentIds(getIds(newValue)); if (parentIds != null && parentIds.size() > 1) { newDBObject.put(newKey, new BasicDBObject("$in", parentIds)); } else if (parentIds != null && parentIds.size() == 1) { newDBObject.put(newKey, parentIds.iterator().next()); } } catch (InvalidIdException e) { LOG.info("There was an invalid Id exception. Ignoring."); // child id does not have parent id, qppend the subfield to original // query, this may trigger table scan if subFiled._id is not // indexed // newKey = subField + "." + key; // newDBObject.put(newKey, newValue); LOG.error( "Embedded entity's ID does not contain parentId. Cannot determine parent superdoc. ID: {}", newValue); } } if (lookup.containsKey(updatedKey)) { if (newDBObject.get(updatedKey) != null) { Object idList = newDBObject.get(updatedKey); Set<String> combined = new HashSet<String>(); combined.addAll(extractIdSet(idList)); combined.addAll(extractIdSet(newValue)); newDBObject.put(lookup.get(updatedKey), new BasicDBObject("$in", combined)); } else { newDBObject.put(lookup.get(key.replace("body.", "")), newValue); } } else { // for other query, append the subfield to original key newKey = subField + "." + key; newDBObject.put(newKey, newValue); } } else if (key.equals("$or") || key.equals("$and")) { List<DBObject> dbObjects = (List<DBObject>) originalDBObject.get(key); List<DBObject> orQueryDBObjects = new ArrayList<DBObject>(); for (DBObject dbObject : dbObjects) { DBObject subQuery = toSubDocQuery(dbObject, isParentQuery); if (subQuery.get("_id") != null) { addId(newDBObject, subQuery.get("_id")); subQuery.removeField("_id"); } orQueryDBObjects.add(subQuery); } newDBObject.put(key, orQueryDBObjects); } } return newDBObject; } private void addId(DBObject newDBObject, Object id) { Set<String> combined = new HashSet<String>(); combined.addAll(extractIdSet(newDBObject)); combined.addAll(extractIdSet(id)); newDBObject.put("_id", new BasicDBObject("$in", combined)); } // retrieve the ids from DBObject value for "_id" field @SuppressWarnings({ "unchecked" }) private Set<String> getIds(Object queryValue) { Set<String> ids = new HashSet<String>(); if (queryValue instanceof String) { ids.add((String) queryValue); } else if (queryValue instanceof DBObject) { DBObject dbValue = (DBObject) queryValue; Object inQuery = dbValue.get("$in"); if (inQuery != null && inQuery instanceof List<?>) { ids.addAll((List<String>) inQuery); } else if (inQuery != null && inQuery instanceof String[]) { ids.addAll(Arrays.asList(((String[]) inQuery))); } } return ids; } private int countSubDocs(DBObject parentQuery) { simplifyParentQuery(parentQuery); DBObject idQuery = buildIdQuery(parentQuery); // String queryCommand = buildAggregateQuery((idQuery == null ? parentQuery.toString() : idQuery.toString()), // parentQuery.toString(), ", {$group: { _id: null, count: {$sum: 1}}}"); String groupQuery = ", {$group: { _id: null, count: {$sum: 1}}}"; String queryCommand; if (idQuery == null) { queryCommand = buildAggregateQuery(parentQuery.toString(), null, groupQuery); } else { queryCommand = buildAggregateQuery(idQuery.toString(), parentQuery.toString(), groupQuery); } TenantContext.setIsSystemCall(false); CommandResult result = template.executeCommand(queryCommand); @SuppressWarnings("unchecked") Iterator<DBObject> resultList = ((List<DBObject>) result.get("result")).iterator(); if (resultList.hasNext()) { return (Integer) (resultList.next().get("count")); } else { return 0; } } @SuppressWarnings("unchecked") private List<Entity> findSubDocs(DBObject parentQuery, DBObject limitQuery) { StringBuilder limitQuerySB = new StringBuilder(); if (limitQuery != null && limitQuery.keySet().size() > 0) { if (limitQuery.get("$sort") != null) { limitQuerySB.append(",{$sort:" + limitQuery.get("$sort").toString() + "}"); } if (limitQuery.get("$skip") != null) { limitQuerySB.append(",{$skip:" + limitQuery.get("$skip") + "}"); } if (limitQuery.get("$limit") != null) { limitQuerySB.append(",{$limit:" + limitQuery.get("$limit") + "}"); } } simplifyParentQuery(parentQuery); DBObject idQuery = buildIdQuery(parentQuery); // String queryCommand = buildAggregateQuery(idQuery != null ? idQuery.toString() : parentQuery.toString(), // parentQuery.toString(), limitQuerySB.toString()); String queryCommand; if (idQuery == null) { queryCommand = buildAggregateQuery(parentQuery.toString(), null, limitQuerySB.toString()); } else { queryCommand = buildAggregateQuery(idQuery.toString(), parentQuery.toString(), limitQuerySB.toString()); } TenantContext.setIsSystemCall(false); CommandResult result = template.executeCommand(queryCommand); List<DBObject> subDocs = (List<DBObject>) result.get("result"); List<Entity> entities = new ArrayList<Entity>(); if (subDocs != null && subDocs.size() > 0) { for (DBObject dbObject : subDocs) { entities.add(convertDBObjectToSubDoc(((DBObject) dbObject.get(subField)))); } } return entities; } private String buildAggregateQuery(String match1, String match2, String others) { StringBuilder queryStringBuilder = new StringBuilder(); queryStringBuilder.append("{aggregate : \"").append(collection).append("\", pipeline:["); if (match1 != null) { queryStringBuilder.append("{$match : ").append(match1).append("},"); } queryStringBuilder.append("{$project : {\"").append(subField).append("\":1,\"_id\":0 } },"); queryStringBuilder.append("{$unwind: \"$").append(subField).append("\"}"); if (match2 != null) { queryStringBuilder.append(",{$match : ").append(match2).append("}"); } queryStringBuilder.append(others).append("]}"); return queryStringBuilder.toString(); } private DBObject buildIdQuery(DBObject parentQuery) { DBObject idQuery = new Query().getQueryObject(); Set<String> parentQueryKeys = parentQuery.keySet(); Set<String> removeFieldKeys = new HashSet<String>(); if (parentQuery.containsField("_id")) { Object idFinalList = parentQuery.get("_id"); if (idFinalList instanceof List) { idQuery.put("_id", new BasicDBObject("$in", idFinalList)); } else { idQuery.put("_id", idFinalList); } parentQuery.removeField("_id"); } else { for (String parentQueryKey : parentQueryKeys) { if (parentQueryKey.startsWith(subField + ".body.") && (parentQueryKey.endsWith("Id") || parentQueryKey.endsWith("learningObjectives") || parentQueryKey.endsWith("assessmentItemRefs"))) { idQuery.put(parentQueryKey, parentQuery.get(parentQueryKey)); } else if (parentQueryKey.startsWith("body")) { idQuery.put(parentQueryKey, parentQuery.get(parentQueryKey)); removeFieldKeys.add(parentQueryKey); } } } if (!removeFieldKeys.isEmpty()) { for (String parentQueryKey : removeFieldKeys) { parentQuery.removeField(parentQueryKey); } } if (idQuery.keySet().size() == 0) { return null; } return idQuery; } @SuppressWarnings("unchecked") private Set<String> extractIdSet(final Object obj) { if (obj instanceof DBObject) { Object dbObj = ((DBObject) obj).get("$in"); if (dbObj instanceof List) { return new HashSet<String>((List<String>) dbObj); } } else if (obj instanceof String) { return new HashSet<String>(Arrays.asList((String) obj)); } return Collections.emptySet(); } @SuppressWarnings("unchecked") private void simplifyParentQuery(final DBObject query) { final Set<String> parentSet = new HashSet<String>(); if (isSubDoc(this.subField) && subDoc(this.subField).collection.equals(this.collection)) { final String childLoc = this.subField.concat("._id"); final String parentLoc = "_id"; final Object dbOrObj = query.get("$or"); if (dbOrObj != null && dbOrObj instanceof List) { final List<DBObject> dbOrList = (List<DBObject>) dbOrObj; for (DBObject childQuery : dbOrList) { Object childInQuery = childQuery.get(childLoc); if (childInQuery instanceof DBObject && ((DBObject) childInQuery).containsField("$in")) { Object inList = ((DBObject) childInQuery).get("$in"); try { Object id = query.get("_id"); if (id != null && id instanceof String) { String singleId = (String) id; if (getParentIds(inList).contains(singleId)) { parentSet.add(singleId); } else { // No union of constraining criteria --> return return; } } else { parentSet.addAll(getParentIds(inList)); } } catch (InvalidIdException e) { // IDs aren't valid, we can't simplify the query return; } if (parentSet.size() > 0) { if (dbOrList.size() == 1) { query.removeField("$or"); } else { dbOrList.remove(childQuery); } } } } } if (parentSet.size() == 1) { LOG.info("Putting parent id {} in {}", parentSet.iterator().next(), parentLoc); query.put(parentLoc, parentSet.iterator().next()); } else if (parentSet.size() > 1) { LOG.info("Putting parent ids $in[{}] in {}", parentSet, parentLoc); query.put(parentLoc, new BasicDBObject("$in", parentSet)); } } } @SuppressWarnings("unchecked") private Set<String> getParentIds(final Object childIds) throws InvalidIdException { final Set<String> parentSet = new HashSet<String>(); if (childIds instanceof Iterable) { for (String childId : (Iterable<String>) childIds) { addParentId(parentSet, childId); } } else if (childIds instanceof String) { addParentId(parentSet, (String) childIds); } return parentSet; } private void addParentId(final Set<String> parentIds, final String childId) throws InvalidIdException { final String parentId = SuperDocIdUtility.getParentId(childId); if (childId.equals(parentId)) { throw new InvalidIdException("ChildId == ParentId"); } parentIds.add(parentId); } public boolean exists(String id) { TenantContext.setIsSystemCall(false); return findById(id) != null; } public long count(Query query) { DBObject parentQueryDBObject = toSubDocQuery(query, true); return countSubDocs(parentQueryDBObject); } // public boolean delete(String id) { // String targetDoc = "body." + lookup.get("_id") + "." + id; // DBObject query = this.getExactSubDocQuery(id); // Update update = new Update(); // update.unset(targetDoc); // return this.doUpdate(query, update); // } public void deleteAll(Query query) { for (Entity e : findAll(query)) { Entity entity = findById(e.getEntityId()); delete(entity); } } @SuppressWarnings("serial") private class InvalidIdException extends Exception { public InvalidIdException(String s) { super(s); } } } public boolean isSubDoc(String docType) { return locations.containsKey(docType); } public Location subDoc(String docType) { return locations.get(docType); } /* * Check if a map of data is "effectively" empty - has only values that are null, * empty string, or empty lists. This allows us to get rid of * "garbage" super-docs after the last subdoc item is deleted, so as not to leave * potentially invalid indexing information in the data store. * * This method is used as criteria for total deletion of a document, and therefore * should be "conservative," only returning true if it is certain all content * is of known (and empty) type. */ private static boolean isEffectivelyEmpty(Map<String, Object> map) { for (String key : map.keySet()) { Object val = map.get(key); if ((null == val) || ((val instanceof String) && ((String) val).length() == 0) || ((val instanceof Map) && isEffectivelyEmpty((Map<String, Object>) val)) || ((val instanceof List<?>) && ((List<?>) val).isEmpty())) { continue; } return false; } return true; } public static void main(String[] args) { PrintStream output = System.out; SubDocAccessor accessor = new SubDocAccessor(null, null, null); ConcurrentMap<String, List<String>> collections = new ConcurrentHashMap<String, List<String>>(); for (Entry<String, Location> entry : accessor.locations.entrySet()) { String type = entry.getKey(); String collectionName = entry.getValue().collection; collections.putIfAbsent(collectionName, new ArrayList<String>()); collections.get(collectionName).add(type); } for (Entry<String, List<String>> entry : collections.entrySet()) { output.println(entry.getKey() + ": " + StringUtils.join(entry.getValue().toArray(), ", ")); } } }