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}