Package org.sakaiproject.entitybus.rest

Source Code of org.sakaiproject.entitybus.rest.EntityEncodingManager

/**
* $Id: EntityEncodingManager.java 56 2009-03-24 18:16:55Z azeckoski $
* $URL: http://entitybus.googlecode.com/svn/tags/entitybus-1.0.8/rest/src/main/java/org/sakaiproject/entitybus/rest/EntityEncodingManager.java $
* EntityEncodingManager.java - entity-broker - Jul 23, 2008 3:25:32 PM - azeckoski
**************************************************************************
* Copyright (c) 2008 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.osedu.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.entitybus.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.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.entitybus.EntityBrokerManager;
import org.sakaiproject.entitybus.EntityReference;
import org.sakaiproject.entitybus.EntityView;
import org.sakaiproject.entitybus.entityprovider.EntityProviderManager;
import org.sakaiproject.entitybus.entityprovider.annotations.EntityFieldRequired;
import org.sakaiproject.entitybus.entityprovider.capabilities.Createable;
import org.sakaiproject.entitybus.entityprovider.capabilities.Deleteable;
import org.sakaiproject.entitybus.entityprovider.capabilities.InputTranslatable;
import org.sakaiproject.entitybus.entityprovider.capabilities.Inputable;
import org.sakaiproject.entitybus.entityprovider.capabilities.OutputFormattable;
import org.sakaiproject.entitybus.entityprovider.capabilities.OutputSerializable;
import org.sakaiproject.entitybus.entityprovider.capabilities.Outputable;
import org.sakaiproject.entitybus.entityprovider.capabilities.Updateable;
import org.sakaiproject.entitybus.entityprovider.extension.EntityData;
import org.sakaiproject.entitybus.entityprovider.extension.Formats;
import org.sakaiproject.entitybus.entityprovider.search.Search;
import org.sakaiproject.entitybus.exception.EntityEncodingException;
import org.sakaiproject.entitybus.exception.EntityException;
import org.sakaiproject.entitybus.exception.FormatUnsupportedException;
import org.sakaiproject.entitybus.providers.EntityRequestHandler;
import org.sakaiproject.entitybus.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.HTML, Formats.FORM };

    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 + ")");
        }

        String encoded = null;
        if (EntityView.VIEW_LIST.equals(view.getViewKey())
                || ref.getId() == null) {
            // encoding a collection of entities
            StringBuilder sb = new StringBuilder(40);

            if (!("discover".equals(ref.getPrefix())||"statistics".equals(ref.getPrefix()))) {
                // 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)) {
                    sb.append("{\"" + ref.getPrefix() + "\": [\n");
                } else if (Formats.XML.equals(format)) {
                    sb.append("<" + ref.getPrefix() + ">\n");
                } else { // general case
                    sb.append(ref.getPrefix() + COLLECTION + "\n");
                }
            }

            // loop through and encode items
            int encodedEntities = 0;
            for (EntityData entity : entities) {
                try {
                    String encode = encodeEntity(ref.getPrefix(), format, entity, view);
                    if (encode.length() > 3) {
                        if (Formats.JSON.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);
                }
            }

            if (!("discover".equals(ref.getPrefix())||"statistics".equals(ref.getPrefix()))) {
                // 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)) {
                    sb.append("\n]}");
                } else if (Formats.XML.equals(format)) {
                    sb.append("</"+ ref.getPrefix() + ">");
                } else { // general case
                    sb.append("\nSize: " + encodedEntities + "\n");
                }
            }

            encoded = sb.toString();
        } else {
            // encoding a single entity
            EntityData toEncode = entities.get(0);
            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 {
                    String prefix = ref.getPrefix();

                    String segments[] = {};
                    if (params.get("pathInfo") != null) {
                        segments = params.get("pathInfo").toString().split("/");
                    }
                    if (segments.length > 3) {
                        prefix = segments[segments.length - 1].substring(0, segments[segments.length - 1].lastIndexOf("."));
                    }else if (segments[segments.length - 1].startsWith("count")) {
                        prefix = "count";
                    }

                        encoded = encodeEntity(prefix, 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)) {
            if (!("discover".equals(ref.getPrefix())||"statistics".equals(ref.getPrefix()))) {
                encoded = XML_HEADER + 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");
            sb.append("    <div style='font-weight:bold;'>"+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>"+entityData.getEntityReference()+"</td></tr>\n");
                sb.append("      <tr><td>entityURL</td><td>"+entityData.getEntityURL()+"</td></tr>\n");
                if (entityData.getEntityRef() != null) {
                    sb.append("      <tr><td>entityPrefix</td><td>"+entityData.getEntityRef().getPrefix()+"</td></tr>\n");
                    if (entityData.getEntityRef().getId() != null) {
                        sb.append("      <tr><td>entityID</td><td>"+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>"+entry.getKey()+"</td><td>"+entry.getValue()+"</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( (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)+")&nbsp;</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='"+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(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.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 {
                if (!("discover".equals(prefix)||"statistics".equals(prefix))) {
                    encoded = encodeData(toEncode, format, prefix, entityProps);
                }else{
                    encoded = (String) toEncode;
                }
            } 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);
            transcoders.put(jt.getHandledFormat(), 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) {
            Transcoder transcoder = getTranscoder(format);
            try {
                encoded = transcoder.encode(data, name, properties);
            } catch (RuntimeException e) {
                // convert failure to UOE
                throw new UnsupportedOperationException("Failure encoding data ("+data+") of type ("+data.getClass()+"): " + e.getMessage(), e);
            }
        }
        return encoded;
    }

    /**
     * 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;
    //    }

}
TOP

Related Classes of org.sakaiproject.entitybus.rest.EntityEncodingManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.