All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.cq.social.srp.internal.AbstractSchemaMapper Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2014 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.cq.social.srp.internal;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.social.srp.utilities.internal.InternalSocialResourceUtilities;
import com.adobe.granite.activitystreams.JsonConstants;

/**
 * This class maps from SoCo keys and values to those used by the common schema developed by Adobe Cloud Storage. This
 * class is only intended to be used by Communities code.
 */
public abstract class AbstractSchemaMapper {
    /* ---------------- Stored Document fields ---------------- */
    /** the different descriptions that can be possible. */
    public static final String AS_ACCEPTFILETYPES = "acceptFileTypes";
    public static final String AS_ALLOWFILEUPLOADS = "allowFileUploads";
    public static final String AS_ALLOW_REPLIES = "allowRepliesToComments";
    public static final String AS_APPROVED = "approved_b";
    public static final String AS_AUTHOR = "author_username";
    public static final String AS_AUTHORIZABLE_ID = "authorizableId_t";
    public static final String AS_AUTHOR_DISPLAY_NAME = "author_display_name";
    public static final String AS_AUTHOR_DISPLAY_NAME_CI = "author_display_name_ci";
    public static final String AS_AUTHOR_PROFILE_URL = "author_profile_url";
    public static final String AS_AUTHOR_CI = "author_username_ci";
    public static final String AS_AUTHOR_IMAGE_URL = "author_image_url";
    public static final String AS_BASE_TYPE = "base_type_s";
    public static final String AS_IS_CLOSED = "is_closed_b";
    public static final String AS_COMPANY_ID = "company_db_id";
    public static final String AS_COMPOSED_BY = "composedBy_t";
    public static final String AS_CQ_DATA = "cqdata";
    public static final String AS_CQTAGS = "cqtags_ss";
    public static final String AS_CREATED = "timestamp";
    public static final String AS_ENTITY_URL = "entity_url";
    public static final String AS_FEATURES = "features";
    public static final String AS_ID = "_id"; // OPADS used to have this as id
    public static final String AS_IS_DRAFT = "is_draft_b";
    public static final String AS_IS_FLAGGED = "is_flagged_b";
    public static final String AS_IS_FLAGGED_HIDDEN = "is_flaggedHidden_b";
    public static final String AS_IS_REPLY = "is_reply_b";
    public static final String AS_MAXFILESIZE = "maxFileSize";
    public static final String AS_MAXIMAGEFILESIZE = "maxImageFileSize";
    public static final String AS_MODERATE_COMMENTS = "moderateComments";
    public static final String AS_PARENT_ID = "parent_id_s";
    public static final String AS_PINNED = "pinned_b";
    public static final String AS_PROVIDER_ID = "provider_id";
    public static final String AS_PUBLISH_DATE = "publishDate_dt";
    public static final String AS_PUBLISH_JOB_ID = "publishJobId_s";
    public static final String AS_REPORT_SUITE = "report_suite";
    public static final String AS_REQUIRELOGIN = "requireLogin";
    public static final String AS_RESOURCE_TYPE = "resource_type_s";
    public static final String AS_ROOT_COMMENT_SYSTEM = "thread_id_s"; // OPADS used to have rootcommentsystem_s
    public static final String AS_RTEENABLED = "rteEnabled";
    public static final String AS_TALLY_RESPONSE = "response_s";
    public static final String AS_TITLE = "title_t";
    public static final String AS_VERBATIM = "verbatim";
    public static final String AS_VERBATIM_LANGUAGE_INDEX = "verbatim_";
    public static final String AS_ATTACHMENT_LENGTH = "length";
    public static final String AS_UPLOADDATE = "uploadDate";
    public static final String AS_ADDRESS = "address_s";

    // Unique solr 'id' required to be present
    public static final String SOLR_ID = "id";

    /*
     * Even though the sentiment field name is the same in AS and SoCo, we have to define it so that it doesn't go in
     * cq_data
     */
    public static final String AS_SENTIMENT = "sentiment";

    /* ---------------- Stored attachment fields ---------------- */
    public static final String AS_CONTENT_TYPE = "content-type";
    public static final String AS_ATTACHMENT = "attachment";

    /* ---------------- Activity Streams searchable fields ---------------- */
    public static final String AS_VERB = "verb_s";
    public static final String AS_ACTOR_ID = "actorid_s";

    /* ---------------- SoCo doc fields ---------------- */
    private static final String CQ_ACCEPTFILETYPES = "acceptFileTypes";
    public static final String CQ_ADDED = InternalSocialResourceUtilities.PN_DATE;
    public static final String CQ_ALLOWFILEUPLOADS = "allowFileUploads";
    public static final String CQ_ALLOW_REPLIES = "allowRepliesToComments";
    public static final String CQ_APPROVED = "approved";
    public static final String CQ_AUTHORIZABLE_ID = "authorizableId";
    public static final String CQ_AUTHOR_IMAGE_URL = "author_image_url";
    public static final String CQ_AUTHOR_PROFILE_URL = "author_profile_url";
    public static final String CQ_AUTHOR_DISPLAY_NAME = "author_display_name";
    public static final String CQ_AUTHOR_EMAIL = "author_display_name";
    public static final String CQ_BASE_TYPE = InternalSocialResourceUtilities.PN_BASETYPE;
    public static final String CQ_COMPOSED_BY = "composedBy";
    public static final String CQ_DRAFT = "isDraft";
    public static final String CQ_IS_CLOSED = "isClosed";
    public static final String CQ_ENTITY_URL = "entity_url";
    public static final String CQ_IS_FLAGGED = "isFlagged";
    public static final String CQ_FLAGGED_HIDDEN = "isFlaggedHidden";
    public static final String CQ_IS_REPLY = InternalSocialResourceUtilities.PN_IS_REPLY;
    public static final String CQ_KEY = InternalSocialResourceUtilities.PN_DS_KEY;
    public static final String CQ_MAXFILESIZE = "maxFileSize";
    public static final String CQ_MAXIMAGEFILESIZE = "maxImageFileSize";
    public static final String CQ_MODERATE_COMMENTS = "moderateComments";
    public static final String CQ_PARENT_ID = InternalSocialResourceUtilities.PN_PARENTID;
    public static final String CQ_PINNED = "pinned";
    public static final String CQ_PUBLISH_DATE = "publishDate";
    public static final String CQ_PUBLISH_JOB_ID = "publishJobId";
    public static final String CQ_REQUIRELOGIN = "requireLogin";
    public static final String CQ_RESOURCE_TYPE = SlingConstants.NAMESPACE_PREFIX + ":"
            + SlingConstants.PROPERTY_RESOURCE_TYPE;
    public static final String CQ_ROOT_COMMENT_SYSTEM = InternalSocialResourceUtilities.PN_CS_ROOT;
    public static final String CQ_RTEENABLED = "rteEnabled";
    public static final String CQ_SPAM = "isSpam";
    public static final String CQ_TAGS = "cq:tags";
    public static final String CQ_TALLY_RESPONSE = "response";
    public static final String CQ_TALLY_TIMESTAMP = "timestamp";
    public static final String CQ_TITLE = "jcr:title";
    public static final String CQ_USER_IDENTIFIER = "userIdentifier";
    public static final String CQ_DESCRIPTION = "jcr:description";
    public static final String CQ_LAST_MODIFIED = "cq:lastModified";
    public static final String CQ_SENTIMENT = "sentiment";

    /* ---------------- SoCo attachment fields ---------------- */
    public static final String CQ_ATTACHMENT = "nt:file";
    public static final String CQ_CONTENT_TYPE = "mimetype";

    /**
     * Search collections has its own mapping for some fields.
     */
    public static final String CQ_SEARCH_PATH = ":path"; // path filter from isearch collections
    public static final String CQ_SEARCH_TAG_NAME = "cq:tags";  // tag in lucene query string from search collections
    public static final String CQ_SEARCH_PARENT_ID = ":parent"; // path filter from isearch collections

    /*
     * Machine Translation Fields
     */
    public static final String CQ_MT_TRANS_DATE = "translationDate";
    public static final String CQ_MT_TRANSLATION = "translation";
    protected static final String CQ_MT_LANGAUGE = "mtlanguage";

    /*
     * Activity Streams fields
     */
    public static final String CQ_AS_VERB = JsonConstants.PROPERTY_VERB;
    public static final String CQ_AS_ACTOR_ID = JsonConstants.PROPERTY_ACTOR_ID;

    /* suffix pattern to identify searchable keys, as defined in the solr schema_soco.xml config file */
    private static final String SEARCHABLE_SUFFIX =
        "s|ss|t|ts|b|bs|i|is|tl|tls|f|fs|d|ds|dt|dts|tf|tfs|td|tds|tdt|tdts|latlong|latlongs|tg|tgs|en|ens|ja|jas";
    /*
     * This is the regular exp. that determines if a key (name) is searchable or not. [^\\s] - must start with one or
     * more characters except white space \\_ - follow by a underscore '_' ?i - ignore case in match $ - end of the
     * string
     */
    private static final String SUFFIX_PATTERN = "([^\\s]+(\\_(?i)(" + SEARCHABLE_SUFFIX + "))$)";
    private static final Pattern PATTERN = Pattern.compile(SUFFIX_PATTERN);

    private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    private static final String CQ_DATE_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy";

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSchemaMapper.class);

    // These are the sets of attachment related strings that need explicit mapping. Everything else is blindly
    // copied to the cqdata subhash (if not a searchable key) or to the toplevel hash (if a searchable key)..
    private final Set CQ_ATTACHMENT_KEYS;
    private final Set AS_ATTACHMENT_KEYS;

    private final Map TOP_LEVEL_TO_SCHEMA_KEYS;
    private final Map TOP_LEVEL_TO_CQ_KEYS;
    private final Map TRANSLATION_TO_SCHEMA_KEYS;

    // Don't want to maintain it in two places. Using dates would be nice, but AS JSOn would be a pain.
    private static final List SUBHASH_DATE_FIELDS = Arrays.asList(CQ_LAST_MODIFIED, CQ_TALLY_TIMESTAMP,
        CQ_MT_TRANS_DATE, "jcr:created", "jcr:lastModified");

    /**
     * Prefix for non dynamic dates so we can convert from the long format back to calendar.
     */
    public static final String AS_CQ_SUBHASH_DATE = "SRP_CQ_SUBHASH_123098_DATE_";

    /** ctor. */
    public AbstractSchemaMapper() {

        TOP_LEVEL_TO_CQ_KEYS = new HashMap();
        TOP_LEVEL_TO_CQ_KEYS.put(AS_APPROVED, CQ_APPROVED);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_CQTAGS, CQ_TAGS);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_CREATED, CQ_ADDED);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_IS_DRAFT, CQ_DRAFT);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_PINNED, CQ_PINNED);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_RESOURCE_TYPE, CQ_RESOURCE_TYPE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_TITLE, CQ_TITLE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_PARENT_ID, CQ_PARENT_ID);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_PROVIDER_ID, CQ_KEY);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_AUTHOR, CQ_USER_IDENTIFIER);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_AUTHORIZABLE_ID, CQ_AUTHORIZABLE_ID);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_COMPOSED_BY, CQ_COMPOSED_BY);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_ROOT_COMMENT_SYSTEM, CQ_ROOT_COMMENT_SYSTEM);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_TALLY_RESPONSE, CQ_TALLY_RESPONSE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_AUTHOR_IMAGE_URL, CQ_AUTHOR_IMAGE_URL);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_AUTHOR_PROFILE_URL, CQ_AUTHOR_PROFILE_URL);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_AUTHOR_DISPLAY_NAME, CQ_AUTHOR_DISPLAY_NAME);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_BASE_TYPE, CQ_BASE_TYPE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_ENTITY_URL, CQ_ENTITY_URL);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_IS_REPLY, CQ_IS_REPLY);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_MODERATE_COMMENTS, CQ_MODERATE_COMMENTS);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_ALLOW_REPLIES, CQ_ALLOW_REPLIES);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_IS_CLOSED, CQ_IS_CLOSED);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_IS_FLAGGED, CQ_IS_FLAGGED);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_IS_FLAGGED_HIDDEN, CQ_FLAGGED_HIDDEN);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_ALLOWFILEUPLOADS, CQ_ALLOWFILEUPLOADS);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_MAXFILESIZE, CQ_MAXFILESIZE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_ACCEPTFILETYPES, CQ_ACCEPTFILETYPES);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_RTEENABLED, CQ_RTEENABLED);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_REQUIRELOGIN, CQ_REQUIRELOGIN);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_MAXIMAGEFILESIZE, CQ_MAXIMAGEFILESIZE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_PUBLISH_DATE, CQ_PUBLISH_DATE);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_PUBLISH_JOB_ID, CQ_PUBLISH_JOB_ID);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_ACTOR_ID, CQ_AS_ACTOR_ID);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_VERB, CQ_AS_VERB);
        TOP_LEVEL_TO_CQ_KEYS.put(AS_SENTIMENT, CQ_SENTIMENT);

        final Map mapperKeys = getMapperSpecificKeys();
        TOP_LEVEL_TO_CQ_KEYS.putAll(mapperKeys);

        TOP_LEVEL_TO_SCHEMA_KEYS = new HashMap(TOP_LEVEL_TO_CQ_KEYS.size());
        for (final Entry entry : TOP_LEVEL_TO_CQ_KEYS.entrySet()) {
            TOP_LEVEL_TO_SCHEMA_KEYS.put(entry.getValue(), entry.getKey());
        }

        AS_ATTACHMENT_KEYS = new HashSet();
        AS_ATTACHMENT_KEYS.add(AS_CONTENT_TYPE);
        AS_ATTACHMENT_KEYS.add(AS_ATTACHMENT);
        AS_ATTACHMENT_KEYS.add(AS_PROVIDER_ID);
        AS_ATTACHMENT_KEYS.add(AS_UPLOADDATE);

        CQ_ATTACHMENT_KEYS = new HashSet();
        CQ_ATTACHMENT_KEYS.add(CQ_CONTENT_TYPE);
        CQ_ATTACHMENT_KEYS.add(CQ_ATTACHMENT);

        TRANSLATION_TO_SCHEMA_KEYS = new HashMap();
        updateTranslationSchemaKeys(TRANSLATION_TO_SCHEMA_KEYS);
    }

    /**
     * Update translation schema keys
     * @param translationSchmeKeys Translation Schema Keys
     */
    protected void updateTranslationSchemaKeys(final Map translationSchmeKeys) {
        // by default this is empty method
    }

    /**
     * fetch the keys that are specific to this impl that need to be mapped.
     * @return the keys (going from the schema key to the soco key) to be mapped
     */
    public abstract Map getMapperSpecificKeys();

    /** @return the report suite used in this mapper. */
    public abstract String getReportSuite();

    /**
     * set the report suite used in this mapper.
     * @param reportSuite the report suite
     */
    public abstract void setReportSuite(String reportSuite);

    /**
     * @return the key used in the db schema that tells the description
     */
    public static String getSchemaDescriptionKey() {
        return AS_VERBATIM;
    }

    /**
     * @return the key used in the schema to tell the resource type.
     */
    public static String getSchemaResourceTypeKey() {
        return AS_RESOURCE_TYPE;
    }

    /**
     * @return the key used in the schema to tell the location of the root comment system.
     */
    public static String getSchemaRootCommentSystemKey() {
        return AS_ROOT_COMMENT_SYSTEM;
    }

    /**
     * @return the key used in soco to tell the author's display name.
     */
    public static String getSocoAuthorDisplayNameKey() {
        return CQ_AUTHOR_DISPLAY_NAME;
    }

    /**
     * @return the key used in the soco to tell the author's avatar url.
     */
    public static String getSocoAuthorImageUrlKey() {
        return CQ_AUTHOR_IMAGE_URL;
    }

    /**
     * @return the key used in the soco to tell the author's avatar url.
     */
    public static String getSocoAuthorProfileUrlKey() {
        return CQ_AUTHOR_PROFILE_URL;
    }

    /**
     * @return the key used in the soco to tell the entity url.
     */
    public static String getSocoEntityUrlKey() {
        return CQ_ENTITY_URL;
    }

    /**
     * @return the key used in the soco to tell approval status.
     */
    public static String getSocoApprovedKey() {
        return CQ_APPROVED;
    }

    /**
     * @return the key used in the soco to tell approval status.
     */
    public static String getSocoFlaggedHiddenKey() {
        return CQ_FLAGGED_HIDDEN;
    }

    public static String getSocoDraftKey() {
        return CQ_DRAFT;
    }

    /**
     * @return the key used in the schema to tell the approval status.
     */
    public static String getSchemaApprovedKey() {
        return AS_APPROVED;
    }

    /**
     * @return the key used in the schema to tell the approval status.
     */
    public static String getSchemaFlaggedHiddenKey() {
        return AS_IS_FLAGGED_HIDDEN;
    }

    public static String getSchemaIsDraftKey() {
        return AS_IS_DRAFT;
    }

    /**
     * @return the key used in the schema to tell the parent id.
     */
    public static String getSchemaParentIdKey() {
        return AS_PARENT_ID;
    }

    /**
     * @return the key used in the schema to tell the provider id.
     */
    public static String getSchemaProviderIdKey() {
        return AS_PROVIDER_ID;
    }

    /**
     * @return the key used in the schema to tell the creation time.
     */
    public static String getSchemaTimestampKey() {
        return AS_CREATED;
    }

    /**
     * @return the key used in schema to tell the attachment length.
     */
    public static String getSchemaAttachmentLengthKey() {
        return AS_ATTACHMENT_LENGTH;
    }

    /**
     * @return the key used in schema to tell the attachment creation time.
     */
    public static String getSchemaAttachmentUploadDateKey() {
        return AS_UPLOADDATE;
    }

    /**
     * @return the key used in soco to tell the creation time.
     */
    public static String getSocoAddedKey() {
        return CQ_ADDED;
    }

    /**
     * @return the key used in soco to tell the parentid.
     */
    public static String getSocoParentIdKey() {
        return CQ_PARENT_ID;
    }

    /**
     * @return the key used in soco to tell the tally timestamp.
     */
    public static String getSocoTallyTimestampKey() {
        return CQ_TALLY_TIMESTAMP;
    }

    /**
     * @return the key used in soco to tell modification date.
     */
    public static String getSocoLastModifiedKey() {
        return CQ_LAST_MODIFIED;
    }

    /**
     * @return the key used in soco to tell if there are attachments.
     */
    public static String getSchemaAttachmentKey() {
        return AS_ATTACHMENT;
    }

    /**
     * @return the key for a piece of ugc.
     */
    public static String getSocoKey() {
        return CQ_KEY;
    }

    /**
     * @return the key used in soco to give the description associated with the ugc.
     */
    public static String getSocoDescriptionKey() {
        return CQ_DESCRIPTION;
    }

    /**
     * Escape things that need to go to solr.
     * @param base the original string
     * @return the converted string.
     */
    public static String escapeForSolr(final String base) {
        return StringUtils.replace(base, ":", "\\:");
    }

    /**
     * @return The prefix used to identify non dynamic user date properties.
     */
    public static String getUserDatePrefix() {
        return AS_CQ_SUBHASH_DATE;
    }

    /**
     * @return The social base type.
     */
    public static String getSchemaBaseType() {
        return AS_BASE_TYPE;
    }

    /**
     * @return The CQ base type.
     */
    public static String getBaseType() {
        return CQ_BASE_TYPE;
    }

    public static String getSchemaCQData() {
        return AS_CQ_DATA;
    }

    /**
     * @return The CQ translation folder name.
     */
    public static String getTranslationFolderName() {
        return CQ_MT_TRANSLATION;
    }

    /**
     * Convert Calendar/Date to millis for storage.
     * @param key Key to convert value for
     * @param asMap The map
     * @return milliseconds
     */
    private static Long toSchemaDate(final String key, final Map asMap) {

        final Object time = asMap.get(key);
        if (time instanceof Date) {
            return ((Date) time).getTime();
        } else if (time instanceof Calendar) {
            return ((Calendar) time).getTimeInMillis();
        } else if (time == null) {
            LOGGER.error("Date property {} doesn't exist or is null. Dropping it.", key);
            return null;
        }

        LOGGER.error("Date property {} in unexpected format. Dropping it. Timestamp: {}. Class: {}", new Object[]{
            key, time, time.getClass()});

        return null;
    }

    /**
     * Convert Calendar/Date to millis for storage.
     * @param time Date/Calendar object
     * @return milliseconds
     */
    private static Long toSchemaDate(final Object time) {
        if (time instanceof Date) {
            return ((Date) time).getTime();
        } else if (time instanceof Calendar) {
            return ((Calendar) time).getTimeInMillis();
        } else if (time == null) {
            return null;
        }
        return null;
    }

    private static void spamFlagsToSchema(final Map cqMap, final Map asMap) {
        final Boolean spam = (Boolean) cqMap.get(CQ_SPAM);
        final Boolean approved = (Boolean) cqMap.get(CQ_APPROVED);
        if (spam == null && approved == null) {
            return;
        }

        if (spam != null) {
            asMap.put(AS_APPROVED, !spam);
        } else {
            asMap.put(AS_APPROVED, approved);
        }
    }

    private void setSolrId(final Map cqFormat, final String key, final Map asMap) {
        final String solrId = (String) cqFormat.get(SOLR_ID);
        if (solrId == null || solrId.isEmpty()) {
            LOGGER.trace("Mandatory unique solr id field is missing for provider_id={}", key);
            asMap.put(SOLR_ID, key);
        }
    }

    /**
     * Map from the cq data format to the format expected by the schema.
     * @param cqFormat the incoming data. Note that this routine can change the contents of the map so that it is
     *            appropriate for caching. If you don't like that, make a copy of the map before sending it in.
     * @param key the key for the data
     * @return the mapped data
     */
    public Map toSchema(final Map cqFormat, final String key) {
        final Map asMap = new HashMap();
        final Map cqDataMap = new HashMap();

        final boolean bTranslationProcessingRequired = isTranslationProcessingRequired(key);
        String strTranslationLanguageKey = null;
        if (cqFormat.containsKey(CQ_TALLY_TIMESTAMP) && !cqFormat.containsKey(AbstractSchemaMapper.getSocoAddedKey())) {
            if (!(cqFormat.get(CQ_TALLY_TIMESTAMP) instanceof Calendar)) { // tally timestamp is Calendar now
                final String timestamp = (String) cqFormat.get(CQ_TALLY_TIMESTAMP);
                final SimpleDateFormat dateFormat = new SimpleDateFormat(CQ_DATE_FORMAT);
                try {
                    final Calendar cal = new GregorianCalendar();
                    cal.setTime(dateFormat.parse(timestamp));
                    cqFormat.put(AbstractSchemaMapper.getSocoAddedKey(), cal);
                } catch (final java.text.ParseException e) {
                    LOGGER.error("Could not parse timestamp. Dropping it. Key: {}, timestamp: {}", key, timestamp);
                }
            } else {
                cqFormat.put(AbstractSchemaMapper.getSocoAddedKey(), cqFormat.get(CQ_TALLY_TIMESTAMP));
            }
        }

        spamFlagsToSchema(cqFormat, asMap);
        for (final Entry entry : cqFormat.entrySet()) {
            if (bTranslationProcessingRequired && TRANSLATION_TO_SCHEMA_KEYS.containsKey(entry.getKey())) {
                if (strTranslationLanguageKey == null) {
                    // get the translation language
                    strTranslationLanguageKey = getTranslationLanguageFromKey(key);
                }
                final String strNewKey = TRANSLATION_TO_SCHEMA_KEYS.get(entry.getKey()) + strTranslationLanguageKey;
                asMap.put(strNewKey, entry.getValue());
            } else if (TOP_LEVEL_TO_SCHEMA_KEYS.containsKey(entry.getKey())) {
                // if it is one of our known keys, put it in the top-level map
                if (!entry.getKey().equals(CQ_APPROVED)) {
                    if (entry.getKey().equals(CQ_PUBLISH_DATE)) {
                        asMap.put(TOP_LEVEL_TO_SCHEMA_KEYS.get(entry.getKey()), toSchemaDate(entry.getValue()));
                    } else {
                        asMap.put(TOP_LEVEL_TO_SCHEMA_KEYS.get(entry.getKey()), entry.getValue());
                    }
                }
            } else {
                if (entry.getKey().equals(CQ_SPAM)) {
                    continue;
                }
                // if a dynamic key, make it visible at top-level map to be indexed by solr
                final Matcher matcher = PATTERN.matcher(entry.getKey());
                if (matcher.matches()) {
                    final Object obj = entry.getValue();
                    if ((obj instanceof Calendar) || (obj instanceof Date)) {
                        asMap.put(entry.getKey(), toSchemaDate(entry.getKey(), cqFormat));
                    } else if ((obj instanceof Calendar[]) || (obj instanceof Date[])) {
                        LOGGER.debug("toSchema Cal/Date array {} - {}", entry.getKey(), entry.getValue());
                        final int len = ((Object[]) obj).length;
                        final Long[] milisArray = new Long[len];
                        for (int i = 0; i < len; i++) {
                            milisArray[i] = toSchemaDate(((Object[]) obj)[i]);
                        }
                        asMap.put(entry.getKey(), milisArray);
                    } else {
                        asMap.put(entry.getKey(), entry.getValue());
                    }
                    // an increment map is special. Have to go through it and put in
                    // appropriate $inc map (main or subhash).
                    //
                } else if (CachingResourceProvider.INC.equals(entry.getKey()) && entry.getValue() instanceof Map) {
                    final Map incMap = (Map) entry.getValue();
                    final Map mainIncMap = new HashMap();
                    final Map subIncMap = new HashMap();

                    for (final Entry incEntry : incMap.entrySet()) {
                        if (TOP_LEVEL_TO_SCHEMA_KEYS.containsKey(incEntry.getKey())) {
                            mainIncMap.put(TOP_LEVEL_TO_SCHEMA_KEYS.get(incEntry.getKey()), incEntry.getValue());
                        } else {
                            final Matcher incMatcher = PATTERN.matcher(incEntry.getKey());
                            if (incMatcher.matches()) {
                                mainIncMap.put(incEntry.getKey(), incEntry.getValue());
                            } else {
                                subIncMap.put(incEntry.getKey(), incEntry.getValue());
                            }
                        }

                    }
                    if (!mainIncMap.isEmpty()) {
                        asMap.put(CachingResourceProvider.INC, mainIncMap);
                    }
                    if (!subIncMap.isEmpty()) {
                        cqDataMap.put(CachingResourceProvider.INC, subIncMap);
                    }
                } else if (entry.getKey().indexOf(AS_VERBATIM_LANGUAGE_INDEX) == 0) {
                    LOGGER.trace("Adding {} to hash.", entry.getKey());
                    asMap.put(entry.getKey(), entry.getValue());
                } else {
                    // Put it in the sub hash (not indexed in solr).
                    // This is common to ASRP and MSRP
                    LOGGER.trace("Adding {} to sub hash.", entry.getKey());
                    // Date/Calendar fields need special handling
                    final Object obj = entry.getValue();
                    final String fieldName = entry.getKey();
                    if ((obj instanceof Calendar) || (obj instanceof Date)) {
                        LOGGER.debug("Have date/calendar field: {}", fieldName);
                        final Long tstamp = toSchemaDate(fieldName, Collections.singletonMap(fieldName, obj));
                        if (tstamp != null) {
                            // Existing data may have these field names, so can't switch to the prefix scheme
                            if (SUBHASH_DATE_FIELDS.contains(fieldName)) {
                                cqDataMap.put(fieldName, tstamp);
                            } else {
                                cqDataMap.put(AbstractSchemaMapper.getUserDatePrefix() + fieldName, tstamp);
                            }
                        } else {
                            LOGGER.warn("Invalid date format for {}.  Field ignored", fieldName);
                        }
                    } else if ((obj instanceof Calendar[]) || (obj instanceof Date[])) {
                        LOGGER.debug("Non dynamic date array {}", fieldName);
                        final int len = ((Object[]) obj).length;
                        final Long[] milisArray = new Long[len];
                        for (int i = 0; i < len; i++) {
                            milisArray[i] = toSchemaDate(((Object[]) obj)[i]);
                        }
                        cqDataMap.put(AbstractSchemaMapper.getUserDatePrefix() + fieldName, milisArray);
                    } else {
                        cqDataMap.put(fieldName, entry.getValue());
                    }
                }
            }
        }

        if (!cqDataMap.isEmpty()) {
            asMap.put(AS_CQ_DATA, cqDataMap);
        }

        asMap.put(AS_PROVIDER_ID, key);
        setSolrId(cqFormat, key, asMap);
        if (getReportSuite() != null) {
            asMap.put(AS_REPORT_SUITE, getReportSuite());
        }

        // timestamp (in millis) is required for AS. Add current time if not present.
        // Don't want the 'dropping' error messages, so not using toSchemaDate.
        Long timestamp = System.currentTimeMillis();
        if (asMap.containsKey(AS_CREATED)) {
            final Object time = asMap.get(AS_CREATED);
            if (time instanceof Date) {
                timestamp = ((Date) time).getTime();
            } else if (time instanceof Calendar) {
                timestamp = ((Calendar) time).getTimeInMillis();
            }
        }
        asMap.put(AS_CREATED, timestamp);

        finalizeSchemaMapping(cqFormat, asMap);

        return asMap;
    }

    /**
     * Check all entries in Translation schema map to find the right key whose value + language matches strInputkey
     */
    private String getTranslationKeyEntry(final String strInputkey, final String strTranslationLanuage) {
        // the last index should be equal to key length - translation language length
        for (final Entry subEntry : TRANSLATION_TO_SCHEMA_KEYS.entrySet()) {
            if (strInputkey.equals(subEntry.getValue() + strTranslationLanuage)) {
                return subEntry.getKey();
            }
        }
        return null;
    }

    /**
     * Returns the translation language specified as the last name in the key path
     * @param strLanguage Language
     * @return String
     */
    public static String getMutliLingualLanguageKey(String strLanguage) {
        if (strLanguage.length() > 2) {
            // if the language is more than 2 char code then we need to shorten it
            // check if it is zh_cn or zh_tw, they are special case
            if ("zh_tw".compareToIgnoreCase(strLanguage) == 0) {
                strLanguage = "zh_TW";
            } else if ("zh_cn".compareToIgnoreCase(strLanguage) == 0) {
                strLanguage = "zh_CN";
            } else {
                strLanguage = strLanguage.substring(0, 2);
            }
        }
        return strLanguage;
    }

    /**
     * Returns the translation language specified as the last name in the key
     * @param key language Key
     * @return String
     */
    private String getTranslationLanguageFromKey(final String key) {
        final String[] paths = key.split("/");
        if (paths != null && paths.length > 0) {
            return getMutliLingualLanguageKey(paths[paths.length - 1]);
        }
        return "";
    }

    /**
     * This method first checks if translation schema map is empty or not, and then check if key provided has
     * translation as second last path, and returns true otherwise false
     * @param key
     */
    private boolean isTranslationProcessingRequired(final String key) {
        // first check if translation schema has keys
        if (!TRANSLATION_TO_SCHEMA_KEYS.isEmpty() && !StringUtils.isEmpty(key)) {
            // now check that key is of type /translation/abc
            final String[] paths = key.split("/");
            if (paths != null && paths.length > 1) {
                final String strSecondLastPath = paths[paths.length - 2];
                return getTranslationFolderName().equals(strSecondLastPath);
            }
        }
        return false;
    }

    /**
     * Subclasses can implement this to muck with the mapping before it is actually committed. This is useful for the
     * little nitty differences between impls.
     * @param socoData the data in the SoCo format
     * @param schemaMap the data to be committed (in the common schema)
     */
    public abstract void finalizeSchemaMapping(Map socoData, Map schemaMap);

    /**
     * Format a Map using the keys expected by the schema.
     * @param data the data to be uploaded
     * @param key the key for the attachment
     * @return the data with the keys expected by the schema
     * @throws IOException on failure
     */
    public Map toAttachmentSchema(final Map data, final String key)
        throws IOException {
        final Map resultMap = new HashMap();
        for (final Entry entry : data.entrySet()) {
            if (!CQ_ATTACHMENT_KEYS.contains(entry.getKey())) {
                resultMap.put(entry.getKey(), entry.getValue());
            }
        }

        resultMap.put(AS_PROVIDER_ID, key);
        resultMap.put(AS_CONTENT_TYPE, data.get(CQ_CONTENT_TYPE));
        resultMap.put(AS_ATTACHMENT, data.get(CQ_ATTACHMENT));

        resultMap.remove(AS_PARENT_ID);

        return resultMap;
    }

    private static void spamFlagsFromSchema(final Map cqMap, final Map asMap) {
        final Boolean approved = (Boolean) asMap.remove(AS_APPROVED);
        if (approved == null) {
            return;
        }

        cqMap.put(CQ_SPAM, !approved);
        cqMap.put(CQ_APPROVED, approved);
    }

    /**
     * Return a Calendar object from a date value in the schema format. If value is an invalid date format (perhaps
     * from a legacy format we no longer know about or bugs from previous coding), return the current time as a place
     * holder.
     * @param key the key to convert
     * @param asDate the original date, in schema format
     * @return the converted date
     */
    public static Calendar fromSchemaDate(final String key, final Object asDate) {
        final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
        Date date = null;
        try {
            if (asDate instanceof String) {
                LOGGER.error("Legacy format data from schema. Key: {}, String date: {}", key, asDate);
                date = dateFormat.parse((String) asDate);
            } else if (asDate instanceof Long) {
                date = new Date((Long) asDate);
            } else if (asDate instanceof Integer) {
                date = new Date((Integer) asDate);
            } else if (asDate instanceof Double) {
                date = new Date(((Double) asDate).longValue());
            } else if (asDate instanceof Float) {
                date = new Date(((Float) asDate).longValue());
            } else if (asDate instanceof Calendar) {
                return (Calendar) asDate;
            } else if (asDate instanceof Date) {
                date = (Date) asDate;
            } else {
                LOGGER.error("Unknown date format. Using current time. Key: {}, Date: {}. Type: "
                        + (asDate == null ? "null" : asDate.getClass().toString()), key, asDate);
            }
        } catch (final ParseException e) {
            LOGGER.error("Could not parse date. Using current time. Key: {}, Date: {}", key, asDate);
        }
        final Calendar cal = new GregorianCalendar();
        if (date != null) {
            cal.setTime(date);
        }
        return cal;
    }

    /**
     * Convert the data from schema into the format expected by SoCo.
     * @param schemaData the data to be converted
     * @return the mapped data
     */
    public Map fromSchema(final Map schemaData) {
        LOGGER.debug("About to map from schema: {}", schemaData);

        final Map asFormatCopy = new HashMap(schemaData);
        final Map cqMap = new HashMap();

        spamFlagsFromSchema(cqMap, asFormatCopy);
        final boolean bTranslationMappingRequired =
            isTranslationProcessingRequired((String) schemaData.get(AS_PROVIDER_ID));
        String strTranslationLanuageKey = null;
        for (final Entry entry : asFormatCopy.entrySet()) {
            if (TOP_LEVEL_TO_CQ_KEYS.containsKey(entry.getKey())) {
                if (entry.getKey().equals(AS_PUBLISH_DATE)) {
                    cqMap.put(TOP_LEVEL_TO_CQ_KEYS.get(entry.getKey()),
                        fromSchemaDate(entry.getKey(), entry.getValue()));
                } else {
                    cqMap.put(TOP_LEVEL_TO_CQ_KEYS.get(entry.getKey()), entry.getValue());
                }
                // Non indexed fields, extract out of the sub hash
            } else if (entry.getKey().equals(AS_CQ_DATA)) {
                @SuppressWarnings("unchecked")
                final Map cqData = (Map) entry.getValue();
                // Handle date fields in sub hash
                for (final Entry subEntry : cqData.entrySet()) {
                    final String fieldName = subEntry.getKey();
                    // The old fixed list that wasn't prefixed (jcr:modified etc.)
                    if (SUBHASH_DATE_FIELDS.contains(subEntry.getKey())) {
                        final Calendar cal =
                            fromSchemaDate((String) schemaData.get(AS_PROVIDER_ID), subEntry.getValue());
                        cqMap.put(fieldName, cal);
                        continue;
                    }
                    // If date prefix present, convert to Calendar.
                    if (StringUtils.startsWith(fieldName, AbstractSchemaMapper.getUserDatePrefix())) {
                        final String realName =
                            StringUtils.remove(fieldName, AbstractSchemaMapper.getUserDatePrefix());
                        final Object datesObj = subEntry.getValue();
                        // Single date
                        if (datesObj instanceof Long) {
                            final Calendar cal =
                                fromSchemaDate((String) schemaData.get(AS_PROVIDER_ID), subEntry.getValue());
                            cqMap.put(realName, cal);
                            continue;
                        }

                        // Array of dates. Comes back as array from AS, a List from Mongo
                        // Changing Mongo code to return array would break attachment handling.
                        Long[] milisArray;
                        if (datesObj instanceof List) {
                            milisArray = ((List) datesObj).toArray(new Long[((List) datesObj).size()]);
                        } else {
                            milisArray = (Long[]) datesObj;
                        }
                        // Convert to Calendar
                        final int len = milisArray.length;
                        final Calendar[] calArray = new Calendar[len];
                        for (int i = 0; i < len; i++) {
                            calArray[i] = fromSchemaDate((String) schemaData.get(AS_PROVIDER_ID), milisArray[i]);
                        }
                        cqMap.put(realName, calArray);
                    } else {
                        cqMap.put(fieldName, subEntry.getValue());
                    }
                }
            } else {
                boolean bProcessingDone = false;
                if (bTranslationMappingRequired) {
                    // get LanguageKey first of all
                    if (strTranslationLanuageKey == null) {
                        strTranslationLanuageKey =
                            getTranslationLanguageFromKey((String) schemaData.get(AS_PROVIDER_ID));
                    }
                    // get a language mapping first of all
                    final String strNewKeyEntry = getTranslationKeyEntry(entry.getKey(), strTranslationLanuageKey);
                    if (!StringUtils.isEmpty(strNewKeyEntry)) {
                        cqMap.put(strNewKeyEntry, entry.getValue());
                        bProcessingDone = true;
                    }
                }
                if (!bProcessingDone) {
                    // Dynamic date fields have to be converted out.
                    if (entry.getKey().endsWith("_dt")) {
                        final Calendar cal =
                            fromSchemaDate((String) schemaData.get(AS_PROVIDER_ID), entry.getValue());
                        cqMap.put(entry.getKey(), cal);
                    } else if (entry.getKey().endsWith("_dts")) {
                        LOGGER.debug("From schema dynamic date array Long to Calendar {}", entry.getKey());
                        final Long[] miliDates = (Long[]) entry.getValue();
                        final Calendar[] calArray = new Calendar[miliDates.length];
                        for (int i = 0; i < miliDates.length; i++) {
                            final Calendar cal =
                                fromSchemaDate((String) schemaData.get(AS_PROVIDER_ID), miliDates[i]);
                            calArray[i] = cal;
                        }
                        cqMap.put(entry.getKey(), calArray);
                    } else {
                        cqMap.put(entry.getKey(), entry.getValue());
                    }
                }
            }
        }

        // filter out Adobe Cloud Storage keys.
        cqMap.remove(AS_REPORT_SUITE);
        cqMap.remove(AS_AUTHOR_CI);
        cqMap.remove(AS_AUTHOR_DISPLAY_NAME_CI);
        cqMap.remove(AS_ID);
        cqMap.remove(AS_COMPANY_ID);
        cqMap.remove(AS_FEATURES);

        if (schemaData.containsKey(AS_PROVIDER_ID)) {
            cqMap.put(CQ_KEY, schemaData.get(AS_PROVIDER_ID));
        }
        // Dates (stored as millis) need to be mapped back out into calendar objects
        if (cqMap.containsKey(CQ_ADDED)) {
            final Calendar cal = fromSchemaDate((String) schemaData.get(AS_PROVIDER_ID), cqMap.get(CQ_ADDED));
            cqMap.put(CQ_ADDED, cal);
        }

        // A little hack to keep the world moving. We moved from resource_type
        // to resource_type_s, title to title_t, and root_id to thread_id_s,
        // but all the data in the database still has resource_type. Remove this when we get a clean database..
        if (!cqMap.containsKey(CQ_RESOURCE_TYPE)) {
            if (schemaData.containsKey("resource_type")) {
                LOGGER.error("Legacy format data from AS. Key: {}, resource_type: {}",
                    schemaData.get(AS_PROVIDER_ID), schemaData.get("resource_type"));
                cqMap.put(CQ_RESOURCE_TYPE, schemaData.get("resource_type"));
            }
        }
        if (!cqMap.containsKey(CQ_TITLE)) {
            if (schemaData.containsKey("title")) {
                LOGGER.error("Legacy format data from AS. Key: {}, title: {}", schemaData.get(AS_PROVIDER_ID),
                    schemaData.get("title"));
                cqMap.put(CQ_TITLE, schemaData.get("title"));
            }
        }

        if (!cqMap.containsKey(CQ_ROOT_COMMENT_SYSTEM)) {
            if (schemaData.containsKey("root_id")) {
                LOGGER.error("Legacy format data from AS. Key: {}, root_id: {}", schemaData.get(AS_PROVIDER_ID),
                    schemaData.get("root_id"));
                cqMap.put(CQ_ROOT_COMMENT_SYSTEM, schemaData.get("root_id"));
            }
        }

        LOGGER.debug("Completed mapping from schema: {}", cqMap);
        return cqMap;
    }

    /**
     * Convert attachment data from the schema to what SoCo expects.
     * @param input the data in the schema
     * @return the data in the SoCo format
     */
    public Map fromAttachmentSchema(final Map input) {
        if (input.isEmpty()) {
            return input;
        }

        input.remove("md5");
        input.remove("documentstore_id");
        input.remove("attachment_id");
        final Map resultMap = new HashMap();
        for (final Entry entry : input.entrySet()) {
            if (!AS_ATTACHMENT_KEYS.contains(entry.getKey())) {
                resultMap.put(entry.getKey(), entry.getValue());
            }
        }

        resultMap.put(CQ_KEY, input.get(AS_PROVIDER_ID));
        resultMap.put(CQ_CONTENT_TYPE, input.get(AS_CONTENT_TYPE));

        // convert the upload date to creation date
        resultMap.put(
            CQ_ADDED,
            fromSchemaDate(AbstractSchemaMapper.getSchemaAttachmentUploadDateKey(),
                input.get(AbstractSchemaMapper.getSchemaAttachmentUploadDateKey())));

        // make sure content length is a Long since AS returns an Integer
        if (resultMap.get(AS_ATTACHMENT_LENGTH) != null) {
            resultMap.put(AS_ATTACHMENT_LENGTH, ((Number) resultMap.get(AS_ATTACHMENT_LENGTH)).longValue());
        }

        return resultMap;
    }

    /**
     * Get the schema mapping of a SoCo field. Needed to map CQ fields in a query to Solr fields, since we don't have
     * information about the field type to be able to come up with the Solr name used when adding to index. Custom
     * fields have Solr schema names, so if not found, assume it is a custom dynamic field.
     * @param cqKey The CQ name of a field
     * @return The Solr index name for the CQ field or the input if not mapped (dynamic field name)
     */
    public String toSchemaKey(final String cqKey) {
        if (TOP_LEVEL_TO_SCHEMA_KEYS.containsKey(cqKey)) {
            return TOP_LEVEL_TO_SCHEMA_KEYS.get(cqKey);
        } else {
            return cqKey;
        }
    }

    /**
     * Get the CQ mapping of a schema field. Needed to map schema key to CQ field. For facet fields etc, where we
     * don't have a map, just a string.
     * @param schemaKey The schema name of a field
     * @return The CQ field name or the input if not mapped (dynamic field name)
     */
    public String fromSchemaKey(final String schemaKey) {
        if (TOP_LEVEL_TO_CQ_KEYS.containsKey(schemaKey)) {
            return TOP_LEVEL_TO_CQ_KEYS.get(schemaKey);
        } else {
            return schemaKey;
        }
    }

    /**
     * Get the schema field name(s) associated with a given SoCo field.
     * @param mltField the SoCo field
     * @return the list of schema field names
     */
    public List toSchemaKeys(final String mltField) {
        final List override = mapToMultipleValues(mltField);

        if (override != null) {
            return override;
        }
        final String ret = TOP_LEVEL_TO_SCHEMA_KEYS.get(mltField);
        if (ret == null) {
            return Arrays.asList(mltField);
        }
        return Arrays.asList(ret);
    }

    /**
     * Each Schema mapper class need to implement this method. Return list of verbatim supported
     * @return String[]
     */
    protected abstract String[] getASVerbatimList();

    private List mapToMultipleValues(final String mltField) {
        if (mltField.equals(getSocoDescriptionKey())) {
            return new ArrayList(Arrays.asList(getASVerbatimList()));
        }
        return null;
    }

    /**
     * Convert a lucene query to Solr query Maps the CQ names to Solr names. Handles the path wild card since Solr
     * does not find strings ending in a wildcard if it does not start with a wildcard.
     * @param luceneQuery the lucene query
     * @return the solr query
     */
    public String luceneToSolr(final String luceneQuery) {

        LOGGER.debug("Lucene query: {}", luceneQuery);
        final StringBuilder sb = new StringBuilder().append(luceneQuery);
        // Full text queries come in with a :fulltext: which needs to be removed
        replaceAll(sb, ":fulltext:", "");
        /*
         * Avoid mapping in search string by replacing only the strings that end with a :
         */
        // For the path query, it needs the wildcard at start, but we already do that in the query construction
        replaceAll(sb, CQ_SEARCH_PATH + ":", AS_PROVIDER_ID + ":");

        // Lucene query coming in has as tags, not cq:tags, so standard mapping will not work
        // Wildcard for now
        replaceAll(sb, CQ_SEARCH_TAG_NAME + ":", AS_CQTAGS + ":");

        // :parent can come when a query is using PathConstraintType.IsDescendantNode
        replaceAll(sb, CQ_SEARCH_PARENT_ID + ":", AS_PARENT_ID + ":");

        // Solr does not like the ranges social-collections lucene parsing gives us.
        replaceAll(sb, "[ TO", "[* TO");
        replaceAll(sb, " TO \uFFFF]", " TO *]");
        LOGGER.debug("Replaced range limits {}", sb.toString());

        // Just the regular CQ-AS mappings.
        for (final Entry entry : TOP_LEVEL_TO_SCHEMA_KEYS.entrySet()) {
            final String cqKey = entry.getKey();
            final List override = mapToMultipleValues(cqKey);
            final String solrKey = entry.getValue();

            if (override != null) {
                // TODO: Proper handling, adding the rest of the AS_VERBATIMS
                replaceAll(sb, cqKey + ":", override.get(0) + ":");
            } else {
                replaceAll(sb, cqKey + ":", solrKey + ":");
            }
        }

        final String solrQuery = sb.toString().trim();

        //This logic is needed to take care of keyword search in calendar addresses because of adresses being strings
        String calendarSolrQuery = null;
        if (solrQuery.indexOf(AS_ADDRESS) >= 0) {
            int addressStartIndex = solrQuery.indexOf(AS_ADDRESS);
            int addressFinalIndex = addressStartIndex + solrQuery.substring(addressStartIndex).indexOf(") ");

            String keyParam = solrQuery.substring(addressStartIndex, addressFinalIndex).trim();
            String searchString = keyParam.substring(keyParam.indexOf(":(") + 2);

            String prefixString = solrQuery.substring(0, addressStartIndex);

            //Process key
            String []literalArray = searchString.split("\\s+");
            StringBuilder key = new StringBuilder();
            for (String literal : literalArray) {
                key.append(literal)
                    .append("\\")
                    .append(" ");
            }
            //Remove extraneous "\ " from the end of search key
            String searchKey = key.toString().trim();
            searchKey = searchKey.substring(0, searchKey.length() - 1);

            //Create new solr query with processed key for address
            calendarSolrQuery = prefixString.concat(AS_ADDRESS).concat(":(").concat("*").concat(searchKey).concat("*").concat(solrQuery.substring(addressFinalIndex));

            LOGGER.debug("Solr query: {}", calendarSolrQuery);
            return calendarSolrQuery;
        } else {
            LOGGER.debug("Solr query: {}", solrQuery);
            return solrQuery;
        }
    }

    /**
     * Replace all occurrences of a string in a StringBuilder.
     * @param builder the StringBuilder (modified)
     * @param from source string
     * @param to replacement string
     */
    private static void replaceAll(final StringBuilder builder, final String from, final String to) {
        int index = builder.indexOf(from);
        while (index != -1) {
            builder.replace(index, index + from.length(), to);
            index += to.length(); // Move to the end of the replacement
            index = builder.indexOf(from, index);
        }
    }

    /**
     * Convert to a format solr likes.
     * @param cqFormat the cq format data
     * @param key the key to convert
     * @return Map the converted data
     */
    public abstract Map toSolrSchema(final Map cqFormat, final String key);

    /**
     * Checks if the 'key' is valid key that can be indexed in social-indexing engine (Solr,ElasticSearch,etc)
     * @param key Key
     * @return boolean
     */
    public boolean isIndexable(String key) {
        if (key.equals(SOLR_ID) || key.equals(AS_REPORT_SUITE)) {
            return true;
        }

        // 2. check if its one of the out-of-box solr fields defined for communities
        boolean matchesOutOfBoxFieldsInSolr = TOP_LEVEL_TO_CQ_KEYS.containsKey(key);
        if (matchesOutOfBoxFieldsInSolr) {
            return true;
        } else {
            // 1. check if given key matches any of the dynamic field patterns
            final Matcher incMatcher = PATTERN.matcher(key);
            return incMatcher.matches();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy