/** * $Id: EntityEncodingManager.java 114087 2012-10-08 14:31:47Z azeckoski@unicon.net $ * $URL: https://source.sakaiproject.org/svn/entitybroker/trunk/rest/src/java/org/sakaiproject/entitybroker/rest/EntityEncodingManager.java $ * EntityEncodingManager.java - entity-broker - Jul 23, 2008 3:25:32 PM - azeckoski ************************************************************************** * Copyright (c) 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.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; 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.apache.commons.lang.StringEscapeUtils; import org.azeckoski.reflectutils.ClassFields; import org.azeckoski.reflectutils.ConstructorUtils; import org.azeckoski.reflectutils.ReflectUtils; import org.azeckoski.reflectutils.StringUtils; import org.azeckoski.reflectutils.ClassFields.FieldsFilter; import org.azeckoski.reflectutils.map.ArrayOrderedMap; import org.azeckoski.reflectutils.transcoders.HTMLTranscoder; import org.azeckoski.reflectutils.transcoders.JSONTranscoder; import org.azeckoski.reflectutils.transcoders.Transcoder; import org.azeckoski.reflectutils.transcoders.XMLTranscoder; import org.sakaiproject.entitybroker.EntityBrokerManager; import org.sakaiproject.entitybroker.EntityReference; import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.entityprovider.EntityProviderManager; import org.sakaiproject.entitybroker.entityprovider.annotations.EntityFieldRequired; import org.sakaiproject.entitybroker.entityprovider.capabilities.Createable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Deleteable; import org.sakaiproject.entitybroker.entityprovider.capabilities.DepthLimitable; 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.OutputSerializable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Outputable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Updateable; import org.sakaiproject.entitybroker.entityprovider.extension.EntityData; import org.sakaiproject.entitybroker.entityprovider.extension.Formats; import org.sakaiproject.entitybroker.entityprovider.search.Search; import org.sakaiproject.entitybroker.exception.EntityEncodingException; import org.sakaiproject.entitybroker.exception.EntityException; import org.sakaiproject.entitybroker.exception.FormatUnsupportedException; import org.sakaiproject.entitybroker.providers.EntityRequestHandler; import org.sakaiproject.entitybroker.util.EntityDataUtils; /** * This handles the internal encoding (translation and formatting) of entity data, * this can be used by various parts of the EB system <br/> * this is for internal use only currently but may be exposed later * * @author Aaron Zeckoski (azeckoski @ gmail.com) */ public class EntityEncodingManager { public static final String ENTITY_REFERENCE = "entityReference"; public static final String ENTITY_ID = "entityId"; public static final String ENTITY_URL = "entityURL"; public static final String ENTITY_TITLE = "entityTitle"; public static final String ENTITY_PREFIX = "entityPrefix"; public static final String COLLECTION = "_collection"; public static final String BATCH_PREFIX = '/' + EntityRequestHandler.BATCH + '?' + EntityBatchHandler.REFS_PARAM_NAME + '='; public static final String[] HANDLED_INPUT_FORMATS = new String[] { Formats.XML, Formats.JSON, Formats.HTML }; public static final String[] HANDLED_OUTPUT_FORMATS = new String[] { Formats.XML, Formats.JSON, Formats.JSONP, Formats.HTML, Formats.FORM }; public static final String JSON_CALLBACK_PARAM = "jsonCallback"; public static final String JSON_DEFAULT_CALLBACK = "jsonEntityFeed"; protected static final String XML_HEADER_PREFIX = "<?"; protected static final String XML_HEADER_SUFFIX = "?>"; protected static final String XML_HEADER = XML_HEADER_PREFIX + "xml version=\"1.0\" encoding=\"UTF-8\" "+XML_HEADER_SUFFIX+"\n"; protected static final String XHTML_HEADER = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" " + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" + "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" + "<head>\n" + " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + " <title>{title}</title>\n" + "</head>\n" + "<body>\n"; protected static final String XHTML_FOOTER = "</body>\n</html>\n"; protected EntityEncodingManager() { } public EntityEncodingManager(EntityProviderManager entityProviderManager, EntityBrokerManager entityBrokerManager) { super(); this.entityProviderManager = entityProviderManager; this.entityBrokerManager = entityBrokerManager; } private EntityProviderManager entityProviderManager; public void setEntityProviderManager(EntityProviderManager entityProviderManager) { this.entityProviderManager = entityProviderManager; } private EntityBrokerManager entityBrokerManager; public void setEntityBrokerManager(EntityBrokerManager entityBrokerManager) { this.entityBrokerManager = entityBrokerManager; } /** * Format and output an entity or collection included or referred to by this entity ref object * into output according to the format string provided, * Should take into account the reference when determining what the entities are * and how to encode them * (This is basically a copy of the code in EntityHandlerImpl with stuff removed) * * @param ref a globally unique reference to an entity, * consists of the entity prefix and optional segments * @param format a string constant indicating the format (from {@link Formats}) * for output, (example: {@link #XML}) * @param entities (optional) a list of entities to create formatted output for, * if this is null then the entities should be retrieved based on the reference, * if this contains only a single item AND the ref refers to a single entity * then the entity should be extracted from the list and encoded without the indication * that it is a collection, for all other cases the encoding should include an indication that * this is a list of entities * @param outputStream the output stream to place the formatted data in, * should be UTF-8 encoded if there is char data * @throws FormatUnsupportedException if you do not handle this format type (passes control to the internal handlers) * @throws EntityEncodingException if you cannot encode the received data into an entity * @throws IllegalArgumentException if any of the arguments are invalid * @throws IllegalStateException for all other failures */ public void formatAndOutputEntity(EntityReference ref, String format, List<EntityData> entities, OutputStream outputStream, Map<String, Object> params) { if (ref == null || format == null || outputStream == null) { throw new IllegalArgumentException("ref, format, and output cannot be null"); } String prefix = ref.getPrefix(); Outputable outputable = (Outputable) entityProviderManager.getProviderByPrefixAndCapability(prefix, Outputable.class); if (outputable != null) { String[] outputFormats = outputable.getHandledOutputFormats(); // check if the output formats are allowed if (outputFormats == null || ReflectUtils.contains(outputFormats, format) ) { boolean handled = false; // if the user wants to serialize their objects specially then allow them to translate them OutputSerializable serializable = entityProviderManager.getProviderByPrefixAndCapability(prefix, OutputSerializable.class); if (serializable != null) { if (entities == null) { // these will be EntityData entities = entityBrokerManager.getEntitiesData(ref, new Search(), params); } if (! entities.isEmpty()) { // find the type of the objects this providers deals in Object sample = entityBrokerManager.getSampleEntityObject(prefix, null); Class<?> entityType = Object.class; if (sample != null) { entityType = sample.getClass(); } // now translate the objects to serialize form for (EntityData entityData : entities) { Object entity = entityData.getData(); // only translate if the entity is set if (entity != null) { // only translate if the type matches if (entityType.isAssignableFrom(entity.getClass())) { try { entity = serializable.makeSerializableObject(ref, entity); entityData.setData(entity); } catch (Exception e) { throw new RuntimeException("Failure while attempting to serialize the object from ("+entity+") for ref("+ref+"): " + e.getMessage(), e); } } } } } } /* 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 = entityProviderManager.getProviderByPrefixAndCapability(prefix, OutputFormattable.class); if (formattable != null) { // use provider's formatter formattable.formatOutput(ref, format, entities, params, outputStream); handled = true; } } catch (FormatUnsupportedException e) { // provider decided not to handle this format handled = false; } if (!handled) { // handle internally or fail internalOutputFormatter(ref, format, entities, params, outputStream, null); } } else { // format type not handled throw new FormatUnsupportedException("Outputable restriction for " + prefix + " blocked handling this format ("+format+")", ref+"", format); } } } /** * Translates the input data stream in the supplied format into an entity object for this reference * (This is basically a copy of the code in EntityHandlerImpl with stuff removed) * * @param ref a globally unique reference to an entity, * consists of the entity prefix and optional segments * @param format a string constant indicating the format (from {@link Formats}) * of the input, (example: {@link #XML}) * @param input a stream which contains the data to make up this entity, * you may assume this is UTF-8 encoded if you don't know anything else about it * @return an entity object of the type used for the given reference * @throws FormatUnsupportedException if you do not handle this format type (passes control to the internal handlers) * @throws EntityEncodingException if you cannot encode the received data into an entity * @throws IllegalArgumentException if any of the arguments are invalid * @throws IllegalStateException for all other failures */ public Object translateInputToEntity(EntityReference ref, String format, InputStream inputStream, Map<String, Object> params) { if (ref == null || format == null || inputStream == null) { throw new IllegalArgumentException("ref, format, and inputStream cannot be null"); } Object entity = null; String prefix = ref.getPrefix(); Inputable inputable = (Inputable) entityProviderManager.getProviderByPrefixAndCapability(prefix, Inputable.class); if (inputable != null) { String[] inputFormats = inputable.getHandledInputFormats(); if (inputFormats == null || ReflectUtils.contains(inputFormats, format) ) { boolean handled = false; /* 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(ref, format, inputStream, params); handled = true; } } catch (FormatUnsupportedException e) { // provider decided not to handle this format handled = false; } if (!handled) { // use internal translators or fail entity = internalInputTranslator(ref, format, inputStream, null); } if (entity == null) { // FAILURE input could not be translated into an entity object throw new EntityEncodingException("Unable to translate entity ("+ref+") with format (" +format+"), translated entity object was null", ref+""); } } else { // format type not handled throw new FormatUnsupportedException("Inputable restriction for " + prefix + " blocked handling this format ("+format+")", ref+"", format); } } return entity; } /** * Will attempt to validate that string data is of a specific format * @param data a chunk of data to validate * @param format the format which the data is supposed encoded in * @return true if the data appears valid for the given format, false otherwise */ public boolean validateFormat(String data, String format) { // note: this is a weak implementation for now -AZ boolean valid = false; if (data == null || format == null) { throw new IllegalArgumentException("Cannot validate format when the data ("+data+") OR the format ("+format+") are null"); } data = data.trim(); if (Formats.XML.equals(format)) { if (data.startsWith("<") && data.endsWith(">")) { valid = true; } } else if (Formats.JSON.equals(format)) { if (data.startsWith("{") && data.endsWith("}")) { valid = true; } } else if (Formats.HTML.equals(format)) { if (data.startsWith("<") && data.endsWith(">")) { valid = true; } } else { valid = true; } return valid; } // FUNCTIONAL CODE BELOW /** * Handled the internal encoding of data into an entity object * * @param ref the entity reference * @param format the format which the input is encoded in * @param input the data being input * @return the entity object based on the data * @throws FormatUnsupportedException if you do not handle this format type (passes control to the internal handlers) * @throws EntityEncodingException if you cannot encode the received data into an entity * @throws IllegalArgumentException if any of the arguments are invalid * @throws IllegalStateException for all other failures */ @SuppressWarnings("unchecked") public Object internalInputTranslator(EntityReference ref, String format, InputStream input, HttpServletRequest req) { Object entity = null; Inputable inputable = entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), Inputable.class); if (inputable != null) { // get a the current entity object or a sample Object current = entityBrokerManager.getSampleEntityObject(ref.getPrefix(), ref.getId()); if (current != null) { if (Formats.HTML.equals(format) || format == null || "".equals(format)) { // html req handled specially if (req != null) { Map<String, String[]> params = req.getParameterMap(); if (params != null && params.size() > 0) { entity = current; try { ReflectUtils.getInstance().populateFromParams(entity, params); } catch (RuntimeException e) { throw new EntityEncodingException("Unable to populate bean for ref ("+ref+") from request: " + e.getMessage(), ref+"", e); } } else { // no request params, bad request throw new EntityException("No request params for html input request (there must be at least one) for reference: " + ref, ref.toString(), HttpServletResponse.SC_BAD_REQUEST); } } } else { // all other formats if (input == null) { // no request params, bad request throw new EntityException("No input for input translation (input cannot be null) for reference: " + ref, ref.toString(), HttpServletResponse.SC_BAD_REQUEST); } else { String data = StringUtils.makeStringFromInputStream(input); Map<String, Object> decoded = null; try { decoded = decodeData(data, format); } catch (IllegalArgumentException iae) { throw new EntityEncodingException("No encoder available for the given format ("+format+"), ref=" + ref + ":" + iae.getMessage(), ref.toString(), iae); } catch (UnsupportedOperationException uoe) { throw new EntityEncodingException("Failure during internal input encoding of entity: " + ref + " to format ("+format+"):" + uoe.getMessage(), ref.toString(), uoe); } entity = current; // handle the special case where the JSON was created by xstream or something else that puts the data inside an object with a "root" if (decoded.size() == 1 && decoded.containsKey(ref.getPrefix())) { Object o = decoded.get(ref.getPrefix()); if (o instanceof Map) { decoded = (Map<String, Object>) o; } } try { ReflectUtils.getInstance().populate(entity, decoded); } catch (RuntimeException e) { throw new EntityEncodingException("Unable to populate bean for ref ("+ref+") from data: " + decoded + ":" + e.getMessage(), ref+"", e); } } } } } else { throw new IllegalArgumentException("This entity ("+ref+") does not allow input translation"); } if (entity == null) { throw new EntityException("Unable to encode entity from input for reference: " + ref, ref.toString(), HttpServletResponse.SC_BAD_REQUEST); } return entity; } /** * Format entities for output based on the reference into a format, * use the provided list or get the entities * * @param ref the entity reference for this, * if this is a reference to a collection then this will be rendered as a collection of entities, * if a reference to a single entity then only the matching one from the collection will be used * @param format the format to use for the output data * @param entities (optional) if this is null then the entities will be fetched * @param output the outputstream to place the encoded data into * @param view (optional) * @throws FormatUnsupportedException if you do not handle this format type (passes control to the internal handlers) * @throws EntityEncodingException if you cannot encode the received data into an entity * @throws IllegalArgumentException if any of the arguments are invalid * @throws IllegalStateException for all other failures */ public void internalOutputFormatter(EntityReference ref, String format, List<EntityData> entities, Map<String, Object> params, OutputStream output, EntityView view) { if (format == null) { format = Outputable.HTML; } // check the format to see if we can handle it if (! ReflectUtils.contains(HANDLED_OUTPUT_FORMATS, format)) { throw new FormatUnsupportedException("Internal output formatter cannot handle format ("+format+") for ref ("+ref+")", ref+"", format); } if (view == null) { view = entityBrokerManager.makeEntityView(ref, null, null); } // get the entities if not supplied if (entities == null) { // these will be EntityData entities = entityBrokerManager.getEntitiesData(ref, new Search(), params); } if (entities.isEmpty()) { // just log this for now System.out.println("INFO: EntityEncodingManager: No entities to format ("+format+") and output for ref (" + ref + ")"); } // SAK-22738 - do not show form editing when batch processing is disabled String replacementEncoding = null; if (Formats.FORM.equals(format) && !entityBrokerManager.getExternalIntegrationProvider().getConfigurationSetting(EntityBatchHandler.CONFIG_BATCH_ENABLE, EntityBatchHandler.CONFIG_BATCH_DEFAULT)) { String msg = "FORM editing is not enabled because the batch provider is disabled by sakai config: "+EntityBatchHandler.CONFIG_BATCH_ENABLE+"=false. Enable this config setting with "+EntityBatchHandler.CONFIG_BATCH_ENABLE+"=true to enable batch handling. See SAK-22619 for details."; replacementEncoding = "<div style=\"font-weight:bold;color:red;\">"+msg+"</div>"; } String encoded = null; if (EntityView.VIEW_LIST.equals(view.getViewKey()) || ref.getId() == null) { // encoding a collection of entities StringBuilder sb = new StringBuilder(40); // make header if (Formats.HTML.equals(format) || Formats.FORM.equals(format)) { sb.append("<h1>"+ref.getPrefix() + COLLECTION + "</h1>\n"); } else if (Formats.JSON.equals(format) || Formats.JSONP.equals(format)) { sb.append("{\""+ENTITY_PREFIX+"\": \""+ref.getPrefix() + "\", \"" + ref.getPrefix() + COLLECTION + "\": [\n"); } else if (Formats.XML.equals(format)) { sb.append("<" + ref.getPrefix() + COLLECTION + " " + ENTITY_PREFIX + "=\"" + ref.getPrefix() + "\">\n"); } else { // general case sb.append(ref.getPrefix() + COLLECTION + "\n"); } int encodedEntities = 0; if (replacementEncoding != null) { sb.append(replacementEncoding); } else { // loop through and encode items for (EntityData entity : entities) { try { String encode = encodeEntity(ref.getPrefix(), format, entity, view); if (encode.length() > 3) { if ((Formats.JSON.equals(format) || Formats.JSONP.equals(format)) && encodedEntities > 0) { sb.append(","); } sb.append(encode); encodedEntities++; } } catch (RuntimeException e) { throw new EntityEncodingException("Failure during internal output encoding of entity set on entity: " + ref, ref.toString(), e); } } } // make footer if (Formats.HTML.equals(format) || Formats.FORM.equals(format)) { sb.append("\n<b>Collection size:</b> "+encodedEntities+"\n"); } else if (Formats.JSON.equals(format) || Formats.JSONP.equals(format)) { sb.append("\n]}"); } else if (Formats.XML.equals(format)) { sb.append("</" + ref.getPrefix() + COLLECTION + ">"); } else { // general case sb.append("\nSize: " + encodedEntities + "\n"); } encoded = sb.toString(); } else { // encoding a single entity EntityData toEncode = entities.get(0); if (replacementEncoding != null) { encoded = replacementEncoding; } else { if (toEncode == null) { throw new EntityEncodingException("Failed to encode data for entity (" + ref + "), entity object to encode could not be found (null object in list)", ref.toString()); } else { try { encoded = encodeEntity(ref.getPrefix(), format, toEncode, view); } catch (RuntimeException e) { throw new EntityEncodingException("Failure during internal output encoding of entity: " + ref, ref.toString(), e); } } } } // add the HTML headers and footers if (Formats.FORM.equals(format)) { String title = view.getViewKey() + ":" + ref; encoded = XML_HEADER + XHTML_HEADER.replace("{title}", title) + encoded + XHTML_FOOTER; } else if (Formats.XML.equals(format)) { encoded = XML_HEADER + encoded; } else if (Formats.JSONP.equals(format)) { String callback = JSON_DEFAULT_CALLBACK; if (params != null && params.containsKey(JSON_CALLBACK_PARAM)) { callback = sanitizeJsonCallback(params.get(JSON_CALLBACK_PARAM)); } encoded = callback + "(" + encoded + ")"; } // put the encoded data into the stream try { byte[] b = encoded.getBytes(Formats.UTF_8); output.write(b); } catch (UnsupportedEncodingException e) { throw new EntityEncodingException("Failed to encode UTF-8: " + ref, ref.toString(), e); } catch (IOException e) { throw new EntityEncodingException("Failed to encode into output stream: " + ref, ref.toString(), e); } } /** * Encodes entity data * @param prefix the entity prefix related to this data * @param format the format to encode the data into * @param entityData (optional) entity data to encode * @param view (optional) used to generate links and determine the correct incoming view * @return the encoded entity or "" if encoding fails */ public String encodeEntity(String prefix, String format, EntityData entityData, EntityView view) { if (prefix == null || format == null) { throw new IllegalArgumentException("prefix and format must not be null"); } if (entityData == null && ! Formats.FORM.equals(format)) { throw new IllegalArgumentException("entityData to encode must not be null for prefix ("+prefix+") and format ("+format+")"); } String encoded = ""; if (Formats.HTML.equals(format)) { // special handling for HTML StringBuilder sb = new StringBuilder(200); sb.append(" <div style='padding-left:1em;'>\n"); if (entityData == null) { sb.append("NO DATA to encode"); } else { sb.append(" <div style='font-weight:bold;'>"+StringEscapeUtils.escapeHtml(entityData.getDisplayTitle())+"</div>\n"); sb.append(" <table border='1'>\n"); sb.append(" <caption style='font-weight:bold;'>Entity Data</caption>\n"); if (! entityData.isDataOnly()) { sb.append(" <tr><td>entityReference</td><td>"+StringEscapeUtils.escapeHtml(entityData.getEntityReference())+"</td></tr>\n"); sb.append(" <tr><td>entityURL</td><td>"+StringEscapeUtils.escapeHtml(entityData.getEntityURL())+"</td></tr>\n"); if (entityData.getEntityRef() != null) { sb.append(" <tr><td>entityPrefix</td><td>"+StringEscapeUtils.escapeHtml(entityData.getEntityRef().getPrefix())+"</td></tr>\n"); if (entityData.getEntityRef().getId() != null) { sb.append(" <tr><td>entityID</td><td>"+StringEscapeUtils.escapeHtml(entityData.getEntityRef().getId())+"</td></tr>\n"); } } } if (entityData.getData() != null) { sb.append(" <tr><td>entity-type</td><td>"+entityData.getData().getClass().getName()+"</td></tr>\n"); // dump entity data sb.append(" <tr><td colspan='2'>Data:<br/>\n"); sb.append( encodeData(entityData.getData(), Formats.HTML, null, null) ); sb.append(" </td></tr>\n"); } else { sb.append(" <tr><td>entity-object</td><td><i>null</i></td></tr>\n"); } sb.append(" </table>\n"); Map<String, Object> props = entityData.getEntityProperties(); if (!props.isEmpty()) { sb.append(" <table border='1'>\n"); sb.append(" <caption style='font-weight:bold;'>Properties</caption>\n"); for (Entry<String, Object> entry : props.entrySet()) { sb.append(" <tr><td>"+StringEscapeUtils.escapeHtml(entry.getKey())+"</td><td>"+StringEscapeUtils.escapeHtml(entry.getValue().toString())+"</td></tr>\n"); } sb.append(" </table>\n"); } } sb.append(" </div>\n"); encoded = sb.toString(); } else if (Formats.FORM.equals(format)) { // special handling for FORM type if (view == null) { throw new IllegalArgumentException("the view must be set for FORM handling and generation"); } boolean handle = false; boolean createable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Createable.class) != null; boolean updateable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Updateable.class) != null; boolean deleteable = entityProviderManager.getProviderByPrefixAndCapability(prefix, Deleteable.class) != null; String viewKey = view.getViewKey(); if (EntityView.VIEW_NEW.equals(viewKey) && createable) { handle = true; } else if (EntityView.VIEW_EDIT.equals(viewKey) && updateable) { handle = true; } else if (EntityView.VIEW_DELETE.equals(viewKey) && deleteable) { handle = true; } else if ( (EntityView.VIEW_LIST.equals(viewKey) || EntityView.VIEW_SHOW.equals(viewKey)) && (updateable || deleteable)) { // we handle these only if the stuff can be changed handle = true; } if (handle) { // fix up URL stuff first String prefixUrl = entityBrokerManager.getServletContext(); if (EntityView.VIEW_LIST.equals(viewKey) && entityData != null && entityData.getEntityId() != null) { // SPECIAL CASE: if this is a list then the view refers to the space only so we have to copy it view = view.copy(); view.setEntityReference( new EntityReference(prefix, entityData.getEntityId()) ); } // now create the output data StringBuilder sb = new StringBuilder(300); String formName = prefix + "-" + (entityData != null ? entityData.getEntityRef().getId() : "xxx"); sb.append(" <div style='font-weight:bold;'>"); sb.append( StringEscapeUtils.escapeHtml(entityData != null ? entityData.getDisplayTitle() : prefix) ); if (createable && ! EntityView.VIEW_NEW.equals(viewKey)) { // add the new link if this is not the create form sb.append(" (<a href='" + prefixUrl + view.getEntityURL(EntityView.VIEW_NEW, Formats.FORM) + "'>NEW</a>) "); } if (deleteable && ! EntityView.VIEW_NEW.equals(viewKey)) { String formAction = makeFormViewUrl(prefixUrl, EntityView.VIEW_DELETE, view) + "&_method=DELETE"; sb.append("\n <form name='"+formName+"-del' action='"+formAction+"' style='margin:0px; display:inline;' method='post'>\n"); sb.append(" <input type='submit' value='DEL' />\n"); sb.append(" </form>\n"); } sb.append("</div>\n"); if (! EntityView.VIEW_DELETE.equals(viewKey)) { // only render all this stuff for non-delete forms if ( (entityData == null || entityData.getData() == null) && (EntityView.VIEW_LIST.equals(viewKey) || EntityView.VIEW_SHOW.equals(viewKey)) ) { // die if we have no data to encode and this is a SHOW/LIST view throw new EntityEncodingException("Unable to find an entity to encode into the update form; prefix="+prefix+":"+view, prefix); } Object entity = (entityData != null ? entityData.getData() : null); if (entity == null) { // get the entity from the URL instead String id = (view.getEntityReference() != null ? view.getEntityReference().getId() : null); if (id == null) { id = (entityData != null ? entityData.getEntityId() : null); } entity = entityBrokerManager.getSampleEntityObject(prefix, id); if (entity == null) { throw new EntityEncodingException("Unable to find entity data to create form from using prefix="+prefix+",id="+id, prefix); } } Class<?> entityClass = entity.getClass(); String formAction; if (EntityView.VIEW_NEW.equals(viewKey)) { formAction = makeFormViewUrl(prefixUrl, EntityView.VIEW_NEW, view); } else { formAction = makeFormViewUrl(prefixUrl, EntityView.VIEW_EDIT, view) + "&_method=PUT"; } sb.append(" <form name='"+formName+"-edit' action='"+formAction+"' style='margin:0px;' method='post'>\n"); sb.append(" <table border='1'>\n"); // get all the read and write fields from this object ClassFields<?> cf = ReflectUtils.getInstance().analyzeClass(entityClass); Map<String, Object> fieldValues = ReflectUtils.getInstance().getObjectValues(entity); Map<String, Class<?>> readTypes = cf.getFieldTypes(FieldsFilter.SERIALIZABLE); Map<String, Class<?>> writeTypes = cf.getFieldTypes(FieldsFilter.WRITEABLE); HashSet<String> requiredFieldNames = new HashSet<String>(cf.getFieldNamesWithAnnotation(EntityFieldRequired.class)); // make sure no one tries to write the id field when not creating entities String idFieldName = EntityDataUtils.getEntityIdField(entityClass); if (idFieldName != null && ! EntityView.VIEW_NEW.equals(viewKey)) { writeTypes.remove(idFieldName); } Map<String, Class<?>> entityTypes = new HashMap<String, Class<?>>(readTypes); entityTypes.putAll(writeTypes); ArrayList<String> keys = new ArrayList<String>(entityTypes.keySet()); Collections.sort(keys); for (int i = 0; i < keys.size(); i++) { String fieldName = keys.get(i); Class<?> type = entityTypes.get(fieldName); boolean read = true; boolean write = false; if (! readTypes.containsKey(fieldName)) { // write only read = false; write = true; } else if (! writeTypes.containsKey(fieldName)) { // read only write = false; } else { // read/write write = true; } boolean required = requiredFieldNames.contains(fieldName); // get the printable type names String typeName = type.getName(); if (String.class.getName().equals(typeName)) { typeName = "string"; } else if (Boolean.class.getName().equals(typeName)) { typeName = "boolean"; } else if (Integer.class.getName().equals(typeName)) { typeName = "int"; } else if (Long.class.getName().equals(typeName)) { typeName = "long"; } sb.append(" <tr><td>"+(i+1)+") </td>" + "<td style='font-weight:bold;'>"+ fieldName +"</td>" + "<td>"+ typeName +"</td><td>"); if (read && write) { Object value = fieldValues.get(fieldName); String sVal = ""; if (value != null) { sVal = ReflectUtils.getInstance().convert(value, String.class); } sb.append("<input type='text' name=\""+fieldName+"\" value=\""+StringEscapeUtils.escapeHtml(sVal)+"\" />"); } else if (write) { sb.append("<input type='text' name='"+fieldName+"' />"); } else if (read) { Object value = fieldValues.get(fieldName); String sVal = ""; if (value != null) { sVal = ReflectUtils.getInstance().convert(value, String.class); } sb.append(StringEscapeUtils.escapeHtml(sVal)); } if (required) { sb.append(" <b style='color:red;'>*</b> "); } sb.append("</td></tr>\n"); } sb.append(" </table>\n"); sb.append(" <input type='submit' value='SAVE' />\n"); sb.append(" </form>\n"); } encoded = sb.toString(); } } else { // encode the entity itself Object toEncode = entityData; // default to encoding the entity data object Map<String, Object> entityProps = new ArrayOrderedMap<String, Object>(); if (entityData != null && entityData.getData() != null) { if (entityData.isDataOnly()) { toEncode = entityData.getData(); // no meta data except properties if there are any entityProps.putAll( entityData.getEntityProperties() ); } else { if (ConstructorUtils.isClassBean(entityData.getData().getClass())) { // encode the bean directly if it is one toEncode = entityData.getData(); // add in the extra props entityProps.put(ENTITY_REFERENCE, entityData.getEntityReference()); entityProps.put(ENTITY_URL, entityData.getEntityURL()); if (entityData.getEntityRef().getId() != null) { entityProps.put(ENTITY_ID, entityData.getEntityRef().getId()); } if (entityData.isDisplayTitleSet()) { entityProps.put(ENTITY_TITLE, entityData.getDisplayTitle()); } } } } // do the encoding try { encoded = encodeData(toEncode, format, prefix, entityProps); } catch (IllegalArgumentException e) { // no transcoder so just toString this and dump it out encoded = prefix + " : " + entityData; } } return encoded; } /** * @return the form view URLs which should be used with the forms */ protected String makeFormViewUrl(String contextUrl, String viewKey, EntityView view) { if (viewKey == null || "".equals(viewKey)) { viewKey = EntityView.VIEW_SHOW; } return contextUrl + BATCH_PREFIX + contextUrl + view.getEntityURL(viewKey, null); } protected static final String DATA_KEY = Transcoder.DATA_KEY; private Map<String, Transcoder> transcoders; public void setTranscoders(Map<String, Transcoder> transcoders) { this.transcoders = transcoders; } /** * Override the transcoder used for a specific format * @param transcoder a transcoder implementation */ public void setTranscoder(Transcoder transcoder) { if (transcoder == null) { throw new IllegalArgumentException("transcoder cannot be null"); } if (transcoders == null) { getTranscoder(Formats.XML); } String format = transcoder.getHandledFormat(); if (format != null && transcoder != null) { transcoders.put(format, transcoder); } } public Transcoder getTranscoder(String format) { if (transcoders == null) { transcoders = new HashMap<String, Transcoder>(); JSONTranscoder jt = new JSONTranscoder(true, true, false); jt.setMaxLevel(entityBrokerManager.getMaxJSONLevel()); transcoders.put(jt.getHandledFormat(), jt); transcoders.put(Formats.JSONP, jt); XMLTranscoder xt = new XMLTranscoder(true, true, false, false); transcoders.put(xt.getHandledFormat(), xt); HTMLTranscoder ht = new HTMLTranscoder(); transcoders.put(ht.getHandledFormat(), ht); } Transcoder transcoder = transcoders.get(format); if (transcoder == null) { throw new IllegalArgumentException("Failed to find a transcoder for format, none exists, cannot encode or decode data for format: " + format); } return transcoder; } /** * Encode data into a given format, can handle any java object, * note that unsupported formats will result in an exception * * @param data the data to encode (can be a POJO or Map or pretty much any java object) * @param format the format to use for output (from {@link Formats}) * @param name (optional) the name to use for the encoded data (e.g. root node for XML) * @param properties (optional) extra properties to add into the encoding, ignored if encoded object is not a map or bean * @return the encoded string * @throws UnsupportedOperationException if the data cannot be encoded */ public String encodeData(Object data, String format, String name, Map<String, Object> properties) { if (format == null) { format = Formats.XML; } String encoded = ""; if (data != null) { int maxDepth = 0; if (name != null) { DepthLimitable provider = (DepthLimitable) entityProviderManager.getProviderByPrefixAndCapability(name, DepthLimitable.class); if (provider != null) { maxDepth = provider.getMaxDepth(); } } Transcoder transcoder = getTranscoder(format); try { if (maxDepth == 0) { encoded = transcoder.encode(data, name, properties); } else { encoded = transcoder.encode(data, name, properties, maxDepth); } } catch (RuntimeException e) { // convert failure to UOE throw new UnsupportedOperationException("Failure encoding data ("+data+") of type ("+data.getClass()+"): " + e.getMessage(), e); } } return encoded; } /** * Clean the JSONP callback parameter to make sure it is sensible * @param param The parameter for the callback, should be a String * @return The string version of the param or the default callback name */ protected String sanitizeJsonCallback(Object param) { //We might want to sanitize down to something that looks like a valid function call //This shouldn't be necessary, though, since it will just either work or not if (param == null || !(param instanceof String)) return JSON_DEFAULT_CALLBACK; else return param.toString(); } /** * Decode a string of a specified format into a java map <br/> * Returned map can be fed into the {@link ReflectUtils#populate(Object, Map)} if you want to convert it * into a known object type <br/> * Types are likely to require conversion as guesses are made about the right formats, * use of the {@link ReflectUtils#convert(Object, Class)} method is recommended * * @param data encoded data * @param format the format of the encoded data (from {@link Formats}) * @return a map containing all the data derived from the encoded data * @throws UnsupportedOperationException if the data cannot be decoded */ @SuppressWarnings("unchecked") public Map<String, Object> decodeData(String data, String format) { if (format == null) { format = Formats.XML; } Map<String, Object> decoded = new ArrayOrderedMap<String, Object>(); if (data != null && ! "".equals(data)) { Object decode = null; Transcoder transcoder = getTranscoder(format); try { decode = transcoder.decode(data); if (decode instanceof Map) { decoded = (Map<String, Object>) decode; } else { decoded.put(DATA_KEY, decode); } } catch (RuntimeException e) { // convert failure to UOE throw new UnsupportedOperationException("Failure decoding data ("+data+") for format ("+format+"): " + e.getMessage(), e); } } return decoded; } /** * Using GSON is hopeless: * http://code.google.com/p/google-gson/issues/detail?id=45 * * Gson gson = getGson(); * encoded = gson.toJson(data, new TypeToken<Map<String, Object>>() {}.getType()); * decoded = gson.fromJson(data, new TypeToken<Map<String, Object>>() {}.getType()); */ // protected SoftReference<Gson> gsonCoder = null; // /** // * Get the gson encoder in an efficient way to avoid recreating it over and over and over again // * @return the gson encoder // */ // protected Gson getGson() { // Gson gson = gsonCoder == null ? null : gsonCoder.get(); // if (gson == null) { // gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); // gsonCoder = new SoftReference<Gson>(gson); // } // return gson; // } }