/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.isis.viewer.restfulobjects.rendering.service.conneg;
import java.util.Collection;
import java.util.List;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.isis.applib.DomainObjectContainer;
import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.Where;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.consent.Consent;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacetUtils;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndAction;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndCollection;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndProperty;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationService;
@DomainService(
nature = NatureOfService.DOMAIN,
menuOrder = "" + (Integer.MAX_VALUE - 10)
)
public class ContentNegotiationServiceOrgApacheIsisV1 extends ContentNegotiationServiceAbstract {
/**
* Unlike RO v1.0, use a single content-type of <code>application/json;profile="urn:org.apache.isis/v1"</code>.
*
* <p>
* The response content types ({@link #CONTENT_TYPE_OAI_V1_OBJECT}, {@link #CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION},
* {@link #CONTENT_TYPE_OAI_V1_LIST}) append the 'repr-type' parameter.
* </p>
*/
public static final String ACCEPT_PROFILE = "urn:org.apache.isis/v1";
/**
* Whether to suppress the RO v1.0 '$$ro' node; suppress='false' or suppress='true'
*/
public static final String ACCEPT_RO = "suppress";
/**
* The media type (as a string) used as the content-Type header when a domain object is rendered.
*
* @see #ACCEPT_PROFILE for discussion.
*/
public static final String CONTENT_TYPE_OAI_V1_OBJECT = "application/json;"
+ "profile=\"" + ACCEPT_PROFILE + "\""
+ ";repr-type=\"object\""
;
/**
* The media type (as a string) used as the content-Type header when a parented collection is rendered.
*
* @see #ACCEPT_PROFILE for discussion.
*/
public static final String CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION = "application/json;"
+ "profile=\"" + ACCEPT_PROFILE + "\""
+ ";repr-type=\"object-collection\""
;
/**
* The media type (as a string) used as the content-Type header when a standalone collection is rendered.
*
* @see #ACCEPT_PROFILE for discussion.
*/
public static final String CONTENT_TYPE_OAI_V1_LIST = "application/json;"
+ "profile=\"" + ACCEPT_PROFILE + "\""
+ ";repr-type=\"list\""
;
private ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0 = new ContentNegotiationServiceForRestfulObjectsV1_0();
/**
* Domain object is returned as a map with the RO 1.0 representation as a special '$$ro' property
* within that map.
*/
public Response.ResponseBuilder buildResponse(
final RepresentationService.Context2 rendererContext,
final ObjectAdapter objectAdapter) {
boolean canAccept = canAccept(rendererContext);
if(!canAccept) {
return null;
}
boolean suppressRO = suppress(rendererContext);
final JsonRepresentation rootRepresentation = JsonRepresentation.newMap();
appendObjectTo(rendererContext, objectAdapter, rootRepresentation);
final JsonRepresentation $$roRepresentation;
if(!suppressRO) {
$$roRepresentation = JsonRepresentation.newMap();
rootRepresentation.mapPut("$$ro", $$roRepresentation);
} else {
$$roRepresentation = null;
}
final Response.ResponseBuilder responseBuilder =
restfulObjectsV1_0.buildResponseTo(
rendererContext, objectAdapter, $$roRepresentation, rootRepresentation);
responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT);
return responseBuilder(responseBuilder);
}
/**
* Individual property of an object is not supported.
*/
@Programmatic
public Response.ResponseBuilder buildResponse(
final RepresentationService.Context2 rendererContext,
final ObjectAndProperty objectAndProperty) {
return null;
}
/**
* Individual (parented) collection of an object is returned as a list with the RO representation
* as an object in the list with a single property named '$$ro'
*/
@Programmatic
public Response.ResponseBuilder buildResponse(
final RepresentationService.Context2 rendererContext,
final ObjectAndCollection objectAndCollection) {
if(!canAccept(rendererContext)) {
return null;
}
boolean suppressRO = suppress(rendererContext);
final JsonRepresentation rootRepresentation = JsonRepresentation.newArray();
ObjectAdapter objectAdapter = objectAndCollection.getObjectAdapter();
OneToManyAssociation collection = objectAndCollection.getMember();
appendCollectionTo(rendererContext, objectAdapter, collection, rootRepresentation);
final JsonRepresentation $$roRepresentation;
if(!suppressRO) {
// $$ro representation will be an object in the list with a single property named "$$ro"
final JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
rootRepresentation.arrayAdd($$roContainerRepresentation);
$$roRepresentation = JsonRepresentation.newMap();
$$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
} else {
$$roRepresentation = null;
}
final Response.ResponseBuilder responseBuilder =
restfulObjectsV1_0.buildResponseTo(
rendererContext, objectAndCollection, $$roRepresentation, rootRepresentation);
responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION);
return responseBuilder(responseBuilder);
}
/**
* Action prompt is not supported.
*/
@Programmatic
public Response.ResponseBuilder buildResponse(
final RepresentationService.Context2 rendererContext,
final ObjectAndAction objectAndAction) {
return null;
}
/**
* Action invocation is supported provided it returns a single domain object or a list of domain objects
* (ie invocations returning void or scalar value are not supported).
*
* Action invocations returning a domain object will be rendered as a map with the RO v1.0 representation as a
* '$$ro' property within (same as {@link #buildResponse(RepresentationService.Context2, ObjectAdapter)}), while
* action invocations returning a list will be rendered as a list with the RO v1.0 representation as a map object
* with a single '$$ro' property (similar to {@link #buildResponse(RepresentationService.Context2, ObjectAndCollection)})
*/
@Programmatic
public Response.ResponseBuilder buildResponse(
final RepresentationService.Context2 rendererContext,
final ObjectAndActionInvocation objectAndActionInvocation) {
if(!canAccept(rendererContext)) {
return null;
}
boolean suppressRO = suppress(rendererContext);
JsonRepresentation rootRepresentation = null;
final JsonRepresentation $$roRepresentation;
if(!suppressRO) {
$$roRepresentation = JsonRepresentation.newMap();
} else {
$$roRepresentation = null;
}
final ObjectAdapter returnedAdapter = objectAndActionInvocation.getReturnedAdapter();
final ObjectSpecification returnType = objectAndActionInvocation.getAction().getReturnType();
if (returnedAdapter == null) {
return null;
}
final ActionResultRepresentation.ResultType resultType = objectAndActionInvocation.determineResultType();
switch (resultType) {
case DOMAIN_OBJECT:
rootRepresentation = JsonRepresentation.newMap();
appendObjectTo(rendererContext, returnedAdapter, rootRepresentation);
break;
case LIST:
rootRepresentation = JsonRepresentation.newArray();
final CollectionFacet collectionFacet = returnType.getFacet(CollectionFacet.class);
final Collection<ObjectAdapter> collectionAdapters = collectionFacet.collection(returnedAdapter);
appendIterableTo(rendererContext, collectionAdapters, rootRepresentation);
// $$ro representation will be an object in the list with a single property named "$$ro"
if(!suppressRO) {
JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
rootRepresentation.arrayAdd($$roContainerRepresentation);
$$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
}
break;
case SCALAR_VALUE:
case VOID:
// not supported
return null;
}
final Response.ResponseBuilder responseBuilder =
restfulObjectsV1_0.buildResponseTo(
rendererContext, objectAndActionInvocation, $$roRepresentation, rootRepresentation);
// set appropriate Content-Type
responseBuilder.type(
resultType == ActionResultRepresentation.ResultType.DOMAIN_OBJECT
? CONTENT_TYPE_OAI_V1_OBJECT
: CONTENT_TYPE_OAI_V1_LIST
);
return responseBuilder(responseBuilder);
}
/**
* For easy subclassing to further customize, eg additional headers
*/
protected Response.ResponseBuilder responseBuilder(final Response.ResponseBuilder responseBuilder) {
return responseBuilder;
}
boolean canAccept(final RepresentationService.Context2 rendererContext) {
final List<MediaType> acceptableMediaTypes = rendererContext.getAcceptableMediaTypes();
return mediaTypeParameterMatches(acceptableMediaTypes, "profile", ACCEPT_PROFILE);
}
protected boolean suppress(
final RepresentationService.Context2 rendererContext) {
final List<MediaType> acceptableMediaTypes = rendererContext.getAcceptableMediaTypes();
return mediaTypeParameterMatches(acceptableMediaTypes, "suppress", "true");
}
private void appendObjectTo(
final RepresentationService.Context2 rendererContext,
final ObjectAdapter objectAdapter,
final JsonRepresentation rootRepresentation) {
appendPropertiesTo(rendererContext, objectAdapter, rootRepresentation);
final List<OneToManyAssociation> collections = objectAdapter.getSpecification().getCollections(Contributed.INCLUDED);
final Where where = rendererContext.getWhere();
for (final OneToManyAssociation collection : collections) {
final JsonRepresentation collectionRepresentation = JsonRepresentation.newArray();
rootRepresentation.mapPut(collection.getId(), collectionRepresentation);
final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(rendererContext);
final Consent visibility = collection.isVisible(objectAdapter, interactionInitiatedBy, where);
if (!visibility.isAllowed()) {
continue;
}
appendCollectionTo(rendererContext, objectAdapter, collection, collectionRepresentation);
}
}
private void appendPropertiesTo(
final RepresentationService.Context2 rendererContext,
final ObjectAdapter objectAdapter,
final JsonRepresentation rootRepresentation) {
final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(rendererContext);
final Where where = rendererContext.getWhere();
List<OneToOneAssociation> properties = objectAdapter.getSpecification().getProperties(Contributed.INCLUDED);
for (final OneToOneAssociation property : properties) {
final Consent visibility = property.isVisible(objectAdapter, interactionInitiatedBy, where);
if (!visibility.isAllowed()) {
continue;
}
final JsonRepresentation propertyRepresentation = JsonRepresentation.newMap();
final ObjectPropertyReprRenderer renderer =
new ObjectPropertyReprRenderer(rendererContext, null, property.getId(), propertyRepresentation)
.asStandalone();
renderer.with(new ObjectAndProperty(objectAdapter, property));
final JsonRepresentation propertyValueRepresentation = renderer.render();
final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
rootRepresentation.mapPut("$$href", upHref);
final String upTitle = propertyValueRepresentation.getString("links[rel=up].title");
rootRepresentation.mapPut("$$title", upTitle);
final String upInstanceId = upHref.substring(upHref.lastIndexOf("/")+1);
rootRepresentation.mapPut("$$instanceId", upInstanceId);
final JsonRepresentation value = propertyValueRepresentation.getRepresentation("value");
rootRepresentation.mapPut(property.getId(), value);
}
}
private void appendCollectionTo(
final RepresentationService.Context2 rendererContext,
final ObjectAdapter objectAdapter,
final OneToManyAssociation collection,
final JsonRepresentation representation) {
final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(rendererContext);
final ObjectAdapter valueAdapter = collection.get(objectAdapter, interactionInitiatedBy);
if (valueAdapter == null) {
return;
}
final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(valueAdapter);
final Iterable<ObjectAdapter> iterable = facet.iterable(valueAdapter);
appendIterableTo(rendererContext, iterable, representation);
}
private void appendIterableTo(
final RepresentationService.Context2 rendererContext,
final Iterable<ObjectAdapter> iterable,
final JsonRepresentation collectionRepresentation) {
for (final ObjectAdapter elementAdapter : iterable) {
JsonRepresentation elementRepresentation = JsonRepresentation.newMap();
appendPropertiesTo(rendererContext, elementAdapter, elementRepresentation);
collectionRepresentation.arrayAdd(elementRepresentation);
}
}
private static InteractionInitiatedBy determineInteractionInitiatedByFrom(
final RendererContext rendererContext) {
if (rendererContext instanceof RepresentationService.Context4) {
return ((RepresentationService.Context4) rendererContext).getInteractionInitiatedBy();
} else {
// fallback
return InteractionInitiatedBy.USER;
}
}
@javax.inject.Inject
protected DomainObjectContainer container;
}