001// Generated by delombok at Thu Mar 19 15:24:58 GMT 2020
002/*
003 *  Licensed to the Apache Software Foundation (ASF) under one
004 *  or more contributor license agreements.  See the NOTICE file
005 *  distributed with this work for additional information
006 *  regarding copyright ownership.  The ASF licenses this file
007 *  to you under the Apache License, Version 2.0 (the
008 *  "License"); you may not use this file except in compliance
009 *  with the License.  You may obtain a copy of the License at
010 *
011 *        http://www.apache.org/licenses/LICENSE-2.0
012 *
013 *  Unless required by applicable law or agreed to in writing,
014 *  software distributed under the License is distributed on an
015 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 *  KIND, either express or implied.  See the License for the
017 *  specific language governing permissions and limitations
018 *  under the License.
019 */
020package org.apache.isis.viewer.restfulobjects.rendering.service.conneg;
021
022import java.util.EnumSet;
023import java.util.List;
024import java.util.stream.Stream;
025import javax.inject.Named;
026import javax.ws.rs.core.MediaType;
027import javax.ws.rs.core.Response;
028import org.springframework.beans.factory.annotation.Qualifier;
029import org.springframework.core.annotation.Order;
030import org.springframework.stereotype.Service;
031import org.apache.isis.applib.annotation.OrderPrecedence;
032import org.apache.isis.applib.annotation.Where;
033import org.apache.isis.applib.client.SuppressionType;
034import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
035import org.apache.isis.core.metamodel.consent.Consent;
036import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
037import org.apache.isis.core.metamodel.facets.collections.CollectionFacet;
038import org.apache.isis.core.metamodel.spec.ManagedObject;
039import org.apache.isis.core.metamodel.spec.feature.Contributed;
040import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
041import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
042import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
043import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
044import org.apache.isis.viewer.restfulobjects.rendering.IResourceContext;
045import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndAction;
046import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
047import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndCollection;
048import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndProperty;
049import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
050import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationService;
051
052@Service
053@Named("isisRoRendering.ContentNegotiationServiceOrgApacheIsisV1")
054@Order(OrderPrecedence.MIDPOINT - 200)
055@Qualifier("OrgApacheIsisV1")
056public class ContentNegotiationServiceOrgApacheIsisV1 extends ContentNegotiationServiceAbstract {
057    /**
058     * Unlike RO v1.0, use a single content-type of <code>application/json;profile="urn:org.apache.isis/v1"</code>.
059
060     *
061
062     * <p>
063
064     * The response content types ({@link #CONTENT_TYPE_OAI_V1_OBJECT}, {@link #CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION},
065
066     * {@link #CONTENT_TYPE_OAI_V1_LIST}) append the 'repr-type' parameter.
067
068     * </p>
069     */
070    public static final String ACCEPT_PROFILE = "urn:org.apache.isis/v1";
071    /**
072     * The media type (as a string) used as the content-Type header when a domain object is rendered.
073
074     *
075
076     * @see #ACCEPT_PROFILE for discussion.
077     */
078    public static final String CONTENT_TYPE_OAI_V1_OBJECT = "application/json;profile=\"" + ACCEPT_PROFILE + "\";repr-type=\"object\"";
079    /**
080     * The media type (as a string) used as the content-Type header when a parented collection is rendered.
081
082     *
083
084     * @see #ACCEPT_PROFILE for discussion.
085     */
086    public static final String CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION = "application/json;profile=\"" + ACCEPT_PROFILE + "\";repr-type=\"object-collection\"";
087    /**
088     * The media type (as a string) used as the content-Type header when a standalone collection is rendered.
089
090     *
091
092     * @see #ACCEPT_PROFILE for discussion.
093     */
094    public static final String CONTENT_TYPE_OAI_V1_LIST = "application/json;profile=\"" + ACCEPT_PROFILE + "\";repr-type=\"list\"";
095    private final ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0;
096
097    public ContentNegotiationServiceOrgApacheIsisV1(final ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0) {
098        this.restfulObjectsV1_0 = restfulObjectsV1_0;
099    }
100
101    /**
102     * Domain object is returned as a map with the RO 1.0 representation as a special '$$ro' property
103
104     * within that map.
105     */
106    @Override
107    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedObject objectAdapter) {
108        boolean canAccept = canAccept(resourceContext);
109        if (!canAccept) {
110            return null;
111        }
112        final EnumSet<SuppressionType> suppression = suppress(resourceContext);
113        final boolean suppressRO = suppression.contains(SuppressionType.RO);
114        final JsonRepresentation rootRepresentation = JsonRepresentation.newMap();
115        appendObjectTo(resourceContext, objectAdapter, rootRepresentation, suppression);
116        final JsonRepresentation $$roRepresentation;
117        if (!suppressRO) {
118            $$roRepresentation = JsonRepresentation.newMap();
119            rootRepresentation.mapPut("$$ro", $$roRepresentation);
120        } else {
121            $$roRepresentation = null;
122        }
123        final Response.ResponseBuilder responseBuilder = restfulObjectsV1_0.buildResponseTo(resourceContext, objectAdapter, $$roRepresentation, rootRepresentation);
124        responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT);
125        return responseBuilder(responseBuilder);
126    }
127
128    /**
129     * Individual property of an object is not supported.
130     */
131    @Override
132    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ObjectAndProperty objectAndProperty) {
133        return null;
134    }
135
136    /**
137     * Individual (parented) collection of an object is returned as a list with the RO representation
138
139     * as an object in the list with a single property named '$$ro'
140     */
141    @Override
142    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ObjectAndCollection objectAndCollection) {
143        if (!canAccept(resourceContext)) {
144            return null;
145        }
146        final EnumSet<SuppressionType> suppression = suppress(resourceContext);
147        final boolean suppressRO = suppression.contains(SuppressionType.RO);
148        final JsonRepresentation rootRepresentation = JsonRepresentation.newArray();
149        ManagedObject objectAdapter = objectAndCollection.getObjectAdapter();
150        OneToManyAssociation collection = objectAndCollection.getMember();
151        appendCollectionTo(resourceContext, objectAdapter, collection, rootRepresentation, suppression);
152        final JsonRepresentation $$roRepresentation;
153        if (!suppressRO) {
154            // $$ro representation will be an object in the list with a single property named "$$ro"
155            final JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
156            rootRepresentation.arrayAdd($$roContainerRepresentation);
157            $$roRepresentation = JsonRepresentation.newMap();
158            $$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
159        } else {
160            $$roRepresentation = null;
161        }
162        final Response.ResponseBuilder responseBuilder = restfulObjectsV1_0.buildResponseTo(resourceContext, objectAndCollection, $$roRepresentation, rootRepresentation);
163        responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION);
164        return responseBuilder(responseBuilder);
165    }
166
167    /**
168     * Action prompt is not supported.
169     */
170    @Override
171    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ObjectAndAction objectAndAction) {
172        return null;
173    }
174
175    /**
176     * Action invocation is supported provided it returns a single domain object or a list of domain objects
177
178     * (ie invocations returning void or scalar value are not supported).
179
180     *
181
182     * Action invocations returning a domain object will be rendered as a map with the RO v1.0 representation as a
183
184     * '$$ro' property within (same as {@link #buildResponse(RepresentationService.Context2, ManagedObject)}), while
185
186     * action invocations returning a list will be rendered as a list with the RO v1.0 representation as a map object
187
188     * with a single '$$ro' property (similar to {@link #buildResponse(RepresentationService.Context2, ObjectAndCollection)})
189     */
190    @Override
191    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ObjectAndActionInvocation objectAndActionInvocation) {
192        if (!canAccept(resourceContext)) {
193            return null;
194        }
195        final EnumSet<SuppressionType> suppression = suppress(resourceContext);
196        final boolean suppressRO = suppression.contains(SuppressionType.RO);
197        JsonRepresentation rootRepresentation = null;
198        final JsonRepresentation $$roRepresentation;
199        if (!suppressRO) {
200            $$roRepresentation = JsonRepresentation.newMap();
201        } else {
202            $$roRepresentation = null;
203        }
204        final ManagedObject returnedAdapter = objectAndActionInvocation.getReturnedAdapter();
205        //final ObjectSpecification returnType = objectAndActionInvocation.getAction().getReturnType();
206        if (returnedAdapter == null) {
207            return null;
208        }
209        final ActionResultRepresentation.ResultType resultType = objectAndActionInvocation.determineResultType();
210        switch (resultType) {
211        case DOMAIN_OBJECT: 
212            rootRepresentation = JsonRepresentation.newMap();
213            appendObjectTo(resourceContext, returnedAdapter, rootRepresentation, suppression);
214            break;
215
216        case LIST: 
217            rootRepresentation = JsonRepresentation.newArray();
218            //final CollectionFacet collectionFacet = returnType.getFacet(CollectionFacet.class);
219            final Stream<ManagedObject> collectionAdapters = CollectionFacet.streamAdapters(returnedAdapter);
220            appendStreamTo(resourceContext, collectionAdapters, rootRepresentation, suppression);
221            // $$ro representation will be an object in the list with a single property named "$$ro"
222            if (!suppressRO) {
223                JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
224                rootRepresentation.arrayAdd($$roContainerRepresentation);
225                $$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
226            }
227            break;
228
229        case SCALAR_VALUE: 
230
231        case VOID: 
232            // not supported
233            return null;
234        }
235        final Response.ResponseBuilder responseBuilder = restfulObjectsV1_0.buildResponseTo(resourceContext, objectAndActionInvocation, $$roRepresentation, rootRepresentation);
236        // set appropriate Content-Type
237        responseBuilder.type(resultType == ActionResultRepresentation.ResultType.DOMAIN_OBJECT ? CONTENT_TYPE_OAI_V1_OBJECT : CONTENT_TYPE_OAI_V1_LIST);
238        return responseBuilder(responseBuilder);
239    }
240
241    /**
242     * For easy subclassing to further customize, eg additional headers
243     */
244    protected Response.ResponseBuilder responseBuilder(final Response.ResponseBuilder responseBuilder) {
245        return responseBuilder;
246    }
247
248    boolean canAccept(final IResourceContext resourceContext) {
249        final List<MediaType> acceptableMediaTypes = resourceContext.getAcceptableMediaTypes();
250        return mediaTypeParameterMatches(acceptableMediaTypes, "profile", ACCEPT_PROFILE);
251    }
252
253    protected EnumSet<SuppressionType> suppress(final IResourceContext resourceContext) {
254        final List<MediaType> acceptableMediaTypes = resourceContext.getAcceptableMediaTypes();
255        return SuppressionType.ParseUtil.parse(mediaTypeParameterList(acceptableMediaTypes, "suppress"));
256    }
257
258    private void appendObjectTo(final IResourceContext resourceContext, final ManagedObject objectAdapter, final JsonRepresentation rootRepresentation, final EnumSet<SuppressionType> suppression) {
259        appendPropertiesTo(resourceContext, objectAdapter, rootRepresentation, suppression);
260        final Where where = resourceContext.getWhere();
261        final Stream<OneToManyAssociation> collections = objectAdapter.getSpecification().streamCollections(Contributed.INCLUDED);
262        collections.forEach(collection -> {
263            final JsonRepresentation collectionRepresentation = JsonRepresentation.newArray();
264            rootRepresentation.mapPut(collection.getId(), collectionRepresentation);
265            final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(resourceContext);
266            final Consent visibility = collection.isVisible(objectAdapter, interactionInitiatedBy, where);
267            if (!visibility.isAllowed()) {
268                return;
269            }
270            appendCollectionTo(resourceContext, objectAdapter, collection, collectionRepresentation, suppression);
271        });
272    }
273
274    private void appendPropertiesTo(final IResourceContext resourceContext, final ManagedObject objectAdapter, final JsonRepresentation rootRepresentation, final EnumSet<SuppressionType> suppression) {
275        final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(resourceContext);
276        final Where where = resourceContext.getWhere();
277        final Stream<OneToOneAssociation> properties = objectAdapter.getSpecification().streamProperties(Contributed.INCLUDED);
278        properties.forEach(property -> {
279            final Consent visibility = property.isVisible(objectAdapter, interactionInitiatedBy, where);
280            if (!visibility.isAllowed()) {
281                return;
282            }
283            final JsonRepresentation propertyRepresentation = JsonRepresentation.newMap();
284            final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(resourceContext, null, property.getId(), propertyRepresentation).asStandalone();
285            renderer.with(new ObjectAndProperty(objectAdapter, property));
286            final JsonRepresentation propertyValueRepresentation = renderer.render();
287            if (!suppression.contains(SuppressionType.HREF)) {
288                final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
289                rootRepresentation.mapPut("$$href", upHref);
290            }
291            if (!suppression.contains(SuppressionType.TITLE)) {
292                final String upTitle = propertyValueRepresentation.getString("links[rel=up].title");
293                rootRepresentation.mapPut("$$title", upTitle);
294            }
295            if (!suppression.contains(SuppressionType.DOMAIN_TYPE)) {
296                final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
297                final String[] parts = upHref.split("[/]");
298                if (parts.length > 2) {
299                    final String upObjectType = parts[parts.length - 2];
300                    rootRepresentation.mapPut("$$domainType", upObjectType);
301                }
302            }
303            if (!suppression.contains(SuppressionType.ID)) {
304                final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
305                final String[] parts = upHref.split("[/]");
306                if (parts.length > 1) {
307                    final String upInstanceId = parts[parts.length - 1];
308                    rootRepresentation.mapPut("$$instanceId", upInstanceId);
309                }
310            }
311            final JsonRepresentation value = propertyValueRepresentation.getRepresentation("value");
312            rootRepresentation.mapPut(property.getId(), value);
313        });
314    }
315
316    private void appendCollectionTo(final IResourceContext resourceContext, final ManagedObject objectAdapter, final OneToManyAssociation collection, final JsonRepresentation representation, final EnumSet<SuppressionType> suppression) {
317        final org.apache.isis.core.metamodel.consent.InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(resourceContext);
318        final org.apache.isis.core.metamodel.spec.ManagedObject valueAdapter = collection.get(objectAdapter, interactionInitiatedBy);
319        if (valueAdapter == null) {
320            return;
321        }
322        final Stream<ManagedObject> adapters = CollectionFacet.streamAdapters(valueAdapter);
323        appendStreamTo(resourceContext, adapters, representation, suppression);
324    }
325
326    private void appendStreamTo(final IResourceContext resourceContext, final Stream<ManagedObject> adapters, final JsonRepresentation collectionRepresentation, final EnumSet<SuppressionType> suppression) {
327        adapters.forEach(elementAdapter -> {
328            JsonRepresentation elementRepresentation = JsonRepresentation.newMap();
329            appendPropertiesTo(resourceContext, elementAdapter, elementRepresentation, suppression);
330            collectionRepresentation.arrayAdd(elementRepresentation);
331        });
332    }
333
334    private static InteractionInitiatedBy determineInteractionInitiatedByFrom(final IResourceContext resourceContext) {
335        return resourceContext.getInteractionInitiatedBy();
336    }
337}