/* * Copyright (C) 2013 Intel Corporation * All rights reserved. */ package com.intel.mtwilson.jaxrs2.server.resource; import com.intel.mtwilson.jaxrs2.mediatype.CryptoMediaType; import com.intel.mtwilson.jaxrs2.server.PATCH; import com.intel.dcsg.cpg.io.UUID; import com.intel.dcsg.cpg.validation.ValidationUtil; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import com.intel.mtwilson.jaxrs2.Document; import com.intel.mtwilson.jaxrs2.DocumentCollection; import com.intel.mtwilson.repository.FilterCriteria; import com.intel.mtwilson.jaxrs2.Patch; import com.intel.mtwilson.jaxrs2.PatchLink; import com.intel.mtwilson.jaxrs2.mediatype.DataMediaType; import java.util.List; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.BeanParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * Reference: https://jersey.java.net/documentation/latest/user-guide.html * https://jersey.java.net/documentation/latest/media.html * https://jersey.java.net/documentation/latest/deployment.html * https://wikis.oracle.com/display/Jersey/Overview+of+JAX-RS+1.0+Features * http://docs.oracle.com/cd/E19776-01/820-4867/6nga7f5o5/index.html * * When listing the media types in * * @Produces annotations, it's important to put application/json before * application/vnd.api+json so it will be chosen as the default if the browser * doesn't specify an Accept header. If application/json is first, accessing * /hosts will display the JSON output in the browser. If * application/vnd.api+json is first, the browser will not recognize that * content type (even though it has +json) and will download it as a file. The * .json file extension maps to application/json specifically. JSON API clients * must set the Accept header if they want to receive the output with * Content-Type: application/vnd.api+json * * * Example simple JSON output: * * * {"hosts":[{"id":"06285da4-e170-4322-a843-480f3a55feec","name":"hostabc","connection_url":"http://1.2.3.4","description":"test * host","bios_mle":"bios-4.3.2"}]} * * Example XML output: * * <host_collection><hosts><host><id>bd7094d2-2ed3-468e-9c16-40999f9e4b8c</id><name>hostabc</name><connectionUrl>http://1.2.3.4</connectionUrl><description>test * host</description><biosMLE>bios-4.3.2</biosMLE></host></hosts></host_collection> * * This abstract class defines the following HTTP interface: * * GET /collection -> application/vnd.api+json, application/json, * application/xml, application/yaml, text/yaml * * POST /collection Content-Type: application/json, application/xml, * application/yaml, text/yaml -> application/json, application/xml, * application/yaml, text/yaml * * * * @author jbuhacoff */ //@Stateless //@Path("/hosts") public abstract class AbstractResource<T extends Document, C extends DocumentCollection<T>, F extends FilterCriteria<T>, L extends PatchLink<T>> { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractResource.class); /** * Given criteria encapsulated in a POJO, returns a collection of items * matching the criteria. * * @param criteria * @return */ protected abstract C search(F criteria); /** * Given an item identifier, returns the specified item if it exists or null * if the item doesn't exist. * * Subclass is responsible for validating the id in whatever manner it needs to. * Most will return null if !UUID.isValid(id) but we don't do it here because * a resource might want to allow using something other than uuid as the url * key, for example a Host resource might accept uuid OR hostname as {id} * * @param id * @return */ protected abstract T retrieve(String id); /** * Given an item instance, stores the item into permanent storage replacing * any existing item with the same id. * * Pre-Condition: Item should already exist. * * The item will already have a client-provided or auto-generated UUID. * * @param id * @param item */ protected abstract void store(T item); /** * Creates a new instance and stores it in the DB. * * The item will already have a client-provided or auto-generated UUID. * * @param id * @param item */ protected abstract void create(T item); /** * Given an item identifier, delete the corresponding item from permanent * storage. * * If the item does not already exist then no action should be taken - it is * not an error to request deletion of an non-existent item. * * @param id */ protected abstract void delete(String id); // protected abstract F createFilterCriteriaWithId(String id); // hopefully just a temporary helper so we can make the json api work ok with our generics; looking for another way to do this but at least the implementations for this will be really easy 2-liners protected abstract C createEmptyCollection(); /** * Search for items. Input Content-Type is not applicable because GET * requests do not have a request body. Output Content-Type is any of * application/json, application/xml, application/yaml, or text/yaml * depending on the client's Accept header. * * The result is a collection of items, even if only one item was found. If * no items were found the result is an empty collection. * * This is the only method where the result for application/vnd.api+json is * equivalent to the result for application/json. * * @param criteria * @return */ /* @GET @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML, OtherMediaType.APPLICATION_YAML, OtherMediaType.TEXT_YAML}) public List<T> searchList(@BeanParam F criteria) { log.debug("searchCollection"); C collection = search(criteria); return collection.getDocuments(); }*/ /** * Search for items. Input Content-Type is not applicable because GET * requests do not have a request body. Output Content-Type is any of * application/vnd.api+json, application/json, application/xml, * application/yaml, or text/yaml * * depending on the client's Accept header. * * The result is a collection of items, even if only one item was found. If * no items were found the result is an empty collection. * * This is the only method where the result for application/vnd.api+json is * equivalent to the result for application/json. * * @param criteria * @return S */ @GET // @Produces({OtherMediaType.APPLICATION_VND_API_JSON}) @Produces({DataMediaType.APPLICATION_VND_API_JSON, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) public C searchCollection(@BeanParam F criteria) { log.debug("searchCollection"); ValidationUtil.validate(criteria); // throw new MWException(e, ErrorCode.AS_INPUT_VALIDATION_ERROR, input, method.getName()); return search(criteria); } /** * Add an item to the collection. Input Content-Type is any of * application/json, application/xml, application/yaml, or text/yaml Output * Content-Type is any of application/json, application/xml, * application/yaml, or text/yaml * * The input must represent a single item NOT wrapped in a collection. * * @param item * @return */ @POST @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) public T createOne(T item) { log.debug("create"); ValidationUtil.validate(item); // throw new MWException(e, ErrorCode.AS_INPUT_VALIDATION_ERROR, input, method.getName()); if (item.getId() == null) { item.setId(new UUID()); } create(item); return item; } // the delete method is on a specific resource id and because we don't return any content it's the same whether its simple object or json api // jersey automatically returns status code 204 No Content (successful) to the client because // we have a void return type @Path("/{id}") @DELETE public void deleteOne(@PathParam("id") String id) { log.debug("delete"); T item = retrieve(id); // subclass is responsible for validating the id in whatever manner it needs to; most will return null if !UUID.isValid(id) but we don't do it here because a resource might want to allow using something other than uuid as the url key, for example uuid OR hostname for hosts if (item == null) { throw new WebApplicationException(Response.Status.NOT_FOUND); } delete(id); } /** * Retrieve an item from the collection. Input Content-Type is not * applicable. Output Content-Type is any of application/json, * application/xml, application/yaml, or text/yaml * * The output represents a single item NOT wrapped in a collection. * * @param id * @return */ @Path("/{id}") @GET @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) public T retrieveOne(@PathParam("id") String id) { log.debug("retrieve"); T item = retrieve(id); // subclass is responsible for validating the id in whatever manner it needs to; most will return null if !UUID.isValid(id) but we don't do it here because a resource might want to allow using something other than uuid as the url key, for example uuid OR hostname for hosts if (item == null) { throw new WebApplicationException(Response.Status.NOT_FOUND); } return item; } /** * Replace an item in the collection. Input Content-Type is any of * application/json, application/xml, application/yaml, or text/yaml Output * Content-Type is any of application/json, application/xml, * application/yaml, or text/yaml * * The input is a single item NOT wrapped in a collection. The output is the * single item NOT wrapped in a collection. * * @param id * @param item * @return */ @Path("/{id}") @PUT @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) public T storeOne(@PathParam("id") String id, T item) { log.debug("store"); ValidationUtil.validate(item); item.setId(UUID.valueOf(id)); T existing = retrieve(id); // subclass is responsible for validating id if (existing == null) { create(item); } else { store(item); } return item; } // the patch method only accepts the patch content type /** * Update an item in the collection. Input Content-Type is a special patch * document format. Output Content-Type is any of application/json, * application/xml, application/yaml, or text/yaml. * * The input is a patch format for a single item. The output is a single * item after applying the patch, NOT wrapped in a collection. * * @param id * @return */ @Path("/{id}") @PATCH @Consumes({DataMediaType.APPLICATION_RELATIONAL_PATCH_JSON}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, DataMediaType.APPLICATION_YAML, DataMediaType.TEXT_YAML}) public T patchOne(@PathParam("id") String id, Patch<T, F, L>[] patchArray) { log.debug("patch"); T item = retrieve(id); // subclass is responsible for validating id if (item == null) { throw new WebApplicationException(Response.Status.NOT_FOUND); } ValidationUtil.validate(patchArray); for (int i = 0; i < patchArray.length; i++) { log.debug("Processing patch #{} of {}", i + 1, patchArray.length); if (false /* error during processing */) { // 400 bad request or 500 internal server error return null; } } //return new Host(); // return patch(null); return null; } /////////////////////////// JSON API /////////////////////////// /** * Add an item to the collection. Input Content-Type is * application/vnd.api+json Output Content-Type is application/vnd.api+json * * The input must represent a collection of items to add, even if the * collection only contains a single item. * * * @param hosts * @return */ @POST @Consumes({DataMediaType.APPLICATION_VND_API_JSON}) @Produces({DataMediaType.APPLICATION_VND_API_JSON}) public C createCollection(C collection) { log.debug("createCollection"); ValidationUtil.validate(collection); // this behavior of autmoatically generating uuids if client didn't provide could be implemented in one place and reused in all create() methods... the utility could accept a DocumentCollection and set the ids... for (T item : collection.getDocuments()) { if (item.getId() == null) { item.setId(new UUID()); } create(item); } return collection; } /** * Retrieve an item from the collection. Input Content-Type is not * applicable. Output Content-Type is application/vnd.api+json * * The output item is always wrapped in a collection. * * @param id * @return */ @Path("/{id}") @GET @Produces({DataMediaType.APPLICATION_VND_API_JSON}) public C retrieveCollection(@PathParam("id") String id) { // misnomer, what we really mean is "retrieve one but wrapped ina collection for jsonapi" log.debug("retrieveCollection"); T item = retrieve(id); // subclass is responsible for validating id if (item == null) { throw new WebApplicationException(Response.Status.NOT_FOUND); } C collection = createEmptyCollection(); collection.getDocuments().add(item); return collection; } /** * Replace an item in the collection. Input Content-Type is * application/vnd.api+json Output Content-Type is application/vnd.api+json * * The input item must be wrapped in a collection. The output item is always * wrapped in a collection. * * @param id * @param hostCollection * @return */ @Path("/{id}") @PUT @Consumes(DataMediaType.APPLICATION_VND_API_JSON) @Produces(DataMediaType.APPLICATION_VND_API_JSON) public C storeCollection(@PathParam("id") String id, C collection) {// misnomer, what we really mean is "store one but wrapped ina collection for jsonapi" log.debug("storeCollection"); ValidationUtil.validate(collection); List<T> list = collection.getDocuments(); if (list == null || list.isEmpty()) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } T item = list.get(0); if (item == null) { create(item); } else { store(item); } return collection; } /** * Update an item in the collection. Input Content-Type is * application/vnd.api+json Output Content-Type is application/vnd.api+json * * The input is a JSON PATCH document. There are restrictions on what * operations are allowed because the back-end storage is a database schema * not a JSON document. The output is the modified item wrapped in a * collection. * * @param id * @return */ @Path("/{id}") @PATCH @Consumes(DataMediaType.APPLICATION_RELATIONAL_PATCH_JSON) @Produces(DataMediaType.APPLICATION_VND_API_JSON) public C patchCollection(@PathParam("id") String id /*, PatchDocumentCollection patch */) { log.debug("patchCollection"); // HostFilterCriteria criteria = new HostFilterCriteria(); // criteria.id = UUID.valueOf(id); // return searchCollection(criteria); return null; } /* private void validate(Object input) { try { ValidationUtil.validate(input); } catch(Exception e) { throw new MWException(e, ErrorCode.AS_INPUT_VALIDATION_ERROR, input, method.getName()); } } */ }