001/*
002 * (C) Copyright 2016 Nuxeo SA (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.ecm.core.io.registry.MarshallingConstants.FETCH_PROPERTIES;
022import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.MAX_DEPTH_PARAM;
023import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.TRANSLATE_PROPERTIES;
024import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
025import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
026
027import java.io.Closeable;
028import java.io.IOException;
029import java.lang.reflect.Type;
030import java.util.List;
031
032import javax.inject.Inject;
033import javax.ws.rs.core.MediaType;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.ecm.core.api.model.Property;
038import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl;
039import org.nuxeo.ecm.core.api.model.impl.PropertyFactory;
040import org.nuxeo.ecm.core.io.marshallers.json.ExtensibleEntityJsonWriter;
041import org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter;
042import org.nuxeo.ecm.core.io.registry.reflect.Setup;
043import org.nuxeo.ecm.core.schema.SchemaManager;
044import org.nuxeo.ecm.core.schema.types.Field;
045import org.nuxeo.ecm.core.schema.types.ListType;
046import org.nuxeo.ecm.core.schema.types.ListTypeImpl;
047import org.nuxeo.ecm.core.schema.types.Schema;
048import org.nuxeo.ecm.core.schema.types.SchemaImpl;
049import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
050import org.nuxeo.ecm.core.schema.types.primitives.StringType;
051import org.nuxeo.ecm.core.schema.utils.DateParser;
052import org.nuxeo.ecm.directory.io.DirectoryEntryJsonWriter;
053import org.nuxeo.ecm.platform.query.api.Aggregate;
054import org.nuxeo.ecm.platform.query.api.Bucket;
055import org.nuxeo.ecm.platform.query.core.BucketRange;
056import org.nuxeo.ecm.platform.query.core.BucketRangeDate;
057import org.nuxeo.elasticsearch.aggregate.SignificantTermAggregate;
058import org.nuxeo.elasticsearch.aggregate.SingleBucketAggregate;
059import org.nuxeo.elasticsearch.aggregate.SingleValueMetricAggregate;
060import org.nuxeo.elasticsearch.aggregate.TermAggregate;
061
062import com.fasterxml.jackson.core.JsonGenerationException;
063import com.fasterxml.jackson.core.JsonGenerator;
064
065/**
066 * @since 8.4
067 */
068@SuppressWarnings("rawtypes")
069@Setup(mode = SINGLETON, priority = REFERENCE)
070public class AggregateJsonWriter extends ExtensibleEntityJsonWriter<Aggregate> {
071
072    public static final String ENTITY_TYPE = "aggregate";
073
074    public static final String FETCH_KEY = "key";
075
076    private static final Log log = LogFactory.getLog(AggregateJsonWriter.class);
077
078    /** Fake schema for system properties usable as a page provider aggregate */
079    protected static final Schema SYSTEM_SCHEMA = new SchemaImpl("system", null);
080
081    static {
082        SYSTEM_SCHEMA.addField("ecm:mixinType", new ListTypeImpl("system", "", StringType.INSTANCE), null, 0, null);
083        SYSTEM_SCHEMA.addField("ecm:tag", new ListTypeImpl("system", "", StringType.INSTANCE), null, 0, null);
084        SYSTEM_SCHEMA.addField("ecm:primaryType", StringType.INSTANCE, null, 0, null);
085        SYSTEM_SCHEMA.addField("ecm:currentLifeCycleState", StringType.INSTANCE, null, 0, null);
086        SYSTEM_SCHEMA.addField("ecm:versionLabel", StringType.INSTANCE, null, 0, null);
087        SYSTEM_SCHEMA.addField("ecm:isTrashed", BooleanType.INSTANCE, null, 0, null);
088    }
089
090    /**
091     * @since 10.3
092     */
093    protected Field getSystemField(String name) {
094        Field result = SYSTEM_SCHEMA.getField(name);
095        if (result == null && name.startsWith("ecm:path@level")) {
096            SYSTEM_SCHEMA.addField(name, StringType.INSTANCE, null, 0, null);
097            return SYSTEM_SCHEMA.getField(name);
098        }
099        return result;
100    }
101
102    @Inject
103    private SchemaManager schemaManager;
104
105    public AggregateJsonWriter() {
106        super(ENTITY_TYPE, Aggregate.class);
107    }
108
109    public AggregateJsonWriter(String entityType, Class<Aggregate> entityClass) {
110        super(entityType, entityClass);
111    }
112
113    @Override
114    public boolean accept(Class<?> clazz, Type genericType, MediaType mediatype) {
115        return true;
116    }
117
118    @SuppressWarnings("unchecked")
119    @Override
120    protected void writeEntityBody(Aggregate agg, JsonGenerator jg) throws IOException {
121        boolean fetch = ctx.getFetched(ENTITY_TYPE).contains(FETCH_KEY);
122        String fieldName = agg.getField();
123        Field field;
124        if (fieldName.startsWith("ecm:")) {
125            field = getSystemField(fieldName);
126            if (field == null) {
127                log.warn(String.format("%s is not a valid field for aggregates", fieldName));
128                return;
129            }
130        } else {
131            field = schemaManager.getField(fieldName);
132        }
133        jg.writeObjectField("id", agg.getId());
134        jg.writeObjectField("field", agg.getField());
135        jg.writeObjectField("properties", agg.getProperties());
136        jg.writeObjectField("ranges", agg.getRanges());
137        jg.writeObjectField("selection", agg.getSelection());
138        jg.writeObjectField("type", agg.getType());
139        if (agg instanceof SingleValueMetricAggregate) {
140            Double val = ((SingleValueMetricAggregate) agg).getValue();
141            jg.writeObjectField("value", Double.isFinite(val) ? val : null);
142        } else if (agg instanceof SingleBucketAggregate) {
143            jg.writeObjectField("value", ((SingleBucketAggregate) agg).getDocCount());
144        } else if (!fetch || !(agg instanceof TermAggregate || agg instanceof SignificantTermAggregate)) {
145            jg.writeObjectField("buckets", agg.getBuckets());
146            jg.writeObjectField("extendedBuckets", agg.getExtendedBuckets());
147        } else {
148            if (field != null) {
149                try (Closeable resource = ctx.wrap()
150                                             .with(FETCH_PROPERTIES + "." + DocumentModelJsonWriter.ENTITY_TYPE,
151                                                     "properties")
152                                             .with(FETCH_PROPERTIES + "." + DirectoryEntryJsonWriter.ENTITY_TYPE,
153                                                     "parent")
154                                             .with(TRANSLATE_PROPERTIES + "." + DirectoryEntryJsonWriter.ENTITY_TYPE,
155                                                     "label")
156                                             .with(MAX_DEPTH_PARAM, "max")
157                                             .open()) {
158
159                    writeBuckets("buckets", agg.getBuckets(), field, jg);
160                    writeBuckets("extendedBuckets", agg.getExtendedBuckets(), field, jg);
161                }
162            } else {
163                log.warn(String.format("Could not resolve field %s for aggregate %s", fieldName, agg.getId()));
164                jg.writeObjectField("buckets", agg.getBuckets());
165                jg.writeObjectField("extendedBuckets", agg.getExtendedBuckets());
166            }
167        }
168    }
169
170    protected void writeBuckets(String fieldName, List<Bucket> buckets, Field field, JsonGenerator jg)
171            throws IOException, JsonGenerationException {
172        // prepare document part in order to use property
173        Schema schema = field.getDeclaringType().getSchema();
174        DocumentPartImpl part = new DocumentPartImpl(schema);
175        // write data
176        jg.writeArrayFieldStart(fieldName);
177        for (Bucket bucket : buckets) {
178            jg.writeStartObject();
179
180            jg.writeObjectField("key", bucket.getKey());
181
182            Property prop = PropertyFactory.createProperty(part, field, Property.NONE);
183            if (prop.isList()) {
184                ListType t = (ListType) prop.getType();
185                t.getField();
186                prop = PropertyFactory.createProperty(part, t.getField(), Property.NONE);
187            }
188            log.debug(String.format("Writing %s for field %s resolved to %s", fieldName, field.getName().toString(),
189                    prop.getName()));
190            prop.setValue(bucket.getKey());
191
192            writeEntityField("fetchedKey", prop, jg);
193            jg.writeNumberField("docCount", bucket.getDocCount());
194            jg.writeEndObject();
195
196            if (bucket instanceof BucketRange) {
197                BucketRange bucketRange = (BucketRange) bucket;
198                jg.writeNumberField("from", bucketRange.getFrom());
199                jg.writeNumberField("to", bucketRange.getTo());
200            }
201
202            if (bucket instanceof BucketRangeDate) {
203                BucketRangeDate bucketRange = (BucketRangeDate) bucket;
204                jg.writeStringField("fromAsDate", DateParser.formatW3CDateTime(bucketRange.getFromAsDate().toDate()));
205                jg.writeStringField("toAsDate", DateParser.formatW3CDateTime(bucketRange.getToAsDate().toDate()));
206            }
207        }
208        jg.writeEndArray();
209    }
210
211}