de.xwic.appkit.webbase.editors.EditorContext.java Source code

Java tutorial

Introduction

Here is the source code for de.xwic.appkit.webbase.editors.EditorContext.java

Source

/*******************************************************************************
 * Copyright 2015 xWic group (http://www.xwic.de)
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *******************************************************************************/
package de.xwic.appkit.webbase.editors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import de.xwic.appkit.core.dao.*;
import de.xwic.appkit.webbase.editors.events.IEditorListenerFactory;
import de.xwic.appkit.webbase.editors.events.ValidationEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.jwic.base.IControl;
import de.xwic.appkit.core.config.Bundle;
import de.xwic.appkit.core.config.ConfigurationException;
import de.xwic.appkit.core.config.editor.EditorConfiguration;
import de.xwic.appkit.core.config.editor.UIElement;
import de.xwic.appkit.core.config.model.EntityDescriptor;
import de.xwic.appkit.core.config.model.Property;
import de.xwic.appkit.core.dao.ValidationResult.Severity;
import de.xwic.appkit.core.model.EntityModelException;
import de.xwic.appkit.core.model.EntityModelFactory;
import de.xwic.appkit.core.model.IEntityModel;
import de.xwic.appkit.core.script.ScriptEngineProvider;
import de.xwic.appkit.webbase.editors.events.EditorEvent;
import de.xwic.appkit.webbase.editors.events.EditorListener;
import de.xwic.appkit.webbase.editors.mappers.MappingException;
import de.xwic.appkit.webbase.editors.mappers.PropertyMapper;
import de.xwic.appkit.webbase.editors.mappers.MapperFactory;

/**
 * Contains the widgtes and property-mappers for the editor session.
 *
 * @author Florian Lippisch
 */
public class EditorContext implements IBuilderContext {

    static final int EVENT_AFTERSAVE = 0;
    static final int EVENT_LOADED = 1;
    static final int EVENT_BEFORESAVE = 2;
    static final int EVENT_PAGES_CREATED = 3;

    private final static Log log = LogFactory.getLog(EditorContext.class);
    /**
     * Editor entity listener custom extension point name.
     */
    private static final String EP_EDITOR_LISTENER = "editorListener";
    /**
     * Editor entity validator custom extension point name.
     */
    private static final String EP_EDITOR_VALIDATOR = "editorValidator";

    private GenericEditorInput input = null;
    private EditorConfiguration config = null;
    private IEntityModel model = null;
    private Bundle bundle = null;
    private boolean dirty = false;
    private boolean editable = true;

    /** indicates if the fields are modified during save/load operation */
    private boolean inTransaction = false;
    private Map<String, PropertyMapper<IControl>> mappers = new HashMap<String, PropertyMapper<IControl>>();

    // properties - page assignment
    private EditorContentPage currPage = null;
    private Map<String, EditorContentPage> propertyPageMap = new HashMap<String, EditorContentPage>();
    private Map<String, IControl> widgetMap = new HashMap<String, IControl>();
    private List<EditorContentPage> pages = new ArrayList<EditorContentPage>();
    private List<EditorListener> listeners = new ArrayList<EditorListener>();
    private List<HideWhenWidgetWrapper> hideWhenWidgets = new ArrayList<HideWhenWidgetWrapper>();

    /** This list contains errors that happened during the start. They are displayed to the user. */
    private List<String> initErrors = new ArrayList<String>();

    private ScriptEngine scriptEngine;

    /**
     * @param input
     * @throws ConfigurationException
     * @throws EntityModelException
     */
    public EditorContext(GenericEditorInput input, String langId)
            throws ConfigurationException, EntityModelException {
        this.input = input;
        this.config = input.getConfig();
        this.bundle = config.getEntityType().getDomain().getBundle(langId);
        this.model = EntityModelFactory.createModel(input.getEntity());
        this.dirty = input.getEntity().getId() == 0; // is a new entity.

        DAO<?> dao = DAOSystem.findDAOforEntity(config.getEntityType().getClassname());
        this.editable = dao.hasRight(input.getEntity(), "UPDATE") && !input.getEntity().isDeleted()
                && !(input.getEntity() instanceof IHistory);

        scriptEngine = ScriptEngineProvider.instance()
                .createEngine("Editor(" + config.getEntityType().getId() + ":" + config.getId() + ")");
        Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
        bindings.put("entity", model);
        bindings.put("bundle", bundle);
        bindings.put("ctx", this);

        try {
            if (config.getGlobalScript() != null && !config.getGlobalScript().trim().isEmpty()) {
                scriptEngine.eval(config.getGlobalScript());
            }
        } catch (ScriptException se) {
            initErrors.add("ScriptException in GlobalScript for editor configuration (" + se + ")");
            log.error("Error evaluating global script in Editor(" + config.getEntityType() + ":" + config.getId()
                    + ")", se);
        }
        final String entityType = getEntityDescriptor().getClassname();
        final List<IEditorListenerFactory> listenerExtensions = EditorExtensionUtils
                .getExtensions(EP_EDITOR_LISTENER, entityType);
        for (IEditorListenerFactory listenerExtension : listenerExtensions) {
            EditorListener editorListener = listenerExtension.createListener();
            addEditorListener(editorListener);
        }
    }

    /**
     * Add an EditorListener.
     *
     * @param listener
     */
    public synchronized void addEditorListener(EditorListener listener) {
        listeners.add(listener);
    }

    /**
     * Remove an EditorListener.
     *
     * @param listener
     */
    public synchronized void removeEditorListener(EditorListener listener) {
        listeners.remove(listener);
    }

    /**
     * Fire an event.
     *
     * @param event
     */
    void fireEvent(int eventId, boolean isNewEntity) {
        Object[] tmp = listeners.toArray();
        EditorEvent event = new EditorEvent(this, isNewEntity);
        for (int i = 0; i < tmp.length; i++) {
            EditorListener listener = (EditorListener) tmp[i];
            switch (eventId) {
            case EVENT_AFTERSAVE:
                listener.afterSave(event);
                break;
            case EVENT_LOADED:
                listener.entityLoaded(event);
                break;
            case EVENT_BEFORESAVE:
                listener.beforeSave(event);
                break;
            case EVENT_PAGES_CREATED:
                listener.pagesCreated(event);
                break;
            }
        }
    }

    /**
     * Register a field that uses a custom mapper.
     *
     * @param property
     * @param widget
     * @param id
     * @param customMapper
     */
    public void registerField(Property[] property, IControl widget, UIElement uiDef, String mapperId) {
        registerField(property, widget, uiDef, mapperId, false);
    }

    /* (non-Javadoc)
     * @see de.xwic.appkit.core.client.uitools.editors.IBuilderContext#registerField(de.xwic.appkit.core.config.model.Property[], org.eclipse.swt.widgets.Widget, java.lang.String, java.lang.String, boolean)
     */
    public void registerField(Property[] property, IControl widget, UIElement uiDef, String mapperId,
            boolean infoMode) {

        PropertyMapper<IControl> mapper = mappers.get(mapperId);
        if (mapper == null) {
            try {
                mapper = MapperFactory.instance().createMapper(mapperId, getEntityDescriptor());
            } catch (Exception e) {
                String msg = "Error creating mapper " + mapperId;
                log.error(msg, e);
                initErrors.add(msg + " (" + e.toString() + ")");
                return;
            }
            mappers.put(mapperId, mapper);
        }
        mapper.registerProperty(widget, property, infoMode);

        if (currPage == null) {
            // This is a problem when the editor container is not creating the tabs properly.
            // before creating a tab, the current page should be set and after creating the
            // currPage should be set to null again.
            throw new IllegalStateException("No current page assigned!");
        }
        // build property name
        StringBuffer sb = new StringBuffer();
        Property finalprop = null;
        if (null != property) {
            finalprop = property[property.length - 1];
            for (int i = 0; i < property.length; i++) {
                if (i > 0) {
                    sb.append(".");
                }
                sb.append(property[i].getName());
            }
        }
        //propertyPageMap.put(sb.toString(), currPage);
        if (finalprop != null) {
            propertyPageMap.put(finalprop.getEntityDescriptor().getClassname() + "." + finalprop.getName(),
                    currPage);
        }

        registerWidget(widget, uiDef);

        // set initial editable flag.
        mapper.setEditable(widget, property, editable && (finalprop == null || finalprop.hasReadWriteAccess()));
    }

    /**
     * Register a widget with the given id. If the id is <code>null</code>, the widget
     * will not be registered but no exception is thrown.
     * <p>This is useful for widgets that should be accessible from script but are not a field by
     * itself, like a container.
     *
     * @param id
     * @param widget
     */
    public void registerWidget(IControl widget, UIElement uiDef) {
        // register widget
        String id = uiDef.getId();
        if (id != null && !id.isEmpty()) {
            if (widgetMap.containsKey(id)) {
                // print a warning, in case this already exists. Either an id was used twice (where it
                // should be unique), or a builder has called registerWidget for a widget where
                // registerField(..) was already invoked.
                log.warn("A widget with the id '" + id + "' is already registered!");
            }
            widgetMap.put(id, widget);
        }

        if (uiDef.getHideWhen() != null && !uiDef.getHideWhen().trim().isEmpty()) {
            // the widget has some logic that is to be evaluated after refreshes. Add the
            // control to the hideWhenField list
            hideWhenWidgets.add(new HideWhenWidgetWrapper(widget, uiDef.getHideWhen()));
        }
    }

    /**
     * Changes the editable flag of all registered widgets/properties.
     * @param editable
     */
    public void setAllEditable(boolean editable) {
        if (!editable || this.editable) {
            for (PropertyMapper<IControl> mapper : mappers.values()) {
                mapper.setEditable(editable);
            }
        }
    }

    /**
     * Change the editable flag of a specified property.
     * @param editable
     * @param propertyKey
     */
    public void setFieldEditable(boolean editable, String propertyKey) {
        if (!editable || this.editable) { // prevent listeners to enable editable when the entity is not editable.
            for (PropertyMapper<IControl> mapper : mappers.values()) {
                mapper.setFieldEditable(editable, propertyKey);
            }
        }
    }

    /**
     * Load the values from the entity model into the fields.
     *
     * @throws MappingException
     */
    public void loadFromEntity() throws MappingException {
        inTransaction = true;
        try {
            IEntity entity = (IEntity) model;
            for (PropertyMapper<IControl> mapper : mappers.values()) {
                mapper.loadContent(entity);
            }
            fireEvent(EVENT_LOADED, entity.getId() == 0);

            // if not new, validate
            if (entity.getId() != 0) {
                displayValidationResults(validateFields());
            } else {
                // must call displayValidationResult to make sure
                // that the error-composite is initialized correct.
                // this fixed an isse where the scrollBar was not visible
                // on new entities.
                displayValidationResults(new ValidationResult());
            }

            evaluateHideWhens();

        } finally {
            inTransaction = false;
        }
    }

    /**
     * Run through all widgets with a hideWhen formula and execute it to
     * hide/show those widgets.
     */
    private void evaluateHideWhens() {

        for (HideWhenWidgetWrapper hwWidget : hideWhenWidgets) {
            try {
                hwWidget.evaluate(scriptEngine);
            } catch (ScriptException e) {
                log.error("Error evaluating 'hideWhen'", e);
            }
        }

    }

    /**
     * Validates the fields on the form.
     *
     * @return A ValidationResult containing all errors
     * @throws MappingException
     */
    public ValidationResult validateFields() {
        inTransaction = true;
        ValidationResult result = new ValidationResult();
        try {
            // perform UI side validation first...
            for (PropertyMapper<IControl> mapper : mappers.values()) {
                ValidationResult tempResult = mapper.validateWidgets();
                result.addErrors(tempResult.getErrorMap());
                result.addWarnings(tempResult.getWarningMap());
            }

            DAO<?> dao = DAOSystem.findDAOforEntity(config.getEntityType().getClassname());
            if (dao == null) {
                throw new DataAccessException(
                        "Can not find a DAO for entity type " + config.getEntityType().getClassname());
            }
            // then perform DAO (backend) side validation
            ValidationResult daoResult = dao.validateEntity((IEntity) model);
            result.addErrors(daoResult.getErrorMap());
            result.addWarnings(daoResult.getWarningMap());

            fireValidated(result, getEntityDescriptor().getClassname(), false);
            // now that all validations are done, highlight fields with errors
            for (PropertyMapper<IControl> mapper : mappers.values()) {
                mapper.highlightValidationResults(result);
            }
            return result;
        } finally {
            inTransaction = false;
        }
    }

    /**
     * Validate entity model using extension validator.
     * Iterate all possible validation extensions and perform validation.
     *
     * @param result validation result
     * @param entityType type of entity to validate
     */
    private void fireValidated(ValidationResult result, String entityType, boolean onSave) {
        final List<IEditorValidatorListener> listenerExtensions = EditorExtensionUtils
                .getExtensions(EP_EDITOR_VALIDATOR, entityType);
        for (IEditorValidatorListener editorValidator : listenerExtensions) {
            editorValidator.modelValidated(new ValidationEvent(model, result, onSave));
        }
    }

    /**
     * @return ValidationResult
     * @throws ValidationException
     * @throws MappingException
     * @throws EntityModelException
     */
    public ValidationResult saveToEntity() throws ValidationException, MappingException, EntityModelException {

        if (!initErrors.isEmpty()) {
            throw new IllegalStateException(
                    "Save is disabled as there have been exceptions during initialization of the editor.");
        }

        inTransaction = true;
        try {
            IEntity entity = (IEntity) model;
            fireEvent(EVENT_BEFORESAVE, entity.getId() == 0);
            updateModel();

            DAO<?> dao = DAOSystem.findDAOforEntity(config.getEntityType().getClassname());
            if (dao == null) {
                throw new MappingException(
                        "Can not find a DAO for entity type " + config.getEntityType().getClassname());
            }
            ValidationResult result = validateFields();
            fireValidated(result, getEntityDescriptor().getClassname(), true);

            if (!result.hasErrors()) {
                model.commit();
                boolean isNew = model.getOriginalEntity().getId() == 0;
                dao.update(model.getOriginalEntity());
                inTransaction = false;
                setDirty(false);
                fireEvent(EVENT_AFTERSAVE, isNew); // revalidate
                if (isNew) {
                    //EntityBroker.getEntityBroker().fireEntitySavedNew(model.getOriginalEntity());
                } else {
                    //EntityBroker.getEntityBroker().fireEntitySavedExisting(model.getOriginalEntity());
                }
                loadFromEntity(); // load probably modified data.
                result = dao.validateEntity(entity); // revalidate after
                // save.
            } else {
                // RCPErrorDialog.openError(UIToolsPlugin.getResourceString("error.dialog.EditorContext.validationfail"));
            }
            displayValidationResults(result);
            return result;

        } finally {
            inTransaction = false;
        }
    }

    /* (non-Javadoc)
     * @see de.xwic.appkit.webbase.editors.IBuilderContext#fieldChanged(de.xwic.appkit.core.config.model.Property[])
     */
    @Override
    public void fieldChanged(Property[] property) {

        long t = System.currentTimeMillis();

        if (inTransaction) {
            // ignore the events when the editor is in a transaction, i.e. loading or saving.
            return;
        }

        // something changed? set dirty.
        setDirty(true);

        // update the property on the entity (model), to perform the hideWhen evaluation
        IEntity entity = (IEntity) model;
        try {
            for (PropertyMapper<IControl> mapper : mappers.values()) {
                mapper.storeContent(entity, property);
            }
        } catch (Exception me) {
            // Just log an error, do not notify users
            log.warn("Error handling field change", me);
        }

        evaluateHideWhens();

        long dur = System.currentTimeMillis() - t;
        if (dur > 100) {
            log.info("fieldChange handling took more than 100ms in Editor for "
                    + this.config.getEntityType().getClassname());
        }

    }

    /**
     * Write the property values to the model without saving (commiting) the model.
     * @throws MappingException
     * @throws ValidationException
     */
    public void updateModel() throws MappingException, ValidationException {

        IEntity entity = (IEntity) model;
        for (PropertyMapper<IControl> mapper : mappers.values()) {
            mapper.storeContent(entity);
        }
    }

    /**
     * @param result
     */
    public void displayValidationResults(ValidationResult result) {

        for (EditorContentPage page : pages) {
            page.resetMessages();
        }
        EditorContentPage mainPage = pages.get(0);

        // add any initialization error
        for (String msg : initErrors) {
            mainPage.addError(msg);
        }

        // assign warnings
        Map<String, String> warnings = result.getWarningMap();
        for (String prop : warnings.keySet()) {
            String msg = (String) warnings.get(prop);
            EditorContentPage page = propertyPageMap.get(prop);
            String title;
            if (prop.startsWith("#")) {
                title = bundle.getString(prop.substring(1));
            } else {
                title = bundle.getString(prop);
            }
            String message = title + ": " + bundle.getString(msg);
            if (page != null) {
                page.addWarn(message);
            } else {
                mainPage.addWarn(message);
            }
        }

        // assign errors
        Map<String, String> errors = result.getErrorMap();
        for (String prop : errors.keySet()) {
            String msg = (String) errors.get(prop);
            EditorContentPage page = propertyPageMap.get(prop);
            String title;
            if (prop.startsWith("#")) {
                title = bundle.getString(prop.substring(1));
            } else {
                title = bundle.getString(prop);
            }
            String message = title + ": " + bundle.getString(msg);
            if (page != null) {
                page.addError(message);
            } else {
                mainPage.addError(message);
            }
        }

        for (EditorContentPage page : pages) {
            if (page.hasErrors()) {
                page.setStateIndicator(Severity.ERROR);
            } else if (page.hasWarnings()) {
                page.setStateIndicator(Severity.WARN);
            } else if (page.hasInfos()) {
                page.setStateIndicator(null);
            } else {
                page.setStateIndicator(null);
            }
        }

    }

    /**
     * Returns the EditorConfiguration.
     *
     * @return
     */
    public EditorConfiguration getEditorConfiguration() {
        return config;
    }

    /**
     * @return the input
     */
    public GenericEditorInput getInput() {
        return input;
    }

    /**
     * @return the dirty
     */
    public boolean isDirty() {
        return dirty;
    }

    /**
     * @param dirty
     *            the dirty to set
     */
    public void setDirty(boolean dirty) {
        if (dirty != this.dirty && !inTransaction) {
            this.dirty = dirty;
        }
    }

    /**
     * When a page is rendered, it must set itself as current page so that the
     * context can assign created properties to the page. Warnings and Error
     * messages for properties can then be assigned to the right page.
     *
     * @param currPage
     *            the currPage to set
     */
    public void setCurrPage(EditorContentPage currPage) {
        if (this.currPage != null && currPage != null) {
            throw new IllegalStateException("Another process is still creating properties on a page.");
        }
        this.currPage = currPage;
        if (currPage != null) {
            if (!pages.contains(currPage)) {
                pages.add(currPage);
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see de.xwic.appkit.core.client.uitools.editors.IBuilderContext#getEntityDescriptor()
     */
    public EntityDescriptor getEntityDescriptor() {
        return getEditorConfiguration().getEntityType();
    }

    /**
     * @return the inTransaction
     */
    public boolean isInTransaction() {
        return inTransaction;
    }

    /**
     * @param inTransaction the inTransaction to set
     */
    public void setInTransaction(boolean inTransaction) {
        this.inTransaction = inTransaction;
    }

    /**
     * Returns the model.
     * @return
     */
    public IEntityModel getModel() {
        return model;
    }

    /**
     * @return the editable
     */
    public boolean isEditable() {
        return editable;
    }

    public Bundle getBundle() {
        return bundle;
    }

    public String getResString(String key) {
        if (key != null && key.startsWith("#")) {
            return bundle != null ? bundle.getString(key.substring(1)) : "!" + key + "!";
        }
        return key;
    }

    public String getResString(Property property) {
        String key = property.getEntityDescriptor().getClassname() + "." + property.getName();
        return bundle != null ? bundle.getString(key) : "!" + key + "!";
    }

    public IControl getControlById(String id) {
        return (IControl) widgetMap.get(id);
    }

    /**
     * Returns true if the entity being edited is new and unsaved.
     * @return
     */
    public boolean isNew() {
        return ((IEntity) model).getId() == 0;
    }

}