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}