com.ikanow.infinit.e.data_model.index.document.DocumentPojoIndexMap.java Source code

Java tutorial

Introduction

Here is the source code for com.ikanow.infinit.e.data_model.index.document.DocumentPojoIndexMap.java

Source

/*******************************************************************************
 * Copyright 2012 The Infinit.e Open Source Project
 * 
 * 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 com.ikanow.infinit.e.data_model.index.document;

import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.elasticsearch.index.search.geo.GeoHashUtils;

import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.ikanow.infinit.e.data_model.index.BaseIndexPojo;
import com.ikanow.infinit.e.data_model.index.BasePojoIndexMap;
import com.ikanow.infinit.e.data_model.index.ElasticSearchPojos;
import com.ikanow.infinit.e.data_model.store.config.source.SourcePojo;
import com.ikanow.infinit.e.data_model.store.document.DocumentPojo;
import com.ikanow.infinit.e.data_model.store.document.EntityPojo;
import com.ikanow.infinit.e.data_model.store.document.AssociationPojo;
import com.ikanow.infinit.e.data_model.utils.GeoOntologyMapping;

public class DocumentPojoIndexMap implements BasePojoIndexMap<DocumentPojo> {
    ////////////////////////////////////////////////////////////////////////////////

    // Some useful string constants:

    public final static String globalDocumentIndex_ = "document_index";
    public final static String globalDocumentIndexCollection_ = "docs_index";
    public final static String manyGeoDocumentIndex_ = "doc_manygeos";
    public final static String manyGeoDocumentIndexCollection_ = "docs_manygeos";
    public final static String dummyDocumentIndex_ = "doc_dummy";
    public final static String documentType_ = "document_index";

    ////////////////////////////////////////////////////////////////////////////////

    // Due to multi-valued field memory handling within ES, need to put "large" arrays
    // of any non-nested objects into their own index: currently this is just geo
    // (can't nest geo due to geo decay function - anyway, it's a bit painful because
    //  of the ontological type issues)

    // This function fills "locs" in and returns true, if the document needs to be treated separately

    public static boolean hasManyGeos(DocumentPojo doc) {

        // Filter, if applied
        SourcePojo.SourceSearchIndexFilter filter = null;
        if (null != doc.getTempSource())
            filter = doc.getTempSource().getSearchIndexFilter();

        doc.setLocs(new TreeSet<String>()); // (TreeSet not HashSet to use Comparable)

        if (null != doc.getDocGeo()) {

            boolean bAddDocGeoToFilter = true; // (support indexing filter)
            if ((null != filter) && (null != filter.fieldList)) {
                if (filter.fieldList.startsWith("-") && filter.fieldList.contains(DocumentPojo.docGeo_)) {
                    bAddDocGeoToFilter = false;
                } else if (!filter.fieldList.contains(DocumentPojo.docGeo_)) {
                    bAddDocGeoToFilter = false;
                }
            }
            //TESTED: positively in, positively out, and not mentioned

            if (bAddDocGeoToFilter) {
                doc.getLocs().add(new StringBuffer("p#")
                        .append(GeoHashUtils.encode(doc.getDocGeo().lat, doc.getDocGeo().lon)).toString());
            }
        }

        // Write local geo - from entities

        if (null != doc.getEntities())
            for (EntityPojo ent : doc.getEntities()) {
                if (null != ent.getGeotag()) {
                    // Apply index filter, if it exists:
                    if (null != filter) {
                        Pattern whichRegex = null;
                        String whichPattern = null;
                        if (null != filter.entityGeoFilterRegex) {
                            whichRegex = filter.entityGeoFilterRegex;
                            whichPattern = filter.entityGeoFilter;
                        } else {
                            whichRegex = filter.entityFilterRegex;
                            whichPattern = filter.entityFilter;
                        } // (end which regex to pick)
                        if (null != whichRegex) {
                            if (whichPattern.startsWith("-")) {
                                if (whichRegex.matcher(ent.getIndex()).find()) {
                                    continue;
                                }
                            } else if (!whichRegex.matcher(ent.getIndex()).find()) {
                                continue;
                            }
                        } // (end if regex exists)
                    } //TESTED (positive and negative geo and normal entities)

                    if ((null != ent.getGeotag().lat) && (null != ent.getGeotag().lon)) {
                        if ((ent.getGeotag().lat >= -90.0) && (ent.getGeotag().lon >= -180.0)
                                && (ent.getGeotag().lat <= 90.0) && (ent.getGeotag().lon <= 180.0)) {
                            // (sigh need to check ranges)                     
                            doc.getLocs()
                                    .add(new StringBuffer()
                                            .append(GeoOntologyMapping.encodeOntologyCode(ent.getOntology_type()))
                                            .append('#')
                                            .append(GeoHashUtils.encode(ent.getGeotag().lat, ent.getGeotag().lon))
                                            .toString());
                            //^^^(convert onto type to character for efficiency (the es "script"/jar-let is going to get called *a lot*))                      
                        } else { // Invalid geotag, need to null out since now search on this in the index
                            ent.setGeotag(null);
                        }
                    }
                }
            } //TESTED

        // Write local geo - from events

        if (null != doc.getAssociations())
            for (AssociationPojo evt : doc.getAssociations()) {
                if (null != evt.getGeotag()) {
                    // Apply association index filter, if it exists
                    if (null != filter) {
                        Pattern whichRegex = null;
                        String whichPattern = null;
                        if (null != filter.assocGeoFilterRegex) {
                            whichRegex = filter.assocGeoFilterRegex;
                            whichPattern = filter.assocGeoFilter;
                        } else {
                            whichRegex = filter.assocFilterRegex;
                            whichPattern = filter.assocFilter;
                        } // (end which regex to pick)
                        if (null != whichRegex) {
                            if (whichPattern.startsWith("-")) {
                                if (whichRegex.matcher(AssociationPojoIndexMap.serialize(evt)).find()) {
                                    continue;
                                }
                            } else if (!whichRegex.matcher(AssociationPojoIndexMap.serialize(evt)).find()) {
                                continue;
                            }

                        } // (end if regex exists)
                    } //(end if filter exists)
                      //TESTED: only by cut and paste from normal association filtering below

                    if (null != evt.getGeotag().lat && null != evt.getGeotag().lon) { // (post sync - or entity didn't have a loc)
                        if ((evt.getGeotag().lat >= -90.0) && (evt.getGeotag().lon >= -180.0)
                                && (evt.getGeotag().lat <= 90.0) && (evt.getGeotag().lon <= 180.0)) {
                            // (sigh need to check ranges)
                            doc.getLocs()
                                    .add(new StringBuffer("p#")
                                            .append(GeoHashUtils.encode(evt.getGeotag().lat, evt.getGeotag().lon))
                                            .toString());
                        } else { // Invalid geotag, need to null out since now search on this in the index
                            evt.setGeotag(null);
                        }
                    }
                } //TESTED
            }

        return (doc.getLocs().size() >= 20);
    }

    ////////////////////////////////////////////////////////////////////////////////

    // Transform the pojo ready for insertion into the index (has already been added to DB at this point)

    @Override
    public GsonBuilder extendBuilder(GsonBuilder gp) {
        return new EntityPojoIndexMap().extendBuilder(new AssociationPojoIndexMap()
                .extendBuilder(gp.registerTypeAdapter(DocumentPojo.class, new DocumentPojoSerializer())));
    }

    protected static class DocumentPojoSerializer implements JsonSerializer<DocumentPojo> {
        // Utility function for serialization

        private static void synchronizeWithIndex(DocumentPojo doc) {
            // Filter, if applied
            SourcePojo.SourceSearchIndexFilter filter = null;
            if (null != doc.getTempSource())
                filter = doc.getTempSource().getSearchIndexFilter();

            doc.setIndex(null); // (this is the index to which we're about to store the feed, so obviously redundant by this point)
            doc.setDisplayUrl(null); // (this is just for the GUI)

            if (null == doc.getLocs()) { // (should always have been filled in by hasManyGeos)
                hasManyGeos(doc);
            }
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

            // If the entity list exists then filter it
            if ((null != filter) && (null != doc.getEntities())
                    && ((null != filter.entityFilter) || (null != filter.entityGeoFilter))) {
                Iterator<EntityPojo> it = doc.getEntities().iterator();
                while (it.hasNext()) {
                    EntityPojo ent = it.next();
                    Pattern whichRegex = null;
                    String whichPattern = null;
                    if (null != ent.getGeotag() && (null != filter.entityGeoFilterRegex)) {
                        whichRegex = filter.entityGeoFilterRegex;
                        whichPattern = filter.entityGeoFilter;
                    } else {
                        whichRegex = filter.entityFilterRegex;
                        whichPattern = filter.entityFilter;
                    } // (end which regex to pick)
                    if (null != whichRegex) {
                        if (whichPattern.startsWith("-")) {
                            if (whichRegex.matcher(ent.getIndex()).find()) {
                                it.remove();
                                continue;
                            }
                        } else if (!whichRegex.matcher(ent.getIndex()).find()) {
                            it.remove();
                            continue;
                        }
                    } // (end if regex exists)
                } //TESTED positive and negative geo and normal entities
            } // (end if filter entities out)

            if (null != doc.getAssociations()) {
                Iterator<AssociationPojo> it = doc.getAssociations().iterator();
                while (it.hasNext()) {
                    AssociationPojo evt = it.next();
                    try {
                        // Apply association index filter, if it exists
                        if (null != filter) {
                            Pattern whichRegex = null;
                            String whichPattern = null;
                            if ((null != evt.getGeotag()) && (null != filter.assocGeoFilterRegex)) {
                                whichRegex = filter.assocGeoFilterRegex;
                                whichPattern = filter.assocGeoFilter;
                            } else {
                                whichRegex = filter.assocFilterRegex;
                                whichPattern = filter.assocFilter;
                            } // (end which regex to pick)
                            if (null != whichRegex) {
                                if (whichPattern.startsWith("-")) {
                                    if (whichRegex.matcher(AssociationPojoIndexMap.serialize(evt)).find()) {
                                        it.remove();
                                        continue;
                                    }
                                } else if (!whichRegex.matcher(AssociationPojoIndexMap.serialize(evt)).find()) {
                                    it.remove();
                                    continue;
                                }

                            } // (end if regex exists)
                        }
                        //TESTED: positive and negative associations, normal only (geo from cut and paste from entities)

                        // Add event date ranges month by month (eg for histograms)
                        if (null != evt.getTime_start()) {
                            Date d1 = sdf.parse(evt.getTime_start());
                            if (null == doc.getMonths()) {
                                doc.setMonths(new TreeSet<Integer>());
                            }

                            if (null == evt.getTime_end()) {
                                Calendar c = Calendar.getInstance();
                                c.setTime(d1);
                                doc.getMonths().add(c.get(Calendar.YEAR) * 100 + c.get(Calendar.MONTH) + 1);

                            } else {
                                Date d2 = sdf.parse(evt.getTime_end());
                                Calendar c = Calendar.getInstance();
                                c.setTime(d1);

                                int nStartYr = c.get(Calendar.YEAR);
                                int nStartMo = c.get(Calendar.MONTH) + 1;
                                c.setTime(d2);
                                int nEndYr = c.get(Calendar.YEAR);
                                int nEndMo = c.get(Calendar.MONTH) + 1;

                                if (nStartYr == nEndYr) {
                                    for (int i = nStartMo; i <= nEndMo; ++i) {
                                        doc.getMonths().add(nStartYr * 100 + i);
                                    }
                                } else if ((nStartYr + 1) == nEndYr) {
                                    for (int i = nStartMo; i <= 12; ++i) {
                                        doc.getMonths().add(nStartYr * 100 + i);
                                    }
                                    for (int i = 1; i <= nEndMo; ++i) {
                                        doc.getMonths().add(nEndYr * 100 + i);
                                    }
                                }
                                // (else too long a period, ignore)
                            }
                        }
                    } //TESTED (single date range, within yr, spanning yrs, > 1yr span) 
                    catch (Exception e) {
                    } // Parsing error, carry on... 
                } // (End loop over associations)
            } //TESTED

            // modified: Don't index 
            doc.setModified(null);

            // Write content into non-transient field:
            doc.makeFullTextNonTransient();

            // Remove #N/#NN distribution in source keys
            doc.setSourceKey(doc.getSourceKey());

            // If filter specified for metadata then remove unwanted fields
            if ((null != filter) && (null != filter.metadataFieldList) && (null != doc.getMetadata())) {
                String metaFields = filter.metadataFieldList;
                boolean bInclude = true;
                if (metaFields.startsWith("+")) {
                    metaFields = metaFields.substring(1);
                } else if (metaFields.startsWith("-")) {
                    metaFields = metaFields.substring(1);
                    bInclude = false;
                }
                String[] metaFieldArray = metaFields.split("\\s*,\\s*");
                if (bInclude) {
                    Set<String> metaFieldSet = new HashSet<String>();
                    metaFieldSet.addAll(Arrays.asList(metaFieldArray));
                    Iterator<Entry<String, Object[]>> metaField = doc.getMetadata().entrySet().iterator();
                    while (metaField.hasNext()) {
                        Entry<String, Object[]> metaFieldIt = metaField.next();
                        if (!metaFieldSet.contains(metaFieldIt.getKey())) {
                            metaField.remove();
                        }
                    }
                } else { // exclude case, easier
                    for (String metaField : metaFieldArray) {
                        doc.getMetadata().remove(metaField);
                    }
                } // (end include vs exclude)

                //TESTED: (copy/paste from Structured Analysis Handler) by eye: positive, negative, default metadata
            } // (end if filter specified)

        }//TESTED (see above clauses): see MongoDocumentTxfer test cases 

        ////////////////////////////////////////////////////////////////////////////////

        // Actual serialization:

        @Override
        public JsonElement serialize(DocumentPojo doc, Type typeOfT, JsonSerializationContext context) {
            synchronizeWithIndex(doc);
            // (does most of the transformations - the default index pojo does the rest)

            // Going to do tags manually to convert them to lower case:
            Set<String> tags = doc.getTags();
            doc.setTags(null);

            // GSON transformation:
            JsonElement je = new EntityPojoIndexMap()
                    .extendBuilder(new AssociationPojoIndexMap().extendBuilder(BaseIndexPojo.getDefaultBuilder()))
                    .create().toJsonTree(doc, typeOfT);

            // Write tags back in (as lower case):
            if (null != tags) {
                JsonArray ja = new JsonArray();
                for (String tag : tags) {
                    ja.add(new JsonPrimitive(tag.toLowerCase()));
                }
                je.getAsJsonObject().add(DocumentPojo.tags_, ja);
            } //TESTED

            // Convert object names in metadata
            if ((null != doc.getMetadata()) && !doc.getMetadata().isEmpty()) {
                if (je.isJsonObject()) {
                    JsonElement metadata = je.getAsJsonObject().get("metadata");
                    if (null != metadata) {
                        enforceTypeNamingPolicy(metadata, 0);
                    }
                }
            }

            // Filter, if applied
            SourcePojo.SourceSearchIndexFilter filter = null;
            if (null != doc.getTempSource())
                filter = doc.getTempSource().getSearchIndexFilter();

            // If filter specified for metadata then remove unwanted fields
            if ((null != filter) && (null != filter.fieldList)) {
                String docFields = filter.fieldList;
                boolean bInclude = true;
                if (docFields.startsWith("+")) {
                    docFields = docFields.substring(1);
                } else if (docFields.startsWith("-")) {
                    docFields = docFields.substring(1);
                    bInclude = false;
                }
                String[] docFieldArray = docFields.split("\\s*,\\s*");
                if (bInclude) {
                    Set<String> docFieldSet = new HashSet<String>();
                    docFieldSet.addAll(Arrays.asList(docFieldArray));
                    // If entity/association/metadata specified, then leave them in by default
                    if ((null != filter.entityFilter) || (null != filter.entityGeoFilter)) {
                        docFieldSet.add(DocumentPojo.entities_);
                    }
                    if ((null != filter.assocFilter) || (null != filter.assocGeoFilter)) {
                        docFieldSet.add(DocumentPojo.associations_);
                    }
                    if (null != filter.metadataFieldList) {
                        docFieldSet.add(DocumentPojo.metadata_);
                    }
                    Iterator<Entry<String, JsonElement>> docField = je.getAsJsonObject().entrySet().iterator();
                    while (docField.hasNext()) {
                        Entry<String, JsonElement> docFieldIt = docField.next();
                        String docFieldEl = docFieldIt.getKey();

                        if (docFieldEl.equals(DocumentPojo.fullText_)
                                || docFieldEl.equals(DocumentPojo.description_)
                                || docFieldEl.equals(DocumentPojo.tags_) || docFieldEl.equals(DocumentPojo.docGeo_)
                                || docFieldEl.equals(DocumentPojo.metadata_)
                                || docFieldEl.equals(DocumentPojo.entities_)
                                || docFieldEl.equals(DocumentPojo.associations_)) {
                            if (!docFieldSet.contains(docFieldIt.getKey())) {
                                docField.remove();
                            }
                        } // (if this field is allowed to be delete)
                    }
                } else { // exclude case, easier
                    for (String docField : docFieldArray) {
                        if (docField.equals(DocumentPojo.fullText_) || docField.equals(DocumentPojo.description_)
                                || docField.equals(DocumentPojo.tags_) || docField.equals(DocumentPojo.docGeo_)
                                || docField.equals(DocumentPojo.metadata_)
                                || docField.equals(DocumentPojo.entities_)
                                || docField.equals(DocumentPojo.associations_)) {
                            if (je.getAsJsonObject().has(docField)) {
                                je.getAsJsonObject().remove(docField);
                            }
                        } // (end if field that is allowed to be deleted)
                    }
                } // (end include vs exclude)
                  //TESTED: positive and negative filter, can't filter "obligatory fields"
            } // (end if filter specified)

            // Convert to record format:
            JsonObject jo = je.getAsJsonObject();
            jo.add("message", jo.get("title"));
            jo.add("type", jo.get("mediaType"));
            jo.add("@timestamp", jo.get("publishedDate"));

            return je;
        }
    }

    //TODO (INF-1566): TOTEST still: tags, MongoDocumentTxfer::rebuildIndex, AssociationPojoIndexMap (print out string plz)

    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    // ElasticSearch mapping:   

    public static class Mapping {

        public static class RootObject {
            ElasticSearchPojos.SourcePojo _source = new ElasticSearchPojos.SourcePojo(true, "_id", "message",
                    "@timestamp", "url", "sourceKey", "sourceUrl", "displayUrl", "docGeo", "type", "tags",
                    "record.*");

            ElasticSearchPojos.AllPojo _all = new ElasticSearchPojos.AllPojo(true);

            public static class RootProperties {
                ElasticSearchPojos.FieldStringPojo _id = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();

                // Basic metadata
                ElasticSearchPojos.FieldStringPojo title = new ElasticSearchPojos.FieldStringPojo("yes", "analyzed",
                        null);
                ElasticSearchPojos.FieldStringPojo url = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();
                // (note this field needs to remain stored in order for sync to work)

                // Dates
                ElasticSearchPojos.FieldDatePojo publishedDate = new ElasticSearchPojos.FieldDatePojo("yes", null,
                        null, null).excludeFromAll();
                ElasticSearchPojos.FieldDatePojo created = new ElasticSearchPojos.FieldDatePojo("yes", null, null,
                        null).excludeFromAll();

                // Source management
                ElasticSearchPojos.FieldStringPojo source = new ElasticSearchPojos.FieldStringPojo("yes",
                        "analyzed", null).excludeFromAll();
                ElasticSearchPojos.FieldStringPojo sourceKey = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();
                ElasticSearchPojos.FieldStringPojo mediaType = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();
                ElasticSearchPojos.FieldStringPojo tags = new ElasticSearchPojos.FieldStringPojo("no",
                        "not_analyzed", null).excludeFromAll();
                ElasticSearchPojos.FieldStringPojo sourceUrl = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();

                // Social/source
                ElasticSearchPojos.FieldStringPojo communityId = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();

                // Content
                ElasticSearchPojos.FieldStringPojo description = new ElasticSearchPojos.FieldStringPojo("no",
                        "analyzed", null);
                ElasticSearchPojos.FieldStringPojo fullText = new ElasticSearchPojos.FieldStringPojo("no",
                        "analyzed", null).excludeFromAll();

                // Enriched content
                EntityPojoIndexMap.Mapping entities = new EntityPojoIndexMap.Mapping();
                ElasticSearchPojos.FieldStringPojo locs = new ElasticSearchPojos.FieldStringPojo("yes",
                        "not_analyzed", null).excludeFromAll();
                ElasticSearchPojos.FieldGeoPointPojo docGeo = new ElasticSearchPojos.FieldGeoPointPojo("yes");
                ElasticSearchPojos.FieldGeoPointPojo timeRanges = new ElasticSearchPojos.FieldGeoPointPojo("yes"); // (currently unused)
                ElasticSearchPojos.FieldIntegerPojo months = new ElasticSearchPojos.FieldIntegerPojo("yes", null,
                        null).excludeFromAll();
                AssociationPojoIndexMap.Mapping associations = new AssociationPojoIndexMap.Mapping();

                // User content enrichment
                // No default mapping required, see below for dynamic template mapping

                // "Record" metadata
                // _id
                // title<->message
                ElasticSearchPojos.FieldStringPojo message = new ElasticSearchPojos.FieldStringPojo("no", "no",
                        null).excludeFromAll();
                // type<->mediaType
                ElasticSearchPojos.FieldStringPojo type = new ElasticSearchPojos.FieldStringPojo("no", "no", null)
                        .excludeFromAll();
                // publishedDate<->@timestamp
                ElasticSearchPojos.FieldDatePojo __AMP__timestamp = new ElasticSearchPojos.FieldDatePojo("yes",
                        null, null, null).excludeFromAll();
                // record: copied fields from metadata, will use dynamic template mapping like metadata
            }

            public RootProperties properties = new RootProperties();

            // Default templates for metadata:
            public ElasticSearchPojos.DynamicTemplateList dynamic_templates[] = ElasticSearchPojos.DynamicTemplateList
                    .generateDefaultTemplates(generateRecordDynamicMapping(), generateMetadataDynamicMapping());

            private static ElasticSearchPojos.DynamicTemplateList generateRecordDynamicMapping() {
                ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping rawMapping = new ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping(
                        "string", "yes", "not_analyzed", null, null, null, null, (Integer) 256);
                TreeMap<String, ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping> fields = new TreeMap<String, ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping>();
                fields.put("raw", rawMapping);

                return new ElasticSearchPojos.DynamicTemplateList("template_record_mapping",
                        new ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate(null, "record.*", "string",
                                new ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping("string",
                                        "yes", "analyzed", null, null, null, null, null, fields)));
            }

            private static ElasticSearchPojos.DynamicTemplateList generateMetadataDynamicMapping() {
                ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping rawMapping = new ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping(
                        "string", "yes", "not_analyzed", null, null, null, null, (Integer) 256);
                TreeMap<String, ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping> fields = new TreeMap<String, ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping>();
                fields.put("raw", rawMapping);

                return new ElasticSearchPojos.DynamicTemplateList("template_metadata_mapping",
                        new ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate(null, "metadata.*",
                                "string", new ElasticSearchPojos.DynamicTemplateList.FieldGenericTemplate.Mapping(
                                        "string", "yes", "analyzed", null, null, null, null, null, fields)));
            }

            // Turn number/date detection off for metadata:
            public Boolean date_detection = false;
            public Boolean numeric_detection = false;
        }

        public RootObject document_index = new RootObject();
    }

    //////////////////////////////////////////////////////////////////////////////////////////

    // Utility function for parsing native GSON to rename object fieldnames by appending "__obj"
    // and encoding "."s and "%"s (needed for the DB, duplicated here sigh)

    private static boolean enforceTypeNamingPolicy(JsonElement je, int nDepth) {

        if (je.isJsonPrimitive()) {
            return false; // Done
        } else if (je.isJsonArray()) {
            JsonArray ja = je.getAsJsonArray();
            if (0 == ja.size()) {
                return false; // No idea, carry on
            }
            JsonElement jaje = ja.get(0);
            return enforceTypeNamingPolicy(jaje, nDepth + 1); // keep going until you find primitive/object
        } else if (je.isJsonObject()) {
            JsonObject jo = je.getAsJsonObject();
            // Nested variables:
            Iterator<Entry<String, JsonElement>> it = jo.entrySet().iterator();
            StringBuffer newName = null;
            Map<String, JsonElement> toFixList = null;
            while (it.hasNext()) {
                boolean bFix = false;
                Entry<String, JsonElement> el = it.next();
                String currKey = el.getKey();

                if ((currKey.indexOf('.') >= 0) || (currKey.indexOf('%') >= 0)) {
                    it.remove();
                    currKey = currKey.replace("%", "%25").replace(".", "%2e");
                    bFix = true;
                }
                if (null == el.getValue()) {
                    if (!bFix)
                        it.remove(); // nice easy case, just get rid of it (if bFix, it's already removed)
                    bFix = false;
                } else if (enforceTypeNamingPolicy(el.getValue(), nDepth + 1)) { // rename!
                    if (currKey.indexOf("__") < 0) { // unless it's an es type
                        if (!bFix)
                            it.remove(); // (if bFix, it's already removed)
                        if (null == newName) {
                            newName = new StringBuffer();
                        } else {
                            newName.setLength(0);
                        }
                        currKey = newName.append(currKey).append("__obj").toString();
                        bFix = true;
                    }
                } // (end check if need to rename)
                if (bFix) {
                    if (null == toFixList) {
                        toFixList = new HashMap<String, JsonElement>();
                    }
                    toFixList.put(currKey, el.getValue());
                }
            } // (end loop over params)   
            if (null != toFixList) {
                for (Entry<String, JsonElement> el : toFixList.entrySet()) {
                    jo.add(el.getKey(), el.getValue());
                }
            }
            return true; // (in any case, I get renamed by calling parent)
        }
        return false;
    }
    //TESTED (see DOC_META in test/TestCode)

}//TESTED (by hand/eye, see MongoDocumentTxfer)