org.collectionspace.chain.csp.schema.Record.java Source code

Java tutorial

Introduction

Here is the source code for org.collectionspace.chain.csp.schema.Record.java

Source

/* Copyright 2010-2012 University of Cambridge
 * Licensed under the Educational Community License (ECL), Version 2.0. You may not use this file except in 
 * compliance with this License.
 *
 * You may obtain a copy of the ECL 2.0 License at https://source.collectionspace.org/collection-space/LICENSE.txt
 */
package org.collectionspace.chain.csp.schema;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.collectionspace.chain.csp.config.ReadOnlySection;

import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 
 * @author caret
 * 
 * 
 */
public class Record implements FieldParent {
    private static final Logger log = LoggerFactory.getLogger(Record.class);

    public final static String BLOB_SOURCE_URL = "blobUri"; // BlobClient.BLOB_URI_PARAM; // The 'blobUri' query param used to pass an external URL for the services to download data from
    public final static String BLOB_PURGE_ORIGINAL = "blobPurgeOrig"; // BlobClient.BLOB_PURGE_ORIGINAL;

    private static final String TYPE_AUTHORITY = "Authority";
    private static final String TYPE_AUTHORITY_LOWERCASE = TYPE_AUTHORITY.toLowerCase();

    public static final String SUPPORTS_LOCKING = "supportslocking";
    public static final String SUPPORTS_REPLICATING = "supportsReplicating";
    public static final String REMOTECLIENT_CONFIG_NAME = "remoteClientConfigName";
    public static final String REQUIRES_UNIQUE_SHORTID = "requiresUniqueShortId";
    public static final String SUPPORTS_VERSIONING = "supportsversioning";
    public static final String RANGE_START_SUFFIX = "Start";
    public static final String RANGE_END_SUFFIX = "End";

    public static final String COLLECTIONSPACE_CORE_PART_NAME = "collectionspace_core";
    public static final String COLLECTIONSPACE_COMMON_PART_NAME = "common";
    public static final String COLLECTIONSPACE_SYSTEM_PART_NAME = "system";
    public static final String COLLECTIONSPACE_SCHEMA_LOCATION_SYSTEM = "http://collectionspace.org/services/config/system http://collectionspace.org/services/config/system/system-response.xsd";
    public static final String COLLECTIONSPACE_NAMESPACE_URI_SYSTEM = "http://collectionspace.org/services/config/system";

    protected SchemaUtils utils = new SchemaUtils();

    private String configFileSrcName = null; // The tenant config file name -e.g, botgarden-tenant.xml
    private Map<String, Structure> structure = new HashMap<String, Structure>();
    private Map<String, Map<String, String>> uisection = new HashMap<String, Map<String, String>>();
    private List<FieldSet> selfRenderers = new ArrayList<FieldSet>();
    private Map<String, FieldSet> subrecords = new HashMap<String, FieldSet>();
    private Map<String, Map<String, FieldSet>> subrecordsperm = new HashMap<String, Map<String, FieldSet>>();

    //operation - GET POST PUT SEARCH - as some fields are not available for all operations   
    //genpermfields becomes (servicefieldsbyoperation) - topfieldsbyoperation
    private Map<String, Map<String, FieldSet>> topfieldsbyoperation = new HashMap<String, Map<String, FieldSet>>();
    //allgenpermfields becomes allfieldsbyoperation includes fields that don't existing the service layer
    private Map<String, Map<String, FieldSet>> allfieldsbyoperation = new HashMap<String, Map<String, FieldSet>>();
    //servicepermfields becomes servicefieldsbyoperation
    private Map<String, Map<String, Map<String, FieldSet>>> servicefieldsbyoperation = new HashMap<String, Map<String, Map<String, FieldSet>>>();
    // don't thinkg this is used
    // private Map<String, Map<String, FieldSet>> mergedpermfields = new HashMap<String, Map<String, FieldSet>>();
    //servicefields becomes serviceFieldTopLevel
    private Map<String, FieldSet> serviceFieldTopLevel = new HashMap<String, FieldSet>();
    //fields become fieldTopLevel
    private Map<String, FieldSet> fieldTopLevel = new HashMap<String, FieldSet>();

    //this is actually a list of all fields flattened out rather than hierarchical so includes all children
    //repeatfields become (messageKeyFields) becomes fieldFullList
    private Map<String, FieldSet> fieldFullList = new HashMap<String, FieldSet>();
    //serviceFieldFullList: not sure if this is needed - but is all fields in the service layer
    private Map<String, FieldSet> serviceFieldFullList = new HashMap<String, FieldSet>();
    //searchFieldFullList: all fields used in the advanced search UI
    private Map<String, FieldSet> searchFieldFullList = new HashMap<String, FieldSet>();

    //merged fields are fields in the UI that take multiple service layer fields
    private Map<String, FieldSet> mergedfields = new HashMap<String, FieldSet>();

    //list of all 'record' e.g. structuredDates, dimensions etc that are included
    private Map<String, String> nestedFieldList = new HashMap<String, String>();

    private Map<String, Instance> instances = new LinkedHashMap<String, Instance>();
    private Map<String, FieldSet> summarylist = new HashMap<String, FieldSet>();
    private Map<String, Map<String, FieldSet>> minidataset = new HashMap<String, Map<String, FieldSet>>();
    private Spec spec;
    private FieldSet mini_summary, mini_number, display_name;
    public String whoamI = ""; // Used for debugging purposes.
    private HashSet<String> authTypeTokenSet = new HashSet<String>();
    private Record lastAuthoriyProxy = null; // Used during Service binding generation.  Only the "baseAuthority" record ever uses this member.  Values would be things like PersonAuthority, OrgAuthority, and other authority records.

    // Map field id to id of the field to be used for sorting
    private Map<String, String> sortKeys = new HashMap<String, String>();

    /* Service stuff */
    private Map<String, String> services_record_paths = new HashMap<String, String>();
    private Map<String, String> services_instances_paths = new HashMap<String, String>();
    private Map<String, Field> services_filter_param = new HashMap<String, Field>();

    public String getConfigFileName() {
        return this.configFileSrcName;
    }

    public void setConfigFileName(String fileName) {
        this.configFileSrcName = fileName;
    }

    public Record getLastAuthorityProxy() {
        return this.lastAuthoriyProxy;
    }

    public void setLastAuthorityProxy(Record lastAuthoriyProxy) {
        this.lastAuthoriyProxy = lastAuthoriyProxy;
    }

    // XXX utility methods
    Record(Spec parent, ReadOnlySection section, Map<String, String> data) {
        //Map<String,String>data = (Map<String,String>)parent;
        /* parameters */
        // this is what the service layer id defaults to if not specified later
        // standard = singular form of the concept
        utils.initStrings(section, "@id", null);
        whoamI = utils.getString("@id");

        utils.initStrings(section, "@cms-type", "none");
        utils.initBoolean(section, "@generate-services-schema", true);
        utils.initBoolean(section, "@is-extension", false);
        utils.initBoolean(section, "@generate-if-authority", true); // REM: 12/2012 - The Contact service config file will set this value to false for schema generation -"false" since Contact is not a true authority from the Service's perspective.

        //
        // The name for used to create the Services XML Schema.
        //
        utils.initStrings(section, "@services-type", null);

        // record,authority,compute-displayname can have multiple types using
        // commas
        utils.initSet(section, "@type", new String[] { "record" });
        //
        // Service specific config for Nuxeo ECM platform - things added to the "doctype" definitions
        //
        utils.initSet(section, "@services-folder-subtypes", new String[] {});
        utils.initSet(section, "@services-workspace-subtypes", new String[] {});
        utils.initSet(section, "@services-prefetch-fields",
                new String[] { "<!-- Fields to be prefetch by the repository manager. -->" });

        utils.initStrings(section, "showin", "");

        // specified that it is included in the findedit uispec - probably not useful any more?
        //      utils.initBoolean(section,"@in-findedit",false);

        utils.initBoolean(section, "@in-recordlist", true);

        //Record differentiates between things like structureddates and procedures
        utils.initBoolean(section, "@separate-record", true);

        // config whether service layer needs call as multipart or not - authorization is not currently multipart
        utils.initBoolean(section, "is-multipart", true);

        // config whether record type has termsUsed or not (returns empty array
        // in Json if = false )
        utils.initBoolean(section, "terms-used", true);
        // config whether record type has relatedObj/procs or not (returns empty
        // array in Json if = false )
        utils.initBoolean(section, "refobj-used", true);

        // config the keyword to use for searching
        utils.initStrings(section, "services-search-keyword", "kw");

        // Used to differentiate between authority and vocabulary on create
        //utils.initStrings(section,"membership-tag","inAuthority");

        /* UI Layer helpers */
        // ui layer path
        utils.initStrings(section, "web-url", utils.getString("@id"));

        // specify url if not nameAuthority
        //utils.initStrings(section,"terms-used-url", "nameAuthority");

        utils.initStrings(section, "preselector", ".csc-");
        utils.initStrings(section, "decoratorselector", "cs-");

        // ui layer json row
        utils.initStrings(section, "number-selector", utils.getString("preselector") + "entry-number");

        // ui layer json used in list views
        utils.initStrings(section, "row-selector", utils.getString("preselector") + "recordList-row:");

        // ui layer path: defaults to web_url if not specified
        utils.initStrings(section, "ui-url", utils.getString("web-url") + ".html");

        // ui layer path
        utils.initStrings(section, "tab-url", utils.getString("web-url") + "-tab");
        utils.initStrings(section, "search-url", utils.getString("web-url") + "-search");

        utils.initStrings(section, "enum-blank", data.get("blank"));
        /* Service layer helpers */

        // path that the service layer uses to access this record
        utils.initStrings(section, "services-url", utils.getString("@id"));

        // authorization 
        utils.initStrings(section, "authorization-includes", utils.getString("services-url"));

        utils.initBoolean(section, "authorization-view", true);

        // service layer paths to list data for this record type
        utils.initStrings(section, "services-list-path",
                utils.getString("services-url") + "-common-list/" + utils.getString("services-url") + "-list-item");

        // This is relative to the services-list-path, in usage.
        utils.initStrings(section, "services-fields-path", "fieldsReturned");

        // used by service layer to construct authority names
        utils.initStrings(section, "authority-vocab-type", "");
        //
        utils.initStrings(section, "services-instances-path",
                utils.getString("services-url") + "_common:http://collectionspace.org/services/"
                        + utils.getString("services-url") + "," + utils.getString("services-url") + "-common-list/"
                        + utils.getString("services-url") + "-list-item");

        //
        utils.initStrings(section, "services-single-instance-path",
                utils.getString("services-url") + "_common:http://collectionspace.org/services/"
                        + utils.getString("services-url") + "," + utils.getString("services-url") + "-common");
        utils.initStrings(section, "primaryfield", "");
        utils.initBoolean(section, "hasdeletemethod", false);
        utils.initBoolean(section, "hassoftdelete", false);
        utils.initBoolean(section, SUPPORTS_LOCKING, false);
        utils.initBoolean(section, SUPPORTS_REPLICATING, false);
        utils.initStrings(section, REMOTECLIENT_CONFIG_NAME, "");
        utils.initBoolean(section, SUPPORTS_VERSIONING, false);

        //(17:06)The services singular tag should probably be "ServicesDocumentType"

        // The tenant's repository that the service will use.
        utils.initStrings(section, "services-repo-domain", utils.getString("services-repo-domain"));

        utils.initStrings(section, "services-tenant-singular", utils.getString("services-url"));
        utils.initStrings(section, "services-tenant-doctype", null);//, utils.getString("services-tenant-singular"));

        utils.initStrings(section, "services-tenant-plural", utils.getString("services-tenant-singular") + "s");
        utils.initStrings(section, "services-tenant-auth-singular", utils.getString("services-url"));
        utils.initStrings(section, "services-tenant-auth-plural",
                utils.getString("services-tenant-singular") + "s");

        utils.initStrings(section, "services-schema-location", "http://services.collectionspace.org");

        utils.initStrings(section, "services-abstract",
                "org.collectionspace.services." + utils.getString("services-tenant-singular").toLowerCase() + "."
                        + utils.getString("services-tenant-plural") + "CommonList");
        utils.initStrings(section, "services-common", utils.getString("services-abstract") + "$"
                + utils.getString("services-tenant-singular") + "ListItem");

        utils.initStrings(section, "services-dochandler", null);
        utils.initStrings(section, "services-validator", null);

        spec = parent;
    }

    public String getRecordName() {
        return this.whoamI;
    }

    @Override
    public boolean isTrueRepeatField() {
        return false;
    }

    /** field functions **/
    //getPerm is now hasFieldByOperation(fieldId,operation)
    //tests if a specific field exists for a certain operation
    public Boolean hasFieldByOperation(String fieldId, String operation) {
        if (allfieldsbyoperation.containsKey(operation)) {
            if (allfieldsbyoperation.get(operation).containsKey(fieldId)) {
                return true;
            }
        }
        return false;
    }

    /**
     * mark up the different types of repeats repeator are adv search only
     * repeats repeatored are adv search and edit/view repeats repeatable are
     * just edit/view repeats
     * 
     * @param f
     */
    public void addSearchField(FieldSet f) {

        if (!(f.getSearchType().equals(""))) {
            FieldSet searchf = f;
            if (searchf.getSearchType().equals("repeatable")) {
                //need to makeup a repeatable field
                if (f.getParent() instanceof Repeat) {
                    Repeat rs = (Repeat) f.getParent();
                    //XXX this name is terrible - should be a better way
                    rs.setSearchType("repeatored"); //need to set the type of repeat this is e.g. is it just for search or is is also for edit
                    searchFieldFullList.put(rs.getID(), rs);
                } else {
                    Repeat r = new Repeat(searchf.getRecord(), searchf.getID() + "s"); //UI wants 'plurals' for the fake repeat parents
                    r.setSearchOnlyRepeat(true); // Our parent is not a "real" Repeat field.  It's just acting this way for searches.
                    //XXX this name is terrible - should be a better way
                    r.setSearchType("repeator");
                    searchFieldFullList.put(r.getID(), r);

                    searchf.setParent(r);
                    r.addChild(searchf); // this is causing confusing later on... can we separate this some how?
                }
            } else if (searchf.getSearchType().equals("range")) {
                searchf.getRecord().addUISection("search", searchf.getID());
                //need to add label for main bit as well...
                Field fst = new Field(searchf.getRecord(), searchf.getID() + RANGE_START_SUFFIX);
                Field fed = new Field(searchf.getRecord(), searchf.getID() + RANGE_END_SUFFIX);

                fst.setSearchType("range");
                fed.setSearchType("range");
                fed.setType(searchf.getUIType());
                fst.setType(searchf.getUIType());

                if (searchf.getUIType().equals("computed")) {
                    Field field = (Field) searchf;
                    // Copy the necessary attributes into the start and end fields
                    fst.setUIFunc(field.getUIFunc());
                    fst.setDataType(field.getDataType());
                    fst.setLabel(field.getLabel());
                    fst.setReadOnly(field.isReadOnly());
                    fst.setUIArgs(suffixUIArgs(
                            field.getUISearchArgs().equals("") ? field.getUIArgs() : field.getUISearchArgs(),
                            RANGE_START_SUFFIX));

                    fed.setUIFunc(field.getUIFunc());
                    fed.setDataType(field.getDataType());
                    fed.setLabel(field.getLabel());
                    fed.setReadOnly(field.isReadOnly());
                    fed.setUIArgs(suffixUIArgs(
                            field.getUISearchArgs().equals("") ? field.getUIArgs() : field.getUISearchArgs(),
                            RANGE_END_SUFFIX));
                }

                searchFieldFullList.put(fst.getID(), fst);
                searchFieldFullList.put(fed.getID(), fed);
            } else {
                searchFieldFullList.put(searchf.getID(), searchf);
            }
        }
    }

    private String suffixUIArgs(String args, String suffix) {
        String[] elements = args.split(",");

        for (int i = 0; i < elements.length; i++) {
            String element = elements[i];
            elements[i] = element + suffix;
        }

        return StringUtils.join(elements, ",");
    }

    public void addNestedFieldList(String r) {
        nestedFieldList.put(r, r);
    }

    public void addField(FieldSet f) {
        String parentType = f.getParent().getClass().getSimpleName();
        fieldFullList.put(f.getID(), f);
        if (f.isInServices() || f.isServicesDerived()) {
            serviceFieldFullList.put(f.getID(), f);
        } else {
            log.trace(String.format(
                    "%s: isInServices() and isServicesDerived() methods returned false, so we wont add '%s' to the serviceFieldFullList.",
                    f.getID(), f.getID()));
        }
        if (f.isASelfRenderer()) {
            addSelfRenderer(f);
        }

        //is this a search field?
        addSearchField(f);

        if (parentType.equals("Record")) { //toplevel field
            fieldTopLevel.put(f.getID(), f);
            if (f.isInServices() || f.isServicesDerived()) {
                serviceFieldTopLevel.put(f.getID(), f);
                //list fields by the operations they support
                for (String oper : f.getAllFieldOperations()) {
                    hashutil(servicefieldsbyoperation, oper, f.getSection(), f);
                    //subdivide section and then by GET POST PUT etc
                }
            } else {
                log.trace(String.format(
                        "%s: isInServices() and isServicesDerived() methods returned false, so we wont add '%s' to the serviceFieldTopLevel.",
                        f.getID(), f.getID()));
            }
            //list fields by the operations they support
            for (String oper : f.getAllFieldOperations()) {
                hashutil(topfieldsbyoperation, oper, f); //subdivide by GET POST PUT etc
            }
            hashutil(topfieldsbyoperation, "", f); //ALL function
        }
        //list fields by the operations they support
        for (String oper : f.getAllFieldOperations()) {
            hashutil(allfieldsbyoperation, oper, f); //subdivide by GET POST PUT etc
        }
        hashutil(allfieldsbyoperation, "", f); //ALL function
    }

    public FieldSet[] getAllServiceFieldTopLevel(String operation, String section) { //section should be an array?
        if (operation.equals("")) {//return everything
            return serviceFieldTopLevel.values().toArray(new FieldSet[0]);
        }
        if (servicefieldsbyoperation.containsKey(operation)) {
            if (servicefieldsbyoperation.get(operation).containsKey(section)) {
                return servicefieldsbyoperation.get(operation).get(section).values().toArray(new FieldSet[0]);
            }
        }
        return new FieldSet[0];
    }

    public FieldSet[] getAllFieldFullList() {
        return fieldFullList.values().toArray(new FieldSet[0]);
    }

    public FieldSet[] getAllFieldFullList(String operation) {
        if (allfieldsbyoperation.containsKey(operation)) {
            return allfieldsbyoperation.get(operation).values().toArray(new FieldSet[0]);
        }
        return new FieldSet[0];
    }

    public FieldSet[] getAllFieldTopLevel(String operation) {
        if (operation.equals("")) { //return all fields
            return fieldTopLevel.values().toArray(new FieldSet[0]);
        }
        if (operation.equals("search")) {
            return searchFieldFullList.values().toArray(new FieldSet[0]);
        } else if (topfieldsbyoperation.containsKey(operation)) {
            return topfieldsbyoperation.get(operation).values().toArray(new FieldSet[0]);
        }
        return new FieldSet[0];
    }

    //merged fields are fields in the UI that take multiple service layer fields
    public FieldSet[] getAllMergedFields() {
        FieldSet[] merged = mergedfields.values().toArray(new FieldSet[0]);
        return merged;
    }

    public FieldSet getField(String id) {
        return this.fieldFullList.get(id);
    }

    public FieldSet getFieldTopLevel(String id) {
        return fieldTopLevel.get(id);
    }

    public FieldSet getFieldFullList(String id) {
        if (fieldFullList.get(id) != null) {
            return fieldFullList.get(id);
        }
        for (String r : nestedFieldList.values().toArray(new String[0])) {
            Record subitems = this.getSpec().getRecordByServicesUrl(r);
            if (subitems != null) {
                if (subitems.getFieldFullList(id) != null) {
                    return subitems.getFieldFullList(id);
                }
            }
        }
        return null;
    }

    /*
     * Returns a list of fields that have the "authref-in-services" attribute
     * defined
     */
    public ArrayList<FieldSet> getOtherServiceAuthRefFields() {
        ArrayList<FieldSet> result = new ArrayList<FieldSet>();

        for (String key : fieldFullList.keySet()) {
            FieldSet fieldSet = fieldFullList.get(key);
            if (fieldSet.isAuthRefInServices() == true) {
                result.add(fieldSet);
            }
        }

        return result;
    }

    public Map<String, String> getNestedFieldList() {
        return this.nestedFieldList;
    }

    public FieldSet getServiceFieldFullList(String id) {
        return serviceFieldFullList.get(id);
    }

    public FieldSet getSearchFieldFullList(String id) {
        return searchFieldFullList.get(id);
    }

    public Boolean hasSearchField(String id) {
        return searchFieldFullList.containsKey(id);
    }

    public void setMerged(Field f) {
        mergedfields.put(f.getID(), f);
    }

    public Boolean hasMerged() {
        if (mergedfields.isEmpty())
            return false;

        return true;
    }

    //generic function to simplify adding to hashmaps
    private void hashutil(Map<String, Map<String, Map<String, FieldSet>>> testfields, String level1, String level2,
            FieldSet fs) {
        if (!testfields.containsKey(level1)) {
            testfields.put(level1, new HashMap<String, Map<String, FieldSet>>());
        }
        if (!testfields.get(level1).containsKey(level2)) {
            testfields.get(level1).put(level2, new HashMap<String, FieldSet>());
        }
        testfields.get(level1).get(level2).put(fs.getID(), fs);
    }

    //generic function to simplify adding to hashmaps
    private void hashutil(Map<String, Map<String, FieldSet>> testfields, String level1, FieldSet fs) {
        if (!testfields.containsKey(level1)) {
            testfields.put(level1, new HashMap<String, FieldSet>());
        }
        testfields.get(level1).put(fs.getID(), fs);

    }

    /** end field functions **/
    @Override
    public String toString() {
        return getID();
    }

    @Override
    public String getID() {
        return utils.getString("@id");
    }

    public String getWebURL() {
        return utils.getString("web-url");
    }

    //used in tenantUIServlet for overlaying stuff and testing if I am showing the right page
    public String getUIURL() {
        return utils.getString("ui-url");
    }

    public String getTabURL() {
        return utils.getString("tab-url");
    }

    public String getSearchURL() {
        return utils.getString("search-url");
    }

    public boolean isShowType(String k) {
        if (utils.getString("showin").equals("")) {
            return utils.getSet("@type").contains(k);
        } else {
            return utils.getString("showin").equals(k);
        }
    }

    public boolean isAuthorityItemType() {
        return isType(TYPE_AUTHORITY_LOWERCASE) && shouldGenerateAuthoritySchema();
    }

    public boolean isType(String k) {
        return utils.getSet("@type").contains(k);
    }

    public Set<String> getServicesFolderSubtypes() {
        return utils.getSet("@services-folder-subtypes");
    }

    public Set<String> getServicesWorkspaceSubtypes() {
        return utils.getSet("@services-workspace-subtypes");
    }

    public Set<String> getServicesPrefetchFields() {
        return utils.getSet("@services-prefetch-fields");
    }

    public Spec getSpec() {
        return spec;
    }

    @Override
    public FieldParent getParent() {
        return null;
    }

    public String getPreSelector() {
        return utils.getString("preselector");
    }

    public String getDecoratorSelector() {
        return utils.getString("decoratorselector");
    }

    public String getUIprefix() {
        return getPreSelector() + "";
    }

    public String getUILabel(String id) {
        return utils.getString("@id") + "-" + id + "Label";
    }

    public String getServicesType() {
        String result = utils.getString("@services-type");
        return result;
    }

    public String getServicesCmsType() {
        String result = utils.getString("@cms-type");
        return result;
    }

    private boolean shouldGenerateAuthoritySchema() {
        boolean result = utils.getBoolean("@generate-if-authority");
        return result;
    }

    public boolean isServicesExtension() {
        boolean result = utils.getBoolean("@is-extension");
        return result;
    }

    public boolean isGenerateServicesSchema() {
        boolean result = utils.getBoolean("@generate-services-schema");
        return result;
    }

    public String getUILabelSelector(String id) {
        return getPreSelector() + utils.getString("@id") + "-" + id + "-label";
    }

    public String getUILabelSelector() {
        return getUIprefix() + utils.getString("@id") + "-label";
    }

    public String[] getAllUISections(String section) {
        if (section.equals("")) {
            section = "base";
        }
        if (uisection.containsKey(section)) {
            return uisection.get(section).values().toArray(new String[0]);
        }
        return null;
    }

    public String getUISections(String section, String id) {
        if (section.equals("")) {
            section = "base";
        }
        if (uisection.containsKey(section)) {
            return uisection.get(section).get(id);
        }
        return null;
    }

    @Override
    public String enumBlankValue() {
        return utils.getString("enum-blank");
    }

    public Structure getStructure(String id) {
        // fall back if structure isn't defined used defaults
        if (!structure.containsKey(id)) {
            Structure s = new Structure(this, id);
            structure.put(id, s);
        }
        return structure.get(id);
    }

    public Record getSubrecord(String fieldid) {
        return subrecords.get(fieldid).usesRecordId();
    }

    public FieldSet[] getAllSubRecords(String perm) {
        if (!subrecordsperm.containsKey(perm)) {
            subrecordsperm.put(perm, new HashMap<String, FieldSet>());
            for (FieldSet fs : this.getAllFieldFullList(perm)) {
                if (fs.usesRecord()) {
                    this.addSubRecord(fs, perm);
                }
            }
        }
        return subrecordsperm.get(perm).values().toArray(new FieldSet[0]);
    }

    public String getPrimaryField() {
        return utils.getString("primaryfield");
    };

    public Boolean hasPrimaryField() {
        if (getPrimaryField().equals("")) {
            return false;
        } else {
            return true;
        }
    }

    public String getNumberSelector() {
        return utils.getString("number-selector");
    }

    public String getRowSelector() {
        return utils.getString("row-selector");
    }

    public boolean isInRecordList() {
        return utils.getBoolean("@in-recordlist");
    }

    public boolean isRealRecord() {
        return utils.getBoolean("@separate-record");
    }

    public boolean isMultipart() {
        return utils.getBoolean("is-multipart");
    }

    public boolean hasTermsUsed() {
        return utils.getBoolean("terms-used");
    }

    public boolean hasRefObjUsed() {
        return utils.getBoolean("refobj-used");
    }

    public boolean hasHierarchyUsed(String type) {
        return this.getStructure(type).showHierarchySection();
    }

    public boolean hasDeleteMethod() {
        return utils.getBoolean("hasdeletemethod");
    }

    public boolean hasSoftDeleteMethod() {
        return utils.getBoolean("hassoftdelete");
    }

    public boolean supportsLocking() {
        return utils.getBoolean(SUPPORTS_LOCKING);
    }

    public boolean supportsReplicating() {
        return utils.getBoolean(SUPPORTS_REPLICATING);
    }

    public String getRemoteClientConfigName() {
        return utils.getString(REMOTECLIENT_CONFIG_NAME);
    }

    public boolean supportsVersioning() {
        return utils.getBoolean(SUPPORTS_VERSIONING);
    }

    public String getServicesSearchKeyword() {
        return utils.getString("services-search-keyword");
    }

    public String getVocabType() {
        return utils.getString("authority-vocab-type");
    }

    public Instance[] getAllInstances() {
        return instances.values().toArray(new Instance[0]);
    }

    public int getNumInstances() {
        return instances.size();
    }

    public Instance getInstance(String key) {
        return instances.get(key);
    }

    public Boolean hasInstance(String key) {
        return instances.containsKey(key);
    }

    public String getServicesURL() {
        return utils.getString("services-url");
    }

    public String getServicesTenantSg() {
        return utils.getString("services-tenant-singular");
    }

    public String getServicesTenantPl() {
        return utils.getString("services-tenant-plural");
    }

    public String getServicesTenantAuthSg() {
        return utils.getString("services-tenant-auth-singular");
    }

    /*
     * By convention, the value from getServicesTenantSg() is the Nuxeo doctype
     * name. However, if the record explicitly declares a doctype using the
     * "services-tenant-doctype" element then that value is used. Also,
     * Authorities are a special case, the doctype should be the value from
     * getServicesTenantAuthSg for Authorites.
     */
    public String getServicesTenantDoctype(boolean isAuthority) {
        String result = this.getServicesTenantSg();

        String elementVal = utils.getString("services-tenant-doctype");
        if (elementVal != null && elementVal.trim().isEmpty() == false) {
            result = elementVal;
        }
        //
        // Handle special case for Authorities
        // 
        if (isAuthority == true) {
            result = this.getServicesTenantAuthSg();
        }

        return result;
    }

    public String getServicesTenantAuthPl() {
        return utils.getString("services-tenant-auth-plural");
    }

    public String getServicesAbstractCommonList() {
        return utils.getString("services-abstract");
    }

    public String getServicesCommonList() {
        return utils.getString("services-common");
    }

    public String getServicesSchemaBaseLocation() {
        return utils.getString("services-schema-location");
    }

    /*
     * The "<services-tenant-auth-singular>" element of the config record for
     * authorities is used to create the Nuxeo doctype names. We also need to
     * use it to generate the Service layer's document handler class name for
     * the service bindings. Unfortunately, most of the existing authorities use
     * something like "Personauthority" for the Nuxeo document name and
     * "PersonAuthorityDocument..." for the document handler class name.
     * Therefore, we need to convert the lowercase 'a' to an uppercase 'A' in
     * the "Authority" substring. For example, we convert the substring
     * "Personauthority" to "PersonAuthority".
     */
    private String getAuthorityForm(String handlerName) {
        String result = handlerName;

        String servicesTenantSg = this.getServicesTenantSg();
        String servicesTenantAuthSg = this.getServicesTenantAuthSg();
        String authorityName = servicesTenantAuthSg.replace(TYPE_AUTHORITY_LOWERCASE, TYPE_AUTHORITY); // replace "authority" with "Authority"
        result = result.replace(servicesTenantSg, authorityName);

        return result;
    }

    /*
     * Gets the last word at the end of a URL.
     */
    private static String getURLTail(String url) {
        String result = null;

        Scanner scanner = null;
        try {
            scanner = new Scanner(url).useDelimiter("\\/+");
            while (scanner.hasNext() == true) {
                result = scanner.next();
            }
        } catch (Exception e) {
            // Ignore the exception or logger.trace(e);
        } finally {
            scanner.close();
        }

        return result;
    }

    /*
     * Gets the Service's document model handler by using standard naming conventions.
     */
    public String getServicesDocHandler(Boolean isAuthority) {
        String result = utils.getString("services-dochandler");

        if (result == null) {
            result = "org.collectionspace.services."
                    + getURLTail(getServicesSchemaNameSpaceURI(COLLECTIONSPACE_COMMON_PART_NAME)) + ".nuxeo."
                    + utils.getString("services-tenant-singular") + "DocumentModelHandler";
        }

        if (isAuthority == true) {
            result = getAuthorityForm(result);
        }

        return result;
    }

    /*
     * Gets the Service's document model validation handler by using standard naming conventions.
     * For example, org.collectionspace.services.collectionobject.nuxeo.CollectionObjectValidatorHandler
     */
    public String getServicesValidatorHandler(Boolean isAuthority) {
        String result = utils.getString("services-validator");

        if (result == null) {
            result = "org.collectionspace.services."
                    + getURLTail(getServicesSchemaNameSpaceURI(COLLECTIONSPACE_COMMON_PART_NAME)) + ".nuxeo."
                    + utils.getString("services-tenant-singular") + "ValidatorHandler";
        }

        if (isAuthority == true) {
            result = getAuthorityForm(result);
        }

        return result;
    }

    //
    // For example, org.collectionspace.services.client.CollectionObjectClient
    //
    public String getServicesClientHandler(Boolean isAuthority) {
        String result = utils.getString("services-client");

        if (result == null) {
            result = "org.collectionspace.services.client." + utils.getString("services-tenant-singular")
                    + "Client";
        }

        if (isAuthority == true) {
            result = getAuthorityForm(result);
        }

        return result;
    }

    public String getServicesRepositoryDomain() {
        return utils.getString("services-repo-domain");
    }

    public String getServicesListPath() {
        return utils.getString("services-list-path");
    }

    public String getServicesFieldsPath() {
        return utils.getString("services-fields-path");
    }

    public String getServicesInstancesPath() {
        return utils.getString("services-instances-path");
    }

    public String getServicesSingleInstancePath() {
        return utils.getString("services-single-instance-path");
    }

    public String[] getServicesRecordPathKeys() {
        return services_record_paths.keySet().toArray(new String[0]);
    }

    public String[] getServicesInstancesPaths() {
        return services_instances_paths.keySet().toArray(new String[0]);
    }

    public String getServicesRecordPath(String name) {
        if (services_record_paths.get(name) != null) {
            return services_record_paths.get(name).trim();
        }
        return null;
    }

    public String getServicesSchemaName(String sectionName) {
        String result = null;

        String servicesRecordPath = getServicesRecordPath(sectionName);
        if (servicesRecordPath != null) {
            result = servicesRecordPath.split(":", 2)[0];
        }

        return result;
    }

    public String getAuthoritySchemaName() {
        return getServicesSingleInstancePath().split(":", 2)[0];
    }

    public String getServicesSchemaNameSpaceURI(String sectionName) {
        String path = getServicesRecordPath(sectionName);
        String[] pathParts = path.split(":", 2);
        String schemaLocationCore = pathParts[1].split(",", 2)[0];

        return schemaLocationCore;
    }

    public String getXMLSchemaLocation(String sectionName) {
        String result;

        String schemaName = this.getServicesSchemaName(sectionName);
        String schemaNamespaceUri = this.getServicesSchemaNameSpaceURI(sectionName);
        String schemaXsdLocation = schemaNamespaceUri.replace(sectionName, schemaName);
        result = schemaNamespaceUri + " " + schemaXsdLocation + ".xsd";

        return result;
    }

    public String getServicesPartLabel(String sectionName) {
        String[] pathParts = getServicesRecordPath(sectionName).split(":", 2);
        String schemaPartLabel = pathParts[1].split(",", 2)[1];
        return schemaPartLabel;
    }

    public Boolean hasServicesRecordPath(String name) {
        if (services_record_paths.containsKey(name)) {
            return true;
        }
        return false;
    }

    public String getServicesInstancesPath(String name) {
        return services_instances_paths.get(name);
    }

    public Boolean hasServicesInstancesPath(String name) {
        if (services_instances_paths.containsKey(name)) {
            return true;
        }
        return false;
    }

    void setMiniNumber(FieldSet f) {
        mini_number = f;
    }

    void setMiniSummary(FieldSet f) {
        mini_summary = f;
    }

    void setDisplayName(Field f) {
        display_name = f;
    }

    void setServicesRecordPath(String section, String path) {
        services_record_paths.put(section, path);
    }

    void setServicesInstancePath(String section, String path) {
        services_instances_paths.put(section, path);
    }

    void setServicesFilterParam(String param, Field field) {
        services_filter_param.put(param, field);
    }

    public FieldSet getMiniNumber() {
        return mini_number;
    }

    private void findMiniNumber() {
        if (mini_number == null) {
            // Try child expanders to see if the mini_number is there.
            // This handles case of mini_number for terms in the "preferred" termList sub-record
            for (FieldSet fs : selfRenderers) {
                Record subrecord = fs.getSelfRendererRecord();
                FieldSet miniNumberFS = subrecord.getMiniNumber();
                if (miniNumberFS != null) {
                    mini_number = miniNumberFS;
                    return;
                }
            }
        }
    }

    public FieldSet getMiniSummary() {
        return mini_summary;
    }

    private void findMiniSummary() {
        if (mini_summary == null) {
            // Try child expanders to see if the mini_summary is there.
            // This handles case of mini_summary for terms in the "preferred" termList sub-record
            for (FieldSet fs : selfRenderers) {
                Record subrecord = fs.getSelfRendererRecord();
                FieldSet miniSummaryFS = subrecord.getMiniNumber();
                if (miniSummaryFS != null) {
                    mini_summary = miniSummaryFS;
                    return;
                }
            }
        }
    }

    public FieldSet[] getAllMiniSummaryList() {
        return summarylist.values().toArray(new FieldSet[0]);
    }

    public FieldSet getMiniSummaryList(String key) {
        return summarylist.get(key);
    }

    // obsolete?
    public void addMiniSummaryList(FieldSet f) {
        summarylist.put(f.getID(), f);
    }

    private void mergeNestedSummaryLists() {
        for (FieldSet fs : selfRenderers) {
            Record subrecord = fs.getSelfRendererRecord();
            if (!subrecord.summarylist.isEmpty())
                summarylist.putAll(subrecord.summarylist);
        }
    }

    /**
     * For selfrenderer children, like the preferredTerm block in authorities,
     * we need to gather up all the declared search fields into this parent, so
     * that when we generate a UISpec or UISchema for search, we get those
     * nested fields as well.
     */
    private void mergeSearchLists() {
        for (FieldSet fs : selfRenderers) {
            Record subrecord = fs.getSelfRendererRecord();
            if (!subrecord.searchFieldFullList.isEmpty())
                searchFieldFullList.putAll(subrecord.searchFieldFullList);
        }
    }

    public FieldSet getDisplayNameField() {
        return display_name;
    }

    private void findDisplayNameField() {
        if (display_name == null) {
            // Try child expanders to see if the displayName is there.
            // This handles case of displayName for terms in the "preferred" termList sub-record
            for (FieldSet fs : selfRenderers) {
                Record subrecord = fs.getSelfRendererRecord();
                FieldSet displayNameFS = subrecord.getDisplayNameField();
                if (displayNameFS != null) {
                    display_name = displayNameFS;
                    return;
                }
            }
        }
    }

    private void buildAuthTypeTokenSet() {
        String authTypeTokens[] = getAuthorizationType().split("/");
        for (String token : authTypeTokens) {
            authTypeTokenSet.add(token);
        }
    }

    // authorization
    public Boolean getAuthorizationView() {
        return utils.getBoolean("authorization-view");
    }

    public String getAuthorizationType() {
        return utils.getString("authorization-includes");
    }

    public Boolean isAuthorizationType(String name) {
        // This is too simplistic. We need to either match, 
        // or allow a set of tokens in a path to match, but
        // the contains model is broken. 
        String authType = getAuthorizationType();
        if (StringUtils.isEmpty(authType))
            return false;
        if (authType.equals(name))
            return true;
        // Okay, now we get fancy. Build a set of tokens, and match up
        String nameTokens[] = name.split("/");
        for (String nameToken : nameTokens) {
            // A missing token means we do not match
            if (!authTypeTokenSet.contains(nameToken))
                return false;
        }
        // If we get there, the auth type contained all the name tokens, so we return true.
        return true;
    }

    public void addUISection(String section, String id) {
        if (section.equals("")) {
            section = "base";
        }
        if (!uisection.containsKey(section)) {
            uisection.put(section, new HashMap<String, String>());
        }
        uisection.get(section).put(id, id);
    }

    public void addStructure(Structure s) {
        structure.put(s.getID(), s);
    }

    public void addSubRecord(FieldSet fs, String perm) {
        subrecordsperm.get(perm).put(fs.getID(), fs);
    }

    protected void addSelfRenderer(FieldSet fs) {
        selfRenderers.add(fs);
    }

    public void addInstance(Instance n) {
        instances.put(n.getID(), n);
        spec.addInstance(n);
    }

    public void addMiniDataSet(FieldSet f, String s) {
        // s:{ name: field, name: field, name: field }
        if (!minidataset.containsKey(s)) {
            Map<String, FieldSet> subdata = new HashMap<String, FieldSet>();
            minidataset.put(s, subdata);
        }
        minidataset.get(s).put(f.getID(), f);
    }

    public void addMiniDataSet(Repeat r, String s) {
        // s:{ name: field, name: field, name: field }
        if (!minidataset.containsKey(s)) {
            Map<String, FieldSet> subdata = new HashMap<String, FieldSet>();
            minidataset.put(s, subdata);
        }
        minidataset.get(s).put(r.getID(), r);
    }

    public FieldSet[] getMiniDataSetByName(String s) {
        if (minidataset.containsKey(s)) {
            return minidataset.get(s).values().toArray(new FieldSet[0]);
        }
        return new FieldSet[0];
    }

    public String[] getAllMiniDataSets() {
        if (minidataset.isEmpty()) {
            return new String[0];
        }
        return minidataset.keySet().toArray(new String[0]);
    }

    private void mergeNestedMiniLists() {
        for (FieldSet fs : selfRenderers) {
            Record subrecord = fs.getSelfRendererRecord();
            for (String listName : subrecord.minidataset.keySet()) {
                Map<String, FieldSet> subRecordMiniList = subrecord.minidataset.get(listName);
                if (!subRecordMiniList.isEmpty()) {
                    Map<String, FieldSet> fieldsForList;
                    if (!minidataset.containsKey(listName)) {
                        fieldsForList = new HashMap<String, FieldSet>();
                        minidataset.put(listName, fieldsForList);
                    } else {
                        fieldsForList = minidataset.get(listName);
                    }
                    fieldsForList.putAll(subRecordMiniList);
                }
            }
        }
    }

    void dump(StringBuffer out) {
        out.append("  record id=" + this.getID() + "\n");
        out.append("    web_url=" + getWebURL() + "\n");
        out.append("    type=" + utils.getSet("@type") + "\n");
        utils.dump(out);
    }

    void dumpJson(JSONObject out) throws JSONException {
        JSONObject record = new JSONObject();
        utils.dumpJson(record, "attributes");
        record.put("id", this.getID());
        record.put("web_url", getWebURL());
        record.put("type", utils.getSet("@type"));
        out.put(this.getID(), record);
    }

    void dumpJsonFields(JSONObject out) throws JSONException {
        JSONObject record = new JSONObject();
        String[] allStrings = null;
        String[] allBooleans = null;
        String[] allSets = null;
        JSONObject RecordTable = new JSONObject();
        for (FieldSet fs : this.getAllFieldFullList("")) {
            if (allStrings == null) {
                allStrings = fs.getUtils().getAllString();
                allBooleans = fs.getUtils().getAllBoolean();
                allSets = fs.getUtils().getAllSets();

                for (String s : allStrings) {
                    JSONObject data = new JSONObject();
                    data.put("default", fs.getUtils().getDefaultString(s));
                    RecordTable.put("String:" + s, data);
                }
                for (String s : allBooleans) {
                    JSONObject data = new JSONObject();
                    data.put("default", fs.getUtils().getDefaultBoolean(s));
                    RecordTable.put("Boolean:" + s, data);
                }
                for (String s : allSets) {
                    JSONObject data = new JSONObject();
                    data.put("default", fs.getUtils().getDefaultSet(s));
                    RecordTable.put("Set:" + s, data);
                }
            } else {

                for (String s : allStrings) {
                    JSONObject data = RecordTable.getJSONObject("String:" + s);
                    data.put(fs.getID(), fs.getUtils().getString(s));
                    RecordTable.put("String:" + s, data);
                }
                for (String s : allBooleans) {
                    JSONObject data = RecordTable.getJSONObject("Boolean:" + s);
                    data.put(fs.getID(), fs.getUtils().getBoolean(s));
                    RecordTable.put("Boolean:" + s, data);
                }
                for (String s : allSets) {
                    JSONObject data = RecordTable.getJSONObject("Set:" + s);
                    data.put(fs.getID(), fs.getUtils().getSet(s));
                    RecordTable.put("Set:" + s, data);
                }
            }
        }

        out.put("allfields", RecordTable);
        out.put(this.getID(), record);
    }

    String dumpFields() throws JSONException {
        StringBuffer out = new StringBuffer();
        String[] allStrings = null;
        String[] allBooleans = null;
        String[] allSets = null;
        Map<String, StringBuffer> rout = new HashMap<String, StringBuffer>();

        allStrings = this.getAllFieldFullList("")[0].getUtils().getAllString();
        allBooleans = this.getAllFieldFullList("")[0].getUtils().getAllBoolean();
        allSets = this.getAllFieldFullList("")[0].getUtils().getAllSets();

        out.append("field" + ",");
        rout.put("default", new StringBuffer());
        for (FieldSet fs : this.getAllFieldFullList("")) {
            rout.put(fs.getID(), new StringBuffer());
        }

        for (String s : allStrings) {
            out.append(s + ",");
            rout.get("default").append(this.getAllFieldFullList("")[0].getUtils().getDefaultString(s) + ",");
            for (FieldSet fs : this.getAllFieldFullList("")) {
                rout.get(fs.getID()).append(fs.getUtils().getString(s) + ",");
            }
        }
        for (String s : allBooleans) {
            out.append(s + ",");
            rout.get("default").append(this.getAllFieldFullList("")[0].getUtils().getDefaultBoolean(s) + ",");
            for (FieldSet fs : this.getAllFieldFullList("")) {
                rout.get(fs.getID()).append(fs.getUtils().getBoolean(s) + ",");
            }
        }
        for (String s : allSets) {
            out.append(s + ",");
            rout.get("default")
                    .append("\"" + this.getAllFieldFullList("")[0].getUtils().allDefaultSets.get(s) + "\"" + ",");
            for (FieldSet fs : this.getAllFieldFullList("")) {
                rout.get(fs.getID()).append("\"" + fs.getUtils().allSets.get(s) + "\"" + ",");
            }
        }

        for (Map.Entry<String, StringBuffer> entry : rout.entrySet()) {
            String key = entry.getKey();
            StringBuffer value = entry.getValue();
            out.append("\n");
            out.append(key + ",");
            out.append(value);
            // ...
        }
        return out.toString();
        //out.put("allfields", RecordTable);
        //out.put(this.getID(), record);
    }

    @Override
    public Record getRecord() {
        return this;
    }

    public void config_finish(Spec spec) {
        for (Instance n : instances.values())
            n.config_finish(spec);
        for (FieldSet fs : fieldTopLevel.values())
            fs.config_finish(spec);
        // Handle nested self-renderers, to harvest things from nested like displayName, 
        // mini lists, etc. 
        findDisplayNameField();
        findMiniNumber();
        findMiniSummary();
        mergeNestedSummaryLists();
        mergeNestedMiniLists();
        mergeSearchLists();
        buildAuthTypeTokenSet();
    }

    @Override
    public boolean isExpander() {
        return false;
    }

    public void setSortKey(String fieldId, String sortFieldId) {
        sortKeys.put(fieldId, sortFieldId);
    }

    public String getSortKey(String fieldId) {
        return sortKeys.get(fieldId);
    }
}