1 package org.collectionspace.services.nuxeo.elasticsearch;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.Calendar;
7 import java.util.Collections;
8 import java.util.GregorianCalendar;
9 import java.util.HashSet;
10 import java.util.Iterator;
11 import java.util.List;
15 import javax.ws.rs.core.HttpHeaders;
17 import org.apache.commons.lang3.StringUtils;
18 import org.codehaus.jackson.JsonGenerator;
19 import org.codehaus.jackson.JsonNode;
20 import org.codehaus.jackson.map.ObjectMapper;
21 import org.codehaus.jackson.node.IntNode;
22 import org.codehaus.jackson.node.ObjectNode;
23 import org.codehaus.jackson.node.TextNode;
24 import org.collectionspace.services.common.api.RefNameUtils;
25 import org.nuxeo.ecm.automation.jaxrs.io.documents.JsonESDocumentWriter;
26 import org.nuxeo.ecm.core.api.CoreSession;
27 import org.nuxeo.ecm.core.api.DocumentModel;
28 import org.nuxeo.ecm.core.api.DocumentModelList;
30 public class DefaultESDocumentWriter extends JsonESDocumentWriter {
31 private static ObjectMapper objectMapper = new ObjectMapper();
34 public void writeDoc(JsonGenerator jg, DocumentModel doc, String[] schemas,
35 Map<String, String> contextParameters, HttpHeaders headers)
38 ObjectNode denormValues = getDenormValues(doc);
40 jg.writeStartObject();
42 writeSystemProperties(jg, doc);
43 writeSchemas(jg, doc, schemas);
44 writeContextParameters(jg, doc, contextParameters);
45 writeDenormValues(jg, doc, denormValues);
51 public ObjectNode getDenormValues(DocumentModel doc) {
52 ObjectNode denormValues = objectMapper.createObjectNode();
53 String docType = doc.getType();
55 if (docType.startsWith("CollectionObject")) {
56 CoreSession session = doc.getCoreSession();
57 String csid = doc.getName();
58 String tenantId = (String) doc.getProperty("collectionspace_core", "tenantId");
60 denormMediaRecords(session, csid, tenantId, denormValues);
61 denormAcquisitionRecords(session, csid, tenantId, denormValues);
62 denormExhibitionRecords(session, csid, tenantId, denormValues);
63 denormConceptFields(doc, denormValues);
64 denormMaterialFields(doc, denormValues);
65 denormObjectNameFields(doc, denormValues);
67 // Compute the title of the record for the public browser, and store it so that it can
68 // be used for sorting ES query results.
70 String title = computeTitle(doc);
73 denormValues.put("title", title);
76 // Create a list of production years from the production date structured dates.
78 List<Map<String, Object>> prodDateGroupList = (List<Map<String, Object>>) doc.getProperty("collectionobjects_common", "objectProductionDateGroupList");
80 denormValues.putArray("prodYears").addAll(structDatesToYearNodes(prodDateGroupList));
81 } else if ("Media".equals(docType) && isMediaPublished(doc)) {
82 CoreSession session = doc.getCoreSession();
83 String csid = doc.getName();
84 String tenantId = (String) doc.getProperty("collectionspace_core", "tenantId");
86 // Add media-specific denormalized fields
87 denormRelatedObjects(session, csid, tenantId, denormValues);
93 public void writeDenormValues(JsonGenerator jg, DocumentModel doc, ObjectNode denormValues) throws IOException {
94 if (denormValues != null && denormValues.size() > 0) {
95 if (jg.getCodec() == null) {
96 jg.setCodec(objectMapper);
99 Iterator<Map.Entry<String, JsonNode>> entries = denormValues.getFields();
101 while (entries.hasNext()) {
102 Map.Entry<String, JsonNode> entry = entries.next();
104 jg.writeFieldName("collectionspace_denorm:" + entry.getKey());
105 jg.writeTree(entry.getValue());
110 private void denormMediaRecords(CoreSession session, String csid, String tenantId, ObjectNode denormValues) {
111 // Store the csid and alt text of media records that are related to this object.
113 String relatedRecordQuery = String.format("SELECT * FROM Relation WHERE relations_common:subjectCsid = '%s' AND relations_common:objectDocumentType = 'Media' AND ecm:currentLifeCycleState = 'project' AND collectionspace_core:tenantId = '%s'", csid, tenantId);
114 DocumentModelList relationDocs = session.query(relatedRecordQuery);
115 List<JsonNode> mediaCsids = new ArrayList<JsonNode>();
116 List<JsonNode> mediaAltTexts = new ArrayList<JsonNode>();
118 if (relationDocs.size() > 0) {
119 Iterator<DocumentModel> iterator = relationDocs.iterator();
121 while (iterator.hasNext()) {
122 DocumentModel relationDoc = iterator.next();
123 String mediaCsid = (String) relationDoc.getProperty("relations_common", "objectCsid");
124 DocumentModel mediaDoc = getRecordByCsid(session, tenantId, "Media", mediaCsid);
126 if (isMediaPublished(mediaDoc)) {
127 mediaCsids.add(new TextNode(mediaCsid));
129 String altText = (String) mediaDoc.getProperty("media_common", "altText");
131 if (altText == null) {
135 mediaAltTexts.add(new TextNode(altText));
140 denormValues.putArray("mediaCsid").addAll(mediaCsids);
141 denormValues.putArray("mediaAltText").addAll(mediaAltTexts);
142 denormValues.put("hasMedia", mediaCsids.size() > 0);
145 private void denormRelatedObjects(CoreSession session, String csid, String tenantId, ObjectNode denormValues) {
146 // Store the objectCsid of objects that are related to this media.
148 String relatedRecordQuery = String.format(
149 "SELECT * FROM Relation WHERE relations_common:objectCsid = '%s' AND relations_common:subjectDocumentType = 'CollectionObject' AND ecm:currentLifeCycleState = 'project' AND collectionspace_core:tenantId = '%s'",
151 DocumentModelList relationDocs = session.query(relatedRecordQuery);
152 List<JsonNode> objectCsids = new ArrayList<JsonNode>();
154 if (relationDocs.size() > 0) {
155 Iterator<DocumentModel> iterator = relationDocs.iterator();
157 while (iterator.hasNext()) {
158 DocumentModel relationDoc = iterator.next();
159 String objectCsid = (String) relationDoc.getProperty("relations_common", "subjectCsid");
161 if (objectCsid != null) {
162 objectCsids.add(new TextNode(objectCsid));
167 denormValues.putArray("objectCsid").addAll(objectCsids);
170 private void denormAcquisitionRecords(CoreSession session, String csid, String tenantId, ObjectNode denormValues) {
171 // Store the credit lines of acquisition records that are related to this object.
173 String relatedRecordQuery = String.format("SELECT * FROM Relation WHERE relations_common:subjectCsid = '%s' AND relations_common:objectDocumentType = 'Acquisition' AND ecm:currentLifeCycleState = 'project' AND collectionspace_core:tenantId = '%s'", csid, tenantId);
174 DocumentModelList relationDocs = session.query(relatedRecordQuery);
175 List<JsonNode> creditLines = new ArrayList<JsonNode>();
177 if (relationDocs.size() > 0) {
178 Iterator<DocumentModel> iterator = relationDocs.iterator();
180 while (iterator.hasNext()) {
181 DocumentModel relationDoc = iterator.next();
182 String acquisitionCsid = (String) relationDoc.getProperty("relations_common", "objectCsid");
183 String creditLine = getCreditLine(session, tenantId, acquisitionCsid);
185 if (creditLine != null && creditLine.length() > 0) {
186 creditLines.add(new TextNode(creditLine));
191 denormValues.putArray("creditLine").addAll(creditLines);
194 private void denormExhibitionRecords(CoreSession session, String csid, String tenantId, ObjectNode denormValues) {
195 // Store the title, general note, and curatorial note of exhibition records that are published, and related to this object.
197 String relatedRecordQuery = String.format("SELECT * FROM Relation WHERE relations_common:subjectCsid = '%s' AND relations_common:objectDocumentType = 'Exhibition' AND ecm:currentLifeCycleState = 'project' AND collectionspace_core:tenantId = '%s'", csid, tenantId);
198 DocumentModelList relationDocs = session.query(relatedRecordQuery);
199 List<JsonNode> exhibitions = new ArrayList<JsonNode>();
201 if (relationDocs.size() > 0) {
202 Iterator<DocumentModel> iterator = relationDocs.iterator();
204 while (iterator.hasNext()) {
205 DocumentModel relationDoc = iterator.next();
206 String exhibitionCsid = (String) relationDoc.getProperty("relations_common", "objectCsid");
207 DocumentModel exhibitionDoc = getRecordByCsid(session, tenantId, "Exhibition", exhibitionCsid);
209 if (exhibitionDoc != null && isExhibitionPublished(exhibitionDoc)) {
210 ObjectNode exhibitionNode = objectMapper.createObjectNode();
212 String title = (String) exhibitionDoc.getProperty("exhibitions_common", "title");
213 String generalNote = (String) exhibitionDoc.getProperty("exhibitions_common", "generalNote");
214 String curatorialNote = (String) exhibitionDoc.getProperty("exhibitions_common", "curatorialNote");
216 exhibitionNode.put("title", title);
217 exhibitionNode.put("generalNote", generalNote);
218 exhibitionNode.put("curatorialNote", curatorialNote);
220 exhibitions.add(exhibitionNode);
225 denormValues.putArray("exhibition").addAll(exhibitions);
229 * Denormalize the material group list for a collectionobject in order to index the controlled or uncontrolled term
231 * @param doc the collectionobject document
232 * @param denormValues the json node for denormalized fields
234 private void denormMaterialFields(DocumentModel doc, ObjectNode denormValues) {
235 List<Map<String, Object>> materialGroupList =
236 (List<Map<String, Object>>) doc.getProperty("collectionobjects_common", "materialGroupList");
238 List<JsonNode> denormMaterials = new ArrayList<>();
239 for (Map<String, Object> materialGroup : materialGroupList) {
240 String controlledMaterial = (String) materialGroup.get("materialControlled");
241 if (controlledMaterial != null) {
242 final ObjectNode node = objectMapper.createObjectNode();
243 node.put("material", RefNameUtils.getDisplayName(controlledMaterial));
244 denormMaterials.add(node);
247 String material = (String) materialGroup.get("material");
248 if (material != null) {
249 final ObjectNode node = objectMapper.createObjectNode();
250 node.put("material", material);
251 denormMaterials.add(node);
255 denormValues.putArray("materialGroupList").addAll(denormMaterials);
259 * Denormalize the object name group list for a collectionobject in order to index the controlled and
262 * @param doc the collectionobject document
263 * @param denormValues the json node for denormalized fields
265 private void denormObjectNameFields(DocumentModel doc, ObjectNode denormValues) {
266 List<Map<String, Object>> objectNameList =
267 (List<Map<String, Object>>) doc.getProperty("collectionobjects_common", "objectNameList");
269 List<JsonNode> denormObjectNames = new ArrayList<>();
270 for (Map<String, Object> objectNameGroup : objectNameList) {
271 String controlledName = (String) objectNameGroup.get("objectNameControlled");
272 if (controlledName != null) {
273 final ObjectNode node = objectMapper.createObjectNode();
274 node.put("objectName", RefNameUtils.getDisplayName(controlledName));
275 denormObjectNames.add(node);
278 String objectName = (String) objectNameGroup.get("objectName");
279 if (objectName != null) {
280 final ObjectNode node = objectMapper.createObjectNode();
281 node.put("objectName", objectName);
282 denormObjectNames.add(node);
286 denormValues.putArray("objectNameList").addAll(denormObjectNames);
290 * Denormalize the content concept, content event, content person, and content organization
291 * fields for a collectionobject so that they are indexed under a single field
293 * @param doc the collectionobject document
294 * @param denormValues the json node for denormalized fields
296 private void denormConceptFields(final DocumentModel doc, final ObjectNode denormValues) {
297 final List<JsonNode> denormContentSubject = new ArrayList<>();
298 final List<String> fields = Arrays.asList("contentConcepts",
301 "contentOrganizations");
303 for (String field : fields) {
304 List<String> contentList = (List<String>) doc.getProperty("collectionobjects_common", field);
306 for (String content : contentList) {
307 if (content != null) {
308 final ObjectNode node = objectMapper.createObjectNode();
309 node.put("subject", RefNameUtils.getDisplayName(content));
310 denormContentSubject.add(node);
315 denormValues.putArray("contentSubjectList").addAll(denormContentSubject);
319 * Compute a title for the public browser. This needs to be indexed in ES so that it can
320 * be used for sorting. (Even if it's just extracting the primary value.)
322 protected String computeTitle(DocumentModel doc) {
323 List<Map<String, Object>> titleGroups = (List<Map<String, Object>>) doc.getProperty("collectionobjects_common", "titleGroupList");
324 String primaryTitle = null;
326 if (titleGroups.size() > 0) {
327 Map<String, Object> primaryTitleGroup = titleGroups.get(0);
328 primaryTitle = (String) primaryTitleGroup.get("title");
331 if (StringUtils.isNotEmpty(primaryTitle)) {
335 List<Map<String, Object>> objectNameGroups = (List<Map<String, Object>>) doc.getProperty("collectionobjects_common", "objectNameList");
336 String primaryObjectName = null;
338 if (objectNameGroups.size() > 0) {
339 Map<String, Object> primaryObjectNameGroup = objectNameGroups.get(0);
340 primaryObjectName = (String) primaryObjectNameGroup.get("objectNameControlled");
341 if (primaryObjectName == null) {
342 primaryObjectName = (String) primaryObjectNameGroup.get("objectName");
345 // The object might be a refname in some profiles/tenants. If it is, use only the display name.
348 String displayName = RefNameUtils.getDisplayName(primaryObjectName);
350 if (displayName != null) {
351 primaryObjectName = displayName;
354 catch (Exception e) {}
357 return primaryObjectName;
360 private boolean isPublished(DocumentModel doc, String publishedFieldPart, String publishedFieldName) {
361 boolean isPublished = false;
364 List<String> publishToValues = (List<String>) doc.getProperty(publishedFieldPart, publishedFieldName);
366 if (publishToValues != null) {
367 for (int i=0; i<publishToValues.size(); i++) {
368 String value = publishToValues.get(i);
369 String shortId = RefNameUtils.getItemShortId(value);
371 if (shortId.equals("all") || shortId.equals("cspacepub")) {
383 private boolean isMediaPublished(DocumentModel mediaDoc) {
384 return isPublished(mediaDoc, "media_common", "publishToList");
387 private boolean isExhibitionPublished(DocumentModel exhibitionDoc) {
388 return isPublished(exhibitionDoc, "exhibitions_common", "publishToList");
391 private String getCreditLine(CoreSession session, String tenantId, String acquisitionCsid) {
392 String creditLine = null;
393 DocumentModel acquisitionDoc = getRecordByCsid(session, tenantId, "Acquisition", acquisitionCsid);
395 if (acquisitionDoc != null) {
396 creditLine = (String) acquisitionDoc.getProperty("acquisitions_common", "creditLine");
402 protected DocumentModel getRecordByCsid(CoreSession session, String tenantId, String recordType, String csid) {
403 String getRecordQuery = String.format("SELECT * FROM %s WHERE ecm:name = '%s' AND ecm:currentLifeCycleState = 'project' AND collectionspace_core:tenantId = '%s'", recordType, csid, tenantId);
405 DocumentModelList docs = session.query(getRecordQuery);
407 if (docs != null && docs.size() > 0) {
414 protected List<JsonNode> structDateToYearNodes(Map<String, Object> structDate) {
415 return structDatesToYearNodes(Arrays.asList(structDate));
418 protected List<JsonNode> structDatesToYearNodes(List<Map<String, Object>> structDates) {
419 Set<Integer> years = new HashSet<Integer>();
421 for (Map<String, Object> structDate : structDates) {
422 if (structDate != null) {
423 GregorianCalendar earliestCalendar = (GregorianCalendar) structDate.get("dateEarliestScalarValue");
424 GregorianCalendar latestCalendar = (GregorianCalendar) structDate.get("dateLatestScalarValue");
426 if (earliestCalendar != null && latestCalendar != null) {
427 // Grr @ latest scalar value historically being exclusive.
428 // Subtract one day to make it inclusive.
429 latestCalendar.add(Calendar.DATE, -1);
431 Integer earliestYear = earliestCalendar.get(Calendar.YEAR);
432 Integer latestYear = latestCalendar.get(Calendar.YEAR);;
434 for (int year = earliestYear; year <= latestYear; year++) {
441 List<Integer> yearList = new ArrayList<Integer>(years);
442 Collections.sort(yearList);
444 List<JsonNode> yearNodes = new ArrayList<JsonNode>();
446 for (Integer year : yearList) {
447 yearNodes.add(new IntNode(year));