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}