001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.isis.viewer.restfulobjects.server.resources; 018 019import java.io.InputStream; 020 021import javax.ws.rs.Consumes; 022import javax.ws.rs.DELETE; 023import javax.ws.rs.GET; 024import javax.ws.rs.POST; 025import javax.ws.rs.PUT; 026import javax.ws.rs.Path; 027import javax.ws.rs.PathParam; 028import javax.ws.rs.Produces; 029import javax.ws.rs.QueryParam; 030import javax.ws.rs.core.MediaType; 031import javax.ws.rs.core.Response; 032 033import org.apache.isis.applib.annotation.Where; 034import org.apache.isis.core.metamodel.adapter.ObjectAdapter; 035import org.apache.isis.core.metamodel.consent.Consent; 036import org.apache.isis.core.metamodel.spec.ObjectSpecId; 037import org.apache.isis.core.metamodel.spec.ObjectSpecification; 038import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation; 039import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; 040import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager; 041import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation; 042import org.apache.isis.viewer.restfulobjects.applib.RepresentationType; 043import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType; 044import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse.HttpStatusCode; 045import org.apache.isis.viewer.restfulobjects.applib.domainobjects.DomainObjectResource; 046import org.apache.isis.viewer.restfulobjects.server.RestfulObjectsApplicationException; 047import org.apache.isis.viewer.restfulobjects.server.resources.DomainResourceHelper.Intent; 048import org.apache.isis.viewer.restfulobjects.server.resources.DomainResourceHelper.MemberMode; 049import org.jboss.resteasy.annotations.ClientResponseType; 050 051@Path("/objects") 052public class DomainObjectResourceServerside extends ResourceAbstract implements DomainObjectResource { 053 054 // ////////////////////////////////////////////////////////// 055 // persist 056 // ////////////////////////////////////////////////////////// 057 058 @Override 059 @POST 060 @Path("/{domainType}") 061 @Consumes({ MediaType.WILDCARD }) 062 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR }) 063 @ClientResponseType(entityType = String.class) 064 public Response persist(@PathParam("domainType") String domainType, final InputStream object) { 065 066 init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS); 067 068 final String objectStr = DomainResourceHelper.asStringUtf8(object); 069 final JsonRepresentation objectRepr = DomainResourceHelper.readAsMap(objectStr); 070 if (!objectRepr.isMap()) { 071 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Body is not a map; got %s", objectRepr); 072 } 073 074 final ObjectSpecification domainTypeSpec = getSpecificationLoader().lookupBySpecId(ObjectSpecId.of(domainType)); 075 if (domainTypeSpec == null) { 076 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Could not determine type of domain object to persist (no class with domainType Id of '%s')", domainType); 077 } 078 079 final ObjectAdapter objectAdapter = getResourceContext().getPersistenceSession().createTransientInstance(domainTypeSpec); 080 081 final JsonRepresentation propertiesList = objectRepr.getArrayEnsured("members[memberType=property]"); 082 if (propertiesList == null) { 083 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Could not find properties list (no members[memberType=property]); got %s", objectRepr); 084 } 085 if (!DomainResourceHelper.copyOverProperties(getResourceContext(), objectAdapter, propertiesList)) { 086 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, objectRepr, "Illegal property value"); 087 } 088 089 final Consent validity = objectAdapter.getSpecification().isValid(objectAdapter); 090 if (validity.isVetoed()) { 091 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, objectRepr, validity.getReason()); 092 } 093 getResourceContext().getPersistenceSession().makePersistent(objectAdapter); 094 095 return new DomainResourceHelper(getResourceContext(), objectAdapter).objectRepresentation(); 096 } 097 098 // ////////////////////////////////////////////////////////// 099 // domain object 100 // ////////////////////////////////////////////////////////// 101 102 @Override 103 @GET 104 @Path("/{domainType}/{instanceId}") 105 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR }) 106 public Response object(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId) { 107 init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS); 108 109 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, instanceId); 110 111 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 112 return helper.objectRepresentation(); 113 } 114 115 @Override 116 @PUT 117 @Path("/{domainType}/{instanceId}") 118 @Consumes({ MediaType.WILDCARD }) 119 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR }) 120 public Response object(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, final InputStream object) { 121 122 init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS); 123 124 final String objectStr = DomainResourceHelper.asStringUtf8(object); 125 final JsonRepresentation objectRepr = DomainResourceHelper.readAsMap(objectStr); 126 if (!objectRepr.isMap()) { 127 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Body is not a map; got %s", objectRepr); 128 } 129 130 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 131 132 final JsonRepresentation propertiesList = objectRepr.getArrayEnsured("members[memberType=property]"); 133 if (propertiesList == null) { 134 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Could not find properties list (no members[memberType=property]); got %s", objectRepr); 135 } 136 137 final IsisTransactionManager transactionManager = getResourceContext().getPersistenceSession().getTransactionManager(); 138 transactionManager.startTransaction(); 139 try { 140 if (!DomainResourceHelper.copyOverProperties(getResourceContext(), objectAdapter, propertiesList)) { 141 transactionManager.abortTransaction(); 142 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, objectRepr, "Illegal property value"); 143 } 144 145 final Consent validity = objectAdapter.getSpecification().isValid(objectAdapter); 146 if (validity.isVetoed()) { 147 transactionManager.abortTransaction(); 148 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, objectRepr, validity.getReason()); 149 } 150 151 transactionManager.endTransaction(); 152 } finally { 153 // in case an exception got thrown somewhere... 154 if (!transactionManager.getTransaction().getState().isComplete()) { 155 transactionManager.abortTransaction(); 156 } 157 } 158 159 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 160 return helper.objectRepresentation(); 161 } 162 163 // ////////////////////////////////////////////////////////// 164 // domain object property 165 // ////////////////////////////////////////////////////////// 166 167 @Override 168 @GET 169 @Path("/{domainType}/{instanceId}/properties/{propertyId}") 170 @Consumes({ MediaType.WILDCARD }) 171 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_JSON_ERROR }) 172 public Response propertyDetails(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("propertyId") final String propertyId) { 173 init(RepresentationType.OBJECT_PROPERTY, Where.OBJECT_FORMS); 174 175 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 176 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 177 178 return helper.propertyDetails(propertyId, MemberMode.NOT_MUTATING, Caching.NONE, getResourceContext().getWhere()); 179 } 180 181 @Override 182 @PUT 183 @Path("/{domainType}/{instanceId}/properties/{propertyId}") 184 @Consumes({ MediaType.WILDCARD }) 185 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR }) 186 public Response modifyProperty(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("propertyId") final String propertyId, final InputStream body) { 187 init(Where.OBJECT_FORMS); 188 189 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 190 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 191 192 final OneToOneAssociation property = helper.getPropertyThatIsVisibleForIntent(propertyId, Intent.MUTATE, getResourceContext().getWhere()); 193 194 final ObjectSpecification propertySpec = property.getSpecification(); 195 final String bodyAsString = DomainResourceHelper.asStringUtf8(body); 196 197 final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(propertySpec, bodyAsString); 198 199 final Consent consent = property.isAssociationValid(objectAdapter, argAdapter); 200 if (consent.isVetoed()) { 201 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.UNAUTHORIZED, consent.getReason()); 202 } 203 204 property.set(objectAdapter, argAdapter); 205 206 return helper.propertyDetails(propertyId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere()); 207 } 208 209 @Override 210 @DELETE 211 @Path("/{domainType}/{instanceId}/properties/{propertyId}") 212 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR }) 213 public Response clearProperty(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("propertyId") final String propertyId) { 214 init(Where.OBJECT_FORMS); 215 216 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 217 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 218 219 final OneToOneAssociation property = helper.getPropertyThatIsVisibleForIntent(propertyId, Intent.MUTATE, getResourceContext().getWhere()); 220 221 final Consent consent = property.isAssociationValid(objectAdapter, null); 222 if (consent.isVetoed()) { 223 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.UNAUTHORIZED, consent.getReason()); 224 } 225 226 property.set(objectAdapter, null); 227 228 return helper.propertyDetails(propertyId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere()); 229 } 230 231 // ////////////////////////////////////////////////////////// 232 // domain object collection 233 // ////////////////////////////////////////////////////////// 234 235 @Override 236 @GET 237 @Path("/{domainType}/{instanceId}/collections/{collectionId}") 238 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_JSON_ERROR }) 239 public Response accessCollection(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("collectionId") final String collectionId) { 240 init(RepresentationType.OBJECT_COLLECTION, Where.PARENTED_TABLES); 241 242 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 243 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 244 245 return helper.collectionDetails(collectionId, MemberMode.NOT_MUTATING, Caching.NONE, getResourceContext().getWhere()); 246 } 247 248 @Override 249 @PUT 250 @Path("/{domainType}/{instanceId}/collections/{collectionId}") 251 @Consumes({ MediaType.WILDCARD }) 252 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR }) 253 public Response addToSet(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("collectionId") final String collectionId, final InputStream body) { 254 init(Where.PARENTED_TABLES); 255 256 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 257 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 258 259 final OneToManyAssociation collection = helper.getCollectionThatIsVisibleForIntent(collectionId, Intent.MUTATE, getResourceContext().getWhere()); 260 261 if (!collection.getCollectionSemantics().isSet()) { 262 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Collection '%s' does not have set semantics", collectionId); 263 } 264 265 final ObjectSpecification collectionSpec = collection.getSpecification(); 266 final String bodyAsString = DomainResourceHelper.asStringUtf8(body); 267 final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, bodyAsString); 268 269 final Consent consent = collection.isValidToAdd(objectAdapter, argAdapter); 270 if (consent.isVetoed()) { 271 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.UNAUTHORIZED, consent.getReason()); 272 } 273 274 collection.addElement(objectAdapter, argAdapter); 275 276 return helper.collectionDetails(collectionId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere()); 277 } 278 279 @Override 280 @POST 281 @Path("/{domainType}/{instanceId}/collections/{collectionId}") 282 @Consumes({ MediaType.WILDCARD }) 283 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR }) 284 public Response addToList(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("collectionId") final String collectionId, final InputStream body) { 285 init(Where.PARENTED_TABLES); 286 287 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 288 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 289 290 final OneToManyAssociation collection = helper.getCollectionThatIsVisibleForIntent(collectionId, Intent.MUTATE, getResourceContext().getWhere()); 291 292 if (!collection.getCollectionSemantics().isListOrArray()) { 293 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Collection '%s' does not have list or array semantics", collectionId); 294 } 295 296 final ObjectSpecification collectionSpec = collection.getSpecification(); 297 final String bodyAsString = DomainResourceHelper.asStringUtf8(body); 298 final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, bodyAsString); 299 300 final Consent consent = collection.isValidToAdd(objectAdapter, argAdapter); 301 if (consent.isVetoed()) { 302 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.UNAUTHORIZED, consent.getReason()); 303 } 304 305 collection.addElement(objectAdapter, argAdapter); 306 307 return helper.collectionDetails(collectionId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere()); 308 } 309 310 @Override 311 @DELETE 312 @Path("/{domainType}/{instanceId}/collections/{collectionId}") 313 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR }) 314 public Response removeFromCollection(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("collectionId") final String collectionId) { 315 init(Where.PARENTED_TABLES); 316 317 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 318 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 319 320 final OneToManyAssociation collection = helper.getCollectionThatIsVisibleForIntent(collectionId, Intent.MUTATE, getResourceContext().getWhere()); 321 322 final ObjectSpecification collectionSpec = collection.getSpecification(); 323 final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, getResourceContext().getQueryString()); 324 325 final Consent consent = collection.isValidToRemove(objectAdapter, argAdapter); 326 if (consent.isVetoed()) { 327 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.UNAUTHORIZED, consent.getReason()); 328 } 329 330 collection.removeElement(objectAdapter, argAdapter); 331 332 return helper.collectionDetails(collectionId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere()); 333 } 334 335 // ////////////////////////////////////////////////////////// 336 // domain object action 337 // ////////////////////////////////////////////////////////// 338 339 @Override 340 @GET 341 @Path("/{domainType}/{instanceId}/actions/{actionId}") 342 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_ACTION, RestfulMediaType.APPLICATION_JSON_ERROR }) 343 public Response actionPrompt(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("actionId") final String actionId) { 344 init(RepresentationType.OBJECT_ACTION, Where.OBJECT_FORMS); 345 346 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 347 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 348 349 return helper.actionPrompt(actionId, getResourceContext().getWhere()); 350 } 351 352 // ////////////////////////////////////////////////////////// 353 // domain object action invoke 354 // ////////////////////////////////////////////////////////// 355 356 @Override 357 @GET 358 @Path("/{domainType}/{instanceId}/actions/{actionId}/invoke") 359 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR }) 360 public Response invokeActionQueryOnly(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("actionId") final String actionId, @QueryParam("x-isis-querystring") final String xIsisQueryString) { 361 init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES, xIsisQueryString); 362 363 final JsonRepresentation arguments = getResourceContext().getQueryStringAsJsonRepr(); 364 365 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 366 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 367 368 return helper.invokeActionQueryOnly(actionId, arguments, getResourceContext().getWhere()); 369 } 370 371 @Override 372 @PUT 373 @Path("/{domainType}/{instanceId}/actions/{actionId}/invoke") 374 @Consumes({ MediaType.WILDCARD }) 375 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR }) 376 public Response invokeActionIdempotent(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("actionId") final String actionId, final InputStream body) { 377 init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES, body); 378 379 final JsonRepresentation arguments = getResourceContext().getQueryStringAsJsonRepr(); 380 381 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 382 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 383 384 return helper.invokeActionIdempotent(actionId, arguments, getResourceContext().getWhere()); 385 } 386 387 @Override 388 @POST 389 @Path("/{domainType}/{instanceId}/actions/{actionId}/invoke") 390 @Consumes({ MediaType.WILDCARD }) 391 @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR }) 392 public Response invokeAction(@PathParam("domainType") String domainType, @PathParam("instanceId") final String oidStr, @PathParam("actionId") final String actionId, final InputStream body) { 393 init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES, body); 394 395 final JsonRepresentation arguments = getResourceContext().getQueryStringAsJsonRepr(); 396 397 final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr); 398 final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter); 399 400 return helper.invokeAction(actionId, arguments, getResourceContext().getWhere()); 401 } 402 403}