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}