001// Generated by delombok at Mon Oct 12 22:51:05 BST 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.client.SuppressionType;
033import org.apache.isis.core.metamodel.consent.Consent;
034import org.apache.isis.core.metamodel.facets.collections.CollectionFacet;
035import org.apache.isis.core.metamodel.interactions.managed.ManagedAction;
036import org.apache.isis.core.metamodel.interactions.managed.ManagedCollection;
037import org.apache.isis.core.metamodel.interactions.managed.ManagedProperty;
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.OneToOneAssociation;
041import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
042import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
043import org.apache.isis.viewer.restfulobjects.rendering.IResourceContext;
044import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
045import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
046import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationService;
047
048@Service
049@Named("isisRoRendering.ContentNegotiationServiceOrgApacheIsisV1")
050@Order(OrderPrecedence.MIDPOINT - 200)
051@Qualifier("OrgApacheIsisV1")
052public class ContentNegotiationServiceOrgApacheIsisV1 extends ContentNegotiationServiceAbstract {
053    /**
054     * Unlike RO v1.0, use a single content-type of <code>application/json;profile="urn:org.apache.isis/v1"</code>.
055
056     *
057
058     * <p>
059
060     * The response content types ({@link #CONTENT_TYPE_OAI_V1_OBJECT}, {@link #CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION},
061
062     * {@link #CONTENT_TYPE_OAI_V1_LIST}) append the 'repr-type' parameter.
063
064     * </p>
065     */
066    public static final String ACCEPT_PROFILE = "urn:org.apache.isis/v1";
067    /**
068     * The media type (as a string) used as the content-Type header when a domain object is rendered.
069
070     *
071
072     * @see #ACCEPT_PROFILE for discussion.
073     */
074    public static final String CONTENT_TYPE_OAI_V1_OBJECT = "application/json;profile=\"" + ACCEPT_PROFILE + "\";repr-type=\"object\"";
075    /**
076     * The media type (as a string) used as the content-Type header when a parented collection is rendered.
077
078     *
079
080     * @see #ACCEPT_PROFILE for discussion.
081     */
082    public static final String CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION = "application/json;profile=\"" + ACCEPT_PROFILE + "\";repr-type=\"object-collection\"";
083    /**
084     * The media type (as a string) used as the content-Type header when a standalone collection is rendered.
085
086     *
087
088     * @see #ACCEPT_PROFILE for discussion.
089     */
090    public static final String CONTENT_TYPE_OAI_V1_LIST = "application/json;profile=\"" + ACCEPT_PROFILE + "\";repr-type=\"list\"";
091    private final ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0;
092
093    public ContentNegotiationServiceOrgApacheIsisV1(final ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0) {
094        this.restfulObjectsV1_0 = restfulObjectsV1_0;
095    }
096
097    /**
098     * Domain object is returned as a map with the RO 1.0 representation as a special '$$ro' property
099
100     * within that map.
101     */
102    @Override
103    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedObject objectAdapter) {
104        boolean canAccept = canAccept(resourceContext);
105        if (!canAccept) {
106            return null;
107        }
108        final EnumSet<SuppressionType> suppression = suppress(resourceContext);
109        final boolean suppressRO = suppression.contains(SuppressionType.RO);
110        final JsonRepresentation rootRepresentation = JsonRepresentation.newMap();
111        appendObjectTo(resourceContext, objectAdapter, rootRepresentation, suppression);
112        final JsonRepresentation $$roRepresentation;
113        if (!suppressRO) {
114            $$roRepresentation = JsonRepresentation.newMap();
115            rootRepresentation.mapPut("$$ro", $$roRepresentation);
116        } else {
117            $$roRepresentation = null;
118        }
119        final Response.ResponseBuilder responseBuilder = restfulObjectsV1_0.buildResponseTo(resourceContext, objectAdapter, $$roRepresentation, rootRepresentation);
120        responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT);
121        return responseBuilder(responseBuilder);
122    }
123
124    /**
125     * Individual property of an object is not supported.
126     */
127    @Override
128    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedProperty objectAndProperty) {
129        return null;
130    }
131
132    /**
133     * Individual (parented) collection of an object is returned as a list with the RO representation
134
135     * as an object in the list with a single property named '$$ro'
136     */
137    @Override
138    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedCollection managedCollection) {
139        if (!canAccept(resourceContext)) {
140            return null;
141        }
142        final EnumSet<SuppressionType> suppression = suppress(resourceContext);
143        final boolean suppressRO = suppression.contains(SuppressionType.RO);
144        final JsonRepresentation rootRepresentation = JsonRepresentation.newArray();
145        appendCollectionTo(resourceContext, managedCollection, rootRepresentation, suppression);
146        final JsonRepresentation $$roRepresentation;
147        if (!suppressRO) {
148            // $$ro representation will be an object in the list with a single property named "$$ro"
149            final JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
150            rootRepresentation.arrayAdd($$roContainerRepresentation);
151            $$roRepresentation = JsonRepresentation.newMap();
152            $$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
153        } else {
154            $$roRepresentation = null;
155        }
156        final Response.ResponseBuilder responseBuilder = restfulObjectsV1_0.buildResponseTo(resourceContext, managedCollection, $$roRepresentation, rootRepresentation);
157        responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION);
158        return responseBuilder(responseBuilder);
159    }
160
161    /**
162     * Action prompt is not supported.
163     */
164    @Override
165    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedAction objectAndAction) {
166        return null;
167    }
168
169    /**
170     * Action invocation is supported provided it returns a single domain object or a list of domain objects
171
172     * (ie invocations returning void or scalar value are not supported).
173
174     *
175
176     * Action invocations returning a domain object will be rendered as a map with the RO v1.0 representation as a
177
178     * '$$ro' property within (same as {@link #buildResponse(RepresentationService.Context2, ManagedObject)}), while
179
180     * action invocations returning a list will be rendered as a list with the RO v1.0 representation as a map object
181
182     * with a single '$$ro' property (similar to {@link #buildResponse(RepresentationService.Context2, ObjectAndCollection)})
183     */
184    @Override
185    public Response.ResponseBuilder buildResponse(final IResourceContext resourceContext, final ObjectAndActionInvocation objectAndActionInvocation) {
186        if (!canAccept(resourceContext)) {
187            return null;
188        }
189        final EnumSet<SuppressionType> suppression = suppress(resourceContext);
190        final boolean suppressRO = suppression.contains(SuppressionType.RO);
191        final JsonRepresentation rootRepresentation;
192        final JsonRepresentation $$roRepresentation;
193        if (!suppressRO) {
194            $$roRepresentation = JsonRepresentation.newMap();
195        } else {
196            $$roRepresentation = null;
197        }
198        final ManagedObject returnedAdapter = objectAndActionInvocation.getReturnedAdapter();
199        //final ObjectSpecification returnType = objectAndActionInvocation.getAction().getReturnType();
200        if (returnedAdapter == null) {
201            return null;
202        }
203        final ActionResultRepresentation.ResultType resultType = objectAndActionInvocation.determineResultType();
204        switch (resultType) {
205        case DOMAIN_OBJECT: 
206            rootRepresentation = JsonRepresentation.newMap();
207            appendObjectTo(resourceContext, returnedAdapter, rootRepresentation, suppression);
208            break;
209        case LIST: 
210            rootRepresentation = JsonRepresentation.newArray();
211            CollectionFacet.streamAdapters(returnedAdapter).forEach(element -> appendElementTo(resourceContext, element, rootRepresentation, suppression));
212            // $$ro representation will be an object in the list with a single property named "$$ro"
213            if (!suppressRO) {
214                JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
215                rootRepresentation.arrayAdd($$roContainerRepresentation);
216                $$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
217            }
218            break;
219        case SCALAR_VALUE: 
220        case VOID: 
221            // not supported
222            return null;
223        default: 
224            rootRepresentation = null; // unexpected code reach
225        }
226        final Response.ResponseBuilder responseBuilder = restfulObjectsV1_0.buildResponseTo(resourceContext, objectAndActionInvocation, $$roRepresentation, rootRepresentation);
227        // set appropriate Content-Type
228        responseBuilder.type(resultType == ActionResultRepresentation.ResultType.DOMAIN_OBJECT ? CONTENT_TYPE_OAI_V1_OBJECT : CONTENT_TYPE_OAI_V1_LIST);
229        return responseBuilder(responseBuilder);
230    }
231
232    /**
233     * For easy subclassing to further customize, eg additional headers
234     */
235    protected Response.ResponseBuilder responseBuilder(final Response.ResponseBuilder responseBuilder) {
236        return responseBuilder;
237    }
238
239    boolean canAccept(final IResourceContext resourceContext) {
240        final List<MediaType> acceptableMediaTypes = resourceContext.getAcceptableMediaTypes();
241        return mediaTypeParameterMatches(acceptableMediaTypes, "profile", ACCEPT_PROFILE);
242    }
243
244    protected EnumSet<SuppressionType> suppress(final IResourceContext resourceContext) {
245        final List<MediaType> acceptableMediaTypes = resourceContext.getAcceptableMediaTypes();
246        return SuppressionType.ParseUtil.parse(mediaTypeParameterList(acceptableMediaTypes, "suppress"));
247    }
248
249    private void appendObjectTo(final IResourceContext resourceContext, final ManagedObject owner, final JsonRepresentation rootRepresentation, final EnumSet<SuppressionType> suppression) {
250        appendPropertiesTo(resourceContext, owner, rootRepresentation, suppression);
251        final org.apache.isis.applib.annotation.Where where = resourceContext.getWhere();
252        owner.getSpecification().streamCollections(Contributed.INCLUDED).forEach(collection -> {
253            final org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation collectionRepresentation = JsonRepresentation.newArray();
254            rootRepresentation.mapPut(collection.getId(), collectionRepresentation);
255            final org.apache.isis.core.metamodel.consent.InteractionInitiatedBy interactionInitiatedBy = resourceContext.getInteractionInitiatedBy();
256            final org.apache.isis.core.metamodel.consent.Consent visibilityConsent = collection.isVisible(owner, interactionInitiatedBy, where);
257            if (!visibilityConsent.isAllowed()) {
258                return;
259            }
260            final org.apache.isis.core.metamodel.interactions.managed.ManagedCollection managedCollection = ManagedCollection.of(owner, collection, where);
261            appendCollectionTo(resourceContext, managedCollection, collectionRepresentation, suppression);
262        });
263    }
264
265    private void appendPropertiesTo(final IResourceContext resourceContext, final ManagedObject objectAdapter, final JsonRepresentation rootRepresentation, final EnumSet<SuppressionType> suppression) {
266        final org.apache.isis.core.metamodel.consent.InteractionInitiatedBy interactionInitiatedBy = resourceContext.getInteractionInitiatedBy();
267        final org.apache.isis.applib.annotation.Where where = resourceContext.getWhere();
268        final Stream<OneToOneAssociation> properties = objectAdapter.getSpecification().streamProperties(Contributed.INCLUDED);
269        properties.forEach(property -> {
270            final Consent visibility = property.isVisible(objectAdapter, interactionInitiatedBy, where);
271            if (!visibility.isAllowed()) {
272                return;
273            }
274            final JsonRepresentation propertyRepresentation = JsonRepresentation.newMap();
275            final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(resourceContext, null, property.getId(), propertyRepresentation).asStandalone();
276            renderer.with(ManagedProperty.of(objectAdapter, property, where));
277            final JsonRepresentation propertyValueRepresentation = renderer.render();
278            if (!suppression.contains(SuppressionType.HREF)) {
279                final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
280                rootRepresentation.mapPut("$$href", upHref);
281            }
282            if (!suppression.contains(SuppressionType.TITLE)) {
283                final String upTitle = propertyValueRepresentation.getString("links[rel=up].title");
284                rootRepresentation.mapPut("$$title", upTitle);
285            }
286            if (!suppression.contains(SuppressionType.DOMAIN_TYPE)) {
287                final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
288                final String[] parts = upHref.split("[/]");
289                if (parts.length > 2) {
290                    final String upObjectType = parts[parts.length - 2];
291                    rootRepresentation.mapPut("$$domainType", upObjectType);
292                }
293            }
294            if (!suppression.contains(SuppressionType.ID)) {
295                final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
296                final String[] parts = upHref.split("[/]");
297                if (parts.length > 1) {
298                    final String upInstanceId = parts[parts.length - 1];
299                    rootRepresentation.mapPut("$$instanceId", upInstanceId);
300                }
301            }
302            final JsonRepresentation value = propertyValueRepresentation.getRepresentation("value");
303            rootRepresentation.mapPut(property.getId(), value);
304        });
305    }
306
307    private void appendCollectionTo(final IResourceContext resourceContext, final ManagedCollection managedCollection, final JsonRepresentation representation, final EnumSet<SuppressionType> suppression) {
308        managedCollection.streamElements(resourceContext.getInteractionInitiatedBy()).forEach(element -> appendElementTo(resourceContext, element, representation, suppression));
309    }
310
311    private void appendElementTo(final IResourceContext resourceContext, final ManagedObject elementAdapter, final JsonRepresentation collectionRepresentation, final EnumSet<SuppressionType> suppression) {
312        final org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation elementRepresentation = JsonRepresentation.newMap();
313        appendPropertiesTo(resourceContext, elementAdapter, elementRepresentation, suppression);
314        collectionRepresentation.arrayAdd(elementRepresentation);
315    }
316}