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