Package org.jboss.elasticsearch.river.jira

Source Code of org.jboss.elasticsearch.river.jira.JIRA5RestIssueIndexStructureBuilder

/*
* JBoss, Home of Professional Open Source
* Copyright 2012 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
*/
package org.jboss.elasticsearch.river.jira;

import static org.elasticsearch.client.Requests.deleteRequest;
import static org.elasticsearch.client.Requests.indexRequest;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;

import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.query.FilterBuilder;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.jboss.elasticsearch.tools.content.StructuredContentPreprocessor;

/**
* JIRA 5 REST API implementation of component responsible to transform issue data obtained from JIRA instance call to
* the document stored in ElasticSearch index. Intended to cooperate with {@link JIRA5RestClient}.
* <p>
* Testing URLs:<br>
* <code>https://issues.jboss.org/rest/api/2/search?jql=project=ORG</code>
* <code>https://issues.jboss.org/rest/api/2/search?jql=project=ORG&fields=</code>
* <code>https://issues.jboss.org/rest/api/2/search?jql=project=ORG&fields=&expand=</code>
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
public class JIRA5RestIssueIndexStructureBuilder implements IJIRAIssueIndexStructureBuilder {

  private static final ESLogger logger = Loggers.getLogger(JIRA5RestIssueIndexStructureBuilder.class);

  /**
   * JIRA REST response field constant - issue key
   */
  public static final String JF_KEY = "key";
  /**
   * JIRA REST response field constant - issue or comment id
   */
  public static final String JF_ID = "id";
  /**
   * JIRA REST response field constant - updated date field
   */
  public static final String JF_UPDATED = "fields.updated";
  /**
   * JIRA REST response field constant - field where structure of comments is stored
   */
  public static final String JF_COMMENT = "fields.comment";
  /**
   * JIRA REST response field constant - field where list of comments is stored
   */
  public static final String JF_COMMENTS = JF_COMMENT + ".comments";

  /**
   * JIRA REST response field constant - field where structure of changelogs is stored
   */
  public static final String JF_CHANGELOG = "changelog";
  /**
   * JIRA REST response field constant - field where list of changelog items is stored
   */
  public static final String JF_CHANGELOG_ARRAY = JF_CHANGELOG + ".histories";

  /**
   * Name of River to be stored in document to mark indexing source
   */
  protected String riverName;

  /**
   * Name of ElasticSearch index used to store issues
   */
  protected String indexName;

  /**
   * Name of ElasticSearch type used to store issues into index
   */
  protected String issueTypeName;

  protected static final String CONFIG_FIELDS = "fields";
  protected static final String CONFIG_FIELDS_JIRAFIELD = "jira_field";
  protected static final String CONFIG_FIELDS_VALUEFILTER = "value_filter";
  protected static final String CONFIG_FILTERS = "value_filters";
  protected static final String CONFIG_JIRAFIELD_ISSUEDOCUMENTID = "jira_field_issue_document_id";
  protected static final String CONFIG_FIELDRIVERNAME = "field_river_name";
  protected static final String CONFIG_FIELDPROJECTKEY = "field_project_key";
  protected static final String CONFIG_FIELDISSUEKEY = "field_issue_key";
  protected static final String CONFIG_FIELDJIRAURL = "field_jira_url";
  protected static final String CONFIG_COMMENTMODE = "comment_mode";
  protected static final String CONFIG_FIELDCOMMENTS = "field_comments";
  protected static final String CONFIG_COMMENTTYPE = "comment_type";
  protected static final String CONFIG_COMMENTFILEDS = "comment_fields";

  protected static final String CONFIG_CHANGELOGMODE = "changelog_mode";
  protected static final String CONFIG_FIELDCHANGELOGS = "field_changelogs";
  protected static final String CONFIG_CHANGELOGTYPE = "changelog_type";
  protected static final String CONFIG_CHANGELOGFILEDS = "changelog_fields";

  /**
   * Field in jira data to get indexed document id from for issue. If empty or do not provide value then issue key is
   * used as document id.
   */
  protected String jiraFieldForIssueDocumentId = null;

  /**
   * Fields configuration structure. Key is name of field in search index. Value is map of configurations for given
   * index field containing <code>CONFIG_FIELDS_xx</code> constants as keys.
   */
  protected Map<String, Map<String, String>> fieldsConfig;

  /**
   * Value filters configuration structure. Key is name of filter. Value is map of filter configurations to be used in
   * {@link Utils#remapDataInMap(Map, Map)}.
   */
  protected Map<String, Map<String, String>> filtersConfig;

  /**
   * Name of field in search index where river name is stored.
   */
  protected String indexFieldForRiverName = null;

  /**
   * Name of field in search index where JIRA project key is stored.
   */
  protected String indexFieldForProjectKey = null;

  /**
   * Name of field in search index where JIRA issue key is stored.
   */
  protected String indexFieldForIssueKey = null;

  /**
   * Name of field in search index where JIRA GUI URL (to issue or comment) is stored.
   */
  protected String indexFieldForJiraURL = null;

  /**
   * Set of issue fields requested from JIRA during call
   */
  protected Set<String> jiraCallFieldSet = new LinkedHashSet<String>();

  /**
   * Set of issue info expands requested from JIRA during call
   */
  protected Set<String> jiraCallExpandSet = new LinkedHashSet<String>();

  /**
   * Base URL used to generate JIRA GUI issue show URL.
   *
   * @see #prepareJIRAGUIUrl(String, String)
   */
  protected String jiraIssueShowUrlBase;

  /**
   * Issue comment indexing mode.
   */
  protected IssueCommentIndexingMode commentIndexingMode;

  /**
   * Name of field in search index issue document where array of comments is stored in case of
   * {@link IssueCommentIndexingMode#EMBEDDED}.
   */
  protected String indexFieldForComments = null;

  /**
   * Name of ElasticSearch type used to store issues comments into index in case of
   * {@link IssueCommentIndexingMode#STANDALONE} or {@link IssueCommentIndexingMode#CHILD}.
   */
  protected String commentTypeName;

  /**
   * Fields configuration structure for comment document. Key is name of field in search index. Value is map of
   * configurations for given index field containing <code>CONFIG_FIELDS_xx</code> constants as keys.
   */
  protected Map<String, Map<String, String>> commentFieldsConfig;

  /**
   * Issue changelog indexing mode.
   */
  protected IssueCommentIndexingMode changelogIndexingMode;

  /**
   * Name of field in search index issue document where array of changelogs is stored in case of
   * {@link IssueCommentIndexingMode#EMBEDDED}.
   */
  protected String indexFieldForChangelogs = null;

  /**
   * Name of ElasticSearch type used to store issues changelogs into index in case of
   * {@link IssueCommentIndexingMode#STANDALONE} or {@link IssueCommentIndexingMode#CHILD}.
   */
  protected String changelogTypeName;

  /**
   * Fields configuration structure for changelog document. Key is name of field in search index. Value is map of
   * configurations for given index field containing <code>CONFIG_FIELDS_xx</code> constants as keys.
   */
  protected Map<String, Map<String, String>> changelogFieldsConfig;

  /**
   * List of data preprocessors used inside {@link #indexIssue(BulkRequestBuilder, String, Map)}.
   */
  protected List<StructuredContentPreprocessor> issueDataPreprocessors = null;

  /**
   * Constructor for unit tests. Nothing is filled inside.
   */
  protected JIRA5RestIssueIndexStructureBuilder() {
    super();
  }

  /**
   * Constructor.
   *
   * @param riverName name of ElasticSearch River instance this indexer is running inside to be stored in search index
   *          to identify indexed documents source.
   * @param indexName name of ElasticSearch index used to store issues
   * @param issueTypeName name of ElasticSearch type used to store issues into index
   * @param settings map to load other structure builder settings from
   * @throws SettingsException
   */
  @SuppressWarnings("unchecked")
  public JIRA5RestIssueIndexStructureBuilder(String riverName, String indexName, String issueTypeName,
      String jiraUrlBase, Map<String, Object> settings) throws SettingsException {
    super();
    this.riverName = riverName;
    this.indexName = indexName;
    this.issueTypeName = issueTypeName;

    constructJIRAIssueShowUrlBase(jiraUrlBase);
    if (settings != null) {
      jiraFieldForIssueDocumentId = Utils.trimToNull(XContentMapValues.nodeStringValue(
          settings.get(CONFIG_JIRAFIELD_ISSUEDOCUMENTID), null));
      indexFieldForRiverName = XContentMapValues.nodeStringValue(settings.get(CONFIG_FIELDRIVERNAME), null);
      indexFieldForProjectKey = XContentMapValues.nodeStringValue(settings.get(CONFIG_FIELDPROJECTKEY), null);
      indexFieldForIssueKey = XContentMapValues.nodeStringValue(settings.get(CONFIG_FIELDISSUEKEY), null);
      indexFieldForJiraURL = XContentMapValues.nodeStringValue(settings.get(CONFIG_FIELDJIRAURL), null);
      filtersConfig = (Map<String, Map<String, String>>) settings.get(CONFIG_FILTERS);
      fieldsConfig = (Map<String, Map<String, String>>) settings.get(CONFIG_FIELDS);

      commentIndexingMode = IssueCommentIndexingMode.parseConfiguration(
          XContentMapValues.nodeStringValue(settings.get(CONFIG_COMMENTMODE), null), IssueCommentIndexingMode.EMBEDDED);
      indexFieldForComments = XContentMapValues.nodeStringValue(settings.get(CONFIG_FIELDCOMMENTS), null);
      commentTypeName = XContentMapValues.nodeStringValue(settings.get(CONFIG_COMMENTTYPE), null);
      commentFieldsConfig = (Map<String, Map<String, String>>) settings.get(CONFIG_COMMENTFILEDS);

      changelogIndexingMode = IssueCommentIndexingMode.parseConfiguration(
          XContentMapValues.nodeStringValue(settings.get(CONFIG_CHANGELOGMODE), null), IssueCommentIndexingMode.NONE);
      indexFieldForChangelogs = XContentMapValues.nodeStringValue(settings.get(CONFIG_FIELDCHANGELOGS), null);
      changelogTypeName = XContentMapValues.nodeStringValue(settings.get(CONFIG_CHANGELOGTYPE), null);
      changelogFieldsConfig = (Map<String, Map<String, String>>) settings.get(CONFIG_CHANGELOGFILEDS);
    }
    loadDefaultsIfNecessary();
    validateConfiguration();
    prepareJiraCallFieldSet();
  }

  private void loadDefaultsIfNecessary() {
    Map<String, Object> settingsDefault = loadDefaultSettingsMapFromFile();

    filtersConfig = loadDefaultMapIfNecessary(filtersConfig, CONFIG_FILTERS, settingsDefault);
    fieldsConfig = loadDefaultMapIfNecessary(fieldsConfig, CONFIG_FIELDS, settingsDefault);
    indexFieldForRiverName = loadDefaultStringIfNecessary(indexFieldForRiverName, CONFIG_FIELDRIVERNAME,
        settingsDefault);
    indexFieldForProjectKey = loadDefaultStringIfNecessary(indexFieldForProjectKey, CONFIG_FIELDPROJECTKEY,
        settingsDefault);
    indexFieldForIssueKey = loadDefaultStringIfNecessary(indexFieldForIssueKey, CONFIG_FIELDISSUEKEY, settingsDefault);
    indexFieldForJiraURL = loadDefaultStringIfNecessary(indexFieldForJiraURL, CONFIG_FIELDJIRAURL, settingsDefault);

    if (commentIndexingMode == null) {
      commentIndexingMode = IssueCommentIndexingMode.parseConfiguration(
          XContentMapValues.nodeStringValue(settingsDefault.get(CONFIG_COMMENTMODE), null),
          IssueCommentIndexingMode.EMBEDDED);
    }
    indexFieldForComments = loadDefaultStringIfNecessary(indexFieldForComments, CONFIG_FIELDCOMMENTS, settingsDefault);
    commentTypeName = loadDefaultStringIfNecessary(commentTypeName, CONFIG_COMMENTTYPE, settingsDefault);
    commentFieldsConfig = loadDefaultMapIfNecessary(commentFieldsConfig, CONFIG_COMMENTFILEDS, settingsDefault);

    if (changelogIndexingMode == null) {
      changelogIndexingMode = IssueCommentIndexingMode.parseConfiguration(
          XContentMapValues.nodeStringValue(settingsDefault.get(CONFIG_CHANGELOGMODE), null),
          IssueCommentIndexingMode.NONE);
    }
    indexFieldForChangelogs = loadDefaultStringIfNecessary(indexFieldForChangelogs, CONFIG_FIELDCHANGELOGS,
        settingsDefault);
    changelogTypeName = loadDefaultStringIfNecessary(changelogTypeName, CONFIG_CHANGELOGTYPE, settingsDefault);
    changelogFieldsConfig = loadDefaultMapIfNecessary(changelogFieldsConfig, CONFIG_CHANGELOGFILEDS, settingsDefault);
  }

  private void validateConfiguration() {

    validateConfigurationObject(filtersConfig, "index/value_filters");
    validateConfigurationObject(fieldsConfig, "index/fields");
    validateConfigurationString(indexFieldForRiverName, "index/field_river_name");
    validateConfigurationString(indexFieldForProjectKey, "index/field_project_key");
    validateConfigurationString(indexFieldForIssueKey, "index/field_issue_key");
    validateConfigurationString(indexFieldForJiraURL, "index/field_jira_url");
    validateConfigurationObject(commentIndexingMode, "index/comment_mode");
    validateConfigurationObject(commentFieldsConfig, "index/comment_fields");
    validateConfigurationString(commentTypeName, "index/comment_type");
    validateConfigurationString(indexFieldForComments, "index/field_comments");

    validateConfigurationObject(changelogIndexingMode, "index/changelog_mode");
    validateConfigurationObject(changelogFieldsConfig, "index/changelog_fields");
    validateConfigurationString(changelogTypeName, "index/changelog_type");
    validateConfigurationString(indexFieldForChangelogs, "index/field_changelogs");

    validateConfigurationFieldsStructure(fieldsConfig, "index/fields");
    validateConfigurationFieldsStructure(commentFieldsConfig, "index/comment_fields");

  }

  protected void prepareJiraCallFieldSet() {
    jiraCallFieldSet.clear();
    jiraCallExpandSet.clear();
    // fields always necessary to get from jira
    jiraCallFieldSet.add(getJiraCallFieldName(JF_UPDATED));
    // other fields from configuration
    for (Map<String, String> fc : fieldsConfig.values()) {
      String jf = getJiraCallFieldName(fc.get(CONFIG_FIELDS_JIRAFIELD));
      if (jf != null) {
        jiraCallFieldSet.add(jf);
      }
    }
    if (commentIndexingMode != null && commentIndexingMode != IssueCommentIndexingMode.NONE) {
      jiraCallFieldSet.add(getJiraCallFieldName(JF_COMMENT));
    }
    if (changelogIndexingMode != null && changelogIndexingMode != IssueCommentIndexingMode.NONE) {
      jiraCallExpandSet.add("changelog");
    }
  }

  @Override
  public void addIssueDataPreprocessor(StructuredContentPreprocessor preprocessor) {
    if (preprocessor == null)
      return;

    if (issueDataPreprocessors == null)
      issueDataPreprocessors = new ArrayList<StructuredContentPreprocessor>();

    issueDataPreprocessors.add(preprocessor);

  }

  @Override
  public String getIssuesSearchIndexName(String jiraProjectKey) {
    return indexName;
  }

  @Override
  public String getRequiredJIRACallIssueFields() {
    return Utils.createCsvString(jiraCallFieldSet);
  }

  @Override
  public String getRequiredJIRACallIssueExpands() {
    return Utils.createCsvString(jiraCallExpandSet);
  }

  @Override
  public void indexIssue(BulkRequestBuilder esBulk, String jiraProjectKey, Map<String, Object> issue) throws Exception {

    issue = preprocessIssueData(jiraProjectKey, issue);
    esBulk.add(indexRequest(indexName).type(issueTypeName).id(prepareIssueDocumentId(issue))
        .source(prepareIssueIndexedDocument(jiraProjectKey, issue)));

    if (commentIndexingMode.isExtraDocumentIndexed()) {
      List<Map<String, Object>> comments = extractIssueComments(issue);
      if (comments != null && !comments.isEmpty()) {
        String issueKey = extractIssueKey(issue);
        for (Map<String, Object> comment : comments) {
          String commentId = extractCommentId(comment);
          IndexRequest irq = indexRequest(indexName).type(commentTypeName).id(commentId)
              .source(prepareCommentIndexedDocument(jiraProjectKey, issueKey, comment));
          if (commentIndexingMode == IssueCommentIndexingMode.CHILD) {
            irq.parent(issueKey);
          }
          esBulk.add(irq);
        }
      }
    }

    if (changelogIndexingMode.isExtraDocumentIndexed()) {
      List<Map<String, Object>> changelogs = extractIssueChangelogs(issue);
      if (changelogs != null && !changelogs.isEmpty()) {
        String issueKey = extractIssueKey(issue);
        for (Map<String, Object> changelog : changelogs) {
          String commentId = extractChangelogId(changelog);
          IndexRequest irq = indexRequest(indexName).type(changelogTypeName).id(commentId)
              .source(prepareChangelogIndexedDocument(jiraProjectKey, issueKey, changelog));
          if (changelogIndexingMode == IssueCommentIndexingMode.CHILD) {
            irq.parent(issueKey);
          }
          esBulk.add(irq);
        }
      }
    }

  }

  protected String prepareIssueDocumentId(Map<String, Object> issue) {
    String documentId = null;
    if (jiraFieldForIssueDocumentId != null) {
      documentId = Utils.trimToNull(XContentMapValues.nodeStringValue(
          XContentMapValues.extractValue(jiraFieldForIssueDocumentId, issue), null));
    }
    if (documentId == null)
      documentId = extractIssueKey(issue);
    return documentId;
  }

  @Override
  public String extractIssueKey(Map<String, Object> issue) {
    return XContentMapValues.nodeStringValue(issue.get(JF_KEY), null);
  }

  @Override
  public Date extractIssueUpdated(Map<String, Object> issue) {
    return DateTimeUtils.parseISODateTime(XContentMapValues.nodeStringValue(
        XContentMapValues.extractValue(JF_UPDATED, issue), null));
  }

  public String extractCommentId(Map<String, Object> comment) {
    return XContentMapValues.nodeStringValue(comment.get(JF_ID), null);
  }

  public String extractChangelogId(Map<String, Object> changelog) {
    return XContentMapValues.nodeStringValue(changelog.get(JF_ID), null);
  }

  /**
   * Preprocess issue data over all configured preprocessors.
   *
   * @param jiraProjectKey issue is for
   * @param issue data to preprocess
   * @return preprocessed issue data
   */
  protected Map<String, Object> preprocessIssueData(String jiraProjectKey, Map<String, Object> issue) {
    if (issueDataPreprocessors != null) {
      for (StructuredContentPreprocessor prepr : issueDataPreprocessors) {
        issue = prepr.preprocessData(issue);
      }
    }
    return issue;
  }

  @Override
  public void buildSearchForIndexedDocumentsNotUpdatedAfter(SearchRequestBuilder srb, String jiraProjectKey, Date date) {
    FilterBuilder filterTime = FilterBuilders.rangeFilter("_timestamp").lt(date);
    FilterBuilder filterProject = FilterBuilders.termFilter(indexFieldForProjectKey, jiraProjectKey);
    FilterBuilder filterSource = FilterBuilders.termFilter(indexFieldForRiverName, riverName);
    FilterBuilder filter = FilterBuilders.boolFilter().must(filterTime).must(filterProject).must(filterSource);
    srb.setQuery(QueryBuilders.matchAllQuery()).addField("_id").setPostFilter(filter);
    Set<String> st = new LinkedHashSet<String>();
    st.add(issueTypeName);
    if (commentIndexingMode.isExtraDocumentIndexed())
      st.add(commentTypeName);
    if (changelogIndexingMode.isExtraDocumentIndexed())
      st.add(changelogTypeName);
    srb.setTypes(st.toArray(new String[st.size()]));
  }

  @Override
  public boolean deleteIssueDocument(BulkRequestBuilder esBulk, SearchHit documentToDelete) throws Exception {
    esBulk.add(deleteRequest(indexName).type(documentToDelete.getType()).id(documentToDelete.getId()));
    return issueTypeName.equals(documentToDelete.getType());
  }

  /**
   * Convert JIRA returned issue REST data into JSON document to be stored in search index.
   *
   * @param jiraProjectKey key of jira project document is for.
   * @param issue issue data from JIRA REST call
   * @return JSON builder with issue document for index
   * @throws Exception
   */
  protected XContentBuilder prepareIssueIndexedDocument(String jiraProjectKey, Map<String, Object> issue)
      throws Exception {
    String issueKey = extractIssueKey(issue);

    XContentBuilder out = jsonBuilder().startObject();
    addValueToTheIndexField(out, indexFieldForRiverName, riverName);
    addValueToTheIndexField(out, indexFieldForProjectKey, jiraProjectKey);
    addValueToTheIndexField(out, indexFieldForIssueKey, issueKey);
    addValueToTheIndexField(out, indexFieldForJiraURL, prepareJIRAGUIUrl(issueKey, null));

    for (String indexFieldName : fieldsConfig.keySet()) {
      Map<String, String> fieldConfig = fieldsConfig.get(indexFieldName);
      addValueToTheIndex(out, indexFieldName, fieldConfig.get(CONFIG_FIELDS_JIRAFIELD), issue,
          fieldConfig.get(CONFIG_FIELDS_VALUEFILTER));
    }

    if (commentIndexingMode == IssueCommentIndexingMode.EMBEDDED) {
      List<Map<String, Object>> comments = extractIssueComments(issue);
      if (comments != null && !comments.isEmpty()) {
        out.startArray(indexFieldForComments);
        for (Map<String, Object> comment : comments) {
          out.startObject();
          addCommonFieldsToCommentIndexedDocument(out, issueKey, comment);
          out.endObject();
        }
        out.endArray();
      }
    }
    if (changelogIndexingMode == IssueCommentIndexingMode.EMBEDDED) {
      List<Map<String, Object>> changelogs = extractIssueChangelogs(issue);
      if (changelogs != null && !changelogs.isEmpty()) {
        out.startArray(indexFieldForChangelogs);
        for (Map<String, Object> changelog : changelogs) {
          out.startObject();
          addCommonFieldsToChangelogIndexedDocument(out, issueKey, changelog);
          out.endObject();
        }
        out.endArray();
      }
    }
    return out.endObject();
  }

  /**
   * Convert JIRA returned REST data into JSON document to be stored in search index for comments in child and
   * standalone mode.
   *
   * @param projectKey key of jira project document is for.
   * @param issueKey this comment is for
   * @param comment data from JIRA REST call
   * @return JSON builder with comment document for index
   * @throws Exception
   */
  protected XContentBuilder prepareCommentIndexedDocument(String projectKey, String issueKey,
      Map<String, Object> comment) throws Exception {
    XContentBuilder out = jsonBuilder().startObject();
    addValueToTheIndexField(out, indexFieldForRiverName, riverName);
    addValueToTheIndexField(out, indexFieldForProjectKey, projectKey);
    addValueToTheIndexField(out, indexFieldForIssueKey, issueKey);
    addCommonFieldsToCommentIndexedDocument(out, issueKey, comment);
    return out.endObject();
  }

  private void addCommonFieldsToCommentIndexedDocument(XContentBuilder out, String issueKey, Map<String, Object> comment)
      throws Exception {
    addValueToTheIndexField(out, indexFieldForJiraURL, prepareJIRAGUIUrl(issueKey, extractCommentId(comment)));
    for (String indexFieldName : commentFieldsConfig.keySet()) {
      Map<String, String> commentFieldConfig = commentFieldsConfig.get(indexFieldName);
      addValueToTheIndex(out, indexFieldName, commentFieldConfig.get(CONFIG_FIELDS_JIRAFIELD), comment,
          commentFieldConfig.get(CONFIG_FIELDS_VALUEFILTER));
    }
  }

  /**
   * Get comments for issue from JIRA JSON data.
   *
   * @param issue Map of Maps issue data structure loaded from JIRA.
   * @return list of comments if available in issu data
   */
  @SuppressWarnings("unchecked")
  protected List<Map<String, Object>> extractIssueComments(Map<String, Object> issue) {
    List<Map<String, Object>> comments = (List<Map<String, Object>>) XContentMapValues.extractValue(JF_COMMENTS, issue);
    return comments;
  }

  /**
   * Convert JIRA returned REST data into JSON document to be stored in search index for changelogs in child and
   * standalone mode.
   *
   * @param projectKey key of jira project document is for.
   * @param issueKey this changelog is for
   * @param changelog data from JIRA REST call
   * @return JSON builder with changelog document for index
   * @throws Exception
   */
  protected XContentBuilder prepareChangelogIndexedDocument(String projectKey, String issueKey,
      Map<String, Object> changelog) throws Exception {
    XContentBuilder out = jsonBuilder().startObject();
    addValueToTheIndexField(out, indexFieldForRiverName, riverName);
    addValueToTheIndexField(out, indexFieldForProjectKey, projectKey);
    addValueToTheIndexField(out, indexFieldForIssueKey, issueKey);
    addCommonFieldsToChangelogIndexedDocument(out, issueKey, changelog);
    return out.endObject();
  }

  private void addCommonFieldsToChangelogIndexedDocument(XContentBuilder out, String issueKey,
      Map<String, Object> changelog) throws Exception {
    addValueToTheIndexField(out, indexFieldForJiraURL, prepareJIRAGUIUrl(issueKey, null));
    for (String indexFieldName : changelogFieldsConfig.keySet()) {
      Map<String, String> changelogFieldConfig = changelogFieldsConfig.get(indexFieldName);
      addValueToTheIndex(out, indexFieldName, changelogFieldConfig.get(CONFIG_FIELDS_JIRAFIELD), changelog,
          changelogFieldConfig.get(CONFIG_FIELDS_VALUEFILTER));
    }
  }

  /**
   * Get changelogs for issue from JIRA JSON data.
   *
   * @param issue Map of Maps issue data structure loaded from JIRA.
   * @return list of changelogs if available in issue data
   */
  @SuppressWarnings("unchecked")
  protected List<Map<String, Object>> extractIssueChangelogs(Map<String, Object> issue) {
    List<Map<String, Object>> changelogs = (List<Map<String, Object>>) XContentMapValues.extractValue(
        JF_CHANGELOG_ARRAY, issue);
    return changelogs;
  }

  /**
   * Prepare URL of issue or comment in JIRA GUI.
   *
   * @param issueKey mandatory key of JIRA issue.
   * @param commentId id of comment, may be null
   * @return URL to show issue or comment in JIRA GUI
   */
  public String prepareJIRAGUIUrl(String issueKey, String commentId) {
    if (commentId == null) {
      return jiraIssueShowUrlBase + issueKey;
    } else {
      return jiraIssueShowUrlBase + issueKey + "?focusedCommentId=" + commentId
          + "&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-" + commentId;
    }
  }

  /**
   * Get defined value from values structure and add it into index document. Calls
   * {@link #addValueToTheIndex(XContentBuilder, String, String, Map, Map)} and receive filter from
   * {@link #filtersConfig} based on passed <code>valueFieldFilterName</code>)
   *
   * @param out content builder to add indexed value field into
   * @param indexField name of field for index
   * @param valuePath path to get value from <code>values</code> structure. Dot notation for nested values can be used
   *          here (see {@link XContentMapValues#extractValue(String, Map)}).
   * @param values structure to get value from. Can be <code>null</code> - nothing added in this case, but not
   *          exception.
   * @param valueFieldFilterName name of filter definition to get it from {@link #filtersConfig}
   * @throws Exception
   */
  protected void addValueToTheIndex(XContentBuilder out, String indexField, String valuePath,
      Map<String, Object> values, String valueFieldFilterName) throws Exception {
    Map<String, String> filter = null;
    if (!Utils.isEmpty(valueFieldFilterName)) {
      filter = filtersConfig.get(valueFieldFilterName);
    }
    addValueToTheIndex(out, indexField, valuePath, values, filter);
  }

  /**
   * Get defined value from values structure and add it into index document.
   *
   * @param out content builder to add indexed value field into
   * @param indexField name of field for index
   * @param valuePath path to get value from <code>values</code> structure. Dot notation for nested values can be used
   *          here (see {@link XContentMapValues#extractValue(String, Map)}).
   * @param values structure to get value from. Can be <code>null</code> - nothing added in this case, but not
   *          exception.
   * @param valueFieldFilter if value is JSON Object (java Map here) or List of JSON Objects, then fields in this
   *          objects are filtered to leave only fields named here and remap them - see
   *          {@link Utils#remapDataInMap(Map, Map)}. No filtering performed if this is <code>null</code>.
   * @throws Exception
   */
  @SuppressWarnings("unchecked")
  protected void addValueToTheIndex(XContentBuilder out, String indexField, String valuePath,
      Map<String, Object> values, Map<String, String> valueFieldFilter) throws Exception {
    if (values == null) {
      return;
    }
    Object v = null;
    if (valuePath.contains(".")) {
      v = XContentMapValues.extractValue(valuePath, values);
    } else {
      v = values.get(valuePath);
    }
    if (v != null && valueFieldFilter != null && !valueFieldFilter.isEmpty()) {
      if (v instanceof Map) {
        Utils.remapDataInMap((Map<String, Object>) v, valueFieldFilter);
      } else if (v instanceof List) {
        for (Object o : (List<?>) v) {
          if (o instanceof Map) {
            Utils.remapDataInMap((Map<String, Object>) o, valueFieldFilter);
          } else {
            logger.warn("Filter defined for field which is not filterable - jira array field '{}' with value: {}",
                valuePath, v);
          }
        }
      } else {
        logger.warn("Filter defined for field which is not filterable - jira field '{}' with value: {}", valuePath, v);
      }
    }
    addValueToTheIndexField(out, indexField, v);
  }

  /**
   * Add value into field in index document. Do not add it if value is <code>null</code>!
   *
   * @param out builder to add field into.
   * @param indexField real name of field used in index.
   * @param value to be added to the index field. Can be <code>null</code>, nothing added in this case
   * @throws Exception
   *
   * @see {@link XContentBuilder#field(String, Object)}.
   */
  protected void addValueToTheIndexField(XContentBuilder out, String indexField, Object value) throws Exception {
    if (value != null)
      out.field(indexField, value);
  }

  /**
   * Get name of JIRA field used in REST call from full jira field name.
   *
   * @param fullJiraFieldName
   * @return call field name or null
   * @see #getRequiredJIRACallIssueFields()
   */
  protected static String getJiraCallFieldName(String fullJiraFieldName) {
    if (Utils.isEmpty(fullJiraFieldName)) {
      return null;
    }
    fullJiraFieldName = fullJiraFieldName.trim();
    if (fullJiraFieldName.startsWith("fields.")) {
      String jcrf = fullJiraFieldName.substring("fields.".length());
      if (Utils.isEmpty(jcrf)) {
        logger.warn("Bad format of jira field name '{}', nothing will be in search index", fullJiraFieldName);
        return null;
      }
      if (jcrf.contains(".")) {
        jcrf = jcrf.substring(0, jcrf.indexOf("."));
      }
      if (Utils.isEmpty(jcrf)) {
        logger.warn("Bad format of jira field name '{}', nothing will be in search index", fullJiraFieldName);
        return null;
      }
      return jcrf.trim();
    } else {
      return null;
    }
  }

  /**
   * Construct value for {@link #jiraIssueShowUrlBase}.
   *
   * @param jiraUrlBase base URL of jira instance
   */
  protected void constructJIRAIssueShowUrlBase(String jiraUrlBase) {
    jiraIssueShowUrlBase = jiraUrlBase;
    if (!jiraIssueShowUrlBase.endsWith("/")) {
      jiraIssueShowUrlBase += "/";
    }
    jiraIssueShowUrlBase += "browse/";
  }

  @SuppressWarnings("unchecked")
  private Map<String, Object> loadDefaultSettingsMapFromFile() throws SettingsException {
    Map<String, Object> json = Utils.loadJSONFromJarPackagedFile("/templates/jira_river_configuration_default.json");
    return (Map<String, Object>) json.get("index");
  }

  private String loadDefaultStringIfNecessary(String valueToCheck, String valueConfigKey,
      Map<String, Object> settingsDefault) {
    if (Utils.isEmpty(valueToCheck)) {
      return XContentMapValues.nodeStringValue(settingsDefault.get(valueConfigKey), null);
    } else {
      return valueToCheck.trim();
    }
  }

  @SuppressWarnings("unchecked")
  private Map<String, Map<String, String>> loadDefaultMapIfNecessary(Map<String, Map<String, String>> valueToCheck,
      String valueConfigKey, Map<String, Object> settingsDefault) {
    if (valueToCheck == null || valueToCheck.isEmpty()) {
      valueToCheck = (Map<String, Map<String, String>>) settingsDefault.get(valueConfigKey);
    }
    return valueToCheck;
  }

  private void validateConfigurationFieldsStructure(Map<String, Map<String, String>> value, String configFieldName) {
    for (String idxFieldName : value.keySet()) {
      if (Utils.isEmpty(idxFieldName)) {
        throw new SettingsException("Empty key found in '" + configFieldName + "' map.");
      }
      Map<String, String> fc = value.get(idxFieldName);
      if (Utils.isEmpty(fc.get(CONFIG_FIELDS_JIRAFIELD))) {
        throw new SettingsException("'jira_field' is not defined in '" + configFieldName + "/" + idxFieldName + "'");
      }
      String fil = fc.get(CONFIG_FIELDS_VALUEFILTER);
      if (fil != null && !filtersConfig.containsKey(fil)) {
        throw new SettingsException("Filter definition not found for filter name '" + fil + "' defined in '"
            + configFieldName + "/" + idxFieldName + "/value_filter'");
      }
    }
  }

  private void validateConfigurationString(String value, String configFieldName) throws SettingsException {
    if (Utils.isEmpty(value)) {
      throw new SettingsException("No default '" + configFieldName + "' configuration found!");
    }
  }

  private void validateConfigurationObject(Object value, String configFieldName) throws SettingsException {
    if (value == null) {
      throw new SettingsException("No default '" + configFieldName + "' configuration found!");
    }
  }

}
TOP

Related Classes of org.jboss.elasticsearch.river.jira.JIRA5RestIssueIndexStructureBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.