com.flexive.war.beans.admin.content.BeContentEditorBean.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.war.beans.admin.content.BeContentEditorBean.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) backend application.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) backend application is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation;
 *  either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/licenses/gpl.html.
 *  A copy is found in the textfile GPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.war.beans.admin.content;

import com.flexive.faces.FxJsf1Utils;
import com.flexive.faces.FxJsfUtils;
import com.flexive.faces.beans.ActionBean;
import com.flexive.faces.beans.FxContentEditorBean;
import com.flexive.faces.beans.SearchResultBean;
import com.flexive.faces.components.content.FxWrappedContent;
import com.flexive.faces.messages.FxFacesMsgErr;
import com.flexive.faces.messages.FxFacesMsgInfo;
import com.flexive.shared.*;
import com.flexive.shared.content.FxContent;
import com.flexive.shared.content.FxContentVersionInfo;
import com.flexive.shared.content.FxDelta;
import com.flexive.shared.content.FxPK;
import com.flexive.shared.exceptions.FxApplicationException;
import com.flexive.shared.exceptions.FxLockException;
import com.flexive.shared.interfaces.ContentEngine;
import com.flexive.shared.interfaces.TreeEngine;
import com.flexive.shared.security.LifeCycleInfo;
import com.flexive.shared.security.UserTicket;
import com.flexive.shared.structure.FxType;
import com.flexive.shared.tree.FxTreeMode;
import com.flexive.shared.tree.FxTreeNode;
import com.flexive.shared.tree.FxTreeNodeEdit;
import com.flexive.shared.tree.FxTreeRemoveOp;
import com.flexive.shared.value.FxReference;
import com.flexive.shared.value.FxValue;
import com.flexive.shared.value.ReferencedContent;
import com.flexive.shared.value.renderer.FxValueFormatter;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.custom.fileupload.UploadedFile;

import javax.faces.model.SelectItem;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.util.*;

/**
 * Backend content editor bean. Use
 * {@link com.flexive.war.beans.admin.content.BeContentEditorBean#initEditor(com.flexive.shared.content.FxContent, boolean)},
 * {@link com.flexive.war.beans.admin.content.BeContentEditorBean#initEditor(com.flexive.shared.content.FxPK, boolean)}, or
 * {@link com.flexive.war.beans.admin.content.BeContentEditorBean#initEditor(long, boolean)}, to initialize the content
 * editor from other beans, or use the url to pass parameters.
 */
public class BeContentEditorBean implements ActionBean, Serializable {
    private static final long serialVersionUID = 1255372771864031452L;
    private static final Log LOG = LogFactory.getLog(BeContentEditorBean.class);

    private static final String FORM_ID = "frm";
    private static final String EDITOR_ID = "CE_ID";
    // edit new content of given type
    private long typeId = -1;
    // edit existing content of given pk
    private FxPK pk;
    // edit given content instance
    private FxContent content;

    private int version;
    private List<FxTreeNode> treeNodes;
    private long treeNodeParent;
    private FxTreeNode treeNode;
    private String infoPanelState;
    private int compareSourceVersion;
    private int compareDestinationVersion;
    private FxValueFormatter valueFormatter;
    private Map<Long, String> treeLabelMap;
    private Map<Long, String> displayTreeLabelMap;
    // content injected from the content editor component
    private FxWrappedContent wrappedContent;
    // edit mode
    private boolean editMode;
    private boolean reset;
    // type id for creating new contents
    private long newTypeId;

    /*import*/
    // uploaded file for import
    private UploadedFile importUpload;
    // Pasted content for import
    private String importPasted;
    // Save after an import or keep editing?
    private boolean importSave;

    private long contentIdToInit = -1;

    private String fxCeRenderFieldsJSFunction = null;
    private boolean beBeanInUse;

    // Flag indicating if the content is opened/edited from a result set
    private boolean fromResultSet;
    // reload the content tree - flag checked in contenteditor.xhtml main page
    private boolean reloadContentTree = false;

    // hold the PKs in the order the user want them
    private String sortedPKs = null;

    private boolean changed = false;

    private CallbackOpts nextOp = null;

    // keep the index of a requested PK for the reqeuest in the hashtable so that we don't need to search for it every time
    private Hashtable<FxPK, Integer> currentIndexCache = new Hashtable<FxPK, Integer>();

    private ArrayList<FxPK> sortedPKArray = new ArrayList<FxPK>();
    private transient HttpSession session = null;
    private final static String SORTED_PK_KEY = "BeContentEditorBean/sortedPK";
    private static final String LOOSE_LOCKED_PKS = "LooseLockedPKs";

    /**
     * {@inheritDoc}
     */
    @Override
    public String getParseRequestParameters() throws FxApplicationException {
        try {
            String action = FxJsfUtils.getParameter("action");
            if (StringUtils.isBlank(action)) {
                return null;
            }
            resetViewStateVars();
            if ("newInstance".equals(action)) {
                long typeId = FxJsfUtils.getLongParameter("typeId", -1);
                if (typeId == -1) {
                    typeId = CacheAdmin.getFilteredEnvironment().getType(FxJsfUtils.getParameter("typeName"))
                            .getId();
                }
                long nodeId = FxJsfUtils.getLongParameter("nodeId", -1);
                this.typeId = typeId;
                if (nodeId != -1) {
                    setTreeNodeParent(nodeId);
                    addTreeNode();
                }
                beBeanInUse = true;
                editMode = true;
                unlockLast();
            } else if ("editInstance".equals(action)) {
                FxPK newPk;
                if (FxJsfUtils.getParameter("pk") != null) {
                    String split[] = FxJsfUtils.getParameter("pk").split("\\.");
                    Long id = Long.valueOf(split[0].trim());
                    Integer ver = Integer.valueOf(split[1].trim());
                    newPk = new FxPK(id, ver);
                } else {
                    newPk = new FxPK(FxJsfUtils.getLongParameter("id"),
                            FxJsfUtils.getBooleanParameter("liveMode", false) ? FxPK.LIVE : FxPK.MAX);
                }
                pk = newPk;
                beBeanInUse = true;
                editMode = FxJsfUtils.getBooleanParameter("editMode", false);
                unlockLast();
            }
        } catch (Throwable t) {
            // TODO possibly pass some error message to the HTML page
            LOG.error("Failed to parse request parameters: " + t.getMessage(), t);
        }
        return null;
    }

    /**
     * Resets all variables that are stored in the view state.
     */
    private void resetViewStateVars() {
        this.content = null;
        this.typeId = -1;
        this.pk = null;
        this.infoPanelState = null;
        this.editMode = false;
        this.reset = true;
        this.treeNodes = null;
        this.fromResultSet = false;
        this.beBeanInUse = false;
        // hack!
        FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
    }

    /**
     * Returns if the content editor is initialized for editing or content viewing
     *
     * @return if the content editor is initialized
     */

    public boolean isInitialized() {
        return pk != null || typeId != -1 || content != null;
    }

    public boolean isReset() {
        return reset;
    }

    public boolean isEditMode() {
        return editMode;
    }

    public void setEditMode(boolean editMode) {
        this.editMode = editMode;
    }

    public String getEditorId() {
        return EDITOR_ID;
    }

    public String getFormId() {
        return FORM_ID;
    }

    public FxContent getContent() {
        return content;
    }

    public void setContent(FxContent content) {
        this.content = content;
    }

    public long getTypeId() {
        return typeId;
    }

    public void setTypeId(long typeId) {
        this.typeId = typeId;
    }

    public long getNewTypeId() {
        return newTypeId;
    }

    public void setNewTypeId(long newTypeId) {
        this.newTypeId = newTypeId;
    }

    public void setWrappedContent(FxWrappedContent wrappedContent) {
        this.wrappedContent = wrappedContent;
    }

    public UploadedFile getImportUpload() {
        return importUpload;
    }

    public void setImportUpload(UploadedFile importUpload) {
        this.importUpload = importUpload;
    }

    public String getImportPasted() {
        return importPasted;
    }

    public void setImportPasted(String importPasted) {
        this.importPasted = importPasted;
    }

    public boolean isImportSave() {
        return importSave;
    }

    public void setImportSave(boolean importSave) {
        this.importSave = importSave;
    }

    public void setFromResultSet(boolean fromResultSet) {
        this.fromResultSet = fromResultSet;
    }

    public boolean isFromResultSet() {
        return fromResultSet;
    }

    public boolean isBeBeanInUse() {
        return beBeanInUse;
    }

    public void setBeBeanInUse(boolean beBeanInUse) {
        this.beBeanInUse = beBeanInUse;
    }

    public boolean isReloadContentTree() {
        return reloadContentTree;
    }

    public void setReloadContentTree(boolean reloadContentTree) {
        this.reloadContentTree = reloadContentTree;
    }

    /**
     * Import a content
     *
     * @return next page
     */
    public String doImport() {
        FxContent _con = null;
        if (importUpload != null && importUpload.getSize() > 0) {
            System.out.println("Uploaded " + importUpload.getSize() + " bytes for " + importUpload.getName());
            try {
                _con = EJBLookup.getContentEngine().importContent(new String(importUpload.getBytes(), "UTF-8"),
                        true);
            } catch (Exception e) {
                new FxFacesMsgErr(e).addToContext();
            }
        } else if (!StringUtils.isEmpty(importPasted)) {
            try {
                _con = EJBLookup.getContentEngine().importContent(importPasted, true);
            } catch (FxApplicationException e) {
                new FxFacesMsgErr(e).addToContext();
            }
        } else {
            new FxFacesMsgInfo("Content.nfo.import.noData").addToContext();
        }
        if (_con != null) {
            this.importPasted = "";
            this.importUpload = null;
            this.infoPanelState = "";
            try {
                wrappedContent.getContent().replaceData(_con);
                ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean"))
                        .setEditorId(wrappedContent.getEditorId());
                compact();
                if (importSave) {
                    wrappedContent.getGuiSettings().setEditMode(false);
                    save();
                }
                new FxFacesMsgInfo("Content.nfo.imported").addToContext();
            } catch (Exception e) {
                new FxFacesMsgErr(e).addToContext();
            }
        }
        return null;
    }

    /**
     * Returns content versions as select items
     *
     * @return content versions as select items
     * @throws FxApplicationException on errors
     */
    public List<SelectItem> getCompareVersions() throws FxApplicationException {
        FxContentVersionInfo versionInfo = pk.isNew() ? FxContentVersionInfo.createEmpty()
                : EJBLookup.getContentEngine().getContentVersionInfo(pk);
        List<SelectItem> items = new ArrayList<SelectItem>(versionInfo.getVersionCount());
        if (versionInfo.getVersionCount() > 0) {
            for (int v : versionInfo.getVersions()) {
                LifeCycleInfo lci = versionInfo.getVersionData(v).getLifeCycleInfo();
                String name = "unknown";
                try {
                    name = EJBLookup.getAccountEngine().load(lci.getModificatorId()).getName();
                } catch (FxApplicationException e) {
                    //ignore
                }
                items.add(new SelectItem(v, "Version " + v + " by " + name + " at "
                        + FxFormatUtils.getDateTimeFormat().format(new Date(lci.getModificationTime()))));
            }
        }
        return items;
    }

    /**
     * Returns FxDeltas between two selected versions.
     *
     * @return FxDeltas between two selected versions
     */
    public List<FxDelta.FxDeltaChange> getCompareEntries() {
        List<FxDelta.FxDeltaChange> emptyResult = new ArrayList<FxDelta.FxDeltaChange>(0);
        try {
            final ContentEngine ce = EJBLookup.getContentEngine();
            FxContentVersionInfo versionInfo = pk.isNew() ? FxContentVersionInfo.createEmpty()
                    : ce.getContentVersionInfo(pk);
            if ("compare".equals(infoPanelState) && compareSourceVersion > 0
                    && compareSourceVersion <= versionInfo.getMaxVersion() && compareDestinationVersion > 0
                    && compareDestinationVersion <= versionInfo.getMaxVersion()) {
                FxContent content1 = ce.load(new FxPK(getId(), compareSourceVersion));
                FxContent content2 = ce.load(new FxPK(getId(), compareDestinationVersion));
                FxDelta delta = FxDelta.processDelta(content1, content2);
                List<FxDelta.FxDeltaChange> changes = delta.getDiff(content1, content2);
                //filter internal
                List<FxDelta.FxDeltaChange> internal = new ArrayList<FxDelta.FxDeltaChange>(5);
                for (FxDelta.FxDeltaChange d : changes)
                    if (d.isInternal())
                        internal.add(d);
                changes.removeAll(internal);
                return changes;
            } else {
                return emptyResult;
            }
        } catch (FxApplicationException e) {
            new FxFacesMsgErr(e).addToContext();
            return emptyResult;
        }
    }

    /**
     * Get the content as XML
     *
     * @return content as XML
     */
    public Map<FxWrappedContent, String> getExportData() {
        return new HashMap<FxWrappedContent, String>() {
            @Override
            public String get(Object key) {
                if ("export".equals(infoPanelState) && key instanceof FxWrappedContent) {
                    FxWrappedContent con = (FxWrappedContent) key;
                    if (con.getContent() != null) {
                        try {
                            return EJBLookup.getContentEngine().exportContent(con.getContent());
                        } catch (FxApplicationException e) {
                            LOG.error(e);
                        }
                    }
                }
                return "";
            }
        };
    }

    public int getCompareSourceVersion() {
        return compareSourceVersion;
    }

    public void setCompareSourceVersion(int compareSourceVersion) {
        this.compareSourceVersion = compareSourceVersion;
    }

    public int getCompareDestinationVersion() {
        return compareDestinationVersion;
    }

    public void setCompareDestinationVersion(int compareDestinationVersion) {
        this.compareDestinationVersion = compareDestinationVersion;
    }

    public String getInfoPanelState() {
        return infoPanelState;
    }

    public void setInfoPanelState(String togglePanelState) {
        this.infoPanelState = togglePanelState;
    }

    public String getEditorPage() {
        return "contentEditor";
    }

    /**
     * Returns referencing tree nodes for stored content instances.
     *
     * @return referencing tree nodes for stored content instances
     */
    public List<FxTreeNode> getTreeNodes() {
        if (treeNodes == null) {
            if (pk != null && !pk.isNew()) {
                try {
                    treeNodes = EJBLookup.getTreeEngine().getNodesWithReference(FxTreeMode.Edit, pk.getId());
                } catch (Throwable t) {
                    new FxFacesMsgErr(t).addToContext();
                }
            } else
                treeNodes = new ArrayList<FxTreeNode>(0);
        }
        return treeNodes;
    }

    public void setTreeNodes(List<FxTreeNode> treeNodes) {
        this.treeNodes = treeNodes;
    }

    public long getTreeNodeParent() {
        return treeNodeParent;
    }

    public void setTreeNodeParent(long treeNode) {
        this.treeNodeParent = treeNode;
    }

    public FxTreeNode getTreeNode() {
        return treeNode;
    }

    public void setTreeNode(FxTreeNode treeNode) {
        this.treeNode = treeNode;
    }

    /**
     * Ajax call to detach content from tree node set via treeNode variable.
     */
    public void removeTreeNode() {
        //FxJsf1Utils.resetFaceletsComponent(FORM_ID+":"+EDITOR_ID);
        for (FxTreeNode node : getTreeNodes()) {
            if (node.getId() == treeNode.getId())
                node.setMarkForDelete(true);
        }
    }

    /**
     * Ajax call to attach content to tree node id set via treeNodeParent variable.
     */
    public void addTreeNode() {
        addTreeNode(treeNodeParent);
    }

    /**
     * Attach content to a given tree node
     *
     * @param _node tree node id
     */
    public void addTreeNode(long _node) {
        try {
            // Only add the path if it does not already exist
            for (FxTreeNode node : getTreeNodes()) {
                if (node.getParentNodeId() == _node) {
                    if (node.isMarkForDelete()) {
                        // was removed before .. just take it in again
                        node.setMarkForDelete(false);
                        return;
                    } else {
                        // exists
                        return;
                    }
                }
            }

            // Add the path if the parent node is valid
            try {
                FxTreeNode tn = EJBLookup.getTreeEngine().getNode(FxTreeMode.Edit, _node);
                if (tn != null)
                    getTreeNodes().add(FxTreeNode.createNewTemporaryChildNode(tn));
            } catch (Throwable t) {
                /* ignore */
            }
        } catch (Exception t) {
            new FxFacesMsgErr(t).addToContext();
        }
    }

    /**
     * Mapped function to return the label path for a tree node (id)
     *
     * @return label path for a given tree node in the calling users locale
     */
    public Map<Long, String> getTreeLabelPath() {
        if (treeLabelMap == null) {
            treeLabelMap = FxSharedUtils.getMappedFunction(new FxSharedUtils.ParameterMapper<Long, String>() {
                @Override
                public String get(Object key) {
                    try {
                        long id = (Long) key;
                        if (id < 0) {
                            for (FxTreeNode node : getTreeNodes()) {
                                if (node.getId() == id)
                                    return EJBLookup.getTreeEngine()
                                            .getLabels(FxTreeMode.Edit, node.getParentNodeId()).get(0) + "/*";
                            }
                            return "unknown/*";
                        } else
                            return EJBLookup.getTreeEngine().getLabels(FxTreeMode.Edit, id).get(0);
                    } catch (FxApplicationException e) {
                        return "unknown";
                    }
                }
            }, true);
        }
        return treeLabelMap;
    }

    /**
     * Like {@link #getTreeLabelPath()}, but trimmed to a sensible maximum length for showing the path in the backend.
     *
     * @return  trimmed label path for a given tree node
     */
    public Map<Long, String> getTreeLabelPathDisplay() {
        if (displayTreeLabelMap == null) {
            displayTreeLabelMap = FxSharedUtils
                    .getMappedFunction(new FxSharedUtils.ParameterMapper<Long, String>() {

                        @Override
                        public String get(Object key) {
                            final int maxLen = 80;
                            final String label = getTreeLabelPath().get((Long) key);
                            if (label != null && label.length() > maxLen) {
                                return label.substring(0, maxLen - 3) + "...";
                            } else {
                                return label;
                            }
                        }

                    }, false);
        }
        return displayTreeLabelMap;
    }

    /**
     * Show the new content creation screen.
     *
     * @return  the action outcome
     */
    public String showNewContent() {
        resetViewStateVars();
        return "contentEditor";
    }

    /**
     * JSF action to create a new instance for a given type id.
     *
     * @return contentEditor navigation case
     */
    public String createNewContent() {
        resetViewStateVars();
        this.typeId = newTypeId;
        beBeanInUse = true;
        this.editMode = true;
        if (treeNodeParent > 0) {
            addTreeNode();
        }
        return getEditorPage();
    }

    public FxPK getPk() {
        return pk;
    }

    public void setPk(FxPK pk) {
        this.pk = pk;
    }

    /**
     * JSF action used to reload currently edited content.
     * Note: Pending changes are lost.
     *
     * @return next page
     */
    public String reload() {
        this.reset = true;
        this.treeNodes = null;
        this.infoPanelState = null;
        FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
        return null;
    }

    /**
     * JSF- action to load the given version
     */
    public void loadVersion() {
        reload();
        this.pk = new FxPK(wrappedContent.getContent().getId(), version);
        this.editMode = wrappedContent.getGuiSettings().isEditMode();
        new FxFacesMsgInfo("ContentEditor.nfo.version.loaded", version, pk).addToContext();
    }

    public long getId() {
        return pk != null ? pk.getId() : -1;
    }

    public void setId(long id) {
        this.contentIdToInit = id;
    }

    public long getContentIdToInit() {
        return contentIdToInit;
    }

    public void setContentIdToInit(long contentIdToInit) {
        this.contentIdToInit = contentIdToInit;
    }

    public void setVersion(int version) {
        this.version = version;
    }

    public int getVersion() {
        return version;
    }

    /**
     * Initialize for an existing content instance.
     * To be used by other beans.
     *
     * @param con      content instance
     * @param editMode edit mode
     * @return editor page
     */
    public String initEditor(FxContent con, boolean editMode) {
        resetViewStateVars();
        content = con;
        this.editMode = editMode;
        beBeanInUse = true;
        return getEditorPage();
    }

    /**
     * Initialize for a given pk (which must not be new).
     * To be used by other beans.
     *
     * @param pk       of a stored content instance
     * @param editMode edit mode
     * @return editor page
     */
    public String initEditor(FxPK pk, boolean editMode) {
        resetViewStateVars();
        this.pk = pk;
        this.editMode = editMode;
        beBeanInUse = true;
        return getEditorPage();
    }

    /**
     * Initialize for a given type id.
     * To be used by other beans.
     *
     * @param typeId   type id
     * @param editMode edit mode
     * @return editor page
     */
    public String initEditor(long typeId, boolean editMode) {
        resetViewStateVars();
        this.typeId = typeId;
        this.editMode = editMode;
        beBeanInUse = true;
        return getEditorPage();
    }

    /**
     * Initialize the content editor from the context menu of a result set
     * with the set version and content id.
     *
     * @return editor page
     */
    public String initEditorFromResultSet() {
        // save edit mode
        boolean editMode = this.editMode;
        // loose the lock (FX828)
        if (editMode && wrappedContent != null) {
            ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean"))
                    .setEditorId(wrappedContent.getEditorId());
            ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).cancel();
        }
        resetViewStateVars();
        // restore edit mode
        this.editMode = editMode;
        // set result set flag
        this.fromResultSet = true;
        this.pk = new FxPK(contentIdToInit, version);
        beBeanInUse = true;
        unlockLast();
        return getEditorPage();
    }

    /**
     * checks if there is an open loose lock for the current session, and if so release it
     *
     * save the current pk as an open loose lock for the next time
     * @since 3.1.4
     */
    private void unlockLast() {
        FxPK currPK = (FxPK) FxJsfUtils.getSession().getAttribute(LOOSE_LOCKED_PKS);
        if (currPK != null) {
            final ContentEngine ce = EJBLookup.getContentEngine();
            final UserTicket ticket = FxContext.getUserTicket();
            FxLock currLock = ce.getLock(currPK);
            if (currLock.getUserId() == ticket.getUserId() && currLock.getLockType() == FxLockType.Loose) {
                try {
                    ce.unlock(currPK);
                } catch (FxLockException e) {
                    new FxFacesMsgErr("ContentEditor.err.lock", e.getMessage()).addToContext();
                }
            }
        }
        FxJsfUtils.getSession().setAttribute(LOOSE_LOCKED_PKS, this.pk);
    }

    /**
     * Returns all editable types.
     *
     * @return all editable types
     */
    public List<SelectItem> getEditableTypes() {
        List<FxType> types = CacheAdmin.getFilteredEnvironment().getTypes(true, true, true, false);
        ArrayList<FxType> result = new ArrayList<FxType>(types.size());
        for (FxType t : types) {
            if (t.getId() != FxType.ROOT_ID) {
                result.add(t);
            }
        }
        return FxJsfUtils.asSelectListWithLabel(result);
    }

    /**
     * Cancel editing
     *
     * @return next page
     */

    public String cancel() {
        if ("import".equals(infoPanelState))
            infoPanelState = null;
        boolean isReferenced = wrappedContent.isReferenced();
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).cancel();
        if (!isReferenced) {
            if (wrappedContent.isNew()) {
                resetViewStateVars();
            } else {
                editMode = false;
                wrappedContent.setReset(true);
                treeNodes = null;
            }
        }
        return null;
    }

    public void callBack() {
        switch (nextOp) {
        case CANCEL:
            cancel();
            break;
        case SET_NEXT_PK:
            initEditorFromResultSet();
            break;
        }
    }

    public CallbackOpts getNextOp() {
        return nextOp;
    }

    public void setNextOp(CallbackOpts nextOp) {
        this.nextOp = nextOp;
    }

    public void preCancel() {
        changed = wrappedContent.wasChanged();
        if (changed) {
            nextOp = CallbackOpts.CANCEL;
        } else {
            cancel();
        }
    }

    public void preInitEditorFromResultSet() {
        changed = wrappedContent.wasChanged();
        if (changed) {
            nextOp = CallbackOpts.SET_NEXT_PK;
        } else {
            initEditorFromResultSet();
        }
    }

    /**
     * Removes all empty elements which are not required.
     *
     * @return the next page to render.
     */
    public String compact() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).compact();
        return null;
    }

    /**
     * Deletes the instance
     *
     * @return the next page to render (= the content editor)
     */
    public String delete() {
        try {
            boolean isReferenced = wrappedContent.isReferenced();
            // check if deleted content was part of result set
            // and update result set if necessary
            FxPK newPK;
            boolean editModeSaved = editMode;
            ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean"))._delete();
            newPK = removePKindex(wrappedContent.getContent().getPk());
            if (!isReferenced) {
                resetViewStateVars();
            }
            if (newPK != null) {
                // reinit content editor with saved values
                this.editMode = editModeSaved;
                this.pk = newPK;
                this.fromResultSet = true;
            }
        } catch (Exception e) {
            new FxFacesMsgErr(e).addToContext();
        } finally {
            FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
        }
        return null;
    }

    /**
     * Deletes the current version
     *
     * @return the next page to render (= the content editor)
     */
    public String deleteCurrentVersion() {
        try {
            boolean isReferenced = wrappedContent.isReferenced();
            FxContentEditorBean ceBean = (FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean");
            // check if deleted content was part of result set
            // and update result set if necessary
            ceBean._deleteCurrentVersion();
            // update search result
            removePKindex(getPk());
            if (!isReferenced) {
                // retrieve new pk from content storage
                pk = new FxPK(pk.getId(), FxPK.MAX);
            }
        } catch (Exception t) {
            new FxFacesMsgErr(t).addToContext();
        } finally {
            FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
        }
        return null;
    }

    /**
     * Deletes a specific version set in "version"
     *
     * @return the next page to render (= the content editor)
     */
    public String deleteVersion() {
        try {
            FxPK pkToDelete = new FxPK(wrappedContent.getContent().getPk().getId(), version);
            EJBLookup.getContentEngine().removeVersion(pkToDelete);
            new FxFacesMsgInfo("Content.nfo.deletedVersion", pkToDelete).addToContext();
            // update search result
            removePKindex(pkToDelete);
            // load highest available version
            this.pk = new FxPK(pk.getId(), FxPK.MAX);
        } catch (Exception t) {
            new FxFacesMsgErr(t).addToContext();
        } finally {
            FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
        }
        return null;
    }

    /**
     * Saves the data in a new version.
     */
    public void saveInNewVersion() {
        _save(true);
        FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
    }

    /**
     * Action method: Saves the data.
     */
    public void save() {
        final UserTicket ticket = FxContext.getUserTicket();
        boolean ownerChange = false;
        // no owner change check if the current user is a supervisor
        if (!ticket.isGlobalSupervisor() || !ticket.isMandatorSupervisor()) {
            ownerChange = checkOwnerChange();
        }

        if (ownerChange) {
            wrappedContent.getGuiSettings().setTakeOver(true);
            new FxFacesMsgErr("ContentEditor.msg.takeOver.warning").addToContext();

        } else {
            _save(false);
        }
        FxJsf1Utils.resetFaceletsComponent(FORM_ID + ":" + EDITOR_ID);
    }

    /**
     * Action method: save the content and cancel editing (remove loose lock)
     */
    public void saveAndCancel() {
        final UserTicket ticket = FxContext.getUserTicket();
        try {
            boolean ownerChange = false;
            // no owner change check if the current user is a supervisor
            if (!ticket.isGlobalSupervisor() || !ticket.isMandatorSupervisor()) {
                ownerChange = checkOwnerChange();
            }

            if (ownerChange) {
                wrappedContent.getGuiSettings().setTakeOver(true);
                new FxFacesMsgErr("ContentEditor.msg.takeOver.warning").addToContext();

            } else {
                _save(false);
                cancel();
            }
        } catch (Exception t) {
            new FxFacesMsgErr(t).addToContext();
        }
    }

    /**
     * Checks whether the currently opened content had it's lock revoked by another user
     * and displays the appropriate message
     *
     * @return returns true if the owner (lock) of the currently opened content changed
     */
    public boolean checkOwnerChange() {
        return ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).checkOwnerChange();
    }

    /**
     * Action method: override a content's lock
     *
     * @return returns the current page
     */
    public String lockOverride() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).lockOverride();
        return getEditorPage();
    }

    /**
     * Action method: override a content's lock and enter edit mode
     *
     * @return returns the current page
     */
    public String lockOverrideAndEdit() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).lockOverrideAndEdit();
        return getEditorPage();
    }

    /**
     * Action method: edit the content in a new (max) version
     *
     * @return returns the current page
     */
    public String enableEditInNewVersion() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).enableEditInNewVersion();
        return getEditorPage();
    }

    /**
     * Saves the data in the current or in a new version.
     *
     * @param newVersion if true a new version is created.
     */
    private void _save(boolean newVersion) {
        try {
            // save old view state
            FxPK oldPk = pk;
            long oldTypeId = typeId;
            FxContent oldContent = content;
            boolean isReferenced = wrappedContent.isReferenced();
            // check if saved content was part of result set
            // and update result set if necessary
            boolean updateSearchResult = isPkInSearchResult(wrappedContent.getContent().getPk());

            FxContentEditorBean ceBean = (FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean");
            this.pk = ceBean._save(newVersion);
            // update search result
            if (updateSearchResult) {
                ((SearchResultBean) FxJsfUtils.getManagedBean("fxSearchResultBean")).refresh();
            }
            if (!isReferenced) {
                // if content was new, reset typeId, content and wrapped content (-> use newPk after save)
                if (wrappedContent.getContent().getPk().isNew()) {
                    typeId = -1;
                    content = null;
                    this.editMode = false;
                    // retrieve saved content from storage and set reset flag,
                    // for edit mode to be deactivated
                    ceBean.getContentStorage().get(wrappedContent.getEditorId()).setReset(true);
                }

                // if no error happened, handle tree
                TreeEngine tree = EJBLookup.getTreeEngine();
                final List<FxTreeNode> treeNodes = tree.getNodesWithReference(FxTreeMode.Edit, pk.getId());
                for (FxTreeNode node : getTreeNodes()) {
                    if (node.isMarkForDelete() && !node.isTemporary()) {
                        tree.remove(node, FxTreeRemoveOp.Unfile, true);
                    } else if (node.isTemporary()) {
                        boolean assignmentExists = false;
                        for (FxTreeNode child : treeNodes) {
                            if (child.getParentNodeId() == node.getParentNodeId()) {
                                // avoid duplicate tree entries
                                assignmentExists = true;
                            }
                        }
                        if (!assignmentExists) {
                            String name = null;
                            if (wrappedContent.getContent().hasCaption()) {
                                name = wrappedContent.getContent().getCaption().getBestTranslation();
                            }
                            if (StringUtils.isBlank(name)) {
                                name = CacheAdmin.getFilteredEnvironment()
                                        .getType(wrappedContent.getContent().getTypeId()).getName() + "_"
                                        + pk.getId() + "." + pk.getVersion();
                            }
                            tree.save(FxTreeNodeEdit.createNew(name).setParentNodeId(node.getParentNodeId())
                                    .setReference(pk).setPosition(Integer.MIN_VALUE));
                        }
                    }
                    // Reload changes
                    this.treeNodes = null;
                    // TODO: only call when tree node labels need to be manually updated
                    // (i.e. after an existing content is edited and caption changed)
                    FxContext.get().setTreeWasModified();
                }
            }
            if (isReferenced) {
                // if content was referenced, reset to saved view state vars
                // (so that base content is displayed again)
                pk = oldPk;
                content = oldContent;
                typeId = oldTypeId;
            }
        } catch (Exception t) {
            new FxFacesMsgErr(t).addToContext();
        }
    }

    /**
     * Action method: enable edit mode
     *
     * @return returns the current view
     */
    public String enableEdit() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).enableEdit();
        return null;
    }

    /**
     * Action method: acquire a permanent lock on a content
     */
    public void acquirePermLock() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).acquirePermLock();
    }

    /**
     * Action method: acquire a loose lock on a content
     */
    public void acquireLooseLock() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).acquireLooseLock();
    }

    /**
     * Action method: unlock a content
     */
    public void unLock() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).unLock();
    }

    /**
     * Action method: compute the remaining lock duration
     */
    public void computeRemainingLockTime() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).computeRemainingLockTime();
    }

    /**
     * Action method: extend the lock duration
     */
    public void extendLock() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).extendLock();
    }

    /**
     * Action method: cancel the lock override, resets form and gui settings
     */
    public void noLockOverride() {
        ((FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean")).noLockOverride();
    }

    /**
     * Return a custom value formatter for FxValueInput if edit mode is disabled.
     *
     * @return the value formatter for FxValueInput
     */
    public FxValueFormatter getValueFormatter() {
        if (valueFormatter == null) {
            valueFormatter = new ReferenceValueFormatter();
        }
        return valueFormatter;
    }

    public boolean isChanged() {
        return changed;
    }

    /**
     * A custom value formatter for content references. Renders content references as
     * links, that trigger the referenced content to be opened in content editor.
     */
    private static class ReferenceValueFormatter implements FxValueFormatter, Serializable {
        private static final long serialVersionUID = 2283199607456062352L;

        @Override
        public String format(FxValue container, Object value, FxLanguage outputLanguage) {
            if (container instanceof FxReference && value instanceof ReferencedContent) {
                return "<a href=\"adm/content/contentEditor.jsf?action=editInstance&readOnly=true&id="
                        + ((ReferencedContent) value).getId() + "\">" + ((ReferencedContent) value).getCaption()
                        + "</a>";
            }
            return null;
        }
    }

    /**
     * Returns whether the given pk is part of the search result.
     *
     * @param pk pk
     * @return whether the content instance with the pk currently being edited
     *         is part of the search result.
     */
    private boolean isPkInSearchResult(FxPK pk) {
        return isFromResultSet() && pk != null && sortedPKArray.size() > 0 && sortedPKArray.contains(pk);
    }

    /**
     * Update the search result and fetch the new PK to edit
     *
     * @param matchVersion if true, the updated search result is
     *                     matched for the new version, otherwise for index.
     * @return the new pk to edit
     */
    private FxPK updateSearchResult(boolean matchVersion) {
        SearchResultBean sb = (SearchResultBean) FxJsfUtils.getManagedBean("fxSearchResultBean");
        int currentIndex = getIndex().get(getPk());
        sb.refresh();
        if (!matchVersion && currentIndex > 0 && sb.getResult().getRowCount() > 0) {
            // index handled by the getPkByIndex function
            return getPkByIndex().get(currentIndex);
        } else if (matchVersion) {
            if (sb.getResult().getResultRow(getPk()) != null)
                return getPk();
        }
        return null;
    }

    /**
     * Mapped access to search result row, matching given pk.
     *
     * @return Mapped access to search result row, matching given pk.
     */
    public Map<FxPK, FxContent> getResultRow() {
        return new HashMap<FxPK, FxContent>() {
            @Override
            public FxContent get(Object key) {
                FxPK pk = (FxPK) key;
                try {
                    return EJBLookup.getContentEngine().load(pk);
                } catch (FxApplicationException e) {
                    return get(removePKindex(pk));
                }
            }
        };
    }

    public String getSortedPKs() {
        if (sortedPKs != null) {
            if (session == null)
                session = FxJsfUtils.getSession();
            session.setAttribute(SORTED_PK_KEY, sortedPKs);
        }
        return sortedPKs;
    }

    /**
     * Removes a FxPK and return the following FxPK
     * @param currentPK the FxPK to remove
     * @return the following FxPK or <code>null</code> if the current PK was not found)
     */
    private FxPK removePKindex(FxPK currentPK) {
        int currentIndex = getIndex().get(currentPK);
        if (currentIndex < 0 || sortedPKArray.size() == 0)
            return null;
        sortedPKArray.remove(currentIndex);
        if (currentIndex >= sortedPKArray.size()) {
            currentIndex = sortedPKArray.size() - 1;
        }
        StringBuilder sb = new StringBuilder(sortedPKs.length());
        for (FxPK tmpPK : sortedPKArray) {
            sb.append(',').append(tmpPK);
        }
        sortedPKs = sb.toString().substring(1); // remove the starting ','
        currentIndexCache.clear();
        return sortedPKArray.get(currentIndex);
    }

    /**
     * returns the index of the given PK according to the sortedPKArray
     * @return the index in the sortedPKArray or -1 if not found
     */
    public Map<FxPK, Integer> getIndex() {
        return new HashMap<FxPK, Integer>() {
            @Override
            public Integer get(Object key) {
                Integer tmp = currentIndexCache.get(key);
                if (tmp == null) {
                    if (StringUtils.isBlank(sortedPKs))
                        setSortedPKs(null);
                    tmp = sortedPKArray.indexOf(FxPK.fromObject(key));
                    currentIndexCache.put((FxPK) key, tmp);
                }
                return tmp;
            }
        };
    }

    public void setSortedPKs(String sortedPKs) {
        if (StringUtils.isBlank(sortedPKs)) {
            if (session == null)
                session = FxJsfUtils.getSession();
            this.sortedPKs = (String) session.getAttribute(SORTED_PK_KEY);
            if (StringUtils.isBlank(this.sortedPKs)) {
                return;
            }
        } else {
            this.sortedPKs = sortedPKs;
        }
        for (String tmpS : this.sortedPKs.split(",")) {
            sortedPKArray.add(FxPK.fromString(tmpS));
        }
        this.currentIndexCache.clear();
    }

    /**
     * Mapped access to the pk of the search result row, matching given index.
     *
     * @return mapped access to the pk of the search result row, matching given index.
     */
    public Map<Integer, FxPK> getPkByIndex() {
        return new HashMap<Integer, FxPK>() {

            @Override
            public FxPK get(Object key) {
                // avoid JSF integer/long conversion bug
                int i = Integer.parseInt(key.toString());
                // cap possible overflows
                if (sortedPKArray.size() <= i) {
                    i = sortedPKArray.size() - 1;
                }
                if (i < 0) {
                    i = 0;
                }
                return sortedPKArray.get(i);
            }
        };
    }

    /**
     * @see com.flexive.faces.beans.FxContentEditorBean#sortProperties()
     * @since 3.1.1
     */
    public void sortProperties() {
        FxContentEditorBean ceBean = (FxContentEditorBean) FxJsfUtils.getManagedBean("fxContentEditorBean");
        ceBean.sortProperties();
    }

    /**
     * @return the confirmKey if in editMode or <code>null</code> if not in editMode
     */
    public String getConfirm() {
        return this.editMode ? "Content.confirm.abort" : null;
    }

    public int getRowCount() {
        return sortedPKArray.size();
    }

    private static enum CallbackOpts {
        CANCEL, SET_NEXT_PK
    }
}