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.Collection;
023import java.util.List;
024import java.util.stream.Collectors;
025import javax.inject.Inject;
026import javax.inject.Named;
027import javax.ws.rs.core.MediaType;
028import javax.ws.rs.core.Response;
029import javax.ws.rs.core.Response.ResponseBuilder;
030import org.springframework.beans.factory.annotation.Qualifier;
031import org.springframework.context.annotation.Primary;
032import org.springframework.core.annotation.Order;
033import org.springframework.stereotype.Service;
034import org.apache.isis.applib.annotation.OrderPrecedence;
035import org.apache.isis.applib.domain.DomainObjectList;
036import org.apache.isis.core.config.IsisConfiguration;
037import org.apache.isis.core.metamodel.facets.actcoll.typeof.TypeOfFacet;
038import org.apache.isis.core.metamodel.facets.collections.CollectionFacet;
039import org.apache.isis.core.metamodel.interactions.managed.ManagedAction;
040import org.apache.isis.core.metamodel.interactions.managed.ManagedCollection;
041import org.apache.isis.core.metamodel.interactions.managed.ManagedProperty;
042import org.apache.isis.core.metamodel.spec.ManagedObject;
043import org.apache.isis.core.metamodel.spec.ObjectSpecification;
044import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
045import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
046import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
047import org.apache.isis.viewer.restfulobjects.applib.RestfulResponse;
048import org.apache.isis.viewer.restfulobjects.rendering.Caching;
049import org.apache.isis.viewer.restfulobjects.rendering.IResourceContext;
050import org.apache.isis.viewer.restfulobjects.rendering.Responses;
051import org.apache.isis.viewer.restfulobjects.rendering.RestfulObjectsApplicationException;
052import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ActionResultReprRenderer;
053import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectReprRenderer;
054import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectActionReprRenderer;
055import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
056import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectCollectionReprRenderer;
057import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
058import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationService;
059
060@Service
061@Named("isisRoRendering.ContentNegotiationServiceForRestfulObjectsV1_0")
062@Order(OrderPrecedence.MIDPOINT)
063@Primary
064@Qualifier("RestfulObjectsV1_0")
065public class ContentNegotiationServiceForRestfulObjectsV1_0 implements ContentNegotiationService {
066    protected final IsisConfiguration configuration;
067    protected final SpecificationLoader specificationLoader;
068    private final boolean strictAcceptChecking;
069
070    @Inject
071    public ContentNegotiationServiceForRestfulObjectsV1_0(final IsisConfiguration configuration, final SpecificationLoader specificationLoader) {
072        this.configuration = configuration;
073        this.specificationLoader = specificationLoader;
074        this.strictAcceptChecking = configuration.getViewer().getRestfulobjects().isStrictAcceptChecking();
075    }
076
077    @Override
078    public ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedObject objectAdapter) {
079        final List<MediaType> list = resourceContext.getAcceptableMediaTypes();
080        ensureCompatibleAcceptHeader(RepresentationType.DOMAIN_OBJECT, list);
081        final ResponseBuilder responseBuilder = buildResponseTo(resourceContext, objectAdapter, JsonRepresentation.newMap(), null);
082        return responseBuilder(responseBuilder);
083    }
084
085    /**
086     * Not API
087     */
088    ResponseBuilder buildResponseTo(final IResourceContext resourceContext, final ManagedObject objectAdapter, final JsonRepresentation representationIfAnyRequired, final JsonRepresentation rootRepresentation) {
089        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(resourceContext, null, representationIfAnyRequired);
090        renderer.with(objectAdapter).includesSelf();
091        final ResponseBuilder responseBuilder = Responses.ofOk(renderer, Caching.NONE, rootRepresentation);
092        {
093            final RepresentationService.Intent intent = resourceContext.getIntent();
094            if (intent == RepresentationService.Intent.JUST_CREATED) {
095                responseBuilder.status(Response.Status.CREATED);
096            }
097        }
098        return responseBuilder;
099    }
100
101    @Override
102    public ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedProperty objectAndProperty) {
103        final List<MediaType> list = resourceContext.getAcceptableMediaTypes();
104        ensureCompatibleAcceptHeader(RepresentationType.OBJECT_PROPERTY, list);
105        final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(resourceContext);
106        renderer.with(objectAndProperty).usingLinkTo(resourceContext.getObjectAdapterLinkTo());
107        final org.apache.isis.core.metamodel.interactions.managed.ManagedMember.RepresentationMode repMode = objectAndProperty.getRepresentationMode();
108        if (repMode.isExplicit()) {
109            renderer.withMemberMode(repMode);
110        }
111        final ResponseBuilder responseBuilder = Responses.ofOk(renderer, Caching.NONE);
112        return responseBuilder;
113    }
114
115    @Override
116    public ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedCollection objectAndCollection) {
117        final List<MediaType> list = resourceContext.getAcceptableMediaTypes();
118        ensureCompatibleAcceptHeader(RepresentationType.OBJECT_COLLECTION, list);
119        final ResponseBuilder responseBuilder = buildResponseTo(resourceContext, objectAndCollection, JsonRepresentation.newMap(), null);
120        return responseBuilder(responseBuilder);
121    }
122
123    /**
124     * Not API
125     */
126    ResponseBuilder buildResponseTo(final IResourceContext resourceContext, final ManagedCollection objectAndCollection, final JsonRepresentation representation, final JsonRepresentation rootRepresentation) {
127        final ObjectCollectionReprRenderer renderer = new ObjectCollectionReprRenderer(resourceContext, null, null, representation);
128        renderer.with(objectAndCollection).usingLinkTo(resourceContext.getObjectAdapterLinkTo());
129        if (objectAndCollection.getRepresentationMode().isExplicit()) {
130            renderer.withMemberMode(objectAndCollection.getRepresentationMode());
131        }
132        return Responses.ofOk(renderer, Caching.NONE, rootRepresentation);
133    }
134
135    @Override
136    public ResponseBuilder buildResponse(final IResourceContext resourceContext, final ManagedAction objectAndAction) {
137        final List<MediaType> list = resourceContext.getAcceptableMediaTypes();
138        ensureCompatibleAcceptHeader(RepresentationType.OBJECT_ACTION, list);
139        final ObjectActionReprRenderer renderer = new ObjectActionReprRenderer(resourceContext);
140        renderer.with(objectAndAction).usingLinkTo(resourceContext.getObjectAdapterLinkTo()).asStandalone();
141        final ResponseBuilder responseBuilder = Responses.ofOk(renderer, Caching.NONE);
142        return responseBuilder(responseBuilder);
143    }
144
145    @Override
146    public ResponseBuilder buildResponse(final IResourceContext resourceContext, final ObjectAndActionInvocation objectAndActionInvocation) {
147        final ResponseBuilder responseBuilder;
148        final List<MediaType> acceptableMediaTypes = resourceContext.getAcceptableMediaTypes();
149        if (isAccepted(RepresentationType.DOMAIN_OBJECT, acceptableMediaTypes, true)) {
150            final ManagedObject adapter;
151            final Collection<ManagedObject> collectionAdapters = objectAdaptersFrom(objectAndActionInvocation);
152            if (collectionAdapters != null) {
153                final ObjectSpecification elementSpec = elementSpecFrom(objectAndActionInvocation);
154                final String actionOwningType = actionOwningTypeFrom(objectAndActionInvocation);
155                final String actionId = actionIdFrom(objectAndActionInvocation);
156                final String actionArguments = actionArgumentsFrom(objectAndActionInvocation);
157                final DomainObjectList list = domainObjectListFrom(collectionAdapters, elementSpec, actionOwningType, actionId, actionArguments);
158                final org.apache.isis.core.metamodel.spec.ObjectSpecification listSpec = resourceContext.getSpecificationLoader().loadSpecification(list.getClass());
159                adapter = ManagedObject.of(listSpec, list);
160            } else {
161                adapter = objectAndActionInvocation.getReturnedAdapter();
162            }
163            responseBuilder = buildResponse(resourceContext, adapter);
164        } else if (isAccepted(RepresentationType.ACTION_RESULT, acceptableMediaTypes)) {
165            responseBuilder = buildResponseTo(resourceContext, objectAndActionInvocation, JsonRepresentation.newMap(), null);
166        } else {
167            throw RestfulObjectsApplicationException.create(RestfulResponse.HttpStatusCode.NOT_ACCEPTABLE);
168        }
169        return responseBuilder(responseBuilder);
170    }
171
172    private static String actionOwningTypeFrom(final ObjectAndActionInvocation objectAndActionInvocation) {
173        return objectAndActionInvocation.getAction().getOnType().getSpecId().asString();
174    }
175
176    private static String actionIdFrom(final ObjectAndActionInvocation objectAndActionInvocation) {
177        return objectAndActionInvocation.getAction().getId();
178    }
179
180    private static String actionArgumentsFrom(final ObjectAndActionInvocation objectAndActionInvocation) {
181        final StringBuilder buf = new StringBuilder();
182        final org.apache.isis.commons.collections.Can<org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter> parameters = objectAndActionInvocation.getAction().getParameters();
183        final org.apache.isis.commons.collections.Can<org.apache.isis.core.metamodel.spec.ManagedObject> argAdapters = objectAndActionInvocation.getArgAdapters();
184        if (parameters.size() == argAdapters.size()) {
185            for (int i = 0; i < parameters.size(); i++) {
186                final int paramIndex = i;
187                final org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter param = parameters.getElseFail(paramIndex);
188                final org.apache.isis.core.metamodel.spec.ManagedObject argAdapter = argAdapters.getElseFail(paramIndex);
189                if (buf.length() > 0) {
190                    buf.append(",");
191                }
192                buf.append(param.getName()).append("=");
193                buf.append(abbreviated(titleOf(argAdapter), 8));
194            }
195        }
196        return buf.toString();
197    }
198
199    private static String titleOf(final ManagedObject argumentAdapter) {
200        return argumentAdapter != null ? argumentAdapter.titleString(null) : "";
201    }
202
203    private static String abbreviated(final String str, final int maxLength) {
204        return str.length() < maxLength ? str : str.substring(0, maxLength - 3) + "...";
205    }
206
207    private static DomainObjectList domainObjectListFrom(final Collection<ManagedObject> collectionAdapters, final ObjectSpecification elementSpec, final String actionOwningType, final String actionId, final String actionArguments) {
208        final String title = titleFrom(collectionAdapters, elementSpec);
209        final DomainObjectList list = new DomainObjectList(title, elementSpec.getSpecId().asString(), actionOwningType, actionId, actionArguments);
210        for (final org.apache.isis.core.metamodel.spec.ManagedObject adapter : collectionAdapters) {
211            list.getObjects().add(adapter.getPojo());
212        }
213        return list;
214    }
215
216    private static String titleFrom(final Collection<ManagedObject> collectionAdapters, final ObjectSpecification elementSpec) {
217        final String singularName = elementSpec.getSingularName();
218        final String pluralName = elementSpec.getPluralName();
219        int size = collectionAdapters.size();
220        final String title;
221        switch (size) {
222        case 0: 
223            title = "0 " + pluralName;
224            break;
225        case 1: 
226            title = "1 " + singularName;
227            break;
228        default: 
229            title = size + " " + pluralName;
230            break;
231        }
232        return title;
233    }
234
235    private ObjectSpecification elementSpecFrom(final ObjectAndActionInvocation objectAndActionInvocation) {
236        final TypeOfFacet typeOfFacet = objectAndActionInvocation.getAction().getFacet(TypeOfFacet.class);
237        return typeOfFacet != null ? typeOfFacet.valueSpec() : specificationLoader.loadSpecification(Object.class);
238    }
239
240    private Collection<ManagedObject> objectAdaptersFrom(final ObjectAndActionInvocation objectAndActionInvocation) {
241        final ManagedObject returnedAdapter = objectAndActionInvocation.getReturnedAdapter();
242        final ObjectSpecification returnType = objectAndActionInvocation.getAction().getReturnType();
243        final CollectionFacet collectionFacet = returnType.getFacet(CollectionFacet.class);
244        return collectionFacet != null ? collectionFacet.stream(returnedAdapter).collect(Collectors.toList()) : null;
245    }
246
247    /**
248     * Not API
249     */
250    ResponseBuilder buildResponseTo(final IResourceContext resourceContext, final ObjectAndActionInvocation objectAndActionInvocation, final JsonRepresentation representation, final JsonRepresentation rootRepresentation) {
251        final ActionResultReprRenderer renderer = new ActionResultReprRenderer(resourceContext, null, objectAndActionInvocation.getSelfLink(), representation);
252        renderer.with(objectAndActionInvocation).using(resourceContext.getObjectAdapterLinkTo());
253        final ResponseBuilder responseBuilder = Responses.ofOk(renderer, Caching.NONE, rootRepresentation);
254        return responseBuilder;
255    }
256
257    /**
258     * For easy subclassing to further customize, eg additional headers
259     */
260    protected ResponseBuilder responseBuilder(final ResponseBuilder responseBuilder) {
261        return responseBuilder;
262    }
263
264    private void ensureCompatibleAcceptHeader(final RepresentationType representationType, final List<MediaType> acceptableMediaTypes) {
265        if (!strictAcceptChecking) {
266            return;
267        }
268        if (representationType == null) {
269            return;
270        }
271        // RestEasy will check the basic media types...
272        // ... so we just need to check the profile paramter
273        final String producedProfile = representationType.getMediaTypeProfile();
274        if (producedProfile == null) {
275            return;
276        }
277        boolean accepted = isAccepted(producedProfile, acceptableMediaTypes);
278        if (!accepted) {
279            throw RestfulObjectsApplicationException.create(RestfulResponse.HttpStatusCode.NOT_ACCEPTABLE);
280        }
281    }
282
283    private boolean isAccepted(final RepresentationType representationType, final List<MediaType> acceptableMediaTypes) {
284        return isAccepted(representationType, acceptableMediaTypes, strictAcceptChecking);
285    }
286
287    private boolean isAccepted(final RepresentationType representationType, final List<MediaType> acceptableMediaTypes, final boolean strictAcceptChecking) {
288        if (!strictAcceptChecking) {
289            return true;
290        }
291        final String producedProfile = representationType.getMediaTypeProfile();
292        if (producedProfile == null) {
293            throw new IllegalArgumentException("RepresentationType " + representationType + " does not specify a \'profile\' parameter");
294        }
295        return isAccepted(producedProfile, acceptableMediaTypes);
296    }
297
298    private static boolean isAccepted(final String producedProfile, final List<MediaType> acceptableMediaTypes) {
299        for (MediaType mediaType : acceptableMediaTypes) {
300            String acceptedProfileValue = mediaType.getParameters().get("profile");
301            if (acceptedProfileValue == null) {
302                continue;
303            }
304            if (!producedProfile.equals(acceptedProfileValue)) {
305                return false;
306            }
307        }
308        return true;
309    }
310}