Package com.psddev.cms.tool

Source Code of com.psddev.cms.tool.ToolPageContext

package com.psddev.cms.tool;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;

import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.PageContext;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;

import com.google.common.collect.ImmutableMap;
import com.psddev.cms.db.Content;
import com.psddev.cms.db.Draft;
import com.psddev.cms.db.History;
import com.psddev.cms.db.ImageTag;
import com.psddev.cms.db.LayoutTag;
import com.psddev.cms.db.Page;
import com.psddev.cms.db.Renderer;
import com.psddev.cms.db.ResizeOption;
import com.psddev.cms.db.Schedule;
import com.psddev.cms.db.Site;
import com.psddev.cms.db.StandardImageSize;
import com.psddev.cms.db.Template;
import com.psddev.cms.db.ToolFormWriter;
import com.psddev.cms.db.ToolUi;
import com.psddev.cms.db.ToolUser;
import com.psddev.cms.db.Trash;
import com.psddev.cms.db.Variation;
import com.psddev.cms.db.WorkStream;
import com.psddev.cms.db.Workflow;
import com.psddev.cms.db.WorkflowLog;
import com.psddev.cms.db.WorkflowState;
import com.psddev.cms.db.WorkflowTransition;
import com.psddev.dari.db.Application;
import com.psddev.dari.db.CompoundPredicate;
import com.psddev.dari.db.Database;
import com.psddev.dari.db.ObjectField;
import com.psddev.dari.db.ObjectFieldComparator;
import com.psddev.dari.db.ObjectIndex;
import com.psddev.dari.db.ObjectStruct;
import com.psddev.dari.db.ObjectType;
import com.psddev.dari.db.Predicate;
import com.psddev.dari.db.PredicateParser;
import com.psddev.dari.db.Query;
import com.psddev.dari.db.Singleton;
import com.psddev.dari.db.State;
import com.psddev.dari.db.StateStatus;
import com.psddev.dari.db.ValidationException;
import com.psddev.dari.util.CodeUtils;
import com.psddev.dari.util.CompactMap;
import com.psddev.dari.util.DebugFilter;
import com.psddev.dari.util.DependencyResolver;
import com.psddev.dari.util.ErrorUtils;
import com.psddev.dari.util.ImageEditor;
import com.psddev.dari.util.JspUtils;
import com.psddev.dari.util.ObjectUtils;
import com.psddev.dari.util.RoutingFilter;
import com.psddev.dari.util.Settings;
import com.psddev.dari.util.StorageItem;
import com.psddev.dari.util.StringUtils;
import com.psddev.dari.util.TypeReference;
import com.psddev.dari.util.Utf8Filter;
import com.psddev.dari.util.WebPageContext;

/**
* {@link WebPageContext} with extra methods that work well with
* pages in {@link Tool}.
*/
public class ToolPageContext extends WebPageContext {

    /**
     * Settings key for tool URL prefix when creating a fully qualified
     * version of a path.
     */
    public static final String TOOL_URL_PREFIX_SETTING = "brightspot/toolUrlPrefix";

    public static final String TYPE_ID_PARAMETER = "typeId";
    public static final String OBJECT_ID_PARAMETER = "id";
    public static final String DRAFT_ID_PARAMETER = "draftId";
    public static final String ORIGINAL_DRAFT_VALUE = "original";
    public static final String HISTORY_ID_PARAMETER = "historyId";
    public static final String VARIATION_ID_PARAMETER = "variationId";
    public static final String RETURN_URL_PARAMETER = "returnUrl";

    private static final String ATTRIBUTE_PREFIX = ToolPageContext.class.getName() + ".";
    private static final String ERRORS_ATTRIBUTE = ATTRIBUTE_PREFIX + "errors";
    private static final String FORM_FIELDS_DISABLED_ATTRIBUTE = ATTRIBUTE_PREFIX + "formFieldsDisabled";
    private static final String TOOL_ATTRIBUTE = ATTRIBUTE_PREFIX + "tool";
    private static final String TOOL_BY_CLASS_ATTRIBUTE = ATTRIBUTE_PREFIX + "toolByClass";
    private static final String TOOL_BY_PATH_ATTRIBUTE = ATTRIBUTE_PREFIX + "toolByPath";

    private static final String EXTRA_PREFIX = "cms.tool.";
    private static final String OVERLAID_DRAFT_EXTRA = EXTRA_PREFIX + "overlaidDraft";
    private static final String OVERLAID_HISTORY_EXTRA = EXTRA_PREFIX + "overlaidHistory";

    /** Creates an instance based on the given {@code pageContext}. */
    public ToolPageContext(PageContext pageContext) {
        super(pageContext);
    }

    /** Creates an instance based on the given servlet parameters. */
    public ToolPageContext(
            ServletContext servletContext,
            HttpServletRequest request,
            HttpServletResponse response) {

        super(servletContext, request, response);
    }

    /**
     * Returns the parameter value as an instance of the given
     * {@code returnClass} associated with the given {@code name}, or if not
     * found, either the {@linkplain #getPageSetting page setting value} or
     * the given {@code defaultValue}.
     */
    @SuppressWarnings("unchecked")
    public <T> T pageParam(Class<T> returnClass, String name, T defaultValue) {
        Class<?> valueClass = PRIMITIVE_CLASSES.get(returnClass);

        if (valueClass == null) {
            valueClass = returnClass;
        }

        HttpServletRequest request = getRequest();
        String valueString = request.getParameter(name);
        Object value = ObjectUtils.to(valueClass, valueString);
        Object userValue = ObjectUtils.to(valueClass, AuthenticationFilter.Static.getPageSetting(request, name));

        if (valueString == null) {
            return ObjectUtils.isBlank(userValue) ? defaultValue : (T) userValue;

        } else {
            if (!ObjectUtils.equals(value, userValue)) {
                AuthenticationFilter.Static.putPageSetting(request, name, value);
            }

            return (T) value;
        }
    }

    /**
     * Returns the parameter value as an instance of the given
     * {@code returnClass} associated with the given {@code name}, or if not
     * found, either the {@linkplain #getPageSetting page setting value} or
     * the given {@code defaultValue}.
     */
    @SuppressWarnings("unchecked")
    public <T> List<T> pageParams(Class<T> returnClass, String name, List<T> defaultValue) {
        Class<?> valueClass = PRIMITIVE_CLASSES.get(returnClass);

        if (valueClass == null) {
            valueClass = returnClass;
        }

        HttpServletRequest request = getRequest();
        List<T> value = params(returnClass, name);
        List<Object> userValue = ObjectUtils.to(new TypeReference<List<Object>>() { }, AuthenticationFilter.Static.getPageSetting(request, name));

        if (value == null || value.isEmpty()) {
            return ObjectUtils.isBlank(userValue) ? defaultValue : (List<T>) userValue;

        } else {
            if (!ObjectUtils.equals(value, userValue)) {
                AuthenticationFilter.Static.putPageSetting(request, name, value);
            }

            return (List<T>) value;
        }
    }

    private static final Map<Class<?>, Class<?>> PRIMITIVE_CLASSES; static {
        Map<Class<?>, Class<?>> m = new HashMap<Class<?>, Class<?>>();

        m.put(boolean.class, Boolean.class);
        m.put(byte.class, Byte.class);
        m.put(char.class, Character.class);
        m.put(double.class, Double.class);
        m.put(float.class, Float.class);
        m.put(int.class, Integer.class);
        m.put(long.class, Long.class);
        m.put(short.class, Short.class);

        PRIMITIVE_CLASSES = Collections.unmodifiableMap(m);
    }

    /**
     * Returns a label, or the given {@code defaultLabel} if one can't be
     * found, for the given {@code object}.
     */
    public String getObjectLabelOrDefault(Object object, String defaultLabel) {
        return Static.getObjectLabelOrDefault(object, defaultLabel);
    }

    /** Returns a label for the given {@code object}. */
    public String getObjectLabel(Object object) {
        return Static.getObjectLabel(object);
    }

    /**
     * Returns a label, or the given {@code defaultLabel} if one can't be
     * found, for the type of the given {@code object}.
     */
    public String getTypeLabelOrDefault(Object object, String defaultLabel) {
        return Static.getTypeLabelOrDefault(object, defaultLabel);
    }

    /** Returns a label for the type of the given {@code object}. */
    public String getTypeLabel(Object object) {
        return Static.getTypeLabel(object);
    }

    /**
     * Returns {@code true} is the given {@code object} is previewable.
     *
     * @param object If {@code null}, always returns {@code false}.
     */
    @SuppressWarnings("deprecation")
    public boolean isPreviewable(Object object) {
        if (object != null) {
            if (object instanceof Page &&
                    !(object instanceof Template)) {
                return true;

            } else if (object instanceof Renderer) {
                return true;

            } else {
                State state = State.getInstance(object);
                ObjectType type = state.getType();

                if (type != null) {
                    if (Template.Static.findUsedTypes(getSite()).contains(type)) {
                        return true;

                    } else {
                        Renderer.TypeModification rendererData = type.as(Renderer.TypeModification.class);

                        return !ObjectUtils.isBlank(rendererData.getPath()) ||
                                !ObjectUtils.isBlank(rendererData.getPaths());
                    }
                }
            }
        }

        return false;
    }

    /**
     * Returns the singleton instance of the given {@code toolClass}.
     * Note that this method caches the result, so it'll return the
     * exact same object every time within a single request.
     */
    @SuppressWarnings("unchecked")
    public <T extends Tool> T getToolByClass(Class<T> toolClass) {
        HttpServletRequest request = getRequest();
        Map<Class<?>, Tool> tools = (Map<Class<?>, Tool>) request.getAttribute(TOOL_BY_CLASS_ATTRIBUTE);

        if (tools == null) {
            tools = new HashMap<Class<?>, Tool>();
            request.setAttribute(TOOL_BY_CLASS_ATTRIBUTE, tools);
        }

        Tool tool = tools.get(toolClass);

        if (!toolClass.isInstance(tool)) {
            tool = Application.Static.getInstance(toolClass);
            tools.put(toolClass, tool);
        }

        return (T) tool;
    }

    /**
     * Returns the CMS tool.
     *
     * @see #getToolByClass
     */
    public CmsTool getCmsTool() {
        return getToolByClass(CmsTool.class);
    }

    /** Returns all embedded tools, keyed by their context paths. */
    @SuppressWarnings("unchecked")
    public Map<String, Tool> getEmbeddedTools() {
        HttpServletRequest request = getRequest();
        Map<String, Tool> tools = (Map<String, Tool>) request.getAttribute(TOOL_BY_PATH_ATTRIBUTE);

        if (tools == null) {
            tools = new LinkedHashMap<String, Tool>();

            for (Map.Entry<String, Properties> entry : JspUtils.getEmbeddedSettings(getServletContext()).entrySet()) {
                String toolClassName = entry.getValue().getProperty(Application.MAIN_CLASS_SETTING);
                Class<?> objectClass = ObjectUtils.getClassByName(toolClassName);

                if (objectClass != null &&
                        Tool.class.isAssignableFrom(objectClass)) {
                    tools.put(entry.getKey(), getToolByClass((Class<Tool>) objectClass));
                }
            }

            if (!tools.containsKey("")) {
                Application app = Application.Static.getMain();
                if (app instanceof Tool) {
                    tools.put("", (Tool) app);
                }
            }

            request.setAttribute(TOOL_BY_PATH_ATTRIBUTE, tools);
        }

        return tools;
    }

    /** Returns the tool that's currently in use. */
    public Tool getTool() {
        ServletContext context = getServletContext();
        HttpServletRequest request = getRequest();
        Tool tool = (Tool) request.getAttribute(TOOL_ATTRIBUTE);

        if (tool == null) {
            String contextPath = JspUtils.getEmbeddedContextPath(context, request.getServletPath());

            tool = getEmbeddedTools().get(contextPath);
            request.setAttribute(TOOL_ATTRIBUTE, tool);
        }

        return tool;
    }

    private class AreaUrl implements Comparable<AreaUrl> {

        private final Area area;
        private final String url;

        public AreaUrl(Area area, String url) {
            this.area = area;
            this.url = url;
        }

        public Area getArea() {
            return area;
        }

        public String getUrl() {
            return url;
        }

        @Override
        public int compareTo(AreaUrl other) {
            return other.url.length() - url.length();
        }
    }

    /**
     * Returns the area that's currently in use.
     *
     * @return May be {@code null}.
     */
    public Area getArea() {
        List<AreaUrl> areaUrls = new ArrayList<AreaUrl>();

        for (Area area : Tool.Static.getPluginsByClass(Area.class)) {
            String url = area.getUrl();

            if (!ObjectUtils.isBlank(url)) {
                Tool tool = area.getTool();

                if (tool != null) {
                    areaUrls.add(new AreaUrl(area, toolUrl(tool, url)));
                }
            }
        }

        Collections.sort(areaUrls);

        String path = getRequest().getServletPath();

        if (path.endsWith("/index.jsp")) {
            path = path.substring(0, path.length() - 9);
        }

        for (AreaUrl areaUrl : areaUrls) {
            if (path.startsWith(areaUrl.getUrl())) {
                return areaUrl.getArea();
            }
        }

        return null;
    }

    private Object[] pushToArray(Object[] array, Object... newItems) {
        int os = array.length;
        int ns = newItems.length;
        Object[] newArray = new Object[os + ns];

        if (os > 0) {
            System.arraycopy(array, 0, newArray, 0, os);
        }

        if (ns > 0) {
            System.arraycopy(newItems, 0, newArray, os, ns);
        }

        return newArray;
    }

    /**
     * Returns an absolute version of the given {@code path} in context
     * of the given {@code tool}, modified by the given {@code parameters}.
     *
     * @param tool Can't be {@code null}.
     * @param path May be {@code null}.
     * @param parameters May be {@code null}.
     */
    @SuppressWarnings("deprecation")
    public String toolUrl(Tool tool, String path, Object... parameters) {
        String url = null;
        String appName = tool.getApplicationName();

        if (appName != null) {
            url = getServletContext().getContextPath() + RoutingFilter.Static.getApplicationPath(appName);

        } else {
            for (Map.Entry<String, Tool> entry : getEmbeddedTools().entrySet()) {
                if (entry.getValue().equals(tool)) {
                    url = entry.getKey();
                    break;
                }
            }

            if (url == null) {
                url = tool.getUrl();

                if (ObjectUtils.isBlank(url)) {
                    url = getServletContext().getContextPath();
                }

            } else {
                url = getServletContext().getContextPath() + url;
            }
        }

        url = url + StringUtils.ensureStart(path, "/");

        return StringUtils.addQueryParameters(url, parameters);
    }

    /**
     * Returns an absolute version of the given {@code path} in context
     * of the instance of the given {@code toolClass}, modified by the given
     * {@code parameters}.
     *
     * @param toolClass Can't be {@code null}.
     * @param path May be {@code null}.
     * @param parameters May be {@code null}.
     */
    public String toolUrl(Class<? extends Tool> toolClass, String path, Object... parameters) {
        return toolUrl(getToolByClass(toolClass), path, parameters);
    }

    /**
     * Returns a fully qualified, absolute version of the given {@code path}
     * in context of the instance of the given {@code toolClass}, modified by
     * the given {@code parameters}.
     *
     * @param toolClass Can't be {@code null}.
     * @param path May be {@code null}.
     * @param parameters May be {@code null}.
     */
    public String fullyQualifiedToolUrl(Class<? extends Tool> toolClass, String path, Object... parameters) {
        String toolUrl = toolUrl(toolClass, path, parameters);
        String prefix = Settings.get(String.class, TOOL_URL_PREFIX_SETTING);

        if (!ObjectUtils.isBlank(prefix)) {
            toolUrl = StringUtils.removeEnd(prefix , "/") + toolUrl;
        }

        return toolUrl;
    }

    /**
     * Returns an absolute version of the given {@code path} in context
     * of the CMS, modified by the given {@code parameters}.
     *
     * @param path May be {@code null}.
     * @param parameters May be {@code null}.
     */
    public String cmsUrl(String path, Object... parameters) {
        return toolUrl(getCmsTool(), path, parameters);
    }

    public String typeUrl(String path, UUID typeId, Object... parameters) {
        return url(path, pushToArray(parameters,
                TYPE_ID_PARAMETER, typeId,
                OBJECT_ID_PARAMETER, null,
                DRAFT_ID_PARAMETER, null,
                HISTORY_ID_PARAMETER, null));
    }

    public String typeUrl(String path, Class<?> objectClass, Object... parameters) {
        UUID typeId = ObjectType.getInstance(objectClass).getId();

        return typeUrl(path, typeId, parameters);
    }

    public String objectUrl(String path, Object object, Object... parameters) {
        if (object instanceof Draft) {
            Draft draft = (Draft) object;

            parameters = pushToArray(parameters,
                    OBJECT_ID_PARAMETER, draft.getObjectId(),
                    DRAFT_ID_PARAMETER, draft.getId(),
                    HISTORY_ID_PARAMETER, null);

        } else if (object instanceof History) {
            History history = (History) object;

            parameters = pushToArray(parameters,
                    OBJECT_ID_PARAMETER, history.getObjectId(),
                    DRAFT_ID_PARAMETER, null,
                    HISTORY_ID_PARAMETER, history.getId());

        } else {
            State state = State.getInstance(object);
            ObjectType type = state.getType();
            UUID objectId = state.getId();
            Draft draft = getOverlaidDraft(object);
            History history = getOverlaidHistory(object);

            parameters = pushToArray(parameters,
                    OBJECT_ID_PARAMETER, objectId,
                    TYPE_ID_PARAMETER, type != null ? type.getId() : null,
                    DRAFT_ID_PARAMETER, draft != null ? draft.getId() : null,
                    HISTORY_ID_PARAMETER, history != null ? history.getId() : null);
        }

        return url(path, parameters);
    }

    public String originalUrl(String path, Object object, Object... parameters) {
        return url(path, pushToArray(parameters,
                OBJECT_ID_PARAMETER, State.getInstance(object).getId(),
                DRAFT_ID_PARAMETER, ORIGINAL_DRAFT_VALUE,
                HISTORY_ID_PARAMETER, null));
    }

    /**
     * Returns an URL for returning to the current page from the request
     * at the given {@code path}, modified by the given {@code parameters}.
     */
    public String returnableUrl(String path, Object... parameters) {
        HttpServletRequest request = getRequest();

        return url(path, pushToArray(parameters,
                RETURN_URL_PARAMETER, JspUtils.getAbsolutePath(request, "")
                .substring(JspUtils.getEmbeddedContextPath(getServletContext(), request.getServletPath()).length())));
    }

    /**
     * Returns an URL to the return to the page specified by a previous
     * call to {@link #returnableUrl(String, Object...)}, modified by the
     * given {@code parameters}.
     */
    public String returnUrl(Object... parameters) {
        String returnUrl = param(String.class, RETURN_URL_PARAMETER);

        if (ObjectUtils.isBlank(returnUrl)) {
            throw new IllegalArgumentException(String.format(
                    "The [%s] parameter is required!", RETURN_URL_PARAMETER));
        }

        return url(returnUrl, parameters);
    }

    /** Returns a modifiable list of all the errors in this page. */
    public List<Throwable> getErrors() {
        @SuppressWarnings("unchecked")
        List<Throwable> errors = (List<Throwable>) getRequest().getAttribute(ERRORS_ATTRIBUTE);

        if (errors == null) {
            errors = new ArrayList<Throwable>();
            getRequest().setAttribute(ERRORS_ATTRIBUTE, errors);
        }

        return errors;
    }

    /**
     * Renders the form inputs appropriate for the given {@code field}
     * using the data from the given {@code object}.
     */
    public void renderField(Object object, ObjectField field) throws IOException {
        @SuppressWarnings("all")
        ToolFormWriter writer = new ToolFormWriter(this);

        writer.inputs(State.getInstance(object), field.getInternalName());
    }

    /**
     * Processes the form inputs for the given {@code field}, rendered in
     * {@link #renderField(Object, ObjectField)}, using the data from the
     * given {@code object}.
     */
    public void processField(Object object, ObjectField field) throws Throwable {
        @SuppressWarnings("all")
        ToolFormWriter writer = new ToolFormWriter(this);

        writer.update(State.getInstance(object), getRequest(), field.getInternalName());
    }

    /** Finds an existing object or reserve one. */
    public Object findOrReserve(Collection<ObjectType> validTypes) {
        UUID objectId = param(UUID.class, OBJECT_ID_PARAMETER);
        Object object;
        WorkStream workStream = Query.findById(WorkStream.class, param(UUID.class, "workStreamId"));

        if (!isFormPost() && workStream != null) {
            object = workStream.next(getUser());

        } else {
            object = Query.findById(Object.class, objectId);
        }

        if (object != null) {
            if (workStream == null) {
                ObjectType objectType = State.getInstance(object).getType();

                if (!ObjectUtils.isBlank(validTypes) &&
                        !validTypes.contains(objectType)) {
                    StringBuilder tb = new StringBuilder();

                    for (ObjectType type : validTypes) {
                        tb.append(type.getLabel());
                        tb.append(", ");
                    }

                    tb.setLength(tb.length() - 2);

                    throw new IllegalArgumentException(String.format(
                            "Expected one of [%s] types for [%s] object but it is of [%s] type",
                            tb,
                            objectId,
                            objectType != null ? objectType.getLabel() : "unknown"));
                }
            }

        } else if (!ObjectUtils.isBlank(validTypes)) {
            ObjectType selectedType = ObjectType.getInstance(param(UUID.class, TYPE_ID_PARAMETER));

            if (selectedType == null) {
                for (ObjectType type : validTypes) {
                    selectedType = type;
                    break;
                }
            }

            if (selectedType != null) {
                if (selectedType.getSourceDatabase() != null) {
                    object = Query.fromType(selectedType).where("_id = ?", objectId).first();
                }

                if (object == null) {
                    if (selectedType.getGroups().contains(Singleton.class.getName())) {
                        object = Query.fromType(selectedType).first();
                    }

                    if (object == null) {
                        object = selectedType.createObject(objectId);
                        State.getInstance(object).as(Site.ObjectModification.class).setOwner(getSite());
                    }
                }
            }
        }

        UUID draftId = param(UUID.class, DRAFT_ID_PARAMETER);

        if (object == null) {
            Object draftObject = Query.fromAll().where("_id = ?", draftId).first();

            if (draftObject instanceof Draft) {
                Draft draft = (Draft) draftObject;
                object = draft.getObject();

                State.getInstance(object).getExtras().put(OVERLAID_DRAFT_EXTRA, draft);
            }

        } else {
            State state = State.getInstance(object);

            History history = Query.
                    from(History.class).
                    where("id = ?", param(UUID.class, HISTORY_ID_PARAMETER)).
                    and("objectId = ?", objectId).
                    first();

            if (history != null) {
                state.getExtras().put(OVERLAID_HISTORY_EXTRA, history);
                state.setValues(history.getObjectOriginals());
                state.setStatus(StateStatus.SAVED);

            } else if (objectId != null) {
                Object draftObject = Query.
                        fromAll().
                        where("id = ?", draftId).
                        and("com.psddev.cms.db.Draft/objectId = ?", objectId).
                        first();

                if (draftObject instanceof Draft) {
                    Draft draft = (Draft) draftObject;

                    state.getExtras().put(OVERLAID_DRAFT_EXTRA, draft);
                    state.getValues().putAll(draft.getObjectChanges());
                }
            }

            UUID variationId = param(UUID.class, VARIATION_ID_PARAMETER);

            if (variationId != null) {
                @SuppressWarnings("unchecked")
                Map<String, Object> variationValues = (Map<String, Object>) state.getByPath("variations/" + variationId.toString());

                if (variationValues != null) {
                    state.putAll(variationValues);
                }
            }
        }

        if (object != null) {
            State.getInstance(object).setResolveInvisible(true);
        }

        Template template = Query.from(Template.class).where("_id = ?", param(UUID.class, "templateId")).first();

        if (template != null) {
            if (object == null) {
                Set<ObjectType> contentTypes = template.getContentTypes();

                if (!contentTypes.isEmpty()) {
                    object = contentTypes.iterator().next().createObject(objectId);
                    State.getInstance(object).as(Site.ObjectModification.class).setOwner(getSite());
                }
            }

            if (object != null) {
                State.getInstance(object).as(Template.ObjectModification.class).setDefault(template);
            }

        } else if (object != null) {
            State state = State.getInstance(object);

            if (state.isNew()) {
                List<Template> templates = Template.Static.findUsable(object);

                if (!templates.isEmpty()) {
                    state.as(Template.ObjectModification.class).setDefault(templates.iterator().next());
                }
            }
        }

        if (object != null) {
            State state = State.getInstance(object);
            Content.ObjectModification contentData = state.as(Content.ObjectModification.class);

            if (contentData.isDraft()) {
                Draft draft = Query.from(Draft.class).where("objectId = ?", state.getId()).first();

                if (draft != null) {
                    state.getExtras().put(OVERLAID_DRAFT_EXTRA, draft);
                }
            }
        }

        return object;
    }

    /** Finds an existing object or reserve one. */
    public Object findOrReserve(UUID... validTypeIds) {
        Set<ObjectType> validTypes = null;

        if (!ObjectUtils.isBlank(validTypeIds)) {
            validTypes = new LinkedHashSet<ObjectType>();

            for (UUID typeId : validTypeIds) {
                ObjectType type = ObjectType.getInstance(typeId);

                if (type != null) {
                    validTypes.add(type);
                }
            }
        }

        return findOrReserve(validTypes);
    }

    /** Finds an existing object or reserve one. */
    public Object findOrReserve(Class<?>... validObjectClasses) {
        Set<ObjectType> validTypes = null;

        if (!ObjectUtils.isBlank(validObjectClasses)) {
            validTypes = new LinkedHashSet<ObjectType>();

            for (Class<?> validObjectClass : validObjectClasses) {
                ObjectType type = ObjectType.getInstance(validObjectClass);

                if (type != null) {
                    validTypes.add(type);
                }
            }
        }

        return findOrReserve(validTypes);
    }

    /** Finds an existing object or reserve one. */
    public Object findOrReserve() {
        UUID selectedTypeId = param(UUID.class, TYPE_ID_PARAMETER);

        return findOrReserve(selectedTypeId != null ?
                new UUID[] { selectedTypeId } :
                new UUID[0]);
    }

    /**
     * Returns the draft that was overlaid on top of the given
     * {@code object}.
     */
    public Draft getOverlaidDraft(Object object) {
        return (Draft) State.getInstance(object).getExtra(OVERLAID_DRAFT_EXTRA);
    }

    /**
     * Returns the past revision that was overlaid on top of the
     * {@code object}.
     */
    public History getOverlaidHistory(Object object) {
        return (History) State.getInstance(object).getExtra(OVERLAID_HISTORY_EXTRA);
    }

    public Predicate siteItemsPredicate() {
        ToolUser user = getUser();

        if (user != null) {
            Site site = user.getCurrentSite();

            if (site != null) {
                return site.itemsPredicate();
            }
        }

        return null;
    }

    public Predicate siteItemsSearchPredicate() {
        Predicate predicate = siteItemsPredicate();

        if (predicate != null) {
            predicate = CompoundPredicate.combine(
                    PredicateParser.AND_OPERATOR,
                    predicate,
                    PredicateParser.Static.parse("* matches *"));
        }

        return predicate;
    }

    private String cmsResource(String path, Object... parameters) {
        ServletContext context = getServletContext();
        path = cmsUrl(path);
        long lastModified = 0;

        try {
            URL resource = context.getResource(path);

            if (resource != null) {
                URLConnection resourceConnection = resource.openConnection();
                InputStream resourceInput = resourceConnection.getInputStream();

                try {
                    lastModified = resourceConnection.getLastModified();
                } finally {
                    resourceInput.close();
                }
            }

        } catch (IOException error) {
            throw new IllegalStateException(error);
        }

        if (lastModified == 0) {
            lastModified = (long) (Math.random() * Long.MAX_VALUE);
        }

        return StringUtils.addQueryParameters(
                StringUtils.addQueryParameters(path, parameters),
                "_", lastModified);
    }

    /**
     * Returns the URL to the preview thumbnail of the given {@code object}.
     *
     * @return May be {@code null}.
     */
    public String getPreviewThumbnailUrl(Object object) {
        if (object != null) {
            StorageItem preview = State.getInstance(object).getPreview();

            if (preview != null) {
                if (ImageEditor.Static.getDefault() != null) {
                    return new ImageTag.Builder(preview).
                            setHeight(300).
                            setResizeOption(ResizeOption.ONLY_SHRINK_LARGER).
                            toUrl();

                } else {
                    return preview.getPublicUrl();
                }
            }
        }

        return null;
    }

    /**
     * Writes a descriptive label HTML for the given {@code object}.
     *
     * @param object If {@code null}, writes {@code N/A}.
     */
    public void writeObjectLabel(Object object) throws IOException {
        if (object == null) {
            writeHtml("N/A");

        } else {
            State state = State.getInstance(object);
            String visibilityLabel = object instanceof Draft ? "Update" : state.getVisibilityLabel();
            String label = state.getLabel();

            if (!ObjectUtils.isBlank(visibilityLabel)) {
                writeStart("span", "class", "visibilityLabel");
                    writeHtml(visibilityLabel);
                writeEnd();

                writeHtml(" ");
            }

            writeHtml(ObjectUtils.isBlank(label) ?
                    state.getId() :
                    state.getLabel());
        }
    }

    /**
     * Writes a descriptive label HTML for the type of the given
     * {@code object}.
     *
     * @param object If it or its type is {@code null}, writes {@code N/A}.
     */
    public void writeTypeLabel(Object object) throws IOException {
        ObjectType type = null;

        if (object != null) {
            if (object instanceof Draft) {
                type = ((Draft) object).getObjectType();

            } else {
                type = State.getInstance(object).getType();
            }
        }

        writeObjectLabel(type);
    }

    /**
     * Writes a descriptive label HTML that contains the type information for
     * the given {@code object}.
     *
     * @param object If {@code null}, writes {@code N/A}.
     */
    public void writeTypeObjectLabel(Object object) throws IOException {
        if (object == null) {
            writeHtml("N/A");

        } else {
            State state = State.getInstance(object);
            ObjectType type = state.getType();
            String visibilityLabel = state.getVisibilityLabel();
            String label = state.getLabel();

            if (!ObjectUtils.isBlank(visibilityLabel)) {
                writeStart("span", "class", "visibilityLabel");
                    writeHtml(visibilityLabel);
                writeEnd();

                writeHtml(" ");
            }

            String typeLabel;

            if (type == null) {
                typeLabel = "Unknown Type";

            } else {
                typeLabel = type.getLabel();

                if (ObjectUtils.isBlank(typeLabel)) {
                    typeLabel = type.getId().toString();
                }
            }

            if (ObjectUtils.isBlank(label)) {
                label = state.getId().toString();
            }

            writeHtml(typeLabel);

            if (!typeLabel.equals(label)) {
                writeHtml(": ");
                writeHtml(label);
            }
        }
    }

    /**
     * Returns the user's time zone.
     *
     * @return Never {@code null}.
     */
    public DateTimeZone getUserDateTimeZone() {
        DateTimeZone timeZone = null;
        ToolUser user = getUser();

        if (user != null) {
            String timeZoneId = user.getTimeZone();

            if (!ObjectUtils.isBlank(timeZoneId)) {
                try {
                    timeZone = DateTimeZone.forID(timeZoneId);
                } catch (IllegalArgumentException error) {
                    // Ignore unparseable time zone IDs.
                }
            }
        }

        return timeZone == null ?
                DateTimeZone.getDefault() :
                timeZone;
    }

    /**
     * Converts the given {@code dateTime} to the user's time zone.
     *
     * @param dateTime If {@code null}, returns {@code null}.
     * @return May be {@code null}.
     */
    public DateTime toUserDateTime(Object dateTime) {
        return dateTime != null ?
                new DateTime(dateTime, getUserDateTimeZone()) :
                null;
    }

    /**
     * Formats the given {@code dateTime} according to the given
     * {@code format}.
     *
     * @param dateTime If {@code null}, returns {@code N/A}.
     * @return Never {@code null}.
     */
    public String formatUserDateTimeWith(Object dateTime, String format) throws IOException {
        return dateTime != null ?
                toUserDateTime(dateTime).toString(format) :
                "N/A";
    }

    /**
     * Formats the given {@code dateTime} according to the default format.
     *
     * @param dateTime If {@code null}, returns {@code N/A}.
     * @return Never {@code null}.
     */
    public String formatUserDateTime(Object dateTime) throws IOException {
        return formatUserDateTimeWith(
                dateTime,
                new DateTime(dateTime).getYear() == new DateTime().getYear() ?
                    "EEE MMM dd hh:mm aa" :
                    "EEE MMM dd yyyy hh:mm aa");
    }

    /**
     * Formats the date part of the given {@code dateTime} according to the
     * default format.
     *
     * @param dateTime If {@code null}, returns {@code N/A}.
     * @return Never {@code null}.
     */
    public String formatUserDate(Object dateTime) throws IOException {
        return formatUserDateTimeWith(
                dateTime,
                new DateTime(dateTime).getYear() == new DateTime().getYear() ?
                    "EEE MMM dd" :
                    "EEE MMM dd yyyy");
    }

    /**
     * Formats the time part of the given {@code dateTime} according to the
     * default format.
     *
     * @param dateTime If {@code null}, returns {@code N/A}.
     * @return Never {@code null}.
     */
    public String formatUserTime(Object dateTime) throws IOException {
        return formatUserDateTimeWith(dateTime, "hh:mm aa");
    }

    /**
     * Writes the tool header with the given {@code title}.
     *
     * @param title If {@code null}, uses the default title.
     * @param requireToolUser If {@code true}, calls {@link #requireUser}.
     */
    public void writeHeader(String title, boolean requireToolUser) throws IOException {
        if (requireToolUser && requireUser()) {
            throw new IllegalStateException();
        }

        if (isAjaxRequest() || param(boolean.class, "_frame")) {
            return;
        }

        CmsTool cms = getCmsTool();
        Area area = getArea();
        String companyName = cms.getCompanyName();
        String environment = cms.getEnvironment();
        ToolUser user = getUser();

        if (ObjectUtils.isBlank(companyName)) {
            companyName = "Brightspot";
        }

        Site site = getSite();
        StorageItem companyLogo = site != null ? site.getCmsLogo() : null;

        if (companyLogo == null) {
            companyLogo = cms.getCompanyLogo();
        }

        writeTag("!doctype html");
        writeTag("html", "class", site != null ? site.getCmsCssClass() : null);
            writeStart("head");
                writeStart("title");
                    if (!ObjectUtils.isBlank(title)) {
                        writeHtml(title);
                        writeHtml(" | ");

                    } else if (area != null) {
                        writeObjectLabel(area);
                        writeHtml(" | ");
                    }

                    writeHtml("CMS | ");
                    writeHtml(companyName);
                writeEnd();

                writeElement("meta", "name", "robots", "content", "noindex");
                writeStylesAndScripts();
            writeEnd();

            Schedule currentSchedule = getUser() != null ? getUser().getCurrentSchedule() : null;
            String broadcastMessage = cms.getBroadcastMessage();
            Date broadcastExpiration = cms.getBroadcastExpiration();
            boolean hasBroadcast = !ObjectUtils.isBlank(broadcastMessage) &&
                    (broadcastExpiration == null ||
                    broadcastExpiration.after(new Date()));

            writeTag("body", "class",
                    (currentSchedule != null || hasBroadcast ? "hasToolBroadcast " : "") +
                    (user != null ? "" : "noToolUser "));
                if (currentSchedule != null || hasBroadcast) {
                    writeStart("div", "class", "toolBroadcast");
                        if (currentSchedule != null) {
                            writeHtml("All editorial changes will be scheduled for: ");

                            writeStart("a",
                                    "href", cmsUrl("/scheduleEdit", "id", currentSchedule.getId()),
                                    "target", "scheduleEdit");
                                writeHtml(getObjectLabel(currentSchedule));
                            writeEnd();

                            writeHtml(" - ");

                            writeStart("form",
                                    "method", "post",
                                    "style", "display: inline;",
                                    "action", cmsUrl("/misc/updateUserSettings",
                                            "action", "scheduleSet",
                                            "returnUrl", url("")));
                                writeStart("button",
                                        "class", "link icon icon-action-cancel");
                                    writeHtml("Stop Scheduling");
                                writeEnd();
                            writeEnd();
                        }

                        if (hasBroadcast) {
                            writeHtml(" - ");
                            writeHtml(broadcastMessage);
                        }
                    writeEnd();
                }

                writeStart("div", "class", "toolHeader" + (!ObjectUtils.isBlank(environment) ? " toolHeader-hasEnvironment" : ""));

                    writeStart("h1", "class", "toolTitle");
                        writeStart("a", "href", cmsUrl("/"));
                            if (companyLogo != null) {
                                writeElement("img", "src", companyLogo.getPublicUrl(), "alt", companyName);
                            } else {
                                writeHtml(companyName);
                            }
                        writeEnd();
                    writeEnd();

                    if (!ObjectUtils.isBlank(environment)) {
                        writeStart("div", "class", "toolEnv");
                            writeHtml(environment);
                        writeEnd();
                    }

                    if (user != null) {
                        int nowHour = new DateTime().getHourOfDay();

                        writeStart("div", "class", "toolAvatar");

                            StorageItem avatar = user.getAvatar();

                            if (avatar != null) {
                                writeStart("a", "href", cmsUrl("/misc/settings.jsp"));
                                    writeTag("img",
                                            "src", ImageEditor.Static.resize(ImageEditor.Static.getDefault(), avatar, null, 50, 50).getPublicUrl());
                                writeEnd();
                            }
                        writeEnd();

                        writeStart("div", "class", "toolProfile");
                            writeHtml("Good ");
                            writeHtml(nowHour >= 2 && nowHour < 12 ? "Morning" : (nowHour >= 12 && nowHour < 18 ? "Afternoon" : "Evening"));
                            writeHtml(", ");
                            writeHtml(getObjectLabel(user));

                            writeStart("ul");
                                if (!Site.Static.findAll().isEmpty()) {
                                    Site currentSite = user.getCurrentSite();

                                    writeStart("li");
                                        writeHtml("Site: ");
                                        writeStart("a", "href", cmsUrl("/misc/sites.jsp"), "target", "misc");
                                            writeHtml(currentSite != null ? currentSite.getLabel() : "Global");
                                        writeEnd();
                                    writeEnd();
                                }

                                writeStart("li");
                                    writeStart("a",
                                            "class", "icon icon-object-history",
                                            "href", cmsUrl("/toolUserHistory"),
                                            "target", "toolUserHistory");
                                        writeHtml("History");
                                    writeEnd();
                                writeEnd();

                                writeStart("li");
                                    writeStart("a",
                                            "class", "icon icon-object-toolUser",
                                            "href", cmsUrl("/misc/settings.jsp"),
                                            "target", "misc");
                                        writeHtml("Profile");
                                    writeEnd();
                                writeEnd();

                                writeStart("li");
                                    writeStart("a",
                                            "class", "action-logOut",
                                            "href", cmsUrl("/misc/logOut.jsp"));
                                        writeHtml("Log Out");
                                    writeEnd();
                                writeEnd();
                            writeEnd();
                        writeEnd();
                    }

                    if (hasPermission("area/dashboard")) {
                        writeStart("form",
                                "class", "toolSearch",
                                "method", "get",
                                "action", cmsUrl("/misc/search.jsp"),
                                "target", "miscSearch");

                            writeElement("input", "type", "hidden", "name", Utf8Filter.CHECK_PARAMETER, "value", Utf8Filter.CHECK_VALUE);
                            writeElement("input", "type", "hidden", "name", Search.NAME_PARAMETER, "value", "global");

                            writeStart("span", "class", "searchInput");
                                writeStart("label", "for", createId()).writeHtml("Search").writeEnd();
                                writeElement("input", "type", "text", "id", getId(), "name", "q");
                                writeStart("button").writeHtml("Go").writeEnd();
                            writeEnd();

                        writeEnd();
                    }

                    if (user != null) {
                        String servletPath = JspUtils.getEmbeddedServletPath(getServletContext(), getRequest().getServletPath());

                        writeStart("ul", "class", "toolNav");
                            for (Area top : Tool.Static.getTopAreas()) {
                                if (!hasPermission(top.getPermissionId())) {
                                    continue;
                                }

                                String topUrl = top.getUrl();
                                String topLabel = getObjectLabel(top);

                                writeStart("li",
                                        "class", (top.hasChildren() ? " isNested" : "") + (area != null && area.getHierarchy().startsWith(top.getHierarchy()) ? " selected" : ""));
                                    writeStart("a", "href", topUrl == null ? "#" : toolUrl(top.getTool(), topUrl));
                                        writeHtml(topLabel);
                                    writeEnd();

                                    if (top.hasChildren()) {
                                        writeStart("ul");
                                            for (Area child : top.getChildren()) {
                                                if (!hasPermission(child.getPermissionId())) {
                                                    continue;
                                                }

                                                writeStart("li", "class", area != null && area.getInternalName().equals(child.getInternalName()) ? "selected" : null);
                                                    writeStart("a", "href", toolUrl(child.getTool(), child.getUrl()));
                                                        writeHtml(getObjectLabel(child));
                                                    writeEnd();
                                                writeEnd();
                                            }
                                        writeEnd();
                                    }
                                writeEnd();
                            }
                        writeEnd();
                    }

                writeEnd();

                writeTag("div", "class", "toolContent");

                    StorageItem backgroundImage = cms.getBackgroundImage();

                    if (backgroundImage != null) {
                        writeStart("svg",
                                "xmlns", "http://www.w3.org/2000/svg",
                                "xmlns:xlink", "http://www.w3.org/1999/xlink",
                                "class", "toolBackground",
                                "width", "100%",
                                "height", "100%");
                            writeStart("defs");
                                writeStart("filter", "id", "blur");
                                    writeStart("feGaussianBlur", "stdDeviation", 40);
                                    writeEnd();
                                writeEnd();
                            writeEnd();

                            writeStart("image",
                                    "xlink:href", ImageEditor.Static.resize(ImageEditor.Static.getDefault(), backgroundImage, null, 400, 400).getPublicUrl(),
                                    "width", "100%",
                                    "height", "100%",
                                    "preserveAspectRatio", "xMidYMid slice",
                                    "filter", "url(#blur)");
                            writeEnd();
                        writeEnd();
                    }
    }

    public void writeStylesAndScripts() throws IOException {
        List<Tool> tools = new ArrayList<Tool>();

        for (ObjectType type : Database.Static.getDefault().getEnvironment().getTypesByGroup(Tool.class.getName())) {
            if (!type.isConcrete()) {
                continue;
            }

            try {
                @SuppressWarnings({ "rawtypes", "unchecked" })
                Class<? extends Tool> toolClass = (Class) type.getObjectClass();

                if (toolClass != null) {
                    tools.add(Application.Static.getInstance(toolClass));
                }

            } catch (ClassCastException error) {
                // Ignore tool instances without backing Java classes.
            }
        }

        CmsTool cms = getCmsTool();
        String companyName = cms.getCompanyName();
        String extraCss = cms.getExtraCss();
        String extraJavaScript = cms.getExtraJavaScript();

        if (ObjectUtils.isBlank(companyName)) {
            companyName = "Brightspot";
        }

        String cssPrefix = ObjectUtils.firstNonNull(cms.getStyleSheetPath(), "/style/");
        cssPrefix = StringUtils.ensureStart(cssPrefix, "/");
        cssPrefix = StringUtils.ensureEnd(cssPrefix, "/");

        if (getCmsTool().isUseNonMinifiedCss()) {
            writeElement("link", "rel", "stylesheet/less", "type", "text/less", "href", cmsResource(cssPrefix + "cms.less"));

        } else {
            writeElement("link", "rel", "stylesheet", "type", "text/css", "href", cmsResource(cssPrefix + "cms.min.css"));
        }

        for (Tool tool : tools) {
            tool.writeHeaderAfterStyles(this);
        }

        if (getCmsTool().isUseNonMinifiedCss()) {
            writeStart("script", "type", "text/javascript", "src", cmsResource("/script/less-dev.js"));
            writeEnd();

            writeStart("script", "type", "text/javascript", "src", cmsResource("/script/husl.js"));
            writeEnd();

            writeStart("script", "type", "text/javascript", "src", cmsResource("/script/husl-less.js"));
            writeEnd();

            writeStart("script", "type", "text/javascript", "src", cmsResource("/script/less.js"));
            writeEnd();
        }

        if (!ObjectUtils.isBlank(extraCss)) {
            writeStart("style", "type", "text/css");
                write(extraCss);
            writeEnd();
        }

        List<Map<String, Object>> cssClassGroups  = new ArrayList<Map<String, Object>>();

        for (CmsTool.CssClassGroup group : cms.getTextCssClassGroups()) {
            Map<String, Object> groupDef = new HashMap<String, Object>();
            cssClassGroups.add(groupDef);

            groupDef.put("internalName", group.getInternalName());
            groupDef.put("displayName", group.getDisplayName());
            groupDef.put("dropDown", group.isDropDown());

            List<Map<String, String>> cssClasses = new ArrayList<Map<String, String>>();
            groupDef.put("cssClasses", cssClasses);

            for (CmsTool.CssClass cssClass : group.getCssClasses()) {
                Map<String, String> cssDef = new HashMap<String, String>();
                cssClasses.add(cssDef);

                cssDef.put("internalName", cssClass.getInternalName());
                cssDef.put("displayName", cssClass.getDisplayName());
                cssDef.put("tag", cssClass.getTag());
            }
        }

        List<Map<String, String>> standardImageSizes = new ArrayList<Map<String, String>>();

        for (StandardImageSize size : StandardImageSize.findAll()) {
            Map<String, String> sizeMap = new CompactMap<String, String>();

            sizeMap.put("internalName", size.getInternalName());
            sizeMap.put("displayName", size.getDisplayName());
            standardImageSizes.add(sizeMap);
        }

        List<Map<String, Object>> commonTimes = new ArrayList<Map<String, Object>>();

        for (CmsTool.CommonTime commonTime : getCmsTool().getCommonTimes()) {
            Map<String, Object> commonTimeMap = new CompactMap<String, Object>();

            commonTimeMap.put("displayName", commonTime.getDisplayName());
            commonTimeMap.put("hour", commonTime.getHour());
            commonTimeMap.put("minute", commonTime.getMinute());
            commonTimes.add(commonTimeMap);
        }

        writeStart("script", "type", "text/javascript");
            write("var CONTEXT_PATH = '", cmsUrl("/"), "';");
            write("var CSS_CLASS_GROUPS = ", ObjectUtils.toJson(cssClassGroups), ";");
            write("var STANDARD_IMAGE_SIZES = ", ObjectUtils.toJson(standardImageSizes), ";");
            write("var RTE_LEGACY_HTML = ", getCmsTool().isLegacyHtml(), ';');
            write("var RTE_ENABLE_ANNOTATIONS = ", getCmsTool().isEnableAnnotations(), ';');
            write("var DISABLE_TOOL_CHECKS = ", getCmsTool().isDisableToolChecks(), ';');
            write("var COMMON_TIMES = ", ObjectUtils.toJson(commonTimes));
        writeEnd();

        writeStart("script", "type", "text/javascript", "src", "//www.google.com/jsapi");
        writeEnd();

        String jsPrefix = getCmsTool().isUseNonMinifiedJavaScript() ? "/script/" : "/script.min/";

        writeStart("script", "type", "text/javascript", "src", cmsResource(jsPrefix + "jquery.js"));
        writeEnd();

        writeStart("script", "type", "text/javascript", "src", cmsResource(jsPrefix + "jquery.extra.js"));
        writeEnd();

        writeStart("script", "type", "text/javascript", "src", cmsResource(jsPrefix + "jquery.handsontable.full.js"));
        writeEnd();

        writeStart("script", "type", "text/javascript", "src", cmsResource(jsPrefix + "d3.js"));
        writeEnd();

        writeStart("script", "type", "text/javascript");
            writeRaw("var require = ");
            writeRaw(ObjectUtils.toJson(ImmutableMap.of(
                    "baseUrl", cmsUrl(jsPrefix),
                    "urlArgs", "_=" + System.currentTimeMillis())));
            writeRaw(";");
        writeEnd();

        writeStart("script", "type", "text/javascript", "src", cmsResource(jsPrefix + "require.js"));
        writeEnd();

        writeStart("script", "type", "text/javascript", "src", cmsResource(jsPrefix + "cms.js"));
        writeEnd();

        String dropboxAppKey = getCmsTool().getDropboxApplicationKey();

        if (!ObjectUtils.isBlank(dropboxAppKey)) {
            writeStart("script",
                    "type", "text/javascript",
                    "src", "https://www.dropbox.com/static/api/1/dropins.js",
                    "id", "dropboxjs",
                    "data-app-key", dropboxAppKey);
            writeEnd();
        }

        for (Tool tool : tools) {
            tool.writeHeaderAfterScripts(this);
        }

        if (!ObjectUtils.isBlank(extraJavaScript)) {
            writeStart("script", "type", "text/javascript");
                write(extraJavaScript);
            writeEnd();
        }
    }

    /**
     * Writes the tool header with the given {@code title}.
     *
     * @param title If {@code null}, uses the default title.
     */
    public void writeHeader(String title) throws IOException {
        writeHeader(title, true);
    }

    /**
     * Writes the tool header with the default title.
     */
    public void writeHeader() throws IOException {
        writeHeader(null, true);
    }

    /** Writes the tool footer. */
    public void writeFooter() throws IOException {
        if (isAjaxRequest() || param(boolean.class, "_frame")) {
            return;
        }

                writeTag("/div");

                writeStart("div", "class", "toolFooter");
                    writeStart("a",
                            "target", "_blank",
                            "href", "http://www.brightspot.com/");
                        writeElement("img",
                                "src", cmsUrl("/style/brightspot.png"),
                                "alt", "Brightspot",
                                "width", 104,
                                "height", 14);
                    writeEnd();
                writeEnd();

                if (getCmsTool().isEnableCrossDomainInlineEditing() &&
                        !Query.from(Site.class).hasMoreThan(100)) {
                    Set<String> siteUrls = new HashSet<String>();

                    for (Site s : Query.from(Site.class).selectAll()) {
                        for (String url : s.getUrls()) {
                            try {
                                siteUrls.add(new URL(url).toURI().resolve("/").toString());
                            } catch (MalformedURLException error) {
                                // Ignore invalid site URL.
                            } catch (URISyntaxException error) {
                                // Ignore invalid site URL.
                            }
                        }
                    }

                    ToolUser user = getUser();
                    String userId = user != null ? user.getId().toString() : UUID.randomUUID().toString();
                    String signature = StringUtils.hex(StringUtils.hmacSha1(Settings.getSecret(), userId));
                    String cookiePath = StringUtils.addQueryParameters(
                            cmsUrl("/inlineEditorCookie"),
                            "userId", userId,
                            "signature", signature).
                            substring(1);

                    for (String siteUrl : siteUrls) {
                        writeStart("img", "src", siteUrl + cookiePath, "style", cssString(
                                "height", "1px",
                                "width", "1px",
                                "visibility", "hidden"));
                        writeEnd();
                    }
                }
            writeTag("/body");
        writeTag("/html");
    }

    /**
     * Writes a {@code <select>} tag that allows the user to pick multiple
     * content types.
     *
     * @param types Types that the user is allowed to select from.
     * If {@code null}, all content types will be available.
     * @param selectedTypes Types that should be initially selected.
     * @param attributes Attributes for the {@code <select>} tag.
     */
    public void writeMultipleTypeSelect(
            Iterable<ObjectType> types,
            Collection<ObjectType> selectedTypes,
            Object... attributes) throws IOException {

        writeTypeSelectReally(
                true,
                types,
                selectedTypes != null ? selectedTypes : Collections.<ObjectType>emptySet(),
                null,
                attributes);
    }

    /**
     * Writes a {@code <select>} tag that allows the user to pick a content
     * type.
     *
     * @param types Types that the user is allowed to select from.
     * If {@code null}, all content types will be available.
     * @param selectedType Type that should be initially selected.
     * @param allLabel Label for the option that selects all types.
     * If {@code null}, the option won't be available.
     * @param attributes Attributes for the {@code <select>} tag.
     */
    public void writeTypeSelect(
            Iterable<ObjectType> types,
            ObjectType selectedType,
            String allLabel,
            Object... attributes) throws IOException {

        writeTypeSelectReally(
                false,
                types,
                selectedType != null ? Arrays.asList(selectedType) : Collections.<ObjectType>emptySet(),
                allLabel,
                attributes);
    }

    private void writeTypeSelectReally(
            boolean multiple,
            Iterable<ObjectType> types,
            Collection<ObjectType> selectedTypes,
            String allLabel,
            Object... attributes) throws IOException {

        if (types == null) {
            types = Database.Static.getDefault().getEnvironment().getTypes();
        }

        List<ObjectType> typesList = ObjectUtils.to(new TypeReference<List<ObjectType>>() { }, types);

        for (Iterator<ObjectType> i = typesList.iterator(); i.hasNext();) {
            ObjectType type = i.next();

            if (!type.isConcrete() ||
                    !hasPermission("type/" + type.getId() + "/write") ||
                    (!getCmsTool().isDisplayTypesNotAssociatedWithJavaClasses() &&
                    type.getObjectClass() == null) ||
                    Draft.class.equals(type.getObjectClass()) ||
                    (type.isDeprecated() &&
                    !Query.fromType(type).hasMoreThan(0))) {
                i.remove();
            }
        }

        Map<String, List<ObjectType>> typeGroups = new LinkedHashMap<String, List<ObjectType>>();
        List<ObjectType> mainTypes = Template.Static.findUsedTypes(getSite());

        mainTypes.retainAll(typesList);
        typesList.removeAll(mainTypes);
        typeGroups.put("Main Content Types", mainTypes);
        typeGroups.put("Misc Content Types", typesList);

        for (Iterator<List<ObjectType>> i = typeGroups.values().iterator(); i.hasNext();) {
            List<ObjectType> typeGroup = i.next();

            if (typeGroup.isEmpty()) {
                i.remove();

            } else {
                Collections.sort(typeGroup);
            }
        }

        writeStart("select",
                "multiple", multiple ? "multiple" : null,
                attributes);

            if (allLabel != null) {
                writeStart("option", "value", "").writeHtml(allLabel).writeEnd();
            }

            if (typeGroups.size() == 1) {
                writeTypeSelectGroup(selectedTypes, typeGroups.values().iterator().next());

            } else {
                for (Map.Entry<String, List<ObjectType>> entry : typeGroups.entrySet()) {
                    writeStart("optgroup", "label", entry.getKey());
                        writeTypeSelectGroup(selectedTypes, entry.getValue());
                    writeEnd();
                }
            }

        writeEnd();
    }

    private void writeTypeSelectGroup(Collection<ObjectType> selectedTypes, List<ObjectType> types) throws IOException {
        String previousLabel = null;

        for (ObjectType type : types) {
            String label = Static.getObjectLabel(type);

            writeStart("option",
                    "selected", selectedTypes.contains(type) ? "selected" : null,
                    "value", type.getId());
                writeHtml(label);
                if (label.equals(previousLabel)) {
                    writeHtml(" (");
                    writeHtml(type.getInternalName());
                    writeHtml(")");
                }
            writeEnd();

            previousLabel = label;
        }
    }

    /**
     * Writes a {@code <select>} or {@code <input>} tag that allows the user
     * to pick a content.
     *
     * @param field Can't be {@code null}.
     * @param value Initial value. May be {@code null}.
     * @param attributes Extra attributes for the HTML tag.
     */
    public void writeObjectSelect(ObjectField field, Object value, Object... attributes) throws IOException {
        ErrorUtils.errorIfNull(field, "field");

        ToolUi ui = field.as(ToolUi.class);
        String placeholder = ObjectUtils.firstNonNull(ui.getPlaceholder(), "");

        if (field.isRequired()) {
            placeholder += " (Required)";
        }

        if (isObjectSelectDropDown(field)) {
            Search dropDownSearch = new Search(field);
            dropDownSearch.setParentId(param(UUID.class, OBJECT_ID_PARAMETER));
            dropDownSearch.setParentTypeId(param(UUID.class, TYPE_ID_PARAMETER));

            List<?> items;
            if (field.getTypes().contains(ObjectType.getInstance(ObjectType.class))) {
                List<ObjectType> types = new ArrayList<ObjectType>();
                Predicate predicate = dropDownSearch.toQuery(getSite()).getPredicate();

                for (ObjectType t : Database.Static.getDefault().getEnvironment().getTypes()) {
                    if (t.is(predicate)) {
                        types.add(t);
                    }
                }
                items = new ArrayList<Object>(types);
            } else {
                items = dropDownSearch.toQuery(getSite()).selectAll();
            }

            Collections.sort(items, new ObjectFieldComparator("_label", false));

            writeStart("select",
                    "data-searchable", "true",
                    attributes);
                writeStart("option", "value", "");
                    writeHtml(placeholder);
                writeEnd();

                for (Object item : items) {
                    State itemState = State.getInstance(item);
                    writeStart("option",
                            "selected", item.equals(value) ? "selected" : null,
                            "value", itemState.getId());
                        writeObjectLabel(item);
                    writeEnd();
                }
            writeEnd();

        } else {
            State state = State.getInstance(value);
            StringBuilder typeIds = new StringBuilder();

            for (ObjectType type : field.getTypes()) {
                typeIds.append(type.getId());
                typeIds.append(',');
            }

            if (typeIds.length() > 0) {
                typeIds.setLength(typeIds.length() - 1);
            }

            writeElement("input",
                    "type", "text",
                    "class", "objectId",
                    "data-additional-query", field.getPredicate(),
                    "data-generic-argument-index", field.getGenericArgumentIndex(),
                    "data-dynamic-placeholder", ui.getPlaceholderDynamicText(),
                    "data-label", value != null ? getObjectLabel(value) : null,
                    "data-pathed", ToolUi.isOnlyPathed(field),
                    "data-preview", getPreviewThumbnailUrl(value),
                    "data-searcher-path", ui.getInputSearcherPath(),
                    "data-suggestions", ui.isEffectivelySuggestions(),
                    "data-typeIds", typeIds,
                    "data-visibility", value != null ? state.getVisibilityLabel() : null,
                    "value", value != null ? state.getId() : null,
                    "placeholder", placeholder,
                    attributes);
        }
    }

    /**
     * Writes a {@code <select>} tag that allows the user to pick a
     * visibility status.
     *
     * @param type May be {@code null}.
     * @param values Initial values. May be {@code null}.
     * @param attributes May be {@code null}.
     */
    public void writeMultipleVisibilitySelect(
            ObjectType type,
            Collection<String> values,
            Object... attributes) throws IOException {

        if (values == null) {
            values = Collections.emptySet();
        }

        Map<String, String> statuses = new HashMap<String, String>();

        statuses.put("p", "Published");

        boolean hasWorkflow = false;

        for (Workflow w : (type == null ?
                Query.from(Workflow.class) :
                Query.from(Workflow.class).where("contentTypes = ?", type)).
                selectAll()) {

            for (WorkflowState s : w.getStates()) {
                hasWorkflow = true;

                statuses.put("w." + s.getName(), s.getDisplayName());
            }
        }

        if (hasWorkflow) {
            statuses.put("w", "In Workflow");
        }

        addVisibilityStatuses(statuses, Database.Static.getDefault().getEnvironment());
        addVisibilityStatuses(statuses, type);

        List<Map.Entry<String, String>> sortedStatuses = new ArrayList<Map.Entry<String, String>>(statuses.entrySet());

        Collections.sort(sortedStatuses, new Comparator<Map.Entry<String, String>>() {

            @Override
            public int compare(Map.Entry<String, String> x, Map.Entry<String, String> y) {
                return x.getValue().compareTo(y.getValue());
            }
        });

        writeStart("select",
                "multiple", "multiple",
                "placeholder", "Status (Published)",
                attributes);

            for (Map.Entry<String, String> entry : sortedStatuses) {
                String key = entry.getKey();

                writeStart("option",
                        "selected", values.contains(key) ? "selected" : null,
                        "value", key);
                    writeHtml(entry.getValue());
                writeEnd();
            }
        writeEnd();
    }

    private void addVisibilityStatuses(Map<String, String> statuses, ObjectStruct struct) {
        if (struct == null) {
            return;
        }

        for (ObjectIndex index : struct.getIndexes()) {
            if (index.isVisibility()) {
                for (String fieldName : index.getFields()) {
                    ObjectField field = struct.getField(fieldName);

                    if (field != null) {
                        String type = field.getInternalItemType();

                        if (ObjectField.BOOLEAN_TYPE.equals(type)) {
                            String displayName = field.getDisplayName();

                            if (displayName.endsWith("?")) {
                                displayName = displayName.substring(0, displayName.length() - 1);
                            }

                            statuses.put("b." + field.getUniqueName(), displayName);

                        } else if (ObjectField.TEXT_TYPE.equals(type)) {
                            Set<ObjectField.Value> values = field.getValues();

                            if (values != null && !values.isEmpty()) {
                                for (ObjectField.Value value : values) {
                                    statuses.put("t." + field.getUniqueName() + "=" + value.getValue(), field.getDisplayName() + ": " + value.getLabel());
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Returns {@code true} if the {@code <select>} tag would be used to allow
     * the user to pick a content for the given {@code field}.
     *
     * @param field Can't be {@code null}.
     */
    public boolean isObjectSelectDropDown(ObjectField field) {
        ErrorUtils.errorIfNull(field, "field");

        if (field.as(ToolUi.class).isDropDown()) {
            long dropDownMaximum = Settings.getOrDefault(long.class, "cms/tool/dropDownMaximum", 250L);

            if (field.getTypes().contains(ObjectType.getInstance(ObjectType.class))) {
                Set<ObjectType> types = Database.Static.getDefault().getEnvironment().getTypes();

                if (types.size() <= dropDownMaximum) {
                    return true;

                } else if (field.getPredicate() != null) {
                    long numFilteredTypes = 0;
                    for (ObjectType type : types) {
                        if (type.is(field.getPredicate())) {
                            if (++numFilteredTypes > dropDownMaximum) {
                                break;
                            }
                        }
                    }

                    return (numFilteredTypes <= dropDownMaximum);
                }

            } else {
                return !new Search(field).toQuery(getSite()).hasMoreThan(dropDownMaximum);
            }

        }

        return false;

    }

    /** Writes all grid CSS, or does nothing if it's already written. */
    public ToolPageContext writeGridCssOnce() throws IOException {
        LayoutTag.Static.writeGridCss(this, getServletContext(), getRequest());
        return this;
    }

    /**
     * Writes the heading that precedes the form to create or update the
     * given {@code object}.
     *
     * @param attributes Extra attributes for the heading element.
     */
    public void writeFormHeading(Object object, Object... attributes) throws IOException {
        State state = State.getInstance(object);
        ObjectType type = state.getType();
        String typeLabel = getTypeLabel(object);
        String iconName = null;

        if (type != null) {
            iconName = type.as(ToolUi.class).getIconName();
        }

        if (ObjectUtils.isBlank(iconName)) {
            iconName = "object";
        }

        writeStart("h1",
                "class", "icon icon-" + iconName,
                attributes);
            if (state.isNew()) {
                writeHtml("New ");
                writeHtml(typeLabel);

            } else {
                writeHtml("Edit ");
                writeHtml(typeLabel);
            }
        writeEnd();
    }

    /**
     * Disables all form fields after this call so that they're displayed but
     * not processed on update.
     */
    public void disableFormFields() {
        HttpServletRequest request = getRequest();
        Integer disabled = (Integer) request.getAttribute(FORM_FIELDS_DISABLED_ATTRIBUTE);

        request.setAttribute(FORM_FIELDS_DISABLED_ATTRIBUTE, disabled != null ? disabled + 1 : 1);
    }

    /**
     * Enables all form fields after this call so that they're both displayed
     * and processed on update.
     */
    public void enableFormFields() {
        HttpServletRequest request = getRequest();
        Integer disabled = (Integer) request.getAttribute(FORM_FIELDS_DISABLED_ATTRIBUTE);

        if (disabled != null) {
            request.setAttribute(FORM_FIELDS_DISABLED_ATTRIBUTE, disabled - 1);
        }
    }

    /**
     * Returns {@code true} if the form fields are enabled to be both
     * displayed and processed on update.
     */
    public boolean isFormFieldsDisabled() {
        Integer disabled = (Integer) getRequest().getAttribute(FORM_FIELDS_DISABLED_ATTRIBUTE);
        return disabled != null && disabled > 0;
    }

    /**
     * Writes a contextual message if the given {@code object} is in trash.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the message was written.
     */
    public boolean writeTrashMessage(Object object) throws IOException {
        State state = State.getInstance(object);
        Content.ObjectModification contentData = state.as(Content.ObjectModification.class);

        if (!contentData.isTrash()) {
            return false;
        }

        writeStart("div", "class", "message message-warning");
            writeStart("p");
                writeHtml("Archived ");
                writeHtml(formatUserDateTime(contentData.getUpdateDate()));
                writeHtml(" by ");
                writeObjectLabel(contentData.getUpdateUser());
                writeHtml(".");
            writeEnd();

            writeStart("div", "class", "actions");
                writeStart("button",
                        "class", "link icon icon-action-restore",
                        "name", "action-restore",
                        "value", "true");
                    writeHtml("Restore");
                writeEnd();

                writeStart("button",
                        "class", "link icon icon-action-delete",
                        "name", "action-delete",
                        "value", "true");
                    writeHtml("Delete Permanently");
                writeEnd();
            writeEnd();
        writeEnd();

        return true;
    }

    private void includeFromCms(String url, Object... attributes) throws IOException, ServletException {
        JspUtils.include(getRequest(), getResponse(), getWriter(), cmsUrl(url), attributes);
    }

    /**
     * Writes all form fields for the given {@code object}.
     *
     * @param object Can't be {@code null}.
     */
    public void writeFormFields(Object object) throws IOException, ServletException {
        includeFromCms("/WEB-INF/objectForm.jsp", "object", object);
    }

    /**
     * Writes a standard form for the given {@code object}.
     *
     * @param object Can't be {@code null}.
     * @param displayTrashAction If {@code null}, displays the trash action
     * instead of the delete action.
     */
    public void writeStandardForm(Object object, boolean displayTrashAction) throws IOException, ServletException {
        State state = State.getInstance(object);
        ObjectType type = state.getType();

        writeFormHeading(object);

        writeStart("div", "class", "widgetControls");
            includeFromCms("/WEB-INF/objectVariation.jsp", "object", object);
        writeEnd();

        includeFromCms("/WEB-INF/objectMessage.jsp", "object", object);

        writeStart("form",
                "class", "standardForm",
                "method", "post",
                "enctype", "multipart/form-data",
                "action", url("", "id", state.getId()),
                "autocomplete", "off",
                "data-type", type != null ? type.getInternalName() : null);
            boolean trash = writeTrashMessage(object);

            writeFormFields(object);

            if (!trash) {
                writeStart("div", "class", "actions");
                    writeStart("button",
                            "class", "icon icon-action-save",
                            "name", "action-save",
                            "value", "true");
                        writeHtml("Save");
                    writeEnd();

                    if (!state.isNew() &&
                            (type == null ||
                            (!type.getGroups().contains(Singleton.class.getName()) &&
                            !type.getGroups().contains(Tool.class.getName())))) {
                        if (displayTrashAction) {
                            writeStart("button",
                                    "class", "icon icon-action-trash action-pullRight link",
                                    "name", "action-trash",
                                    "value", "true");
                                writeHtml("Archive");
                            writeEnd();

                        } else {
                            writeStart("button",
                                    "class", "icon icon-action-delete action-pullRight link",
                                    "name", "action-delete",
                                    "value", "true");
                                writeHtml("Delete");
                            writeEnd();
                        }
                    }
                writeEnd();
            }
        writeEnd();
    }

    /**
     * Writes a standard form for the given {@code object} with the trash
     * action.
     *
     * @param object Can't be {@code null}.
     * @see #writeStandardForm(Object, boolean)
     */
    public void writeStandardForm(Object object) throws IOException, ServletException {
        writeStandardForm(object, true);
    }

    /**
     * Writes a link that points to either the Javadoc or the source for the
     * given {@code objectClass}.
     *
     * @param objectClass Can't be {@code null}.
     */
    public void writeJavaClassLink(Class<?> objectClass) throws IOException {
        String objectClassName = objectClass.getName();
        String javadocUrlPrefix;

        if (objectClassName.startsWith("com.psddev.cms.db.")) {
            javadocUrlPrefix = "http://public.psddev.com/javadoc/brightspot-cms/";

        } else if (objectClassName.startsWith("com.psddev.dari.db.")) {
            javadocUrlPrefix = "http://public.psddev.com/javadoc/dari/";

        } else {
            javadocUrlPrefix = null;
        }

        if (ObjectUtils.isBlank(javadocUrlPrefix)) {
            File source = CodeUtils.getSource(objectClassName);

            if (source != null) {
                writeStart("a",
                        "target", "_blank",
                        "href", DebugFilter.Static.getServletPath(getRequest(), "code",
                                "file", source));
                    writeStart("code");
                        writeHtml(objectClassName);
                    writeEnd();
                writeEnd();

            } else {
                writeStart("code");
                    writeHtml(objectClassName);
                writeEnd();
            }

        } else {
            writeStart("a",
                    "target", "_blank",
                    "href", javadocUrlPrefix + objectClassName.replace('.', '/').replace('$', '.') + ".html");
                writeStart("code");
                    writeHtml(objectClassName);
                writeEnd();
            writeEnd();
        }
    }

    /**
     * Updates the given {@code object} using all request parameters.
     *
     * @param object Can't be {@code null}.
     */
    public void updateUsingParameters(Object object) throws IOException, ServletException {
        includeFromCms("/WEB-INF/objectPost.jsp", "object", object);
    }

    /**
     * Updates the given {@code object} using all widgets with the data from
     * the current request.
     *
     * @param object Can't be {@code null}.
     */
    @SuppressWarnings("deprecation")
    public void updateUsingAllWidgets(Object object) throws Exception {
        ErrorUtils.errorIfNull(object, "object");

        State state = State.getInstance(object);
        List<String> requestWidgets = params(String.class, state.getId() + "/_widget");

        if (requestWidgets.isEmpty()) {
            return;
        }

        DependencyResolver<Widget> widgets = new DependencyResolver<Widget>();

        for (Widget widget : Tool.Static.getPluginsByClass(Widget.class)) {
            widgets.addRequired(widget, widget.getUpdateDependencies());
        }

        for (Widget widget : widgets.resolve()) {
            for (String requestWidget : requestWidgets) {
                if (widget.getInternalName().equals(requestWidget)) {
                    widget.update(this, object);
                    break;
                }
            }
        }

        Page.Layout layout = (Page.Layout) getRequest().getAttribute("layoutHack");

        if (layout != null) {
            ((Page) object).setLayout(layout);
        }
    }

    private void redirectOnSave(String url, Object... parameters) throws IOException {
        if (getUser().isReturnToDashboardOnSave()) {
            getResponse().sendRedirect(cmsUrl("/"));

        } else {
            getResponse().sendRedirect(StringUtils.addQueryParameters(url(url, parameters), "editAnyway", null));
        }
    }

    /**
     * Tries to delete the given {@code object} if the user has asked for it
     * in the current request.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the delete is tried.
     */
    public boolean tryDelete(Object object) {
        if (!isFormPost() ||
                param(String.class, "action-delete") == null) {
            return false;
        }

        try {
            State state = State.getInstance(object);
            Draft draft = getOverlaidDraft(object);

            if (draft != null) {
                draft.delete();

                if (state.as(Content.ObjectModification.class).isDraft()) {
                    state.delete();
                }

                Schedule schedule = draft.getSchedule();

                if (schedule != null &&
                        ObjectUtils.isBlank(schedule.getName())) {
                    schedule.delete();
                }

            } else {
                state.delete();
            }

            redirectOnSave("");
            return true;

        } catch (Exception error) {
            getErrors().add(error);
            return false;
        }
    }

    /**
     * Returns the publish date from the content form.
     *
     * @return May be {@code null}.
     */
    public Date getContentFormPublishDate() {
        Date publishDate = param(Date.class, "publishDate");

        if (publishDate != null) {
            DateTimeZone timeZone = getUserDateTimeZone();
            publishDate = new Date(DateTimeFormat.
                    forPattern("yyyy-MM-dd HH:mm:ss").
                    withZone(timeZone).
                    parseMillis(new DateTime(publishDate).toString("yyyy-MM-dd HH:mm:ss")));

            if (publishDate.before(new Date(new DateTime(timeZone).getMillis()))) {
                publishDate = null;
            }
        }

        return publishDate;
    }

    /**
     * Sets the publish date from the content form as the schedule date
     * on the given {@code object}.
     *
     * @param object Can't be {@code null}.
     */
    public void setContentFormScheduleDate(Object object) {
        State state = State.getInstance(object);
        Content.ObjectModification contentData = state.as(Content.ObjectModification.class);
        Date publishDate = getContentFormPublishDate();

        if (publishDate != null) {
            contentData.setPublishDate(publishDate);
        }

        contentData.setScheduleDate(publishDate);
    }

    /**
     * Tries to save the given {@code object} as a draft if the user has
     * asked for it in the current request.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the save is tried.
     */
    public boolean tryDraft(Object object) {
        if (!isFormPost() ||
                param(String.class, "action-draft") == null) {
            return false;
        }

        setContentFormScheduleDate(object);

        State state = State.getInstance(object);
        Draft draft = getOverlaidDraft(object);
        Site site = getSite();

        try {
            updateUsingParameters(object);
            updateUsingAllWidgets(object);

            if (state.isNew() &&
                    site != null &&
                    site.getDefaultVariation() != null) {
                state.as(Variation.Data.class).setInitialVariation(site.getDefaultVariation());
            }

            if (draft == null) {
                if (state.isNew() ||
                        state.as(Content.ObjectModification.class).isDraft()) {
                    state.as(Content.ObjectModification.class).setDraft(true);
                    publish(state);
                    redirectOnSave("",
                            "_frame", param(boolean.class, "_frame") ? Boolean.TRUE : null,
                            "id", state.getId(),
                            "copyId", null);
                    return true;

                } else if (state.as(Workflow.Data.class).getCurrentState() != null) {
                    publish(state);
                    redirectOnSave("",
                            "_frame", param(boolean.class, "_frame") ? Boolean.TRUE : null);
                    return true;
                }

                draft = new Draft();
                draft.setOwner(getUser());
                draft.setObject(object);

            } else {
                draft.setObject(object);
            }

            publish(draft);
            redirectOnSave("",
                    "_frame", param(boolean.class, "_frame") ? Boolean.TRUE : null,
                    ToolPageContext.DRAFT_ID_PARAMETER, draft.getId(),
                    ToolPageContext.HISTORY_ID_PARAMETER, null);
            return true;

        } catch (Exception error) {
            getErrors().add(error);
            return false;
        }
    }

    /**
     * Tries to publish or schedule the given {@code object} if the user has
     * asked for it in the current request.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the restore is tried.
     */
    public boolean tryPublish(Object object) {
        if (!isFormPost() ||
                param(String.class, "action-publish") == null) {
            return false;
        }

        State state = State.getInstance(object);
        Content.ObjectModification contentData = state.as(Content.ObjectModification.class);
        ToolUser user = getUser();

        if (state.isNew() ||
                object instanceof Draft ||
                contentData.isDraft() ||
                state.as(Workflow.Data.class).getCurrentState() != null) {
            if (getContentFormPublishDate() != null) {
                setContentFormScheduleDate(object);

            } else {
                contentData.setPublishDate(new Date());
                contentData.setPublishUser(user);
            }
        }

        Draft draft = getOverlaidDraft(object);
        UUID variationId = param(UUID.class, "variationId");
        Site site = getSite();

        try {
            state.beginWrites();
            state.as(Workflow.Data.class).changeState(null, user, (WorkflowLog) null);

            if (variationId == null ||
                    (site != null &&
                    ((state.isNew() && site.getDefaultVariation() != null) ||
                    ObjectUtils.equals(site.getDefaultVariation(), state.as(Variation.Data.class).getInitialVariation())))) {
                if (state.isNew() && site != null && site.getDefaultVariation() != null) {
                    state.as(Variation.Data.class).setInitialVariation(site.getDefaultVariation());
                }

                getRequest().setAttribute("original", object);
                includeFromCms("/WEB-INF/objectPost.jsp", "object", object, "original", object);
                updateUsingAllWidgets(object);

                if (variationId != null &&
                        variationId.equals(state.as(Variation.Data.class).getInitialVariation())) {
                    state.putByPath("variations/" + variationId.toString(), null);
                }

            } else {
                Object original = Query.
                        from(Object.class).
                        where("_id = ?", state.getId()).
                        noCache().
                        first();
                Map<String, Object> oldStateValues = State.getInstance(original).getSimpleValues();

                getRequest().setAttribute("original", original);
                includeFromCms("/WEB-INF/objectPost.jsp", "object", object, "original", original);
                updateUsingAllWidgets(object);

                Map<String, Object> newStateValues = state.getSimpleValues();
                Set<String> stateKeys = new LinkedHashSet<String>();
                Map<String, Object> stateValues = new LinkedHashMap<String, Object>();

                stateKeys.addAll(oldStateValues.keySet());
                stateKeys.addAll(newStateValues.keySet());

                for (String key : stateKeys) {
                    Object value = newStateValues.get(key);
                    if (!ObjectUtils.equals(oldStateValues.get(key), value)) {
                        stateValues.put(key, value);
                    }
                }

                State.getInstance(original).putByPath("variations/" + variationId.toString(), stateValues);
                State.getInstance(original).getExtras().put("cms.variedObject", object);
                object = original;
                state = State.getInstance(object);
            }

            Schedule schedule = user.getCurrentSchedule();
            Date publishDate = null;

            if (schedule == null) {
                publishDate = getContentFormPublishDate();

            } else if (draft == null) {
                draft = Query.
                        from(Draft.class).
                        where("schedule = ?", schedule).
                        and("objectId = ?", object).
                        first();
            }

            if (schedule != null || publishDate != null) {
                if (!state.validate()) {
                    throw new ValidationException(Arrays.asList(state));
                }

                if (draft == null || param(boolean.class, "newSchedule")) {
                    draft = new Draft();
                    draft.setOwner(user);
                }

                draft.setObject(object);

                if (state.isNew() || contentData.isDraft()) {
                    contentData.setDraft(true);
                    publish(state);
                    draft.setObjectChanges(null);
                }

                if (schedule == null) {
                    schedule = draft.getSchedule();
                }

                if (schedule == null) {
                    schedule = new Schedule();
                    schedule.setTriggerSite(site);
                    schedule.setTriggerUser(user);
                }

                if (publishDate != null) {
                    schedule.setTriggerDate(publishDate);
                    schedule.save();
                }

                draft.setSchedule(schedule);
                publish(draft);
                state.commitWrites();
                redirectOnSave("",
                        "_frame", param(boolean.class, "_frame") ? Boolean.TRUE : null,
                        ToolPageContext.DRAFT_ID_PARAMETER, draft.getId());

            } else {
                if (draft != null) {
                    draft.delete();
                }

                if (draft != null || contentData.isDraft()) {
                    contentData.setDraft(false);
                }

                if (!state.isVisible()) {
                    contentData.setPublishDate(null);
                    contentData.setPublishUser(null);
                }

                publish(object);
                state.commitWrites();
                redirectOnSave("",
                        "_frame", param(boolean.class, "_frame") ? Boolean.TRUE : null,
                        "typeId", state.getTypeId(),
                        "id", state.getId(),
                        "historyId", null,
                        "copyId", null,
                        "ab", null,
                        "published", System.currentTimeMillis());
            }

            return true;

        } catch (Exception error) {
            getErrors().add(error);
            return false;

        } finally {
            state.endWrites();
        }
    }

    /**
     * Tries to restore the given {@code object} if the user has asked for it
     * in the current request.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the restore is tried.
     */
    public boolean tryRestore(Object object) {
        if (!isFormPost() ||
                param(String.class, "action-restore") == null) {
            return false;
        }

        try {
            Draft draft = getOverlaidDraft(object);
            State state = State.getInstance(draft != null ? draft : object);

            state.as(Content.ObjectModification.class).setTrash(false);
            publish(state);
            redirectOnSave("");
            return true;

        } catch (Exception error) {
            getErrors().add(error);
            return false;
        }
    }

    /**
     * Tries to save the given {@code object} if the user has asked for it
     * in the current request.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the trash is tried.
     */
    public boolean trySave(Object object) {
        if (!isFormPost() ||
                param(String.class, "action-save") == null) {
            return false;
        }

        State state = State.getInstance(object);

        try {
            updateUsingParameters(object);
            state.save();
            redirectOnSave("",
                    "_frame", param(boolean.class, "_frame") ? Boolean.TRUE : null,
                    "id", state.getId());
            return true;

        } catch (Exception error) {
            getErrors().add(error);
            return false;
        }
    }

    /**
     * Tries to apply a standard set of updates to the given {@code object}
     * if the user has asked for any in the current request.
     *
     * <p>This method calls the following methods in order:</p>
     *
     * <ul>
     * <li>{@link #tryDelete}</li>
     * <li>{@link #tryRestore}</li>
     * <li>{@link #trySave}</li>
     * <li>{@link #tryTrash}</li>
     * </ul>
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the trash is tried.
     */
    public boolean tryStandardUpdate(Object object) {
        return tryDelete(object) ||
                tryRestore(object) ||
                trySave(object) ||
                tryTrash(object);
    }

    /**
     * Tries to trash the given {@code object} if the user has asked for it
     * in the current request.
     *
     * @param object Can't be {@code null}.
     * @return {@code true} if the trash is tried.
     */
    public boolean tryTrash(Object object) {
        if (!isFormPost() ||
                param(String.class, "action-trash") == null) {
            return false;
        }

        try {
            Draft draft = getOverlaidDraft(object);

            trash(draft != null ? draft : object);
            redirectOnSave("");
            return true;

        } catch (Exception error) {
            getErrors().add(error);
            return false;
        }
    }

    /**
     * Tries to apply a workflow action to the given {@code object} if the
     * user has asked for it in the current request.
     *
     * @param object Can't be {@code null}.
     * @param {@code true} if the application of a workflow action is tried.
     */
    public boolean tryWorkflow(Object object) {
        if (!isFormPost()) {
            return false;
        }

        String action = param(String.class, "action-workflow");

        if (ObjectUtils.isBlank(action)) {
            return false;
        }

        setContentFormScheduleDate(object);

        State state = State.getInstance(object);
        Draft draft = getOverlaidDraft(object);
        Workflow.Data workflowData = state.as(Workflow.Data.class);
        String oldWorkflowState = workflowData.getCurrentState();

        try {
            state.beginWrites();

            Workflow workflow = Query.from(Workflow.class).where("contentTypes = ?", state.getType()).first();

            if (workflow != null) {
                WorkflowTransition transition = workflow.getTransitions().get(action);

                if (transition != null) {
                    WorkflowLog log = new WorkflowLog();

                    updateUsingParameters(object);
                    updateUsingAllWidgets(object);
                    state.as(Content.ObjectModification.class).setDraft(false);
                    log.getState().setId(param(UUID.class, "workflowLogId"));
                    updateUsingParameters(log);
                    workflowData.changeState(transition, getUser(), log);

                    if (draft == null) {
                        publish(object);

                    } else {
                        draft.as(Workflow.Data.class).changeState(transition, getUser(), log);
                        draft.setObject(object);
                        publish(draft);
                    }

                    state.commitWrites();
                }
            }

            redirectOnSave("", "id", state.getId());
            return true;

        } catch (Exception error) {
            if (draft != null) {
                draft.as(Workflow.Data.class).revertState(oldWorkflowState);
            }

            workflowData.revertState(oldWorkflowState);
            getErrors().add(error);
            return false;

        } finally {
            state.endWrites();
        }
    }

    // --- AuthenticationFilter bridge ---

    /** @see AuthenticationFilter.Static#requireUser */
    public boolean requireUser() throws IOException {
        return AuthenticationFilter.Static.requireUser(getServletContext(), getRequest(), getResponse());
    }

    /**
     * Returns the current user accessing the tool.
     *
     * @see AuthenticationFilter.Static#getUser
     */
    public ToolUser getUser() {
        return AuthenticationFilter.Static.getUser(getRequest());
    }

    /**
     * Returns the current tool user setting value associated with the given
     * {@code key}.
     *
     * @see AuthenticationFilter.Static#getUserSetting
     */
    public Object getUserSetting(String key) {
        return AuthenticationFilter.Static.getUserSetting(getRequest(), key);
    }

    /**
     * Puts the given setting {@code value} at the given {@code key} for
     * the current tool user.
     *
     * @see AuthenticationFilter.Static#putUserSetting
     */
    public void putUserSetting(String key, Object value) {
        AuthenticationFilter.Static.putUserSetting(getRequest(), key, value);
    }

    /**
     * Returns the page setting value associated with the given {@code key}.
     *
     * @see AuthenticationFilter.Static#getPageSetting
     */
    public Object getPageSetting(String key) {
        return AuthenticationFilter.Static.getPageSetting(getRequest(), key);
    }

    /**
     * Puts the page setting {@code value} at the given {@code key}.
     *
     * @see AuthenticationFilter.Static#putPageSetting
     */
    public void putPageSetting(String key, Object value) {
        AuthenticationFilter.Static.putPageSetting(getRequest(), key, value);
    }

    /**
     * Returns the site that the {@linkplain #getUser current user}
     * is accessing.
     */
    public Site getSite() {
        ToolUser user = getUser();
        return user != null ? user.getCurrentSite() : null;
    }

    /**
     * Returns {@code true} if the {@linkplain #getUser current user}
     * is allowed access to the resources identified by the given
     * {@code permissionId}.
     *
     * @param If {@code null}, returns {@code true}.
     */
    public boolean hasPermission(String permissionId) {
        ToolUser user = getUser();

        return user != null &&
                (permissionId == null ||
                user.hasPermission(permissionId));
    }

    public boolean requirePermission(String permissionId) throws IOException {
        if (requireUser()) {
            return true;

        } else {
            if (hasPermission(permissionId)) {
                return false;

            } else {
                getResponse().sendError(Settings.isProduction() ?
                        HttpServletResponse.SC_NOT_FOUND :
                        HttpServletResponse.SC_FORBIDDEN);
                return true;
            }
        }
    }

    // --- Content.Static bridge ---

    /**
     * @see Content.Static#deleteSoftly
     * @deprecated Use {@link #trash} instead.
     */
    @Deprecated
    public Trash deleteSoftly(Object object) {
        return Content.Static.deleteSoftly(object, getSite(), getUser());
    }

    /** @see Content.Static#publish */
    public History publish(Object object) {
        History history = Content.Static.publish(object, getSite(), getUser());

        if (history != null &&
                param(boolean.class, "editAnyway")) {
            history.setLockIgnored(true);
            history.save();
        }

        return history;
    }

    /**
     * @see Content.Static#trash
     */
    public void trash(Object object) {
        Content.Static.trash(object, getSite(), getUser());
    }

    /** @see Content.Static#purge */
    public void purge(Object object) {
        Content.Static.purge(object, getSite(), getUser());
    }

    // --- WebPageContext support ---

    @Deprecated
    private PageWriter pageWriter;

    @Deprecated
    @Override
    public PageWriter getWriter() throws IOException {
        if (pageWriter == null) {
            pageWriter = new PageWriter(super.getWriter());
        }

        return pageWriter;
    }

    /** {@link ToolPageContext} utility methods. */
    public static final class Static {

        private Static() {
        }

        private static String notTooShort(String word) {
            char[] letters = word.toCharArray();
            StringBuilder not = new StringBuilder();
            int index = 0;
            int length = letters.length;

            for (; index < 5 && index < length; ++ index) {
                char letter = letters[index];

                if (Character.isWhitespace(letter)) {
                    not.append('\u00a0');
                } else {
                    not.append(letter);
                }
            }

            if (index < length) {
                not.append(letters, index, length - index);
            }

            return not.toString();
        }

        /**
         * Returns a label, or the given {@code defaultLabel} if one can't be
         * found, for the given {@code object}.
         */
        public static String getObjectLabelOrDefault(Object object, String defaultLabel) {
            State state = State.getInstance(object);

            if (state != null) {
                String label = state.getLabel();

                if (ObjectUtils.to(UUID.class, label) == null) {
                    return notTooShort(label);
                }
            }

            return notTooShort(defaultLabel);
        }

        /** Returns a label for the given {@code object}. */
        public static String getObjectLabel(Object object) {
            State state = State.getInstance(object);
            String label = null;

            if (state != null) {
                label = state.getLabel();
            }

            if (ObjectUtils.isBlank(label)) {
                label = "Not Available";
            }

            return notTooShort(label);
        }

        /**
         * Returns a label, or the given {@code defaultLabel} if one can't be
         * found, for the type of the given {@code object}.
         */
        public static String getTypeLabelOrDefault(Object object, String defaultLabel) {
            State state = State.getInstance(object);

            if (state != null) {
                ObjectType type = state.getType();

                if (type != null) {
                    return getObjectLabel(type);
                }
            }

            return notTooShort(defaultLabel);
        }

        /** Returns a label for the type of the given {@code object}. */
        public static String getTypeLabel(Object object) {
            return getTypeLabelOrDefault(object, "Unknown Type");
        }
    }

    // --- Deprecated ---

    /** @deprecated Use {@link ToolPageContext(ServletContext, HttpServletRequest, HttpServletResponse} instead. */
    @Deprecated
    public ToolPageContext(
            Servlet servlet,
            HttpServletRequest request,
            HttpServletResponse response) {

        super(servlet, request, response);
    }

    /** @deprecated Use {@link Database.Static#getDefault} instead. */
    @Deprecated
    public Database getDatabase() {
        return Database.Static.getDefault();
    }

    /** @deprecated Use {@link Query#from} instead. */
    @Deprecated
    public <T> Query<T> queryFrom(Class<T> objectClass) {
        Query<T> query = Query.from(objectClass);
        query.setDatabase(getDatabase());
        return query;
    }

    /**
     * Returns an HTML-escaped label, or the given {@code defaultLabel} if
     * one can't be found, for the given {@code object}.
     *
     * @deprecated Use {@link #getObjectLabelOrDefault} and {@link #h} instead.
     */
    @Deprecated
    public String objectLabel(Object object, String defaultLabel) {
        return h(getObjectLabelOrDefault(object, defaultLabel));
    }

    /**
     * Returns an HTML-escaped label for the given {@code object}.
     *
     * @deprecated Use {@link getObjectLabel} and {@link #h} instead.
     */
    @Deprecated
    public String objectLabel(Object object) {
        return h(getObjectLabel(object));
    }

    /**
     * Returns an HTML-escaped label, or the given {@code defaultLabel} if
     * one can't be found, for the type of the given {@code object}.
     *
     * @deprecated Use {@link #getTypeLabelOrDefault} and {@link #h} instead.
     */
    @Deprecated
    public String typeLabel(Object object, String defaultLabel) {
        return h(getTypeLabelOrDefault(object, defaultLabel));
    }

    /**
     * Returns an HTML-escaped label for the type of the given
     * {@code object}.
     *
     * @deprecated Use {@link #getTypeLabel} and {@link #h} instead.
     */
    @Deprecated
    public String typeLabel(Object object) {
        return h(getTypeLabel(object));
    }

    /** @deprecated Use {@link writeTypeSelect} instead. */
    @Deprecated
    public void typeSelect(
            Iterable<ObjectType> types,
            ObjectType selectedType,
            String allLabel,
            Object... attributes) throws IOException {

        writeTypeSelect(types, selectedType, allLabel, attributes);
    }

    /** @deprecated Use {@link writeObjectSelect} instead. */
    @Deprecated
    public void objectSelect(ObjectField field, Object value, Object... attributes) throws IOException {
        writeObjectSelect(field, value, attributes);
    }
}
TOP

Related Classes of com.psddev.cms.tool.ToolPageContext

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.