2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
6 * Copyright © 2017 Amdocs
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
21 * ECOMP is a trademark and service mark of AT&T Intellectual Property.
23 package org.onap.aai.datarouter.policy;
25 import java.io.FileNotFoundException;
26 import java.io.IOException;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.List;
36 import org.apache.camel.Exchange;
37 import org.apache.camel.Processor;
38 import org.eclipse.persistence.dynamic.DynamicType;
39 import org.eclipse.persistence.internal.helper.DatabaseField;
40 import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext;
41 import org.json.JSONException;
42 import org.json.JSONObject;
43 import org.onap.aai.cl.api.Logger;
44 import org.onap.aai.cl.eelf.LoggerFactory;
45 import org.onap.aai.cl.mdc.MdcContext;
46 import org.onap.aai.datarouter.entity.SpikeEventEntity;
47 import org.onap.aai.datarouter.entity.DocumentStoreDataEntity;
48 import org.onap.aai.datarouter.entity.SpikeEventVertex;
49 import org.onap.aai.datarouter.entity.OxmEntityDescriptor;
50 import org.onap.aai.datarouter.logging.EntityEventPolicyMsgs;
51 import org.onap.aai.datarouter.util.EntityOxmReferenceHelper;
52 import org.onap.aai.datarouter.util.ExternalOxmModelProcessor;
53 import org.onap.aai.datarouter.util.OxmModelLoader;
54 import org.onap.aai.datarouter.util.RouterServiceUtil;
55 import org.onap.aai.datarouter.util.SearchServiceAgent;
56 import org.onap.aai.restclient.client.Headers;
57 import org.onap.aai.restclient.client.OperationResult;
58 import org.onap.aai.restclient.rest.HttpUtil;
61 import com.fasterxml.jackson.core.JsonProcessingException;
62 import com.fasterxml.jackson.databind.JsonNode;
63 import com.fasterxml.jackson.databind.ObjectMapper;
64 import com.fasterxml.jackson.databind.ObjectWriter;
66 public class SpikeEntityEventPolicy implements Processor {
68 public static final String additionalInfo = "Response of SpikeEntityEventPolicy";
69 private static final String entitySearchSchema = "entitysearch_schema.json";
71 private Collection<ExternalOxmModelProcessor> externalOxmModelProcessors;
74 private final String ACTION_CREATE = "create";
75 private final String EVENT_VERTEX = "vertex";
76 private final static String ACTION_DELETE = "delete";
77 private final String ACTION_UPDATE = "update";
78 private final String PROCESS_SPIKE_EVENT = "Process Spike Event";
79 private final String OPERATION_KEY = "operation";
82 private final List<String> SUPPORTED_ACTIONS =
83 Arrays.asList(ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE);
85 Map<String, DynamicJAXBContext> oxmVersionContextMap = new HashMap<>();
86 private String oxmVersion = null;
88 /** Agent for communicating with the Search Service. */
89 private SearchServiceAgent searchAgent = null;
90 private String entitySearchIndex;
91 private String srcDomain;
93 private Logger logger;
94 private Logger metricsLogger;
96 public enum ResponseType {
97 SUCCESS, PARTIAL_SUCCESS, FAILURE;
100 public SpikeEntityEventPolicy(SpikeEntityEventPolicyConfig config) throws FileNotFoundException {
101 LoggerFactory loggerFactoryInstance = LoggerFactory.getInstance();
102 logger = loggerFactoryInstance.getLogger(SpikeEntityEventPolicy.class.getName());
103 metricsLogger = loggerFactoryInstance.getMetricsLogger(SpikeEntityEventPolicy.class.getName());
106 srcDomain = config.getSourceDomain();
108 // Populate the index names.
109 entitySearchIndex = config.getSearchEntitySearchIndex();
111 // Instantiate the agent that we will use for interacting with the Search Service.
112 searchAgent = new SearchServiceAgent(config.getSearchCertName(), config.getSearchKeystore(),
113 config.getSearchKeystorePwd(),
114 EntityEventPolicy.concatSubUri(config.getSearchBaseUrl(), config.getSearchEndpoint()),
115 config.getSearchEndpointDocuments(), logger);
117 this.externalOxmModelProcessors = new ArrayList<>();
118 this.externalOxmModelProcessors.add(EntityOxmReferenceHelper.getInstance());
119 OxmModelLoader.registerExternalOxmModelProcessors(externalOxmModelProcessors);
120 OxmModelLoader.loadModels();
121 oxmVersionContextMap = OxmModelLoader.getVersionContextMap();
122 parseLatestOxmVersion();
125 private void parseLatestOxmVersion() {
126 int latestVersion = -1;
127 if (oxmVersionContextMap != null) {
128 Iterator it = oxmVersionContextMap.entrySet().iterator();
129 while (it.hasNext()) {
130 Map.Entry pair = (Map.Entry) it.next();
132 String version = pair.getKey().toString();
133 int versionNum = Integer.parseInt(version.substring(1, version.length()));
135 if (versionNum > latestVersion) {
136 latestVersion = versionNum;
137 oxmVersion = pair.getKey().toString();
140 logger.info(EntityEventPolicyMsgs.PROCESS_OXM_MODEL_FOUND, pair.getKey().toString());
143 logger.error(EntityEventPolicyMsgs.PROCESS_OXM_MODEL_MISSING, "");
147 public void startup() {
149 // Create the indexes in the search service if they do not already exist.
150 searchAgent.createSearchIndex(entitySearchIndex, entitySearchSchema);
151 logger.info(EntityEventPolicyMsgs.ENTITY_EVENT_POLICY_REGISTERED);
156 * Convert object to json.
158 * @param object the object
159 * @param pretty the pretty
161 * @throws JsonProcessingException the json processing exception
163 public static String convertObjectToJson(Object object, boolean pretty)
164 throws JsonProcessingException {
168 ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
171 ow = new ObjectMapper().writer();
174 return ow.writeValueAsString(object);
177 public void returnWithError(Exchange exchange, String payload, String errorMsg) {
178 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE, errorMsg);
179 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, errorMsg, payload);
180 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
183 public boolean isJSONValid(String test) {
185 new JSONObject(test);
186 } catch (JSONException ex) {
193 public void process(Exchange exchange) throws Exception {
195 long startTime = System.currentTimeMillis();
196 String uebPayload = exchange.getIn().getBody().toString();
197 if (uebPayload == null || !isJSONValid(uebPayload)) {
198 uebPayload = exchange.getIn().getBody(String.class);
199 if (uebPayload == null || !isJSONValid(uebPayload)) {
200 returnWithError(exchange, uebPayload, "Invalid Payload");
206 JSONObject mainJson = new JSONObject(uebPayload);
207 String action = mainJson.getString(OPERATION_KEY);
208 if (action == null || !SUPPORTED_ACTIONS.contains(action.toLowerCase())) {
209 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
210 "Unrecognized action '" + action + "'", uebPayload);
211 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
212 "Unrecognized action '" + action + "'");
213 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
217 // Load the UEB payload data, any errors will result in a failure and discard
219 JSONObject spikeObjVertex = getUebContentAsJson(uebPayload, EVENT_VERTEX);
220 if (spikeObjVertex == null) {
221 returnWithError(exchange, uebPayload, "Payload is missing " + EVENT_VERTEX);
225 SpikeEventVertex eventVertex = initializeSpikeEventVertex(spikeObjVertex.toString());
227 DynamicJAXBContext oxmJaxbContext = loadOxmContext(oxmVersion.toLowerCase());
228 if (oxmJaxbContext == null) {
229 logger.error(EntityEventPolicyMsgs.OXM_VERSION_NOT_SUPPORTED, oxmVersion);
230 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, "OXM version mismatch", uebPayload);
232 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
238 String entityType = eventVertex.getType();
239 if (entityType == null || entityType.isEmpty()) {
240 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
241 "Payload header missing entity type", uebPayload);
242 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
243 "Payload header missing entity type");
245 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
249 String entityKey = eventVertex.getKey();
250 if (entityKey == null || entityKey.isEmpty()) {
251 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, "Payload vertex missing entity key",
253 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
254 "Payload vertex missing entity key");
256 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
259 String entityLink = eventVertex.getEntityLink();
260 if (entityLink == null || entityLink.isEmpty()) {
261 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
262 "Payload header missing entity link", uebPayload);
263 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
264 "Payload header missing entity link");
266 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
270 // log the fact that all data are in good shape
271 logger.info(EntityEventPolicyMsgs.PROCESS_ENTITY_EVENT_POLICY_NONVERBOSE, action, entityType);
272 logger.debug(EntityEventPolicyMsgs.PROCESS_ENTITY_EVENT_POLICY_VERBOSE, action, entityType,
276 // Process for building SpikeEventEntity object
277 String[] entityTypeArr = entityType.split("-");
278 String oxmEntityType = "";
279 for (String entityWord : entityTypeArr) {
280 oxmEntityType += entityWord.substring(0, 1).toUpperCase() + entityWord.substring(1);
283 List<String> searchableAttr =
284 getOxmAttributes(oxmJaxbContext, oxmEntityType, entityType, "searchable");
285 if (searchableAttr == null) {
286 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
287 "Searchable attribute not found for payload entity type '" + entityType + "'");
288 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
289 "Searchable attribute not found for payload entity type '" + entityType + "'",
292 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
296 String entityPrimaryKeyFieldName =
297 getEntityPrimaryKeyFieldName(oxmJaxbContext, uebPayload, oxmEntityType, entityType);
298 if (entityPrimaryKeyFieldName == null) {
299 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
300 "Payload missing primary key attribute");
301 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
302 "Payload missing primary key attribute", uebPayload);
303 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
306 String entityPrimaryKeyFieldValue = lookupValueUsingKey(uebPayload, entityPrimaryKeyFieldName);
307 if (entityPrimaryKeyFieldValue.isEmpty()) {
308 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
309 "Payload missing primary value attribute");
310 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
311 "Payload missing primary value attribute", uebPayload);
313 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
317 SpikeEventEntity spikeEventEntity = new SpikeEventEntity();
320 * Use the OXM Model to determine the primary key field name based on the entity-type
323 spikeEventEntity.setEntityPrimaryKeyName(entityPrimaryKeyFieldName);
324 spikeEventEntity.setEntityPrimaryKeyValue(entityPrimaryKeyFieldValue);
325 spikeEventEntity.setEntityType(entityType);
326 spikeEventEntity.setLink(entityLink);
328 if (!getSearchTags(spikeEventEntity, searchableAttr, uebPayload, action)) {
329 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
330 "Payload missing searchable attribute for entity type '" + entityType + "'");
331 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
332 "Payload missing searchable attribute for entity type '" + entityType + "'", uebPayload);
334 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
339 spikeEventEntity.deriveFields();
341 } catch (NoSuchAlgorithmException e) {
342 logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, "Cannot create unique SHA digest");
343 logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, "Cannot create unique SHA digest",
346 setResponse(exchange, ResponseType.FAILURE, additionalInfo);
351 handleSearchServiceOperation(spikeEventEntity, action, entitySearchIndex);
353 long stopTime = System.currentTimeMillis();
354 metricsLogger.info(EntityEventPolicyMsgs.OPERATION_RESULT_NO_ERRORS, PROCESS_SPIKE_EVENT,
355 String.valueOf(stopTime - startTime));
357 setResponse(exchange, ResponseType.SUCCESS, additionalInfo);
363 private void setResponse(Exchange exchange, ResponseType responseType, String additionalInfo) {
365 exchange.getOut().setHeader("ResponseType", responseType.toString());
366 exchange.getOut().setBody(additionalInfo);
371 * Load the UEB JSON payload, any errors would result to a failure case response.
373 private JSONObject getUebContentAsJson(String payload, String contentKey) {
375 JSONObject uebJsonObj;
376 JSONObject uebObjContent;
379 uebJsonObj = new JSONObject(payload);
380 } catch (JSONException e) {
381 logger.debug(EntityEventPolicyMsgs.UEB_INVALID_PAYLOAD_JSON_FORMAT, payload);
382 logger.error(EntityEventPolicyMsgs.UEB_INVALID_PAYLOAD_JSON_FORMAT, payload);
386 if (uebJsonObj.has(contentKey)) {
387 uebObjContent = uebJsonObj.getJSONObject(contentKey);
389 logger.debug(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, contentKey);
390 logger.error(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, contentKey);
394 return uebObjContent;
398 private SpikeEventVertex initializeSpikeEventVertex(String payload) {
400 SpikeEventVertex eventVertex = null;
401 ObjectMapper mapper = new ObjectMapper();
403 // Make sure that were were actually passed in a valid string.
404 if (payload == null || payload.isEmpty()) {
405 logger.debug(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, EVENT_VERTEX);
406 logger.error(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, EVENT_VERTEX);
411 // Marshal the supplied string into a UebEventHeader object.
413 eventVertex = mapper.readValue(payload, SpikeEventVertex.class);
414 } catch (JsonProcessingException e) {
415 logger.error(EntityEventPolicyMsgs.UEB_FAILED_UEBEVENTHEADER_CONVERSION, e.toString());
416 } catch (Exception e) {
417 logger.error(EntityEventPolicyMsgs.UEB_FAILED_UEBEVENTHEADER_CONVERSION, e.toString());
420 if (eventVertex != null) {
421 logger.debug(EntityEventPolicyMsgs.UEB_EVENT_HEADER_PARSED, eventVertex.toString());
429 private String getEntityPrimaryKeyFieldName(DynamicJAXBContext oxmJaxbContext, String payload,
430 String oxmEntityType, String entityType) {
432 DynamicType entity = oxmJaxbContext.getDynamicType(oxmEntityType);
433 if (entity == null) {
437 List<DatabaseField> list = entity.getDescriptor().getPrimaryKeyFields();
438 if (list != null && !list.isEmpty()) {
439 String keyName = list.get(0).getName();
440 return keyName.substring(0, keyName.indexOf('/'));
446 private String lookupValueUsingKey(String payload, String key) throws JSONException {
447 JsonNode jsonNode = convertToJsonNode(payload);
448 return RouterServiceUtil.recursivelyLookupJsonPayload(jsonNode, key);
452 private JsonNode convertToJsonNode(String payload) {
454 ObjectMapper mapper = new ObjectMapper();
455 JsonNode jsonNode = null;
457 jsonNode = mapper.readTree(mapper.getJsonFactory().createJsonParser(payload));
458 } catch (IOException e) {
459 logger.debug(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, EVENT_VERTEX + " missing",
461 logger.error(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, EVENT_VERTEX + " missing",
469 private boolean getSearchTags(SpikeEventEntity spikeEventEntity, List<String> searchableAttr,
470 String payload, String action) {
472 boolean hasSearchableAttr = false;
473 for (String searchTagField : searchableAttr) {
474 String searchTagValue;
475 if (searchTagField.equalsIgnoreCase(spikeEventEntity.getEntityPrimaryKeyName())) {
476 searchTagValue = spikeEventEntity.getEntityPrimaryKeyValue();
478 searchTagValue = this.lookupValueUsingKey(payload, searchTagField);
481 if (searchTagValue != null && !searchTagValue.isEmpty()) {
482 hasSearchableAttr = true;
483 spikeEventEntity.addSearchTagWithKey(searchTagValue, searchTagField);
486 return hasSearchableAttr;
490 * Check if OXM version is available. If available, load it.
492 private DynamicJAXBContext loadOxmContext(String version) {
493 if (version == null) {
494 logger.error(EntityEventPolicyMsgs.FAILED_TO_FIND_OXM_VERSION, version);
498 return oxmVersionContextMap.get(version);
501 private List<String> getOxmAttributes(DynamicJAXBContext oxmJaxbContext, String oxmEntityType,
502 String entityType, String fieldName) {
504 DynamicType entity = (DynamicType) oxmJaxbContext.getDynamicType(oxmEntityType);
505 if (entity == null) {
510 * Check for searchable XML tag
512 List<String> fieldValues = null;
513 Map<String, String> properties = entity.getDescriptor().getProperties();
514 for (Map.Entry<String, String> entry : properties.entrySet()) {
515 if (entry.getKey().equalsIgnoreCase(fieldName)) {
516 fieldValues = Arrays.asList(entry.getValue().split(","));
526 protected SpikeEventEntity getPopulatedEntity(JsonNode entityNode,
527 OxmEntityDescriptor resultDescriptor) {
528 SpikeEventEntity d = new SpikeEventEntity();
530 d.setEntityType(resultDescriptor.getEntityName());
532 List<String> primaryKeyValues = new ArrayList<>();
533 List<String> primaryKeyNames = new ArrayList<>();
536 for (String keyName : resultDescriptor.getPrimaryKeyAttributeName()) {
537 pkeyValue = RouterServiceUtil.getNodeFieldAsText(entityNode, keyName);
538 if (pkeyValue != null) {
539 primaryKeyValues.add(pkeyValue);
540 primaryKeyNames.add(keyName);
542 // logger.warn("getPopulatedDocument(), pKeyValue is null for entityType = " +
543 // resultDescriptor.getEntityName());
544 logger.error(EntityEventPolicyMsgs.PRIMARY_KEY_NULL_FOR_ENTITY_TYPE,
545 resultDescriptor.getEntityName());
549 final String primaryCompositeKeyValue = RouterServiceUtil.concatArray(primaryKeyValues, "/");
550 d.setEntityPrimaryKeyValue(primaryCompositeKeyValue);
551 final String primaryCompositeKeyName = RouterServiceUtil.concatArray(primaryKeyNames, "/");
552 d.setEntityPrimaryKeyName(primaryCompositeKeyName);
554 final List<String> searchTagFields = resultDescriptor.getSearchableAttributes();
557 * Based on configuration, use the configured field names for this entity-Type to build a
558 * multi-value collection of search tags for elastic search entity search criteria.
562 for (String searchTagField : searchTagFields) {
563 String searchTagValue = RouterServiceUtil.getNodeFieldAsText(entityNode, searchTagField);
564 if (searchTagValue != null && !searchTagValue.isEmpty()) {
565 d.addSearchTagWithKey(searchTagValue, searchTagField);
574 * Perform create, read, update or delete (CRUD) operation on search engine's suggestive search
577 * @param eventEntity Entity/data to use in operation
578 * @param action The operation to perform
579 * @param target Resource to perform the operation on
580 * @param allowDeleteEvent Allow delete operation to be performed on resource
582 protected void handleSearchServiceOperation(DocumentStoreDataEntity eventEntity, String action,
586 Map<String, List<String>> headers = new HashMap<>();
587 headers.put(Headers.FROM_APP_ID, Arrays.asList("DataLayer"));
588 headers.put(Headers.TRANSACTION_ID, Arrays.asList(MDC.get(MdcContext.MDC_REQUEST_ID)));
590 String entityId = eventEntity.getId();
592 if ((action.equalsIgnoreCase(ACTION_CREATE) && entityId != null)
593 || action.equalsIgnoreCase(ACTION_UPDATE)) {
595 // Run the GET to retrieve the ETAG from the search service
596 OperationResult storedEntity = searchAgent.getDocument(index, entityId);
598 if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
599 List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
601 if (etag != null && !etag.isEmpty()) {
602 headers.put(Headers.IF_MATCH, etag);
604 logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index, entityId);
608 // Write the entity to the search service.
610 searchAgent.putDocument(index, entityId, eventEntity.getAsJson(), headers);
611 } else if (action.equalsIgnoreCase(ACTION_CREATE)) {
612 // Write the entry to the search service.
613 searchAgent.postDocument(index, eventEntity.getAsJson(), headers);
615 } else if (action.equalsIgnoreCase(ACTION_DELETE)) {
616 // Run the GET to retrieve the ETAG from the search service
617 OperationResult storedEntity = searchAgent.getDocument(index, entityId);
619 if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
620 List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
622 if (etag != null && !etag.isEmpty()) {
623 headers.put(Headers.IF_MATCH, etag);
625 logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index, entityId);
628 searchAgent.deleteDocument(index, eventEntity.getId(), headers);
630 logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index, entityId);
633 logger.error(EntityEventPolicyMsgs.ENTITY_OPERATION_NOT_SUPPORTED, action);
635 } catch (IOException e) {
636 logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE, eventEntity.getId(),
643 // put this here until we find a better spot
645 * Helper utility to concatenate substrings of a URI together to form a proper URI.
647 * @param suburis the list of substrings to concatenate together
648 * @return the concatenated list of substrings
650 public static String concatSubUri(String... suburis) {
651 String finalUri = "";
653 for (String suburi : suburis) {
655 if (suburi != null) {
656 // Remove any leading / since we only want to append /
657 suburi = suburi.replaceFirst("^/*", "");
659 // Add a trailing / if one isn't already there
660 finalUri += suburi.endsWith("/") ? suburi : suburi + "/";