/** * $Id: EntityHandlerImpl.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $ * $URL: https://source.sakaiproject.org/svn/entitybroker/trunk/rest/src/java/org/sakaiproject/entitybroker/rest/EntityHandlerImpl.java $ * EntityHandler.java - entity-broker - Apr 6, 2008 9:03:03 AM - azeckoski ************************************************************************** * Copyright (c) 2007, 2008, 2009 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sakaiproject.entitybroker.rest; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.azeckoski.reflectutils.ReflectUtils; import org.azeckoski.reflectutils.exceptions.FieldnameNotFoundException; import org.sakaiproject.entitybroker.EntityBroker; import org.sakaiproject.entitybroker.EntityBrokerManager; import org.sakaiproject.entitybroker.EntityReference; import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.access.AccessFormats; import org.sakaiproject.entitybroker.access.AccessViews; import org.sakaiproject.entitybroker.access.EntityViewAccessProvider; import org.sakaiproject.entitybroker.access.EntityViewAccessProviderManager; import org.sakaiproject.entitybroker.access.HttpServletAccessProvider; import org.sakaiproject.entitybroker.access.HttpServletAccessProviderManager; import org.sakaiproject.entitybroker.entityprovider.EntityProvider; import org.sakaiproject.entitybroker.entityprovider.EntityProviderManager; import org.sakaiproject.entitybroker.entityprovider.annotations.EntityLastModified; import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsExecutable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Createable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Deleteable; import org.sakaiproject.entitybroker.entityprovider.capabilities.InputTranslatable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Inputable; import org.sakaiproject.entitybroker.entityprovider.capabilities.OutputFormattable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Outputable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Redirectable; import org.sakaiproject.entitybroker.entityprovider.capabilities.RequestHandler; import org.sakaiproject.entitybroker.entityprovider.capabilities.RequestInterceptor; import org.sakaiproject.entitybroker.entityprovider.capabilities.Updateable; import org.sakaiproject.entitybroker.entityprovider.extension.ActionReturn; import org.sakaiproject.entitybroker.entityprovider.extension.CustomAction; import org.sakaiproject.entitybroker.entityprovider.extension.EntityData; import org.sakaiproject.entitybroker.entityprovider.extension.Formats; import org.sakaiproject.entitybroker.entityprovider.extension.RequestGetterWrite; import org.sakaiproject.entitybroker.entityprovider.extension.RequestStorage; import org.sakaiproject.entitybroker.entityprovider.extension.RequestStorageWrite; import org.sakaiproject.entitybroker.entityprovider.search.Search; import org.sakaiproject.entitybroker.exception.EntityEncodingException; import org.sakaiproject.entitybroker.exception.EntityException; import org.sakaiproject.entitybroker.exception.EntityNotFoundException; import org.sakaiproject.entitybroker.exception.FormatUnsupportedException; import org.sakaiproject.entitybroker.providers.EntityRequestHandler; import org.sakaiproject.entitybroker.util.ClassLoaderReporter; import org.sakaiproject.entitybroker.util.EntityDataUtils; import org.sakaiproject.entitybroker.util.EntityResponse; import org.sakaiproject.entitybroker.util.http.HttpRESTUtils; import org.sakaiproject.entitybroker.util.http.HttpResponse; import org.sakaiproject.entitybroker.util.http.LazyResponseOutputStream; import org.sakaiproject.entitybroker.util.http.HttpRESTUtils.Method; import org.sakaiproject.entitybroker.util.request.RequestUtils; /** * Implementation of the handler for the EntityBroker system<br/> * This handles all the processing of incoming requests (http based) and includes * method to process the request data and ensure classloader safety * * @author Aaron Zeckoski (aaronz@vt.edu) */ @SuppressWarnings("deprecation") public class EntityHandlerImpl implements EntityRequestHandler { public static String APP_VERSION = "1.0.1"; public static String SVN_REVISION = "$Revision: 105077 $"; public static String SVN_LAST_UPDATE = "$Date: 2012-02-24 17:54:29 -0500 (Fri, 24 Feb 2012) $"; /** * Empty constructor */ protected EntityHandlerImpl() { } /** * Full constructor */ public EntityHandlerImpl(EntityProviderManager entityProviderManager, EntityBrokerManager entityBrokerManager, EntityEncodingManager entityEncodingManager, EntityDescriptionManager entityDescriptionManager, EntityViewAccessProviderManager entityViewAccessProviderManager, RequestGetterWrite requestGetter, EntityActionsManager entityActionsManager, EntityRedirectsManager entityRedirectsManager, EntityBatchHandler entityBatchHandler, RequestStorageWrite requestStorage) { super(); this.entityProviderManager = entityProviderManager; this.entityBrokerManager = entityBrokerManager; this.entityEncodingManager = entityEncodingManager; this.entityDescriptionManager = entityDescriptionManager; this.entityViewAccessProviderManager = entityViewAccessProviderManager; this.requestGetter = requestGetter; this.entityActionsManager = entityActionsManager; this.entityRedirectsManager = entityRedirectsManager; this.requestStorage = requestStorage; setEntityBatchHandler(entityBatchHandler); init(); } public void init() { System.out.println("INFO EntityRequestHandler init complete"); } private EntityProviderManager entityProviderManager; public void setEntityProviderManager(EntityProviderManager entityProviderManager) { this.entityProviderManager = entityProviderManager; } private EntityBrokerManager entityBrokerManager; public void setEntityBrokerManager(EntityBrokerManager entityBrokerManager) { this.entityBrokerManager = entityBrokerManager; } private EntityEncodingManager entityEncodingManager; public void setEntityEncodingManager(EntityEncodingManager entityEncodingManager) { this.entityEncodingManager = entityEncodingManager; } private EntityDescriptionManager entityDescriptionManager; public void setEntityDescriptionManager(EntityDescriptionManager entityDescriptionManager) { this.entityDescriptionManager = entityDescriptionManager; } private HttpServletAccessProviderManager accessProviderManager; public void setAccessProviderManager(HttpServletAccessProviderManager accessProviderManager) { this.accessProviderManager = accessProviderManager; } private EntityViewAccessProviderManager entityViewAccessProviderManager; public void setEntityViewAccessProviderManager( EntityViewAccessProviderManager entityViewAccessProviderManager) { this.entityViewAccessProviderManager = entityViewAccessProviderManager; } private RequestGetterWrite requestGetter; public void setRequestGetter(RequestGetterWrite requestGetter) { this.requestGetter = requestGetter; } private EntityActionsManager entityActionsManager; public void setEntityActionsManager(EntityActionsManager entityActionsManager) { this.entityActionsManager = entityActionsManager; } private EntityRedirectsManager entityRedirectsManager; public void setEntityRedirectsManager(EntityRedirectsManager entityRedirectsManager) { this.entityRedirectsManager = entityRedirectsManager; } private EntityBatchHandler entityBatchHandler; public void setEntityBatchHandler(EntityBatchHandler entityBatchHandler) { this.entityBatchHandler = entityBatchHandler; // NOTE: this is somewhat tricky but it avoids issues related to circular dependencies this.entityBatchHandler.setEntityRequestHandler(this); } private RequestStorageWrite requestStorage; public void setRequestStorage(RequestStorageWrite requestStorage) { this.requestStorage = requestStorage; } // allow the servlet name to be more flexible private String servletContext; public String getServletContext() { if (this.servletContext == null) { return RequestUtils.getServletContext(null); } return this.servletContext; } public void setServletContext(String servletContext) { if (servletContext != null) { this.servletContext = servletContext; //System.out.println("Setting the REST servlet context to: " + servletContext); entityBrokerManager.setServletContext(servletContext); } } /** * If this param is set then the sakai session for the current request is set to this rather than establishing one, * will allow changing the session as well */ /* (non-Javadoc) * @see org.sakaiproject.entitybroker.EntityRequestHandler#handleEntityAccess(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ public String handleEntityAccess(HttpServletRequest req, HttpServletResponse res, String path) { // set the servlet context if not set OR we know for sure we have a request object if (this.servletContext == null || req != null) { setServletContext( RequestUtils.getServletContext(req) ); } // get the path info if not set if (req != null && path == null) { path = req.getPathInfo(); } String handledReference = null; // special handling in case the session ID is sent in the request // (allows setting up and reusing a session over and over without holding cookies) if (entityBrokerManager.getExternalIntegrationProvider() != null) { try { entityBrokerManager.getExternalIntegrationProvider().handleUserSessionKey(req); } catch (Exception e) { System.out.println("WARN: EntityRequestHandler: External handleUserSessionKey method failed, continuing...: " + e); } } if (path == null || "".equals(path) || "/".equals(path)) { // SPECIAL handling for empty path res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); try { res.sendRedirect( res.encodeRedirectURL(getServletContext() + SLASH_DESCRIBE) ); } catch (IOException e) { // should never happen throw new RuntimeException("Could not encode the redirect URL"); } // immediate exit from redirect return "/"; } else { // regular handling for direct URLs if ( (SLASH_DESCRIBE).equals(path) || path.startsWith(SLASH_DESCRIBE + EntityReference.PERIOD)) { // SPECIAL handling for the describe all URL String format = RequestUtils.findAndHandleFormat(req, res, Formats.HTML); String output = entityDescriptionManager.makeDescribeAll(format, req.getLocale()); // possibly get the locale from other places? res.setContentLength(output.getBytes().length); try { res.getWriter().write(output); } catch (IOException e) { // should never happen throw new RuntimeException("Failed to put output into the response writer: " + e.getMessage(), e); } res.setStatus(HttpServletResponse.SC_OK); handledReference = EntityView.SEPARATOR+""; } else { // STANDARD processing for the incoming view EntityView view; try { view = entityBrokerManager.parseEntityURL(path); } catch (IllegalArgumentException e) { // FAILURE indicates we could not parse the reference throw new EntityException("Could not parse entity path ("+path+"): " + e.getMessage(), path, HttpServletResponse.SC_BAD_REQUEST); } if (view == null) { // FAILURE no provider for this entity prefix throw new EntityException( "Could not parse the incoming path ("+path+") and no entity provider could be found to handle the prefix", path, HttpServletResponse.SC_NOT_IMPLEMENTED ); } else if ( DESCRIBE.equals(view.getEntityReference().getId()) ) { // SPECIAL handling for entity describe URLs String format = RequestUtils.findAndHandleFormat(req, res, Formats.HTML); String entityId = req.getParameter("_id"); if (entityId == null || "".equals(entityId)) { entityId = FAKE_ID; } String output = entityDescriptionManager.makeDescribeEntity(view.getEntityReference().getPrefix(), entityId, format, req.getLocale()); res.setContentLength(output.getBytes().length); try { res.getWriter().write(output); } catch (IOException e) { throw new RuntimeException("Failed to put output into the response writer: " + e.getMessage(), e); } res.setStatus(HttpServletResponse.SC_OK); handledReference = view.getEntityReference().getSpaceReference() + SLASH_DESCRIBE; } else { // STANDARD reference successfully parsed String prefix = view.getEntityReference().getPrefix(); // check for redirect Redirectable urlConfigurable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Redirectable.class); if (urlConfigurable != null) { // SPECIAL check for redirect String redirectURL = entityRedirectsManager.checkForTemplateMatch(urlConfigurable, path, req.getQueryString()); if (redirectURL != null) { // SPECIAL handling for redirect if ("".equals(redirectURL)) { // do nothing but return an empty response res.setStatus(HttpServletResponse.SC_OK); } else { // do the redirect System.out.println("INFO: EntityRequestHandler: Entity Redirect: redirecting from ("+path+") to ("+redirectURL+")"); RequestUtils.handleURLRedirect(redirectURL, true, req, res); } return EntityView.SEPARATOR + prefix; // exit here for redirects } } // check for custom action CustomAction customAction = entityActionsManager.getCustomAction(prefix, view.getPathSegment(1)); if (customAction == null) { customAction = entityActionsManager.getCustomAction(prefix, view.getPathSegment(2)); } if (customAction == null) { // check to see if the entity exists if (! entityBrokerManager.entityExists(view.getEntityReference()) ) { // FAILURE invalid entity reference (entity does not exist) throw new EntityException( "Attempted to access an entity URL path (" + path + ") for an entity (" + view.getEntityReference() + ") that does not exist", view.getEntityReference()+"", HttpServletResponse.SC_NOT_FOUND ); } } else { // cleanup the entity reference, this has to be done because otherwise the custom action // on collections appears to be the id of an entity in the collection EntityReference cRef = view.getEntityReference(); if (cRef.getId() != null && cRef.getId().equalsIgnoreCase(customAction.action)) { view.setEntityReference( new EntityReference(prefix, "") ); } } res.setStatus(HttpServletResponse.SC_OK); // default - other things can switch this later on // store format in attribute req.setAttribute("entity-format", view.getFormat()); // STANDARD initial processing complete // wrap in try block so that request storage is always cleaned up try { // store the current request and response requestGetter.setRequest(req); requestGetter.setResponse(res); // set the request variables requestStorage.setRequestValue(RequestStorage.ReservedKeys._requestEntityReference.name(), view.getEntityReference().toString()); requestStorage.setRequestValue(RequestStorage.ReservedKeys._requestOrigin.name(), RequestStorage.RequestOrigin.REST.name()); requestStorage.setRequestValue(RequestStorage.ReservedKeys._requestActive.name(), true); // handle the before interceptor RequestInterceptor interceptor = (RequestInterceptor) entityProviderManager.getProviderByPrefixAndCapability(prefix, RequestInterceptor.class); if (interceptor != null) { interceptor.before(view, req, res); } if (BATCH.equals(prefix)) { // special batch handling // set the default format to JSON for batch handling view.setExtension( RequestUtils.findAndHandleFormat(req, res, Formats.JSON) ); entityBatchHandler.handleBatch(view, req, res); } else { // ensure the format is set correctly for the response and the view String format = RequestUtils.findAndHandleFormat(req, res, Formats.HTML); view.setExtension( format ); // check for provider handling of this request RequestHandler handler = (RequestHandler) entityProviderManager.getProviderByPrefixAndCapability(prefix, RequestHandler.class); if (handler != null) { // SPECIAL provider is handling this request handleClassLoaderAccess(handler, req, res, view); } else { // STANDARD processing of the entity request internally start here // try to handle the request internally if possible // identify the type of request (input or output) and the action (will be encoded in the viewKey) boolean output = RequestUtils.isRequestOutput(req, view); setResponseHeaders(view, res, requestStorage.getStorageMapCopy(), null); boolean handled = false; // PROCESS CUSTOM ACTIONS ActionReturn actionReturn = null; if (customAction != null) { // SPECIAL handle the custom action ActionsExecutable actionProvider = entityProviderManager.getProviderByPrefixAndCapability(prefix, ActionsExecutable.class); if (actionProvider == null) { throw new EntityException( "The provider for prefix ("+prefix+") cannot handle custom actions", view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST ); } // make sure this request is a valid type for this action if (customAction.viewKey != null && ! view.getViewKey().equals(customAction.viewKey)) { throw new EntityException( "Cannot execute custom action ("+customAction.action+") for request method " + req.getMethod() + ", The custom action view key ("+customAction.viewKey+") must match the request view key ("+view.getViewKey()+")", view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST ); } try { actionReturn = entityActionsManager.handleCustomActionRequest(actionProvider, view, customAction.action, req, res, requestStorage.getStorageMapCopy(true, false, true, true) ); } catch (SecurityException se) { // AJAX/WS type security exceptions are handled specially, no redirect throw new EntityException("Security exception handling request for view ("+view+"), " + "this is typically caused by the current user not having access to the " + "data requested or the user not being logged in at all :: message=" + se.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_FORBIDDEN); } catch (EntityNotFoundException e) { throw new EntityException( "Cannot execute custom action ("+customAction.action+"): Could not find entity ("+e.entityReference+"): " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_NOT_FOUND ); } catch (FormatUnsupportedException e) { throw new EntityException( "Cannot execute custom action ("+customAction.action+"): Format not supported ("+e.format+"): " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_NOT_ACCEPTABLE ); } catch (IllegalArgumentException e) { throw new EntityException( "Cannot execute custom action ("+customAction.action+"): Illegal arguments: " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST ); } catch (UnsupportedOperationException e) { throw new EntityException( "Cannot execute custom action ("+customAction.action+"): Could not execute action: " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST ); } if (actionReturn == null || actionReturn.output != null) { // custom action processing complete /* actionReturn.output != null - this means that there is an * outputstream set and that the encoding has been, * handled, however, the response status code should be set still */ handled = true; } else { // if there are headers then set them now addResponseHeaders(res, actionReturn.getHeaders()); // if the custom action returned entity data then we will encode it for output if (actionReturn.entitiesList == null && actionReturn.entityData == null) { handled = true; } else { // there is entity data to return output = true; handled = false; // populate the entity data if (actionReturn.entitiesList != null) { if (actionReturn.entitiesList.size() > 1) { // correct the view key which should be used now view.setViewKey(EntityView.VIEW_LIST); } entityBrokerManager.populateEntityData(actionReturn.entitiesList); } else if (actionReturn.entityData != null) { // correct the view key which should be used now view.setViewKey(EntityView.VIEW_SHOW); entityBrokerManager.populateEntityData( new EntityData[] {actionReturn.entityData} ); } } } } boolean formatInvalidFailure = false; if (!handled) { // INTERNAL PROCESSING OF REQUEST try { if (output) { // output request String viewKey = view.getViewKey(); if (EntityView.VIEW_NEW.equals(viewKey) || EntityView.VIEW_EDIT.equals(viewKey) || EntityView.VIEW_DELETE.equals(viewKey) ) { // request for the create/edit/delete entity forms handled = false; // if we handle this then switch this to true if (Formats.FORM.equals(format)) { // generate new/edit/delete forms internally if the provider allows it Outputable outputable = (Outputable) entityProviderManager.getProviderByPrefixAndCapability(prefix, Outputable.class); if (outputable != null) { String[] outputFormats = outputable.getHandledOutputFormats(); if (outputFormats != null && ReflectUtils.contains(outputFormats, Formats.FORM) ) { // we are handling this type of format for this entity RequestUtils.setResponseEncoding(format, res); if (EntityView.Method.HEAD.name().equals(view.getMethod())) { // HEADER only res.setStatus(HttpServletResponse.SC_NO_CONTENT); handled = true; } else { // GET String form = entityEncodingManager.encodeEntity(prefix, format, null, view); // if the encoder returned something useful then we output it, if nothing comes back we pass on if (form != null && form.length() > 0) { try { res.getWriter().print(form); } catch (IOException e) { throw new RuntimeException("Failed to get writer from response: " + view, e); } handled = true; setNoCacheHeaders(res); res.setStatus(HttpServletResponse.SC_OK); } } } else { // format type not handled throw new FormatUnsupportedException("Outputable restriction (formats list) for " + prefix + " does not allow form generation, add the FORM format (" +Formats.FORM+") to the list of allowed output formats to enable this", view.getEntityReference()+"", format); } } } } else { Outputable outputable = (Outputable) entityProviderManager.getProviderByPrefixAndCapability(prefix, Outputable.class); if (outputable != null) { if (customAction != null) { // override format from the custom action if (actionReturn != null && actionReturn.format != null) { format = actionReturn.format; } } String[] outputFormats = outputable.getHandledOutputFormats(); if (outputFormats == null || ReflectUtils.contains(outputFormats, format) ) { // we are handling this type of format for this entity RequestUtils.setResponseEncoding(format, res); EntityReference ref = view.getEntityReference(); // get the entities to output List<EntityData> entities = null; if (customAction != null && actionReturn != null) { // get entities from a custom action entities = actionReturn.entitiesList; if (entities != null) { // recode the collection if (entities.size() > 0) { EntityData ed = entities.get(0); ref = new EntityReference(ed.getEntityRef().getPrefix(), ""); view.setEntityReference( ref ); view.setViewKey(EntityView.VIEW_LIST); } } else if (actionReturn.entityData != null) { // this was a single object return so it should be encoded as such, thus we will recode the correct reference into the view ArrayList<EntityData> eList = new ArrayList<EntityData>(); EntityData ed = actionReturn.entityData; // set title if not set if (! ed.isDisplayTitleSet()) { ed.setDisplayTitle(customAction.action); } // add to list eList.add( ed ); entities = eList; // make entity reference ref = ed.getEntityRef(); if (ref == null) { ref = new EntityReference(prefix, customAction.action); } else if (ref.getId() == null) { ref = new EntityReference(ref.getPrefix(), customAction.action); } view.setEntityReference( ref ); view.setViewKey(EntityView.VIEW_SHOW); } } else { // get from a search Search search = RequestUtils.makeSearchFromRequestParams(requestStorage.getStorageMapCopy(true, false, true, true)); // leave out headers)); entities = entityBrokerManager.getEntitiesData(ref, search, requestStorage.getStorageMapCopy()); } // set the modified header (use the sole entity in the list if there is one only) setLastModifiedHeaders(res, (entities != null && entities.size()==1 ? entities.get(0) : null), System.currentTimeMillis()); if (EntityView.Method.HEAD.name().equals(view.getMethod())) { // HEADER only res.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { // GET OutputStream outputStream = new LazyResponseOutputStream(res); /* try to use the provider formatter if one available, * if it decided not to handle it or none is available then control passes to internal */ try { OutputFormattable formattable = (OutputFormattable) entityProviderManager.getProviderByPrefixAndCapability(prefix, OutputFormattable.class); if (formattable != null) { // use provider's formatter formattable.formatOutput(ref, format, entities, requestStorage.getStorageMapCopy(), outputStream); handled = true; } } catch (FormatUnsupportedException e) { // provider decided not to handle this format handled = false; } if (!handled) { // handle internally or fail entityEncodingManager.internalOutputFormatter(ref, format, entities, requestStorage.getStorageMapCopy(), outputStream, view); // SPECIAL CASE: FORM if (Formats.FORM.equals(format)) { setNoCacheHeaders(res); } } handled = true; res.setStatus(HttpServletResponse.SC_OK); } } else { // format type not handled throw new FormatUnsupportedException("Outputable restriction (formats list) for " + prefix + " blocked handling this format ("+format+")", view.getEntityReference()+"", format); } } } } else { // input request if (EntityView.VIEW_DELETE.equals(view.getViewKey())) { // delete request Deleteable deleteable = (Deleteable) entityProviderManager.getProviderByPrefixAndCapability(prefix, Deleteable.class); if (deleteable != null) { deleteable.deleteEntity(view.getEntityReference(), requestStorage.getStorageMapCopy()); res.setStatus(HttpServletResponse.SC_NO_CONTENT); handled = true; } } else { // save request Inputable inputable = (Inputable) entityProviderManager.getProviderByPrefixAndCapability(prefix, Inputable.class); if (inputable != null) { String[] inputFormats = inputable.getHandledInputFormats(); if (inputFormats == null || ReflectUtils.contains(inputFormats, format) ) { // we are handling this type of format for this entity Object entity = null; InputStream inputStream = null; try { inputStream = req.getInputStream(); } catch (IOException e) { throw new RuntimeException("Failed to get output stream from response: " + view.getEntityReference(), e); } /* try to use the provider translator if one available, * if it decided not to handle it or none is available then control passes to internal */ try { InputTranslatable translatable = (InputTranslatable) entityProviderManager.getProviderByPrefixAndCapability(prefix, InputTranslatable.class); if (translatable != null) { // use provider's translator entity = translatable.translateFormattedData(view.getEntityReference(), format, inputStream, requestStorage.getStorageMapCopy()); handled = true; } } catch (FormatUnsupportedException e) { // provider decided not to handle this format handled = false; } if (!handled) { // use internal translators or fail entity = entityEncodingManager.internalInputTranslator(view.getEntityReference(), format, inputStream, req); } if (entity == null) { // FAILURE input could not be translated into an entity object handled = false; throw new EntityException("Unable to save entity ("+view.getEntityReference()+") with format (" +format+"), translated entity object was null", view.toString(), HttpServletResponse.SC_BAD_REQUEST); } else { // setup all the headers for the response if (EntityView.VIEW_NEW.equals(view.getViewKey())) { Createable createable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Createable.class); if (createable == null) { throw new EntityException("Unable to create new entity ("+view+"), " +Createable.class.getName()+" is not implemented for this entity type ("+prefix+")", view+"", HttpServletResponse.SC_NOT_IMPLEMENTED); } String createdId = createable.createEntity(view.getEntityReference(), entity, requestStorage.getStorageMapCopy()); if (createdId == null || "".equals(createdId)) { throw new IllegalStateException("Could not get the createdId from the newly created entity for ("+view+"), please ensure the provider is returning a non-null and non-empty value from the create method, if the item was not created then an exception should have been thrown"); } view.setEntityReference( new EntityReference(prefix, createdId) ); // update the entity view res.setHeader(EntityRequestHandler.HEADER_ENTITY_ID, createdId); res.setStatus(HttpServletResponse.SC_CREATED); // added the id to the response to make it easier on Nico try { OutputStream outputStream = res.getOutputStream(); outputStream.write( createdId.getBytes() ); } catch (IOException e) { // oh well, no id in the output } catch (RuntimeException e) { // oh well, no id in the output } } else if (EntityView.VIEW_EDIT.equals(view.getViewKey())) { Updateable updateable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Updateable.class); if (updateable == null) { throw new EntityException("Unable to create new entity ("+view+"), " +Updateable.class.getName()+" is not implemented for this entity type ("+prefix+")", view+"", HttpServletResponse.SC_NOT_IMPLEMENTED); } updateable.updateEntity(view.getEntityReference(), entity, requestStorage.getStorageMapCopy()); res.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { // FAILURE not delete, edit, or new throw new EntityException("Unable to handle entity input ("+view.getEntityReference()+"), " + "action was not understood: " + view.getViewKey(), view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST); } // return the location of this updated or created entity (without any extension) res.setHeader(EntityRequestHandler.HEADER_ENTITY_URL, view.getEntityURL() ); res.setHeader(EntityRequestHandler.HEADER_ENTITY_REFERENCE, view.getEntityReference().toString() ); handled = true; } } else { // format type not handled throw new FormatUnsupportedException("Inputable restriction for " + prefix + " blocked handling this format ("+format+")", view.getEntityReference()+"", format); } } } } } catch (FormatUnsupportedException e) { // this format could not be handled internally so we will pass it to the access provider, nothing else to do here formatInvalidFailure = true; handled = false; } catch (SecurityException se) { // AJAX/WS type security exceptions are handled specially, no redirect throw new EntityException("Security exception handling request for view ("+view+"), " + "this is typically caused by the current user not having access to the " + "data requested or the user not being logged in at all :: message=" + se.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_FORBIDDEN); } catch (EntityEncodingException e) { // translate EEE into EE - internal server error throw new EntityException("EntityEncodingException: Unable to handle " + (output ? "output" : "input") + " request for format "+view.getFormat()+" for this path (" + path + ") for prefix (" + prefix + ") for entity (" + view.getEntityReference() + "), request url (" + view.getOriginalEntityUrl() + "): " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } catch (IllegalArgumentException e) { // translate IAE into EE - bad request throw new EntityException("IllegalArgumentException: Unable to handle " + (output ? "output" : "input") + " request for format "+view.getFormat()+" for this path (" + path + ") for prefix (" + prefix + ") for entity (" + view.getEntityReference() + "), request url (" + view.getOriginalEntityUrl() + "): " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST); } catch (IllegalStateException e) { // translate ISE into EE - internal server error throw new EntityException("IllegalStateException: Unable to handle " + (output ? "output" : "input") + " request for format "+view.getFormat()+" for this path (" + path + ") for prefix (" + prefix + ") for entity (" + view.getEntityReference() + "), request url (" + view.getOriginalEntityUrl() + "): " + e.getMessage(), view.getEntityReference()+"", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } if (! handled) { // default handling, send to the access provider if there is one (if none this will throw EntityException) try { boolean accessProviderExists = handleAccessProvider(view, req, res); if (!accessProviderExists) { if (formatInvalidFailure) { // trigger the format throw new FormatUnsupportedException("Nothing (AP and internal) available to handle the requested format", view.getEntityReference()+"", view.getFormat()); } String message = "Access Provider: Attempted to access an entity URL path (" + view + ") using method ("+view.getMethod()+") for an entity (" + view.getEntityReference() + ") and view ("+view.getViewKey()+") when there is no " + "access provider to handle the request for prefix (" + view.getEntityReference().getPrefix() + ")"; throw new EntityException( message, view.toString(), HttpServletResponse.SC_METHOD_NOT_ALLOWED ); } } catch (FormatUnsupportedException e) { // TODO add in the methods "allowed" header? throw new EntityException( "AccessProvider: Method/Format unsupported: Will not handle " + (output ? "output" : "input") + " request for format "+view.getFormat()+" for this path (" + path + ") for prefix (" + prefix + ") for entity (" + view.getEntityReference() + "), request url (" + view.getOriginalEntityUrl() + ")", view.getEntityReference()+"", HttpServletResponse.SC_NOT_ACCEPTABLE ); } } } } handledReference = view.getEntityReference().toString(); requestStorage.setRequestValue(RequestStorage.ReservedKeys._requestEntityReference.name(), handledReference); // handle the after interceptor if (interceptor != null) { interceptor.after(view, req, res); } } finally { // clear the request data no matter what happens requestStorage.reset(); requestGetter.setRequest(null); requestGetter.setResponse(null); } } } } return handledReference; } /** * @see EntityBroker#fireEntityRequest(String, String, String, Map, Object) */ public EntityResponse fireEntityRequestInternal(String reference, String viewKey, String format, Map<String, String> params, Object entity) { if (reference == null) { throw new IllegalArgumentException("reference must not be null"); } // convert the reference/key/format into a URL EntityReference ref = new EntityReference(reference); EntityView ev = new EntityView(); ev.setEntityReference( ref ); if (viewKey != null && ! "".equals(viewKey)) { ev.setViewKey(viewKey); } if (format != null && ! "".equals(format)) { ev.setExtension(format); } String URL = ev.toString(); // get the right method to use Method method = Method.GET; if (EntityView.VIEW_DELETE.equals(ev.getViewKey())) { method = Method.DELETE; } else if (EntityView.VIEW_EDIT.equals(ev.getViewKey())) { method = Method.PUT; } else if (EntityView.VIEW_NEW.equals(ev.getViewKey())) { method = Method.POST; } else { method = Method.GET; } // handle entity if one was included Object data = null; if (entity != null) { String prefix = ref.getPrefix(); Inputable inputable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Inputable.class); if (inputable == null) { throw new IllegalArgumentException("This entity ("+ref+") is not Inputable so there is no reason to provide " + "a non-null entity, you should leave the entity null when firing requests to this entity"); } Outputable outputable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Outputable.class); if (outputable == null) { throw new IllegalArgumentException("This entity ("+ref+") is not AccessFormats so there is no reason to provide " + "a non-null entity, you should leave the entity null when firing requests to this entity"); } else { // handle outputing the entity data List<EntityData> entities = new ArrayList<EntityData>(); entities.add( EntityDataUtils.makeEntityData(ref, entity) ); ByteArrayOutputStream output = new ByteArrayOutputStream(); entityEncodingManager.formatAndOutputEntity(ref, format, entities, output, null); data = new ByteArrayInputStream(output.toByteArray()); } } HttpResponse httpResponse = HttpRESTUtils.fireRequest(URL, method, params, data, true); // translate response to correct kind EntityResponse response = new EntityResponse(httpResponse.getResponseCode(), httpResponse.getResponseMessage(), httpResponse.getResponseBody(), httpResponse.getResponseHeaders()); return response; } /* (non-Javadoc) * @see org.sakaiproject.entitybroker.EntityRequestHandler#handleEntityError(javax.servlet.http.HttpServletRequest, java.lang.Throwable) */ public String handleEntityError(HttpServletRequest req, Throwable error) { String msg = "Failure processing entity request ("+req.getPathInfo()+"): " + error.getMessage(); if (entityBrokerManager.getExternalIntegrationProvider() != null) { try { msg = entityBrokerManager.getExternalIntegrationProvider().handleEntityError(req, error); } catch (UnsupportedOperationException e) { // nothing to do here, this is OK } catch (Exception e) { System.out.println("WARN: EntityRequestHandler: External handleEntityError method failed, using default instead: " + e); } } return msg; } /** * Will choose whichever access provider is currently available to handle the request * @return true if there is an access provider, false otherwise */ private boolean handleAccessProvider(EntityView view, HttpServletRequest req, HttpServletResponse res) { // no special handling so send on to the standard access provider if one can be found EntityViewAccessProvider evAccessProvider = entityViewAccessProviderManager.getProvider(view.getEntityReference().getPrefix()); if (evAccessProvider == null) { if (accessProviderManager != null) { // try the old type access provider then HttpServletAccessProvider httpAccessProvider = accessProviderManager.getProvider(view.getEntityReference().getPrefix()); if (httpAccessProvider == null) { return false; } else { // classloader protection START ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); try { Object thing = httpAccessProvider; ClassLoader newClassLoader = thing.getClass().getClassLoader(); // check to see if this access provider reports the correct classloader if (thing instanceof ClassLoaderReporter) { newClassLoader = ((ClassLoaderReporter) thing).getSuitableClassLoader(); } Thread.currentThread().setContextClassLoader(newClassLoader); // send request to the access provider which will route it on to the correct entity world httpAccessProvider.handleAccess(req, res, view.getEntityReference()); } finally { Thread.currentThread().setContextClassLoader(currentClassLoader); } // classloader protection END } } } else { // check if this view key is specifically disallowed if (AccessViews.class.isAssignableFrom(evAccessProvider.getClass())) { String[] entityViewKeys = ((AccessViews)evAccessProvider).getHandledEntityViews(); if (entityViewKeys != null && ! ReflectUtils.contains(entityViewKeys, view.getViewKey()) ) { throw new EntityException("Access provider for " + view.getEntityReference().getPrefix() + " will not handle this view ("+view.getViewKey()+"): " + view, view.getEntityReference()+"", HttpServletResponse.SC_BAD_REQUEST); } } // check if this format is specifically disallowed if (AccessFormats.class.isAssignableFrom(evAccessProvider.getClass())) { String[] accessFormats = ((AccessFormats)evAccessProvider).getHandledAccessFormats(); if (accessFormats != null && ! ReflectUtils.contains(accessFormats, view.getFormat()) ) { throw new FormatUnsupportedException("Access provider for " + view.getEntityReference().getPrefix() + " will not handle this format ("+view.getFormat()+")", view.getEntityReference()+"", view.getFormat()); } } handleClassLoaderAccess(evAccessProvider, req, res, view); } return true; } /** * Wrap this in an appropriate classloader before handling the request to ensure we * do not get ugly classloader failures */ private void handleClassLoaderAccess(EntityViewAccessProvider accessProvider, HttpServletRequest req, HttpServletResponse res, EntityView view) { // START classloader protection ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); try { Object classloaderIndicator = accessProvider; ClassLoader newClassLoader = classloaderIndicator.getClass().getClassLoader(); // check to see if this access provider reports the correct classloader if (classloaderIndicator instanceof ClassLoaderReporter) { newClassLoader = ((ClassLoaderReporter) classloaderIndicator).getSuitableClassLoader(); } Thread.currentThread().setContextClassLoader(newClassLoader); // START run in classloader accessProvider.handleAccess(view, req, res); // END run in classloader } finally { Thread.currentThread().setContextClassLoader(currentClassLoader); } // END classloader protection } /** * Force a response to be set for no caching, * can be run after other headers are set * @param res the response */ protected void setNoCacheHeaders(HttpServletResponse res) { long currentTime = System.currentTimeMillis(); res.setDateHeader(ActionReturn.Header.DATE.toString(), currentTime); res.setDateHeader(ActionReturn.Header.EXPIRES.toString(), currentTime + 1000); res.setHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "must-revalidate"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "private"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "no-store"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "max-age=0"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "s-maxage=0"); } /** * Correctly sets up the basic headers for every response, * allows setting caching to be disabled by using the nocache or no-cache param * @param view * @param res * @param params * @param headers any headers to add on */ protected void setResponseHeaders(EntityView view, HttpServletResponse res, Map<String, Object> params, Map<String, String> headers) { boolean noCache = false; long currentTime = System.currentTimeMillis(); long lastModified = currentTime; if (params != null) { if (params.containsKey("no-cache") || params.containsKey("nocache")) { noCache = true; } String key = "last-modified"; if (params.containsKey(key)) { try { lastModified = ((Long) params.get(key)).longValue(); } catch (Exception e) { // nothing to do here but use the default time lastModified = currentTime; } } } setLastModifiedHeaders(res, null, lastModified); // set the cache headers res.setDateHeader(ActionReturn.Header.DATE.toString(), currentTime); res.setDateHeader(ActionReturn.Header.EXPIRES.toString(), currentTime + 600000); if (noCache) { res.setHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "must-revalidate"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "private"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "no-store"); res.setDateHeader(ActionReturn.Header.EXPIRES.toString(), currentTime + 1000); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "max-age=0"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "s-maxage=0"); } else { // response.addHeader("Cache-Control", "must-revalidate"); res.setHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "public"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "max-age=600"); res.addHeader(ActionReturn.Header.CACHE_CONTROL.toString(), "s-maxage=600"); } // set the EB specific headers String prefix = view.getEntityReference().getPrefix(); EntityProvider provider = entityProviderManager.getProviderByPrefix(prefix); res.setHeader("x-entity-prefix", prefix); res.setHeader("x-entity-reference", view.getEntityReference().toString()); res.setHeader("x-entity-url", view.getEntityURL()); res.setHeader("x-entity-format", view.getFormat()); // set Sakai sdata compliant headers res.setHeader("x-sdata-handler", provider == null ? EntityBroker.class.getName() : provider.getClass().getName()); res.setHeader("x-sdata-url", view.getOriginalEntityUrl()); // add in any extra headers last addResponseHeaders(res, headers); } /** * Adds in headers to the response as needed * @param res * @param headers */ protected void addResponseHeaders(HttpServletResponse res, Map<String, String> headers) { // add in any extra headers last if (headers != null && ! headers.isEmpty()) { for (Entry<String, String> entry : headers.entrySet()) { res.addHeader(entry.getKey(), entry.getValue()); } } } /** * @param res the response * @param lastModifiedTime the time to use if none is found any other way * @param ed (optional) some entity data if available */ protected void setLastModifiedHeaders(HttpServletResponse res, EntityData ed, long lastModifiedTime) { long lastModified = System.currentTimeMillis(); if (ed != null) { // try to get from props first boolean found = false; Object lm = ed.getEntityProperties().get("lastModified"); if (lm != null) { Long l = makeLastModified(lm); if (l != null) { lastModified = l.longValue(); found = true; } } if (!found) { if (ed.getData() != null) { // look for the annotation on the entity try { lm = ReflectUtils.getInstance().getFieldValue(ed.getData(), "lastModified", EntityLastModified.class); Long l = makeLastModified(lm); if (l != null) { lastModified = l.longValue(); found = true; } } catch (FieldnameNotFoundException e1) { // nothing to do here } } } } else { lastModified = lastModifiedTime; } // ETag or Last-Modified res.setDateHeader(ActionReturn.Header.LAST_MODIFIED.toString(), lastModified); String currentEtag = String.valueOf(lastModified); res.setHeader(ActionReturn.Header.ETAG.toString(), currentEtag); } /** * Make a last modified long from this object if possible OR return null * @param lm any object that might be the last modified date * @return the long OR null if it cannot be converted */ private Long makeLastModified(Object lm) { Long lastModified = null; if (lm != null) { Class<?> c = lm.getClass(); if (Date.class.isAssignableFrom(c)) { lastModified = ((Date)lm).getTime(); } else if (Long.class.isAssignableFrom(c)) { lastModified = ((Long)lm); } else if (String.class.isAssignableFrom(c)) { try { lastModified = new Long((String)lm); } catch (NumberFormatException e) { // nothing to do here } } else { System.out.println("WARN: EntityRequestHandler: Unknown type returned for 'lastModified' (not Date, Long, String): " + lm.getClass() + ", using the default value of current time instead"); } } return lastModified; } }