2 * This document is a part of the source code and related artifacts
3 * for CollectionSpace, an open source collections management system
4 * for museums and related institutions:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright 2009 University of California at Berkeley
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
16 * https://source.collectionspace.org/collection-space/LICENSE.txt
18 * Unless required by applicable law or agreed to in writing, software
19 * distributed under the License is distributed on an "AS IS" BASIS,
20 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 * See the License for the specific language governing permissions and
22 * limitations under the License.
24 package org.collectionspace.services.common;
26 import java.util.HashMap;
27 import java.util.List;
30 import javax.ws.rs.GET;
31 import javax.ws.rs.Path;
32 import javax.ws.rs.Produces;
33 import javax.ws.rs.core.CacheControl;
34 import javax.ws.rs.core.Context;
35 import javax.ws.rs.core.MultivaluedMap;
36 import javax.ws.rs.core.Request;
37 import javax.ws.rs.core.Response;
38 import javax.ws.rs.core.UriInfo;
40 import org.collectionspace.services.authorization.PermissionException;
41 import org.collectionspace.services.client.CollectionSpaceClient;
42 import org.collectionspace.services.common.CSWebApplicationException;
43 import org.collectionspace.services.common.api.Tools;
44 import org.collectionspace.services.common.config.ServiceConfigUtils;
45 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
46 import org.collectionspace.services.common.context.ServiceContext;
47 import org.collectionspace.services.common.context.ServiceContextProperties;
48 import org.collectionspace.services.common.document.BadRequestException;
49 import org.collectionspace.services.common.document.DocumentException;
50 import org.collectionspace.services.common.document.DocumentHandler;
51 import org.collectionspace.services.common.document.DocumentNotFoundException;
52 import org.collectionspace.services.common.document.TransactionException;
53 import org.collectionspace.services.common.repository.RepositoryClient;
54 import org.collectionspace.services.common.repository.RepositoryClientFactory;
55 import org.collectionspace.services.common.security.SecurityUtils;
56 import org.collectionspace.services.common.security.UnauthorizedException;
57 import org.collectionspace.services.common.storage.StorageClient;
58 import org.collectionspace.services.common.storage.jpa.JpaStorageClientImpl;
59 import org.collectionspace.services.config.service.CacheControlConfig;
60 import org.collectionspace.services.config.service.DocHandlerParams.Params.CacheControlConfigElement;
61 import org.collectionspace.services.config.service.ServiceBindingType;
62 import org.collectionspace.services.config.service.DocHandlerParams.Params;
63 import org.collectionspace.services.description.ServiceDescription;
65 import org.jboss.resteasy.spi.HttpRequest;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * The Class AbstractCollectionSpaceResourceImpl.
72 * @param <IT> the generic type
73 * @param <OT> the generic type
75 public abstract class AbstractCollectionSpaceResourceImpl<IT, OT>
76 implements CollectionSpaceResource<IT, OT> {
78 protected final Logger logger = LoggerFactory.getLogger(this.getClass());
80 protected final ServiceContext<IT, OT> NULL_CONTEXT = null;
81 // Fields for default client factory and client
82 /** The repository client factory. */
83 private RepositoryClientFactory<IT, OT> repositoryClientFactory;
85 /** The repository client. */
86 private RepositoryClient<IT, OT> repositoryClient;
88 /** The storage client. */
89 private StorageClient storageClient;
97 protected static String extractId(Response res) {
98 MultivaluedMap<String, Object> mvm = res.getMetadata();
99 String uri = (String) ((List<Object>) mvm.get("Location")).get(0);
100 String[] segments = uri.split("/");
101 String id = segments[segments.length - 1];
106 * Instantiates a new abstract collection space resource.
108 public AbstractCollectionSpaceResourceImpl() {
109 repositoryClientFactory = (RepositoryClientFactory<IT, OT>) RepositoryClientFactory.getInstance();
113 * @see org.collectionspace.services.common.CollectionSpaceResource#getServiceName()
116 abstract public String getServiceName();
120 * @see org.collectionspace.services.common.CollectionSpaceResource#getRepositoryClient(org.collectionspace.services.common.context.ServiceContext)
123 synchronized public RepositoryClient<IT, OT> getRepositoryClient(ServiceContext<IT, OT> ctx) {
124 if (repositoryClient != null){
125 return repositoryClient;
127 repositoryClient = repositoryClientFactory.getClient(ctx.getRepositoryClientName());
128 return repositoryClient;
132 * @see org.collectionspace.services.common.CollectionSpaceResource#getStorageClient(org.collectionspace.services.common.context.ServiceContext)
135 synchronized public StorageClient getStorageClient(ServiceContext<IT, OT> ctx) {
136 if(storageClient != null) {
137 return storageClient;
139 storageClient = new JpaStorageClientImpl();
140 return storageClient;
144 * @see org.collectionspace.services.common.CollectionSpaceResource#createDocumentHandler(org.collectionspace.services.common.context.ServiceContext)
147 public DocumentHandler createDocumentHandler(ServiceContext<IT, OT> ctx) throws Exception {
148 DocumentHandler docHandler = createDocumentHandler(ctx, ctx.getInput());
153 * Creates the document handler.
156 * @param commonPart the common part
158 * @return the document handler
160 * @throws Exception the exception
162 public DocumentHandler createDocumentHandler(ServiceContext<IT, OT> ctx,
163 Object commonPart) throws Exception {
164 DocumentHandler docHandler = ctx.getDocumentHandler();
165 docHandler.setCommonPart(commonPart);
169 protected ServiceContext<IT, OT> createServiceContext(Request requestInfo, UriInfo uriInfo) throws Exception {
170 ServiceContext<IT, OT> result = this.createServiceContext(uriInfo);
172 result.setRequestInfo(requestInfo);
178 * Creates the service context.
180 * @return the service context< i t, o t>
182 * @throws Exception the exception
184 protected ServiceContext<IT, OT> createServiceContext() throws Exception {
185 ServiceContext<IT, OT> ctx = createServiceContext(this.getServiceName(),
186 (IT)null, //inputType
187 null, // The resource map
188 (UriInfo)null, // The query params
189 this.getCommonPartClass());
194 * Creates the service context.
196 * @param serviceName the service name
198 * @return the service context< i t, o t>
200 * @throws Exception the exception
202 public ServiceContext<IT, OT> createServiceContext(String serviceName) throws Exception {
203 ServiceContext<IT, OT> ctx = createServiceContext(
205 (IT)null, // The input part
206 null, // The resource map
207 (UriInfo)null, // The queryParams
208 (Class<?>)null /*input type's Class*/);
212 protected ServiceContext<IT, OT> createServiceContext(String serviceName, UriInfo ui) throws Exception {
213 ServiceContext<IT, OT> ctx = createServiceContext(
215 (IT)null, // The input part
216 null, // The resource map
217 (UriInfo)null, // The queryParams
218 (Class<?>)null /*input type's Class*/);
224 * Creates the service context.
226 * @param serviceName the service name
227 * @param input the input
229 * @return the service context< i t, o t>
231 * @throws Exception the exception
233 protected ServiceContext<IT, OT> createServiceContext(String serviceName,
234 IT input) throws Exception {
235 ServiceContext<IT, OT> ctx = createServiceContext(serviceName,
237 null, // The resource map
238 (UriInfo)null, /*queryParams*/
239 (Class<?>)null /*input type's Class*/);
243 protected ServiceContext<IT, OT> createServiceContext(String serviceName,
245 UriInfo uriInfo) throws Exception {
246 ServiceContext<IT, OT> ctx = createServiceContext(serviceName,
248 null, // The resource map
249 uriInfo, /*queryParams*/
250 (Class<?>)null /*input type's Class*/);
254 protected ServiceContext<IT, OT> createServiceContext(UriInfo uriInfo) throws Exception {
255 ServiceContext<IT, OT> ctx = createServiceContext(
258 (Class<?>)null /*input type's Class*/);
263 * Creates the service context.
265 * @param input the input
267 * @return the service context< i t, o t>
269 * @throws Exception the exception
271 protected ServiceContext<IT, OT> createServiceContext(IT input) throws Exception {
272 ServiceContext<IT, OT> ctx = createServiceContext(
274 (Class<?>)null /*input type's Class*/);
278 protected ServiceContext<IT, OT> createServiceContext(IT input, UriInfo uriInfo) throws Exception {
279 ServiceContext<IT, OT> ctx = createServiceContext(
282 null ); // The class param/argument
287 * Creates the service context.
289 * @param input the input
290 * @param theClass the the class
292 * @return the service context
294 * @throws Exception the exception
296 protected ServiceContext<IT, OT> createServiceContext(IT input, Class<?> theClass) throws Exception {
297 ServiceContext<IT, OT> ctx = createServiceContext(
299 (UriInfo)null, //queryParams,
304 protected ServiceContext<IT, OT> createServiceContext(IT input, Class<?> theClass, UriInfo uriInfo) throws Exception {
305 ServiceContext<IT, OT> ctx = createServiceContext(
312 protected ServiceContext<IT, OT> createServiceContext(
314 ResourceMap resourceMap,
315 UriInfo uriInfo) throws Exception {
316 ServiceContext<IT, OT> ctx = createServiceContext(
318 null, // The input object
321 null /* the class of the input type */);
325 protected ServiceContext<IT, OT> createServiceContext(
327 ResourceMap resourceMap,
328 UriInfo uriInfo) throws Exception {
329 ServiceContext<IT, OT> ctx = createServiceContext(
330 this.getServiceName(),
334 null /* the class of the input type */);
338 protected ServiceContext<IT, OT> createServiceContext(
341 ResourceMap resourceMap,
342 UriInfo uriInfo) throws Exception {
343 ServiceContext<IT, OT> ctx = createServiceContext(
348 null /* the class of the input type */);
353 * Creates the service context.
355 * @param input the input
356 * @param queryParams the query params
357 * @param theClass the the class
359 * @return the service context< i t, o t>
361 * @throws Exception the exception
363 private ServiceContext<IT, OT> createServiceContext(
366 Class<?> theClass) throws Exception {
367 return createServiceContext(this.getServiceName(),
369 null, // The resource map
375 * Creates the service context.
377 * @param serviceName the service name
378 * @param input the input
379 * @param queryParams the query params
380 * @param theClass the the class
382 * @return the service context< i t, o t>
384 * @throws Exception the exception
386 private ServiceContext<IT, OT> createServiceContext(
389 ResourceMap resourceMap,
391 Class<?> theClass) throws Exception {
392 ServiceContext<IT, OT> ctx = getServiceContextFactory().createServiceContext(
397 theClass != null ? theClass.getPackage().getName() : null,
398 theClass != null ? theClass.getName() : null);
399 if (theClass != null) {
400 ctx.setProperty(ServiceContextProperties.ENTITY_CLASS, theClass);
407 * Gets the version string.
409 * @return the version string
411 abstract protected String getVersionString();
416 * @return the version
420 @Produces("application/xml")
421 public Version getVersion() {
422 Version result = new Version();
424 result.setVersionString(getVersionString());
430 * Get the service description
433 @Path(CollectionSpaceClient.SERVICE_DESCRIPTION_PATH)
434 public ServiceDescription getDescription(@Context UriInfo uriInfo) {
435 ServiceDescription result = null;
437 ServiceContext<IT, OT> ctx = null;
439 ctx = createServiceContext(uriInfo);
440 result = getDescription(ctx);
441 } catch (Exception e) {
442 String errMsg = String.format("Request to get service description information for the '%s' service failed.",
443 this.getServiceContextFactory());
444 throw bigReThrow(e, errMsg);
451 * Each resource can override this method if they need to.
456 public ServiceDescription getDescription(ServiceContext<IT, OT> ctx) {
457 ServiceDescription result = new ServiceDescription();
459 result.setDocumentType(getDocType(ctx.getTenantId()));
464 public void checkResult(Object resultToCheck, String csid, String serviceMessage) throws CSWebApplicationException {
465 if (resultToCheck == null) {
466 Response response = Response.status(Response.Status.NOT_FOUND).entity(
467 serviceMessage + "csid=" + csid
468 + ": was not found.").type(
469 "text/plain").build();
470 throw new CSWebApplicationException(response);
474 protected void ensureCSID(String csid, String crudType) throws CSWebApplicationException {
475 ensureCSID(csid, crudType, "csid");
478 protected void ensureCSID(String csid, String crudType, String whichCsid) throws CSWebApplicationException {
479 if (logger.isDebugEnabled()) {
480 logger.debug(crudType + " for " + getClass().getName() + " with csid=" + csid);
482 if (csid == null || "".equals(csid)) {
483 logger.error(crudType + " for " + getClass().getName() + " missing csid!");
484 Response response = Response.status(Response.Status.BAD_REQUEST).entity(crudType + " failed on " + getClass().getName() + ' '+whichCsid+'=' + csid).type("text/plain").build();
485 throw new CSWebApplicationException(response);
489 protected CSWebApplicationException bigReThrow(Throwable e, String serviceMsg) throws CSWebApplicationException {
490 return bigReThrow(e, serviceMsg, "");
493 protected CSWebApplicationException bigReThrow(Throwable e, String serviceMsg, String csid) throws CSWebApplicationException {
494 boolean logException = true;
495 CSWebApplicationException result = null;
497 String detail = Tools.errorToString(e, true);
498 String detailNoTrace = Tools.errorToString(e, true, 3);
500 if (e instanceof UnauthorizedException) {
501 response = Response.status(Response.Status.UNAUTHORIZED).entity(serviceMsg + e.getMessage()).type("text/plain").build();
502 result = new CSWebApplicationException(e, response);
504 } else if (e instanceof PermissionException) {
505 response = Response.status(Response.Status.FORBIDDEN).entity(serviceMsg + e.getMessage()).type("text/plain").build();
506 result = new CSWebApplicationException(e, response);
508 } else if (e instanceof DocumentNotFoundException) {
510 // Don't log this error unless we're in 'trace' mode
512 logException = false;
513 response = Response.status(Response.Status.NOT_FOUND).entity(serviceMsg + " on " + getClass().getName() + " csid=" + csid).type("text/plain").build();
514 result = new CSWebApplicationException(e, response);
516 } else if (e instanceof TransactionException) {
517 int code = ((TransactionException) e).getErrorCode();
518 response = Response.status(code).entity(e.getMessage()).type("text/plain").build();
519 result = new CSWebApplicationException(e, response);
521 } else if (e instanceof BadRequestException) {
522 int code = ((BadRequestException) e).getErrorCode();
524 code = Response.Status.BAD_REQUEST.getStatusCode();
527 response = Response.status(code).entity(serviceMsg + e.getMessage()).type("text/plain").build();
528 // return new WebApplicationException(e, code);
529 result = new CSWebApplicationException(e, response);
531 } else if (e instanceof DocumentException) {
532 int code = ((DocumentException) e).getErrorCode();
534 code = Response.Status.BAD_REQUEST.getStatusCode();
537 response = Response.status(code).entity(serviceMsg + e.getMessage()).type("text/plain").build();
538 // return new WebApplicationException(e, code);
539 result = new CSWebApplicationException(e, response);
541 } else if (e instanceof org.dom4j.DocumentException) {
542 int code = Response.Status.BAD_REQUEST.getStatusCode();
543 response = Response.status(code).entity(serviceMsg + e.getMessage()).type("text/plain").build();
544 result = new CSWebApplicationException(e, response);
545 } else if (e instanceof CSWebApplicationException) {
546 // subresource may have already thrown this exception
547 // so just pass it on
548 result = (CSWebApplicationException) e;
550 } else { // e is now instanceof Exception
551 response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(serviceMsg + " detail: " + detailNoTrace).type("text/plain").build();
552 result = new CSWebApplicationException(e, response);
555 // Some exceptions like DocumentNotFoundException won't be logged unless we're in 'trace' mode
557 boolean traceEnabled = logger.isTraceEnabled();
558 if (logException == true || traceEnabled == true) {
559 if (traceEnabled == true) {
560 logger.error(getClass().getName() + " detail: " + detail, e);
562 logger.error(getClass().getName() + " detail: " + detailNoTrace);
570 public boolean allowAnonymousAccess(HttpRequest request,
571 Class<?> resourceClass) {
576 * Returns a UriRegistry entry: a map of tenant-qualified URI templates
577 * for the current resource, for all tenants
579 * @return a map of URI templates for the current resource, for all tenants
581 public Map<UriTemplateRegistryKey,StoredValuesUriTemplate> getUriRegistryEntries() {
582 Map<UriTemplateRegistryKey,StoredValuesUriTemplate> uriRegistryEntriesMap =
583 new HashMap<UriTemplateRegistryKey,StoredValuesUriTemplate>();
584 List<String> tenantIds = getTenantBindingsReader().getTenantIds();
585 for (String tenantId : tenantIds) {
586 uriRegistryEntriesMap.putAll(getUriRegistryEntries(tenantId, getDocType(tenantId), UriTemplateFactory.RESOURCE));
588 return uriRegistryEntriesMap;
592 * Returns a resource's document type.
598 public String getDocType(String tenantId) {
599 return getDocType(tenantId, getServiceName());
603 * Returns the document type associated with a specified service, within a specified tenant.
605 * @param tenantId a tenant ID
606 * @param serviceName a service name
607 * @return the Nuxeo document type associated with that service and tenant.
609 // FIXME: This method may properly belong in a different services package or class.
610 // Also, we need to check for any existing methods that may duplicate this one.
611 protected String getDocType(String tenantId, String serviceName) {
613 if (Tools.isBlank(tenantId)) {
616 ServiceBindingType sb = getTenantBindingsReader().getServiceBinding(tenantId, serviceName);
620 docType = sb.getObject().getName(); // Reads the Document Type from tenant bindings configuration
625 * Returns a UriRegistry entry: a map of tenant-qualified URI templates
626 * for the current resource, for a specified tenants
628 * @return a map of URI templates for the current resource, for a specified tenant
631 public Map<UriTemplateRegistryKey,StoredValuesUriTemplate> getUriRegistryEntries(String tenantId,
632 String docType, UriTemplateFactory.UriTemplateType type) {
633 Map<UriTemplateRegistryKey,StoredValuesUriTemplate> uriRegistryEntriesMap =
634 new HashMap<UriTemplateRegistryKey,StoredValuesUriTemplate>();
635 UriTemplateRegistryKey key;
636 if (Tools.isBlank(tenantId) || Tools.isBlank(docType)) {
637 return uriRegistryEntriesMap;
639 key = new UriTemplateRegistryKey();
640 key.setTenantId(tenantId);
641 key.setDocType(docType);
642 uriRegistryEntriesMap.put(key, getUriTemplate(type));
643 return uriRegistryEntriesMap;
647 * Returns a URI template of the appropriate type, populated with the
648 * current service name as one of its stored values.
650 * @param type a URI template type
651 * @return a URI template of the appropriate type.
654 public StoredValuesUriTemplate getUriTemplate(UriTemplateFactory.UriTemplateType type) {
655 Map<String,String> storedValuesMap = new HashMap<String,String>();
656 storedValuesMap.put(UriTemplateFactory.SERVICENAME_VAR, getServiceName());
657 StoredValuesUriTemplate template =
658 UriTemplateFactory.getURITemplate(type, storedValuesMap);
663 * Returns a reader for reading values from tenant bindings configuration
665 * @return a tenant bindings configuration reader
668 public TenantBindingConfigReaderImpl getTenantBindingsReader() {
669 return ServiceMain.getInstance().getTenantBindingConfigReader();
673 * Find a named CacheControlConfig instance.
678 private CacheControlConfig getCacheControl(CacheControlConfigElement element, String cacheKey) {
679 CacheControlConfig result = null;
681 List<CacheControlConfig> list = element.getCacheControlConfigList();
682 for (CacheControlConfig cacheControlConfig : list) {
683 if (cacheControlConfig.getKey().equalsIgnoreCase(cacheKey)) {
684 result = cacheControlConfig;
693 * By default, use the request's URI and HTTP method to form the cache-key to use when looking up the
694 * cache control configuration from the service bindings
696 protected CacheControl getDefaultCacheControl(ServiceContext<IT, OT> ctx) {
697 UriInfo uriInfo = ctx.getUriInfo();
698 Request requestInfo = ctx.getRequestInfo();
700 if (uriInfo != null && requestInfo != null) try {
701 String resName = SecurityUtils.getResourceName(uriInfo);
702 String requestMethod = requestInfo.getMethod();
703 return getCacheControl(ctx, String.format("%s/%s", requestMethod, resName)); // example, "GET/blobs/*/content"
704 } catch (Exception e) {
705 logger.debug(e.getMessage(), e);
708 return getCacheControl(ctx, "default"); // Look for a default one if we couldn't find based on the resource request
712 * FIXME: This code around cache control needs some documentation.
718 protected CacheControl getCacheControl(ServiceContext<IT, OT> ctx, String cacheKey) {
719 CacheControl result = null;
722 Params docHandlerParams = ServiceConfigUtils.getDocHandlerParams(ctx.getTenantId(), ctx.getServiceName());
723 CacheControlConfig cacheControlConfig = getCacheControl(docHandlerParams.getCacheControlConfigElement(), cacheKey);
724 if (cacheControlConfig != null) {
725 result = new CacheControl();
727 if (cacheControlConfig.isPrivate() != null) {
728 result.setPrivate(cacheControlConfig.isPrivate());
731 if (cacheControlConfig.isNoCache() != null) {
732 result.setNoCache(cacheControlConfig.isNoCache());
735 if (cacheControlConfig.isProxyRevalidate() != null) {
736 result.setProxyRevalidate(cacheControlConfig.isProxyRevalidate());
739 if (cacheControlConfig.isMustRevalidate() != null) {
740 result.setMustRevalidate(cacheControlConfig.isMustRevalidate());
743 if (cacheControlConfig.isNoStore() != null) {
744 result.setNoStore(cacheControlConfig.isNoStore());
747 if (cacheControlConfig.isNoTransform() != null) {
748 result.setNoTransform(cacheControlConfig.isNoTransform());
751 if (cacheControlConfig.getMaxAge() != null) {
752 result.setMaxAge(cacheControlConfig.getMaxAge().intValue());
755 if (cacheControlConfig.getSMaxAge() != null) {
756 result.setSMaxAge(cacheControlConfig.getSMaxAge().intValue());
759 } catch (DocumentException e) {
761 logger.debug(String.format("Failed to retrieve CacheControlConfig with key '%s' from service bindings '%s'.", cacheKey, ctx.getServiceName()), e);
762 } catch (NullPointerException npe) {
765 // NPE might mean optional cache-control config is missing -that's usually ok.
766 logger.trace(npe.getLocalizedMessage(), npe);
772 protected Response.ResponseBuilder setCacheControl(ServiceContext<IT, OT> ctx, Response.ResponseBuilder responseBuilder) {
773 CacheControl cacheControl = getDefaultCacheControl(ctx);
775 if (cacheControl != null) {
776 responseBuilder.cacheControl(cacheControl);
777 logger.debug(String.format("Setting default CacheControl for service '%s' responses from the service bindings for tenant ID='%s'.",
778 ctx.getServiceName(), ctx.getTenantId()));
781 return responseBuilder;