org.eclipse.pde.emfforms.editor.EmfFormEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.pde.emfforms.editor.EmfFormEditor.java

Source

/**
 * Copyright (c) 2009 Anyware Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Anyware Technologies - initial API and implementation
 *
 * $Id: EmfFormEditor.java,v 1.37 2010/03/17 10:44:45 bcabe Exp $
 */
package org.eclipse.pde.emfforms.editor;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.List;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.emf.common.command.*;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.ui.MarkerHelper;
import org.eclipse.emf.common.ui.dialogs.DiagnosticDialog;
import org.eclipse.emf.common.ui.viewer.IViewerProvider;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.databinding.EMFDataBindingContext;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EValidator;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.edit.command.CreateChildCommand;
import org.eclipse.emf.edit.domain.*;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory;
import org.eclipse.emf.edit.provider.resource.ResourceItemProviderAdapterFactory;
import org.eclipse.emf.edit.ui.action.EditingDomainActionBarContributor;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.emf.edit.ui.util.EditUIMarkerHelper;
import org.eclipse.emf.edit.ui.util.EditUIUtil;
import org.eclipse.emf.edit.ui.view.ExtendedPropertySheetPage;
import org.eclipse.jface.dialogs.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.Window;
import org.eclipse.pde.emfforms.editor.IEmfFormEditorConfig.VALIDATE_ON_SAVE;
import org.eclipse.pde.emfforms.internal.Activator;
import org.eclipse.pde.emfforms.internal.editor.*;
import org.eclipse.pde.emfforms.internal.validation.ValidatingEContentAdapter;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.*;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.forms.editor.FormEditor;
import org.eclipse.ui.forms.editor.IFormPage;
import org.eclipse.ui.ide.IGotoMarker;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.PropertySheetPage;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

/**
 * A {@link FormEditor} allowing to edit an EMF {@link EObject} in a convenient
 * way
 * 
 * @param <O>
 *            The type of the {@link EObject} to edit.
 */
public abstract class EmfFormEditor<O extends EObject> extends FormEditor implements IEditingDomainProvider,
        ISelectionProvider, IViewerProvider, IResourceChangeListener, IGotoMarker {

    /**
     * This keeps track of the editing domain that is used to track all changes
     * to the model.
     */
    private EditingDomain _editingDomain;

    /**
     */
    private IEmfFormEditorConfig<EmfFormEditor<O>, O> _editorConfig;

    /**
     * This is the one adapter factory used for providing views of the model.
     */
    private ComposedAdapterFactory _adapterFactory;

    private final IObservableValue _observableValue = new WritableValue();

    private O _currentEObject;

    /**
     * This keeps track of all the
     * {@link org.eclipse.jface.viewers.ISelectionChangedListener}s that are
     * listening to this editor.
     */
    protected Collection<ISelectionChangedListener> selectionChangedListeners = new ArrayList<ISelectionChangedListener>();

    protected ISelection editorSelection = StructuredSelection.EMPTY;

    private CommandStackListener _commandStackListener = new CommandStackListener() {
        public void commandStackChanged(final EventObject event) {
            getContainer().getDisplay().asyncExec(new Runnable() {
                public void run() {
                    handleCommandStackChanged(event);
                }
            });
        }
    };

    private PropertySheetPage propertySheetPage;

    /**
     * The MarkerHelper is responsible for creating workspace resource markers presented in Eclipse's Problems View.
     */
    protected MarkerHelper markerHelper = new EditUIMarkerHelper();

    private DataBindingContext _bindingContext;

    private ValidatingEContentAdapter _validator;

    private boolean isSaving = false;

    private EmfContentOutlinePage contentOutlinePage;

    private ResourceDeltaVisitor<O> _visitor;

    public EmfFormEditor() {
        this._editorConfig = getFormEditorConfig();
        init();
    }

    /**
     * @return the {@link IEmfFormEditorConfig} the editor will use as its configuration
     */
    protected IEmfFormEditorConfig<EmfFormEditor<O>, O> getFormEditorConfig() {
        return new DefaultEmfFormEditorConfig<EmfFormEditor<O>, O>(this);
    }

    private void init() {
        _editingDomain = createEditingDomain();
        _bindingContext = new EMFDataBindingContext();
        _validator = new ValidatingEContentAdapter(this);

        // Add a listener to set the most recent command's affected objects to
        // be the selection of the viewer with focus.
        //
        getEditingDomain().getCommandStack().addCommandStackListener(_commandStackListener);

    }

    /**
     * This is called during startup.
     */
    @Override
    public void init(IEditorSite site, IEditorInput editorInput) {
        setSite(site);
        setInputWithNotify(editorInput);

        setPartName(editorInput.getName());
        // -- To manage Copy/Cut/Paste
        site.setSelectionProvider(this);
    }

    @Override
    public void dispose() {
        getEditingDomain().getCommandStack().removeCommandStackListener(_commandStackListener);
        if (_adapterFactory != null) {
            _adapterFactory.dispose();
        }

        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);

        super.dispose();
    }

    /**
     * This sets up the editing domain for the model editor.
     */
    protected EditingDomain createEditingDomain() {
        _adapterFactory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE);

        _adapterFactory.addAdapterFactory(new ResourceItemProviderAdapterFactory());
        _adapterFactory.addAdapterFactory(this.getSpecificAdapterFactory());
        _adapterFactory.addAdapterFactory(new ReflectiveItemProviderAdapterFactory());

        // Create the command stack that will notify this editor as commands are
        // executed.
        BasicCommandStack commandStack = new BasicCommandStack();

        if (getEditorConfig().isUsingSharedClipboard())
            return new SharedClipboardAdapterFactoryEditingDomain(_adapterFactory, commandStack,
                    new HashMap<Resource, Boolean>());
        // else
        return new AdapterFactoryEditingDomain(_adapterFactory, commandStack, new HashMap<Resource, Boolean>());
    }

    protected abstract AdapterFactory getSpecificAdapterFactory();

    @Override
    protected void addPages() {
        // Creates the model from the editor input
        createModel();

        try {
            for (IEmfFormPage page : getPagesToAdd())
                addPage(page);
            addSourcePage();

            int i = 0;
            for (Image img : getPagesImages())
                setPageImage(i++, img);

        } catch (PartInitException e) {
            Activator.logException(e, Messages.EmfFormEditor_InitError);
        }
    }

    private void addSourcePage() throws PartInitException {
        if (!getEditorConfig().isShowSourcePage())
            return;

        boolean isXmlUiBundlePresent = false;
        BundleContext bc = Activator.getDefault().getBundle().getBundleContext();
        for (Bundle b : bc.getBundles()) {
            if ("org.eclipse.wst.xml.ui".equals(b.getSymbolicName())) { //$NON-NLS-1$
                isXmlUiBundlePresent = true;
                break;
            }
        }

        if (isXmlUiBundlePresent)
            addPage(new XmlSourcePage(this));
        else
            addPage(new SimpleSourcePage(this));

    }

    /**
     * @throws PartInitException
     */
    protected abstract List<? extends IEmfFormPage> getPagesToAdd() throws PartInitException;

    protected abstract List<Image> getPagesImages();

    @Override
    public void doSave(IProgressMonitor monitor) {
        try {
            isSaving = true;

            internalDoValidate(monitor);

            // Do the work within an operation because this is a long running
            // activity that modifies the workbench.
            WorkspaceModifyOperation operation = new WorkspaceModifyOperation() {
                // This is the method that gets invoked when the operation runs.
                @Override
                public void execute(IProgressMonitor monitor) throws CoreException {
                    try {
                        internalDoSave(monitor);
                    } catch (IOException ioe) {
                        throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0,
                                Messages.EmfFormEditor_SaveError, ioe));
                    }
                }
            };

            try {
                // This runs the options, and shows progress.
                new ProgressMonitorDialog(getSite().getShell()).run(true, false, operation);

                // Refresh the necessary state.
                // if we have a BasicCommandStack (that's to say almost all the time), don't flush it but call saveIsDone instead  
                if (getEditingDomain().getCommandStack() instanceof BasicCommandStack)
                    ((BasicCommandStack) getEditingDomain().getCommandStack()).saveIsDone();
                else
                    getEditingDomain().getCommandStack().flush();
                firePropertyChange(IEditorPart.PROP_DIRTY);

            } catch (Exception exception) {
                // Something went wrong that shouldn't.
                MessageDialog.openError(getSite().getShell(), Messages.EmfFormEditor_SaveError_Title,
                        Messages.EmfFormEditor_SaveError_Msg);
                Activator.logException(exception, Messages.EmfFormEditor_SaveError_Exception);
            }

        } catch (OperationCanceledException oce) {
            // Do nothing
        } finally {
            isSaving = false;
        }

    }

    /**
     * @param monitor
     * @throws IOException
     */
    protected void internalDoSave(IProgressMonitor monitor) throws IOException {
        // Save only resources that have actually changed.
        final Map<Object, Object> saveOptions = new HashMap<Object, Object>();
        saveOptions.put(Resource.OPTION_SAVE_ONLY_IF_CHANGED, Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER);
        // Save the resources to the file system.
        boolean first = true;
        for (Resource resource : getEditingDomain().getResourceSet().getResources()) {
            if ((first || !resource.getContents().isEmpty() || isPersisted(resource))
                    && !getEditingDomain().isReadOnly(resource)) {
                resource.save(saveOptions);

                first = false;
            }
        }
    }

    /**
     * @param monitor
     */
    protected void internalDoValidate(IProgressMonitor monitor) {
        VALIDATE_ON_SAVE validateOnSave = getEditorConfig().getValidateOnSave();

        // result of the Validation
        Diagnostic diagnostic = Diagnostic.OK_INSTANCE;

        // if the validation is asked by the user
        if (validateOnSave != VALIDATE_ON_SAVE.NO_VALIDATION)
            diagnostic = _validator.validate(getCurrentEObject());

        if (diagnostic.getSeverity() != Diagnostic.OK) {
            switch (validateOnSave) {
            case VALIDATE_AND_WARN:
                int dialogResult = new DiagnosticDialog(getSite().getShell(),
                        Messages.EmfFormEditor_DiaDialog_InvalidModel_Title, null, diagnostic,
                        Diagnostic.ERROR | Diagnostic.WARNING) {
                    @Override
                    protected Control createMessageArea(Composite composite) {
                        message = Messages.EmfFormEditor_ValidationWarn_Msg;
                        return super.createMessageArea(composite);
                    }

                    @Override
                    protected void createButtonsForButtonBar(Composite parent) {
                        // create OK and Details buttons
                        createButton(parent, IDialogConstants.OK_ID, IDialogConstants.YES_LABEL, true);
                        createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.NO_LABEL, true);
                        createDetailsButton(parent);
                    }
                }.open();

                if (dialogResult != Window.OK) {
                    if (monitor != null)
                        monitor.setCanceled(true);
                    throw new OperationCanceledException();
                }
                break;
            case VALIDATE_AND_ABORT:
                new DiagnosticDialog(getSite().getShell(), Messages.EmfFormEditor_DiaDialog_InvalidModel_Title,
                        null, diagnostic, Diagnostic.ERROR | Diagnostic.WARNING) {
                    @Override
                    protected Control createMessageArea(Composite composite) {
                        message = Messages.EmfFormEditor_ValidationError_Msg;
                        return super.createMessageArea(composite);
                    }
                }.open();

                monitor.setCanceled(true);

                throw new OperationCanceledException();
            default:
                // fall through
            }

        }
    }

    /**
     * This returns whether something has been persisted to the URI of the
     * specified resource. The implementation uses the URI converter from the
     * editor's resource set to try to open an input stream.
     */
    protected boolean isPersisted(Resource resource) {
        boolean result = false;
        try {
            InputStream stream = getEditingDomain().getResourceSet().getURIConverter()
                    .createInputStream(resource.getURI());
            if (stream != null) {
                result = true;
                stream.close();
            }
        } catch (IOException e) {
            // Ignore
        }
        return result;
    }

    /**
     * This also changes the editor's input.
     */
    @Override
    public void doSaveAs() {
        SaveAsDialog saveAsDialog = new SaveAsDialog(getSite().getShell());
        saveAsDialog.open();
        IPath path = saveAsDialog.getResult();
        if (path != null) {
            IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(path);
            if (file != null) {
                doSaveAs(URI.createPlatformResourceURI(file.getFullPath().toString(), true),
                        new FileEditorInput(file));
            }
        }
    }

    protected void doSaveAs(URI uri, IEditorInput editorInput) {
        (getEditingDomain().getResourceSet().getResources().get(0)).setURI(uri);
        setInputWithNotify(editorInput);
        setPartName(editorInput.getName());
        IProgressMonitor progressMonitor = getActionBars().getStatusLineManager() != null
                ? getActionBars().getStatusLineManager().getProgressMonitor()
                : new NullProgressMonitor();
        doSave(progressMonitor);
    }

    /**
     * This is for implementing {@link IEditorPart} and simply tests the command
     * stack.
     */
    @Override
    public boolean isDirty() {
        return ((BasicCommandStack) getEditingDomain().getCommandStack()).isSaveNeeded();
    }

    @Override
    public final boolean isSaveAsAllowed() {
        return getEditorConfig().isSaveAsAllowed();
    }

    @Override
    protected PDEFormToolkit createToolkit(Display display) {
        return getEditorConfig().createPDEFormToolkit(display);
    }

    /**
     * This is the method called to load a resource into the editing domain's
     * resource set based on the editor's input.
     */
    public void createModel() {
        URI resourceURI = EditUIUtil.getURI(getEditorInput());
        Resource resource;
        try {
            // Load the resource through the editing domain.
            resource = getEditingDomain().getResourceSet().getResource(resourceURI, true);
        } catch (Exception e) {
            resource = getEditingDomain().getResourceSet().getResource(resourceURI, false);
        }

        setMainResource(resource);

        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    public void setMainResource(Resource resource) {
        if (_currentEObject != null)
            _currentEObject.eAdapters().remove(_validator);

        _currentEObject = (O) resource.getContents().get(0);
        _observableValue.setValue(_currentEObject);
        _currentEObject.eAdapters().add(_validator);
        validate();
    }

    /**
     * This returns the editing domain as required by the
     * {@link IEditingDomainProvider} interface. This is important for
     * implementing the static methods of {@link AdapterFactoryEditingDomain}
     * and for supporting {@link org.eclipse.emf.edit.ui.action.CommandAction}.
     */
    public EditingDomain getEditingDomain() {
        return _editingDomain;
    }

    public EditingDomainActionBarContributor getActionBarContributor() {
        return (EditingDomainActionBarContributor) getEditorSite().getActionBarContributor();
    }

    public IActionBars getActionBars() {
        return getActionBarContributor().getActionBars();
    }

    /**
     * @return The {@link O} currently edited
     */
    public O getCurrentEObject() {
        return _currentEObject;
    }

    public IObservableValue getInputObservable() {
        return _observableValue;
    }

    /**
     * @param viewer
     */
    public void addViewerToListenTo(StructuredViewer viewer) {
        // This listener is used to manage copy/paste...
        viewer.addSelectionChangedListener(new ISelectionChangedListener() {
            public void selectionChanged(SelectionChangedEvent selectionChangedEvent) {
                setSelection(selectionChangedEvent.getSelection());
            }
        });
    }

    public void addSelectionChangedListener(ISelectionChangedListener listener) {
        selectionChangedListeners.add(listener);
    }

    public ISelection getSelection() {
        return editorSelection;
    }

    public boolean isSaving() {
        return isSaving;
    }

    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
        selectionChangedListeners.remove(listener);
    }

    public void setSelection(ISelection selection) {
        editorSelection = selection;

        for (ISelectionChangedListener listener : selectionChangedListeners) {
            listener.selectionChanged(new SelectionChangedEvent(this, selection));
        }
    }

    public Viewer getViewer() {
        AbstractEmfFormPage page = (AbstractEmfFormPage) pages.get(getActivePage());
        return page.getViewer();
    }

    /**
     * @param event
     */
    protected void handleCommandStackChanged(final EventObject event) {
        // the edited object has been changed
        firePropertyChange(IWorkbenchPartConstants.PROP_DIRTY);
        // since the title of the editor is, most of the time, computed using some object attribute, ask the editor to refresh it!
        firePropertyChange(IWorkbenchPartConstants.PROP_TITLE);

        IFormPage activePage = getActivePageInstance();
        if (activePage != null) {
            if (((AbstractEmfFormPage) activePage).getViewer() != null) {
                // Select the affected objects in the viewer
                Command mostRecentCommand = ((CommandStack) event.getSource()).getMostRecentCommand();
                if (mostRecentCommand != null) {
                    if (mostRecentCommand instanceof CreateChildCommand) {
                        Collection<?> objects = mostRecentCommand.getAffectedObjects();
                        if (objects != null && !objects.isEmpty())
                            ((AbstractEmfFormPage) activePage).getViewer().setSelection(
                                    new StructuredSelection(objects.toArray()[objects.size() - 1]), true);
                    }
                }
            }
        }
    }

    /**
     * This accesses a cached version of the content outliner.
     */
    private IContentOutlinePage getContentOutlinePage() {
        if (contentOutlinePage == null) {
            contentOutlinePage = new EmfContentOutlinePage(this);
            contentOutlinePage.setViewerInput(getEditorConfig().getOutlineInput(getCurrentEObject()));
            contentOutlinePage.setViewerComparator(getOutlineComparator());

            // Listen to selection so that we can handle it is a special way.
            contentOutlinePage.addSelectionChangedListener(new ISelectionChangedListener() {
                // This ensures that we handle selections correctly.
                public void selectionChanged(SelectionChangedEvent event) {
                    handleContentOutlineSelection(event.getSelection(), getViewer());
                }
            });

            addSelectionChangedListener(new ISelectionChangedListener() {
                // This ensures that we handle selections correctly.
                public void selectionChanged(SelectionChangedEvent event) {
                    handleContentOutlineSelection(event.getSelection(), contentOutlinePage.getViewer());
                }
            });
        }

        return contentOutlinePage;
    }

    /**
     * Subclasses may override this method to provide their own {@link ViewerComparator} to be used in the outline's {@link TreeViewer}. It could be useful in case you want to rearrange elements
     * @return the {@link ViewerComparator} to use to render the outline's {@link TreeViewer}. Default implementation return <code>null</code>
     */
    protected ViewerComparator getOutlineComparator() {
        return null;
    }

    /**
     * This deals with how we want selection in the outline to affect the other views.<br><br>
     * <strong>Clients can override this method</strong> to have some special behaviour on selection changed events in the outline, such
     * as giving focus to a particular page of the editor. 
     * @param selection the current selection in the outline
     * @param viewerToSnych the viewer of the active page (if any)
     */
    public void handleContentOutlineSelection(ISelection selection, Viewer viewerToSnych) {
        if (!selection.isEmpty() && selection instanceof IStructuredSelection && viewerToSnych != null) {
            if (((IStructuredSelection) selection)
                    .getFirstElement() != ((IStructuredSelection) viewerToSnych.getSelection()).getFirstElement()) {
                viewerToSnych.setSelection(
                        new StructuredSelection(((IStructuredSelection) selection).getFirstElement()));
            }
        }
    }

    public void resourceChanged(IResourceChangeEvent event) {
        IResourceDelta delta = event.getDelta();
        try {
            _visitor = new ResourceDeltaVisitor<O>(this);
            delta.accept(_visitor);
        } catch (CoreException ce) {
            Activator.log(ce);
        }
    }

    public ComposedAdapterFactory getAdapterFactory() {
        return _adapterFactory;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object getAdapter(Class key) {
        if (key.equals(IContentOutlinePage.class)) {
            return getFormEditorConfig().isShowOutlinePage() ? getContentOutlinePage() : null;
        }
        if (key.equals(IPropertySheetPage.class)) {
            return getPropertySheetPage();
        }
        if (key.equals(IGotoMarker.class)) {
            return this;
        }
        return super.getAdapter(key);
    }

    public IPropertySheetPage getPropertySheetPage() {
        if (propertySheetPage == null) {
            propertySheetPage = new ExtendedPropertySheetPage((AdapterFactoryEditingDomain) _editingDomain) {
                @Override
                public void setSelectionToViewer(List<?> selection) {
                    // TODO
                    //   EmfFormEditor.this.setSelectionToViewer(selection);
                    EmfFormEditor.this.setFocus();
                }

                @Override
                public void setActionBars(IActionBars actionBars) {
                    super.setActionBars(actionBars);
                    if (getActionBarContributor() != null) {
                        getActionBarContributor().shareGlobalActions(this, actionBars);
                    }
                }
            };
            propertySheetPage.setPropertySourceProvider(new AdapterFactoryContentProvider(_adapterFactory));
        }

        return propertySheetPage;
    }

    /**
     * {@inheritDoc}
     */
    public void gotoMarker(IMarker marker) {
        if (getViewer() == null)
            return; // atm we only handle in-viewer selection

        try {
            if (marker.getType().equals(EValidator.MARKER)) {
                String uriAttribute = marker.getAttribute(EValidator.URI_ATTRIBUTE, null);
                if (uriAttribute != null) {
                    URI uri = URI.createURI(uriAttribute);
                    EObject eObject = getEditingDomain().getResourceSet().getEObject(uri, true);
                    if (eObject != null && (getEditingDomain() instanceof AdapterFactoryEditingDomain)) {
                        AdapterFactoryEditingDomain editingDomain = (AdapterFactoryEditingDomain) getEditingDomain();
                        if (getViewer() != null) {
                            getViewer().setSelection(new StructuredSelection(
                                    Collections.singleton(editingDomain.getWrapper(eObject)).toArray()));
                        }
                    }
                }
            }
        } catch (CoreException exception) {
            Activator.log(exception);
        }
    }

    public DataBindingContext getDataBindingContext() {
        return _bindingContext;
    }

    @Override
    public void setFocus() {
        super.setFocus();
        // for some reason, the code below is needed to workaround a bug on GTK where the actual
        // content of the first page of the editor doesn't correctly get focus when the editor is opened :-/)
        if (getActivePageInstance() != null)
            getActivePageInstance().setFocus();
    }

    public abstract String getID();

    public IEmfFormEditorConfig<EmfFormEditor<O>, O> getEditorConfig() {
        return _editorConfig;
    }

    /* package */void validate() {
        // TODO perform validation in a separate job
        _validator.validate();
    }
}