001/*
002 * (C) Copyright 2015 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 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.io.marshallers.json;
021
022import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
023import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.ENTITY_FIELD_NAME;
024
025import java.io.IOException;
026import java.lang.reflect.Type;
027import java.util.Collections;
028import java.util.List;
029import java.util.Map;
030import java.util.Map.Entry;
031
032import org.apache.commons.lang3.reflect.TypeUtils;
033import org.nuxeo.ecm.automation.core.util.Paginable;
034import org.nuxeo.ecm.core.api.DocumentModelList;
035import org.nuxeo.ecm.core.api.PartialList;
036import org.nuxeo.ecm.core.io.registry.MarshallerRegistry;
037import org.nuxeo.ecm.core.io.registry.Writer;
038import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
039import org.nuxeo.ecm.platform.query.api.Aggregate;
040import org.nuxeo.ecm.platform.query.api.Bucket;
041import org.nuxeo.ecm.platform.query.api.QuickFilter;
042
043import com.fasterxml.jackson.core.JsonGenerator;
044
045/**
046 * Base class to convert {@link List} as json.
047 * <p>
048 * It follow the classic Nuxeo list format :
049 *
050 * <pre>
051 * {
052 *   "entity-type": "GIVEN_ENTITY_TYPE",
053 *                                  <-- pagination info if available are here.
054 *   "entries": [
055 *     {...}, <-- A {@link Writer} must be able to manage this format.
056 *     {...},
057 *     ...
058 *     {...}
059 *   ]
060 * }
061 * </pre>
062 * <p>
063 * This list generates pagination information if the list is a {@link Paginable}.
064 * </p>
065 * <p>
066 * This reader delegates the marshalling of entries to the {@link MarshallerRegistry}. A Json {@link Writer} compatible
067 * with the required type must be registered.
068 * </p>
069 *
070 * @param <EntityType> The type of the element of this list.
071 * @since 7.2
072 */
073public abstract class DefaultListJsonWriter<EntityType> extends AbstractJsonWriter<List<EntityType>> {
074
075    /**
076     * The "entity-type" of the list.
077     */
078    private final String entityType;
079
080    /**
081     * The Java type of the element of this list.
082     */
083    private final Class<EntityType> elClazz;
084
085    /**
086     * The generic type of the element of this list.
087     */
088    private final Type elGenericType;
089
090    /**
091     * Use this constructor if the element of the list are not based on Java generic type.
092     *
093     * @param entityType The list "entity-type".
094     * @param elClazz The class of the element of the list.
095     */
096    public DefaultListJsonWriter(String entityType, Class<EntityType> elClazz) {
097        super();
098        this.entityType = entityType;
099        this.elClazz = elClazz;
100        this.elGenericType = elClazz;
101    }
102
103    /**
104     * Use this constructor if the element of the list are based on Java generic type.
105     *
106     * @param entityType The list "entity-type".
107     * @param elClazz The class of the element of the list.
108     * @param elGenericType The generic type of the list (you can use {@link TypeUtils#parameterize(Class, Type...) to
109     *            generate it}
110     */
111    public DefaultListJsonWriter(String entityType, Class<EntityType> elClazz, Type elGenericType) {
112        super();
113        this.entityType = entityType;
114        this.elClazz = elClazz;
115        this.elGenericType = elGenericType;
116    }
117
118    @Override
119    public void write(List<EntityType> list, JsonGenerator jg) throws IOException {
120        jg.writeStartObject();
121        ctx.setParameterValues(RenderingContext.RESPONSE_HEADER_ENTITY_TYPE_KEY, this.entityType);
122        jg.writeStringField(ENTITY_FIELD_NAME, entityType);
123        writePaginationInfos(list, jg);
124        Writer<EntityType> documentWriter = registry.getWriter(ctx, elClazz, elGenericType, APPLICATION_JSON_TYPE);
125        jg.writeArrayFieldStart("entries");
126        for (EntityType entity : list) {
127            documentWriter.write(entity, elClazz, elClazz, APPLICATION_JSON_TYPE, new OutputStreamWithJsonWriter(jg));
128        }
129        jg.writeEndArray();
130        extend(list, jg);
131        jg.writeEndObject();
132    }
133
134    private void writePaginationInfos(List<EntityType> list, JsonGenerator jg) throws IOException {
135        if (list instanceof Paginable) {
136            Paginable<?> paginable = (Paginable<?>) list;
137            jg.writeBooleanField("isPaginable", true);
138            jg.writeNumberField("resultsCount", paginable.getResultsCount());
139            jg.writeNumberField("pageSize", paginable.getPageSize());
140            jg.writeNumberField("maxPageSize", paginable.getMaxPageSize());
141            jg.writeNumberField("resultsCountLimit", paginable.getResultsCountLimit());
142            jg.writeNumberField("currentPageSize", paginable.getCurrentPageSize());
143            jg.writeNumberField("currentPageIndex", paginable.getCurrentPageIndex());
144            jg.writeNumberField("currentPageOffset", paginable.getCurrentPageOffset());
145            jg.writeNumberField("numberOfPages", paginable.getNumberOfPages());
146            jg.writeBooleanField("isPreviousPageAvailable", paginable.isPreviousPageAvailable());
147            jg.writeBooleanField("isNextPageAvailable", paginable.isNextPageAvailable());
148            jg.writeBooleanField("isLastPageAvailable", paginable.isLastPageAvailable());
149            jg.writeBooleanField("isSortable", paginable.isSortable());
150            jg.writeBooleanField("hasError", paginable.hasError());
151            jg.writeStringField("errorMessage", paginable.getErrorMessage());
152            // compat fields
153            if (paginable instanceof DocumentModelList) {
154                jg.writeNumberField("totalSize", ((DocumentModelList) paginable).totalSize());
155            }
156            jg.writeNumberField("pageIndex", paginable.getCurrentPageIndex());
157            jg.writeNumberField("pageCount", paginable.getNumberOfPages());
158            if (paginable.hasAggregateSupport()) {
159                Map<String, Aggregate<? extends Bucket>> aggregates = paginable.getAggregates();
160                if (aggregates != null && !paginable.getAggregates().isEmpty()) {
161                    jg.writeObjectFieldStart("aggregations");
162                    for (Entry<String, Aggregate<? extends Bucket>> e : aggregates.entrySet()) {
163                        writeEntityField(e.getKey(), e.getValue(), jg);
164                    }
165                    jg.writeEndObject();
166                }
167
168            }
169            List<QuickFilter> qfs = paginable.getActiveQuickFilters();
170            if (qfs == null) {
171                qfs = Collections.emptyList();
172            }
173
174            List<QuickFilter> aqfs = paginable.getAvailableQuickFilters();
175            if (aqfs != null && !aqfs.isEmpty()) {
176                jg.writeArrayFieldStart("quickFilters");
177                for (QuickFilter aqf : aqfs) {
178                    jg.writeStartObject();
179                    jg.writeStringField("name", aqf.getName());
180                    jg.writeBooleanField("active", qfs.contains(aqf));
181                    jg.writeEndObject();
182                }
183                jg.writeEndArray();
184            }
185        } else if (list instanceof PartialList) {
186            PartialList<EntityType> partial = (PartialList<EntityType>) list;
187            jg.writeNumberField("totalSize", partial.totalSize());
188        }
189    }
190
191    /**
192     * Override this method to write additional information in the list.
193     *
194     * @param list The list to marshal.
195     * @param jg The {@link JsonGenerator} which point inside the list object at the end of standard properties.
196     * @since 7.2
197     */
198    protected void extend(List<EntityType> list, JsonGenerator jg) throws IOException {
199    }
200
201}