001/* 002 * (C) Copyright 2016-2019 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Guillaume Renard <grenard@nuxeo.com> 018 */ 019package org.nuxeo.elasticsearch.io.marshallers.json; 020 021import static org.nuxeo.common.utils.DateUtils.formatISODateTime; 022import static org.nuxeo.common.utils.DateUtils.nowIfNull; 023import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.FETCH_PROPERTIES; 024import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.MAX_DEPTH_PARAM; 025import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.TRANSLATE_PROPERTIES; 026import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON; 027import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE; 028 029import java.io.Closeable; 030import java.io.IOException; 031import java.lang.reflect.Type; 032import java.util.List; 033 034import javax.inject.Inject; 035import javax.ws.rs.core.MediaType; 036 037import org.apache.logging.log4j.LogManager; 038import org.apache.logging.log4j.Logger; 039import org.nuxeo.ecm.core.api.model.Property; 040import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl; 041import org.nuxeo.ecm.core.api.model.impl.PropertyFactory; 042import org.nuxeo.ecm.core.io.marshallers.json.ExtensibleEntityJsonWriter; 043import org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter; 044import org.nuxeo.ecm.core.io.registry.reflect.Setup; 045import org.nuxeo.ecm.core.schema.SchemaManager; 046import org.nuxeo.ecm.core.schema.types.Field; 047import org.nuxeo.ecm.core.schema.types.ListType; 048import org.nuxeo.ecm.core.schema.types.ListTypeImpl; 049import org.nuxeo.ecm.core.schema.types.Schema; 050import org.nuxeo.ecm.core.schema.types.SchemaImpl; 051import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 052import org.nuxeo.ecm.core.schema.types.primitives.DateType; 053import org.nuxeo.ecm.core.schema.types.primitives.StringType; 054import org.nuxeo.ecm.directory.io.DirectoryEntryJsonWriter; 055import org.nuxeo.ecm.platform.query.api.Aggregate; 056import org.nuxeo.ecm.platform.query.api.Bucket; 057import org.nuxeo.ecm.platform.query.core.BucketRange; 058import org.nuxeo.ecm.platform.query.core.BucketRangeDate; 059import org.nuxeo.elasticsearch.aggregate.SignificantTermAggregate; 060import org.nuxeo.elasticsearch.aggregate.SingleBucketAggregate; 061import org.nuxeo.elasticsearch.aggregate.SingleValueMetricAggregate; 062import org.nuxeo.elasticsearch.aggregate.TermAggregate; 063import org.nuxeo.runtime.api.Framework; 064 065import com.fasterxml.jackson.core.JsonGenerator; 066 067/** 068 * @since 8.4 069 */ 070@SuppressWarnings("rawtypes") 071@Setup(mode = SINGLETON, priority = REFERENCE) 072public class AggregateJsonWriter extends ExtensibleEntityJsonWriter<Aggregate> { 073 074 public static final String ENTITY_TYPE = "aggregate"; 075 076 public static final String FETCH_KEY = "key"; 077 078 private static final Logger log = LogManager.getLogger(AggregateJsonWriter.class); 079 080 /** Fake schema for system properties usable as a page provider aggregate */ 081 protected static final Schema SYSTEM_SCHEMA = new SchemaImpl("system", null); 082 083 static { 084 SYSTEM_SCHEMA.addField("ecm:mixinType", new ListTypeImpl("system", "", StringType.INSTANCE), null, 0, null); 085 SYSTEM_SCHEMA.addField("ecm:tag", new ListTypeImpl("system", "", StringType.INSTANCE), null, 0, null); 086 SYSTEM_SCHEMA.addField("ecm:primaryType", StringType.INSTANCE, null, 0, null); 087 SYSTEM_SCHEMA.addField("ecm:currentLifeCycleState", StringType.INSTANCE, null, 0, null); 088 SYSTEM_SCHEMA.addField("ecm:versionLabel", StringType.INSTANCE, null, 0, null); 089 SYSTEM_SCHEMA.addField("ecm:isProxy", BooleanType.INSTANCE, null, 0, null); 090 SYSTEM_SCHEMA.addField("ecm:isRecord", BooleanType.INSTANCE, null, 0, null); 091 SYSTEM_SCHEMA.addField("ecm:retainUntil", DateType.INSTANCE, null, 0, null); 092 SYSTEM_SCHEMA.addField("ecm:hasLegalHold", BooleanType.INSTANCE, null, 0, null); 093 SYSTEM_SCHEMA.addField("ecm:isTrashed", BooleanType.INSTANCE, null, 0, null); 094 } 095 096 /** 097 * @since 10.3 098 */ 099 protected Field getSystemField(String name) { 100 Field result = SYSTEM_SCHEMA.getField(name); 101 if (result == null && name.startsWith("ecm:path@level")) { 102 SYSTEM_SCHEMA.addField(name, StringType.INSTANCE, null, 0, null); 103 return SYSTEM_SCHEMA.getField(name); 104 } 105 return result; 106 } 107 108 @Inject 109 private SchemaManager schemaManager; 110 111 public AggregateJsonWriter() { 112 super(ENTITY_TYPE, Aggregate.class); 113 } 114 115 public AggregateJsonWriter(String entityType, Class<Aggregate> entityClass) { 116 super(entityType, entityClass); 117 } 118 119 @Override 120 public boolean accept(Class<?> clazz, Type genericType, MediaType mediatype) { 121 return true; 122 } 123 124 @SuppressWarnings("unchecked") 125 @Override 126 protected void writeEntityBody(Aggregate agg, JsonGenerator jg) throws IOException { 127 boolean fetch = ctx.getFetched(ENTITY_TYPE).contains(FETCH_KEY); 128 String fieldName = agg.getField(); 129 Field field; 130 if (fieldName.startsWith("ecm:")) { 131 field = getSystemField(fieldName); 132 if (field == null) { 133 log.warn("Field: {} is not a valid field for aggregates", fieldName); 134 return; 135 } 136 } else { 137 field = schemaManager.getField(agg.getXPathField()); 138 } 139 jg.writeObjectField("id", agg.getId()); 140 jg.writeObjectField("field", agg.getField()); 141 jg.writeObjectField("properties", agg.getProperties()); 142 jg.writeObjectField("ranges", agg.getRanges()); 143 jg.writeObjectField("selection", agg.getSelection()); 144 jg.writeObjectField("type", agg.getType()); 145 if (agg instanceof SingleValueMetricAggregate) { 146 Double val = ((SingleValueMetricAggregate) agg).getValue(); 147 jg.writeObjectField("value", Double.isFinite(val) ? val : null); 148 } else if (agg instanceof SingleBucketAggregate) { 149 jg.writeObjectField("value", ((SingleBucketAggregate) agg).getDocCount()); 150 } else if (!fetch || !(agg instanceof TermAggregate || agg instanceof SignificantTermAggregate)) { 151 jg.writeObjectField("buckets", agg.getBuckets()); 152 jg.writeObjectField("extendedBuckets", agg.getExtendedBuckets()); 153 } else { 154 if (field != null) { 155 try (Closeable resource = ctx.wrap() 156 .with(FETCH_PROPERTIES + "." + DocumentModelJsonWriter.ENTITY_TYPE, 157 "properties") 158 .with(FETCH_PROPERTIES + "." + DirectoryEntryJsonWriter.ENTITY_TYPE, 159 "parent") 160 .with(TRANSLATE_PROPERTIES + "." + DirectoryEntryJsonWriter.ENTITY_TYPE, 161 "label") 162 .with(MAX_DEPTH_PARAM, "max") 163 .open()) { 164 // write buckets with privilege because we create a property to leverage marshallers 165 Framework.doPrivilegedThrowing(() -> { 166 writeBuckets("buckets", agg.getBuckets(), field, jg); 167 writeBuckets("extendedBuckets", agg.getExtendedBuckets(), field, jg); 168 }); 169 } 170 } else { 171 log.warn("Could not resolve field: {} for aggregate: {}", fieldName, agg.getId()); 172 jg.writeObjectField("buckets", agg.getBuckets()); 173 jg.writeObjectField("extendedBuckets", agg.getExtendedBuckets()); 174 } 175 } 176 } 177 178 protected void writeBuckets(String fieldName, List<Bucket> buckets, Field field, JsonGenerator jg) 179 throws IOException { 180 // prepare document part in order to use property 181 Schema schema = field.getDeclaringType().getSchema(); 182 DocumentPartImpl part = new DocumentPartImpl(schema); 183 // write data 184 jg.writeArrayFieldStart(fieldName); 185 for (Bucket bucket : buckets) { 186 jg.writeStartObject(); 187 188 jg.writeObjectField("key", bucket.getKey()); 189 190 Property prop = PropertyFactory.createProperty(part, field, Property.NONE); 191 if (prop.isList()) { 192 ListType t = (ListType) prop.getType(); 193 t.getField(); 194 prop = PropertyFactory.createProperty(part, t.getField(), Property.NONE); 195 } 196 log.debug("Writing value: {} for field: {} resolved to: {}", fieldName, field.getName(), prop.getName()); 197 prop.setValue(bucket.getKey()); 198 199 writeEntityField("fetchedKey", prop, jg); 200 jg.writeNumberField("docCount", bucket.getDocCount()); 201 jg.writeEndObject(); 202 203 if (bucket instanceof BucketRange) { 204 BucketRange bucketRange = (BucketRange) bucket; 205 jg.writeNumberField("from", bucketRange.getFrom()); 206 jg.writeNumberField("to", bucketRange.getTo()); 207 } 208 209 if (bucket instanceof BucketRangeDate) { 210 BucketRangeDate bucketRange = (BucketRangeDate) bucket; 211 jg.writeStringField("fromAsDate", formatISODateTime(nowIfNull(bucketRange.getFromAsDate()))); 212 jg.writeStringField("toAsDate", formatISODateTime(nowIfNull(bucketRange.getToAsDate()))); 213 } 214 } 215 jg.writeEndArray(); 216 } 217 218}