Package eu.planets_project.pp.plato.action.workflow

Source Code of eu.planets_project.pp.plato.action.workflow.IdentifyRequirementsAction

/*******************************************************************************
* Copyright (c) 2006-2010 Vienna University of Technology,
* Department of Software Technology and Interactive Systems
*
* All rights reserved. This program and the accompanying
* materials are made available under the terms of the
* Apache License, Version 2.0 which accompanies
* this distribution, and is available at
* http://www.apache.org/licenses/LICENSE-2.0
*******************************************************************************/

package eu.planets_project.pp.plato.action.workflow;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.faces.application.FacesMessage;
import javax.persistence.NoResultException;

import org.apache.commons.logging.Log;
import org.jboss.annotation.ejb.cache.Cache;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.RaiseEvent;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.datamodel.DataModel;
import org.jboss.seam.annotations.datamodel.DataModelSelection;
import org.jboss.seam.faces.FacesMessages;

import eu.planets_project.pp.plato.action.TestDataLoader;
import eu.planets_project.pp.plato.action.interfaces.IDefineAlternatives;
import eu.planets_project.pp.plato.action.interfaces.IIdentifyRequirements;
import eu.planets_project.pp.plato.action.interfaces.IRequirementsExpert;
import eu.planets_project.pp.plato.action.interfaces.IWorkflowStep;
import eu.planets_project.pp.plato.bean.BooleanCapsule;
import eu.planets_project.pp.plato.bean.MeasurablePropertyMapper;
import eu.planets_project.pp.plato.bean.PrepareChangesForPersist;
import eu.planets_project.pp.plato.bean.TreeHelperBean;
import eu.planets_project.pp.plato.evaluation.MeasurementsDescriptor;
import eu.planets_project.pp.plato.evaluation.MiniRED;
import eu.planets_project.pp.plato.model.DigitalObject;
import eu.planets_project.pp.plato.model.PlanState;
import eu.planets_project.pp.plato.model.User;
import eu.planets_project.pp.plato.model.scales.Scale;
import eu.planets_project.pp.plato.model.tree.Leaf;
import eu.planets_project.pp.plato.model.tree.Node;
import eu.planets_project.pp.plato.model.tree.ObjectiveTree;
import eu.planets_project.pp.plato.model.tree.TemplateTree;
import eu.planets_project.pp.plato.model.tree.TreeNode;
import eu.planets_project.pp.plato.util.Downloader;
import eu.planets_project.pp.plato.util.PlatoLogger;
import eu.planets_project.pp.plato.validators.INodeValidator;
import eu.planets_project.pp.plato.validators.ITreeValidator;
import eu.planets_project.pp.plato.validators.TreeValidator;
import eu.planets_project.pp.plato.xml.ProjectExporter;
import eu.planets_project.pp.plato.xml.TreeLoader;

/**
* Implements actions for workflow step 'Identify Requirements'.
* Includes tree manipulation and
* @author Hannes Kulovits
* @author Christoph Becker
* @author Michael Kraxner
* @author Riccardo Gottardi
*/
@Stateful
@Scope(ScopeType.SESSION)
@Name("identifyRequirements")
@Cache(org.jboss.ejb3.cache.NoPassivationCache.class)
public class IdentifyRequirementsAction extends AbstractWorkflowStep implements
        IIdentifyRequirements {

    /**
     *
     */
    private static final long serialVersionUID = 2989873312263543863L;

    private static final Log log = PlatoLogger.getLogger(IdentifyRequirementsAction.class);

    /**
     * Indicates whether an uploaded FreeMind file contains units
     */
    @Out
    private BooleanCapsule hasUnits = new BooleanCapsule(false);

    /**
     * Indicates whether the nodes comments are edited or scale,restriction, unit ...
     */
    @Out
    private BooleanCapsule doEditNodeComments = new BooleanCapsule(false);

    @In(create = true)
    IDefineAlternatives defineAlternatives;

    /**
     * filename of the uploaded file hopefully containing an objective tree in .mm format.
     * Is injected via the file upload form.
     */
    @In(required = false)
    private String fileName;

    private List<TreeNode> nodesToDelete = new ArrayList<TreeNode>();

    /**
     * User currently logged in
     */
    @In(required=false)
    private User user;
   
    @In(create=true)
    @Out
    private TreeHelperBean treeHelper;

    /**
     * kind of a 'touch' that forces ajax to send field values.
     */
    public void scaleChanged(Scale v) {
    }
   
    /**
     * FreeMind upload
     */
    private byte[] file;

    /**
     * Several files important to the defined requirements can be attached to the preservation plan.
     */
    @Out(required=false)
    private DigitalObject fileToAttach = new DigitalObject();

    /**
     * Uploaded file
     */
    @DataModel
    private List<DigitalObject> requirementsUploads;

   
   
    /**
     * File selected from list {@link #requirementsUploads}
     */
    @DataModelSelection
    private DigitalObject selectedUpload;

    /**
     * Validator for the main objective tree
     */
    private ITreeValidator validator = new TreeValidator();


    /**
     * Reference to the fragments tree
     */
    @In(required = false)
    @Out(required = false)
    private TemplateTree fragmentRoot;

    /**
     * Reference to the template tree
     */
    @In(required = false)
    @Out(required = false)
    private TemplateTree templateRoot;

    @In(create = true)
    private IRequirementsExpert requirementsExpert;

   
    /**
     * If a fragment-operation is taking place, this is a reference to the
     * {@link TreeNode} in the objective tree which will either be stored in the
     * template library (if {@link #saveFragment} is true) or the insertion target for
     * a fragment from the library (if {@link saveFragment} is false)
     *
     * @see #selectFragmentForSaving(Object)
     * @see #selectInsertionTarget(Object)
     */
// MK: it is not used in the view!   
//    @Out(required = false)
    private TreeNode selectedFragment;


    /**
     * The TestDataLoader is used to insert public fragments/templates if they are not
     * present yet
     *
     * @see #selectTemplateLibrary()
     */
    @In(create = true)
    private TestDataLoader testDataLoader;
   
    @In(create = true)
    private MeasurablePropertyMapper measurablePropertyMapper;
   
    private MeasurementsDescriptor descriptor = null;
   

    public IdentifyRequirementsAction() {
        requiredPlanState = new Integer(PlanState.RECORDS_CHOSEN);
    }

    protected IWorkflowStep getSuccessor() {
        return defineAlternatives;
    }

    /**
     * @see AbstractWorkflowStep#init()
     */
    public void init() {
       
        log.info("Calling init of IdentifyRequirements");
        this.requirementsUploads = selectedPlan.getRequirementsDefinition().getUploads();
       
        // Initialize PP5-Characterisation
        measurablePropertyMapper.init(selectedPlan);

        nodesToDelete.clear();
       
        descriptor =  MiniRED.getInstance().getMeasurementsDescriptor();
    }
   
    public byte[] getFile() {
        return file;
    }

    public void setFile(byte[] file) {
        this.file = file;
    }

    /**
     * Resets the transformer of all leaves to the default transformer.
     * @see eu.planets_project.pp.plato.action.workflow.IdentifyRequirementsAction#resetTransformers()
     */
    private void resetTransformers(){
        TreeNode root = this.selectedPlan.getTree().getRoot();
        for (Leaf leaf : root.getAllLeaves()) {
            /*
             * maybe the scaletype is not set yet
             * -> leaf.setDefaultTransformer has to handle null-values itself
             */
            if ((leaf.getScale() == null) ||
                (leaf.getScale().isDirty())) {
                leaf.setDefaultTransformer();
            }
        }
    }

    /**
     * Writes {@link eu.planets_project.pp.plato.model.Plan#getRequirementsDefinition()} to the database.
     */
    private void saveRequirementsDefinition() {
        /** dont forget to prepare changed entities e.g. set current user */
        PrepareChangesForPersist prep = new PrepareChangesForPersist(user.getUsername());

        for (DigitalObject u : selectedPlan.getRequirementsDefinition().getUploads()) {
            prep.prepare(u);
            if (u.getId() == 0) {
                em.persist(u);
            } else {
                em.persist(em.merge(u));
            }
        }

        save(selectedPlan.getRequirementsDefinition());
    }

    /**
     * Persists the currently visible fragment/template-tree
     */
    public String saveLibrary() {
        /* dont forget to prepare changed entities e.g. set current user */
        PrepareChangesForPersist prep = new PrepareChangesForPersist(user.getUsername());
        if (fragmentRoot != null) {
            prep.prepare(fragmentRoot);
            em.persist(em.merge(fragmentRoot));
        }
        if (templateRoot != null) {
            prep.prepare(templateRoot);
            em.persist(em.merge(templateRoot));
        }
        cancelFragmentOperation();
        return null;
    }

    /**
     * @see AbstractWorkflowStep#save()
     */
    @Override
    public String save() {
        log.info("--- Trying to save requirements");

        saveRequirementsDefinition();

        this.resetTransformers();
        for (Leaf leaf : selectedPlan.getTree().getRoot().getAllLeaves()) {
            leaf.resetValues(selectedPlan.getAlternativesDefinition().getConsideredAlternatives());
        }
        /*
         * the properties of the whole tree have to be validated before persisting
         * (jsf cannot check the object-level constraints!)
         */
        if (validateProperties(selectedPlan.getTree(), ObjectiveTree.class, true)) {
            if (selectedPlan.getTree().getRoot().getId() == 0) {
                // this means the reference to the root has been changed, e.g. by useTemplate (I think thats the only case, actually) -
                // so we need to get this ID back (otherwise, each subsequent SAVE will result in a new entity persist)
                // simplest way: reload
                selectedPlan.getTree().setRoot(em.merge(selectedPlan.getTree().getRoot()));
            }
            save(selectedPlan.getTree());
            changed = "";
        }
       
        for (TreeNode n : nodesToDelete) {
          if (n.getId() != 0) {
            em.remove(em.merge(n));
          }
        }
        em.flush();
        nodesToDelete.clear();

        log.info("--- saved requirements");
       
        if (selectedPlan.getTree().getRoot().getId() == 0) {
            // this means the reference to the root has been changed, e.g. by useTemplate (I think thats the only case, actually) -
            // so we need to get this ID back (otherwise, each subsequent SAVE will result in a new entity persist)
            // simplest way: reload
            selectedPlan.getTree().setRoot(em.merge(selectedPlan.getTree().getRoot()));
        }
       
        return null;
    }

    /**
     * Uploads a new objective tree from a FreeMind file.
     *
     * @return null Always returns null.
     */
    public String upload() {
        log.debug("FileName: " + fileName);
        log.debug("Length of File: " + file.length);
        log.debug("HasUnits is: " + hasUnits.isBool());
        if (fileName.endsWith("mm")) {
             InputStream istream = new ByteArrayInputStream(this.file);
             ObjectiveTree newtree = new TreeLoader().loadFreeMindStream(
                  istream, this.hasUnits.isBool(), true);

            if (newtree == null) {
                log.debug("File is corrupted and new Tree cannot be built");
                FacesMessages.instance().add(FacesMessage.SEVERITY_ERROR, "This is not a valid Freemind file, maybe it is corrupted. Please make sure you added at least one level of nodes to the midmap.");
                return null;
            }
            // delete old tree
            nodesToDelete.add(selectedPlan.getTree().getRoot());

           
            selectedPlan.getTree().setRoot(newtree.getRoot());
            // make sure all scales are set according to measurement infos
            selectedPlan.getTree().adjustScalesToMeasurements(descriptor);
            selectedPlan.getTree().setWeightsInitialized(false);

        } else if ("".equals(fileName)) {
            FacesMessages.instance().add(FacesMessage.SEVERITY_ERROR, "Please select a Freemind file first.");
        } else {
            FacesMessages.instance().add(FacesMessage.SEVERITY_ERROR, "You have to upload Freemind files.");
        }
        return null;
    }

    /**
     * Adds {@link #fileToAttach} to {@link #requirementsUploads}.
     */
    public void attachFile() {
        if (!fileToAttach.isDataExistent()) {

            log.debug("No file for upload selected.");
            FacesMessages.instance().add(FacesMessage.SEVERITY_ERROR,
                    "You have to select a file before starting upload.");

            return;
        }
        requirementsUploads.add(fileToAttach.clone());
    }

    /**
     * Removes file selected by the user from list of uploads ({@link #requirementsUploads})
     */
    public void removeAttachedFile () {

        if (selectedUpload == null) {
            return;
        }

        requirementsUploads.remove(selectedUpload);
    }

    /**
     * Attaches a new Leaf to the given object (which is, hopefully, a Node)
     */
    public void addLeaf(Object object) {
        if (object instanceof Node) {
            Node node = (Node) object;
            node.addChild(new Leaf());
            log.debug("Leaf added: to NODE");
            // this node has been changed()
            node.touch();
            expandNode(node);
           
        }
    }

    /**
     * Attaches a new Node to the given object (which is, hopefully, a Node)
     */
    public void addNode(Object object) {
        if (object instanceof Node) {
            Node node = (Node) object;
            node.addChild(new Node());
            // this node has been changed()
            node.touch();
            expandNode(node);
            }
    }
   
    public void downloadTree() {
        ProjectExporter exporter = new ProjectExporter();
        Downloader.instance().downloadMM(
                exporter.exportTreeToFreemind(selectedPlan),
                selectedPlan.getPlanProperties().getName()+".mm");
    }
   
    /**
     * Downloads the result file of a specific sample record.
     *
     * @param object sample record the user wants to download the result file.
     */
    public void downloadAttachment() {
        if (selectedUpload == null) {
            return;
        }
        Downloader.instance().download(em.merge(selectedUpload));
    }
   
   
    private void expandNode(TreeNode node) {
        treeHelper.expandNode(node);
        Set<TreeNode> parents = node.getAllParents();
        for (TreeNode parent : parents) {
            treeHelper.expandNode(parent);
        }
       
    }

    /**
     * @see AbstractWorkflowStep#discard()
     */
    @Override
    @RaiseEvent("reload")
    public String discard() {
        String ret = super.discard();
        if (! "success".equals(ret)) {
            log.warn("Discard called, but failed.");
            return ret;
        }
        // do NOT delete nodes which have been marked for deletion
        nodesToDelete.clear();

        requirementsUploads = selectedPlan.getRequirementsDefinition().getUploads();

        return "success";
    }

    /**
     * Removes a node from its objective tree.
     *
     * @param object {@link eu.planets_project.pp.plato.model.tree.TreeNode} to remove from objective tree.
     *
     * @return Always returns null.
     */
    public String remove(Object object) {

        removeNode(object);
        return null;
    }


    private void removeNode(Object nodeToRemove) {
        TreeNode node = (TreeNode) nodeToRemove;
        if (node.getParent() != null) {
            // parent has been changed
            ((Node) node.getParent()).touch();
            ((Node) node.getParent()).removeChild(node);
            if(treeHelper != null) {
                treeHelper.closeNode(node);
            }
        }
        nodesToDelete.add(node);
    }
   
    public String initTemplates() {
        selectTemplateLibrary("Public Templates");
        return null;
    }

    public void selectTemplateLibrary(String selectedTemplateLibrary) {
        if (templateRoot == null) {
            templateRoot = getLibrary(selectedTemplateLibrary);
        }
    }
   
    public String initFragments() {
        selectFragmentLibrary("Public Fragments");
        return null;
    }
   
    public void selectFragmentLibrary(String selectedTemplateLibrary) {
        if (fragmentRoot == null) {
            fragmentRoot = getLibrary(selectedTemplateLibrary);
        }
    }

    /**
     * Retrieves and displays the tree of the newly selected template library
     * which is determined by {@link #selectedTemplateLibrary}
     *
     * Invoked when the drowndown-box is manipulated.
     */
    private TemplateTree getLibrary(String selectedTemplateLibrary) {
        TemplateTree tt = null;
        //log.debug("Getting template tree " + selectedTemplateLibrary + " for user " + user.getUsername());
            try {
                tt = (TemplateTree) em.createQuery("select n from TemplateTree n where name = ?").setParameter(1, selectedTemplateLibrary).getSingleResult();
                if (tt == null || tt.getRoot() == null || tt.getRoot().getChildren().size() == 0) {
                    testDataLoader.insertTemplateTree();
                    tt = (TemplateTree) em.createQuery("select n from TemplateTree n where name = ?").setParameter(1, selectedTemplateLibrary).getSingleResult();                   
                }
            } catch (NoResultException e) {
                testDataLoader.insertTemplateTree();
                tt = (TemplateTree) em.createQuery("select n from TemplateTree n where name = ?").setParameter(1, selectedTemplateLibrary).getSingleResult();
            }
            return tt;
    }

    /**
     * Performs cleanup after a completed fragment insertion- or saving-operation.
     * Disables auto-downscrolling to template library and forgets the previously
     * selected fragment.
     */
    private void fragmentOperationCompleted() {
        selectedFragment = null;
    }

    public String selectTreeForSaving() {
        // Store root
        selectedFragment = selectedPlan.getTree().getRoot();
        // Set "Save"-mode
        // MK: saveFragment is never read
        // saveFragment.setBool(true);
       
        // outject name and description
        tempNode= new Node();
        tempNode.setName( selectedFragment.getName());
        tempNode.setDescription(selectedFragment.getDescription());
       
        selectTemplateLibrary("Public Templates");
        return null;
    }
   
    @Out(required=false)
    private Node tempNode;
   
    /**
     * Sets the given {@link TreeNode} as the new {@link #selectedFragment} and the
     * template-library mode to "save" (which causes the "Save here"-buttons to appear
     * in the template library). Also makes the template-tree visible if it was
     * not displayed yet.
     *
     * Invoked when the user selects a node from the objective tree to be saved
     * into the fragments library.
     *
     * @param object the newly selected TreeNode that is supposed to be stored in the template library
     *
     * @see #saveFragmentHere(Object)
     */
    public String selectFragmentForSaving(Object object) {
        if (object instanceof TreeNode) {
            // Store active selection
            selectedFragment = (TreeNode) object;
           
            selectFragmentLibrary("Public Fragments");

           
            // outject name and description
            tempNode= new Node();
            tempNode.setName(selectedFragment.getName());
            tempNode.setDescription(selectedFragment.getDescription());
        }
        return null;
    }
   
    public String cancelFragmentOperation() {
     // Store active selection
        selectedFragment = null;
        return null;
    }

    /**
     * Completes a save-into-template-library-operation by adding a clone of the
     * previously selected {@link #selectedFragment} as a new child of the given
     * {@link Node} from the template library.
     *
     * @param object The {@link Node} in the template library where the previously
     * selected {@link #selectedFragment} should be saved to.
     *
     * @see #selectFragmentForSaving(Object)
     */
    public String saveFragmentHere(Object object) {
        log.info("Saving " + selectedFragment + " to " + object);
        Node target = (Node) object;
        TreeNode clone = selectedFragment.clone();
       
        clone.setName(tempNode.getName());
        clone.setDescription(tempNode.getDescription());
       
        target.addChild(clone);
        clone.touchAll(user.getUsername()); // the newly added TreeNode should be touched.
        //FacesMessages.instance().add(FacesMessage.SEVERITY_INFO, "Successfully stored node \"" + selectedFragment.getName() + "\" into fragment library as a child node of \"" + ((Node) object).getName() + "\"");
       
        em.persist(em.merge(target));
        fragmentOperationCompleted();
        return null;
    }

    public String saveTemp() {
        log.debug("saving temp "+tempNode.getName()+", "+tempNode.getDescription());
        return null;
    }
   
    /**
     * Sets the given {@link Node} as the new {@link #selectedFragment} and the
     * template-library mode to "insert" (which causes the "Insert this"-buttons to
     * appear in the template library). Also makes the template-tree visible if it was
     * not displayed yet.
     *
     * Invoked when the user selects a node from the objective tree as the target
     * of a fragments-insertion-operation.
     *
     * @param object the newly selected TreeNode where a fragment from the template library is supposed to be inserted
     *
     * @see #insertThisFragment(Object)
     */
    public String selectInsertionTarget(Object object) {
        if (object instanceof Node) {
            // Store active selection as new insertion target
            selectedFragment = (TreeNode) object;
           
            selectFragmentLibrary("Public Fragments");
        }
        return null;
    }

    /**
     * Inserts a clone of the given {@link TreeNode} from the template library as a
     * new child of the previously selected {@link #selectedFragment} from the objective
     * tree.
     * @param object the fragment from the template-library that will be cloned and inserted
     *  into the current objective tree
     *
     * @see {@link #selectInsertionTarget(Object)}
     */
    public String insertThisFragment(Object object) {

        TreeNode clone = ((TreeNode) object).clone();
       
        selectedPlan.getTree().getRoot().touch();

        // Insert into the tree
        ((Node) selectedFragment).addChild(clone);

        // Explicitly touch the node where the fragment was inserted
        selectedFragment.touch();

        // Not all browsers seem to correctly detect that the tree has been
        // changed after an insertion operation in all cases. Therefore we
        // explicitly set the session variable "changed" to "true" in order
        // to prevent the user from selecting an arbitrary workflow-step from
        // the menu which could potentially cause a crash.
        changed = "true";

        //FacesMessages.instance().add(FacesMessage.SEVERITY_INFO, "Successfully inserted fragment \"" + ((TreeNode) object).getName() + "\" into your objective tree as a child of \"" + selectedFragment.getName() + "\"");

        fragmentOperationCompleted();
        return null;
    }

    /**
     * Replaces the current objective tree with a clone of the selected template.
     *
     * @param object The selected {@link TreeNode} from the template-tree which is in fact the root node of a template that will be cloned to replace the current objective tree
     */
    @RaiseEvent("reload")
    public String useTemplate(Object object) {
        log.info("Using template " + ((TreeNode) object).getName());
        TreeNode newRoot = ((TreeNode) object).clone();
                    
        selectedPlan.getTree().setWeightsInitialized(false);
        nodesToDelete.add(selectedPlan.getTree().getRoot());
        selectedPlan.getTree().setRoot(newRoot);
        changed = "true";
       
        selectedPlan.getTree().getRoot().touch();
       
        // we have to return "something" else the reload-event is not raised
        return "success";
    }

    /**
     * @see AbstractWorkflowStep#destroy()
     */
    @Destroy
    @Remove
    public void destroy() {
    }

    /**
     * checks if the user is allowed to proceed ....
     */
    public boolean validate(boolean showValidationErrors) {

        /*
         * validation errors of tree-properties need not be shown again,
         * this already happened before persisting, in save() -
         * this is why we call the validation method with showValidationErrors=false
         */
        if (!validateProperties(selectedPlan.getTree(), ObjectiveTree.class, false)) {
            return false;
        }

        /*
         * Check if we have a validator.
         */
        if (validator == null) {
            log.warn("CHECK THIS: validator is null in identify");
            return true;
        }

        ArrayList<TreeNode> errorNodes = new ArrayList<TreeNode>();
        boolean isValid = validator.validate(selectedPlan.getTree().getRoot(), this,
                errorNodes, showValidationErrors);
       
        for (TreeNode treeNode : errorNodes) {
            expandNode(treeNode);
        }

        return isValid;
    }


    /**
     * @see INodeValidator#validateNode(TreeNode, List, List)
     */
    public boolean validateNode(TreeNode node, List<String> errorMessages,
            List<TreeNode> nodes) {
        boolean isValid = node.isCompletelySpecified(errorMessages);
        if (!isValid) {
            nodes.add(node);
        }
        return isValid;
    }

    protected String getWorkflowstepName() {
        return "identifyRequirements";
    }

    public ITreeValidator getValidator() {
        return validator;
    }

    public void setValidator(ITreeValidator validator) {
        this.validator = validator;
    }

    public List<DigitalObject> getRequirementsUploads() {
        return requirementsUploads;
    }

    public void setRequirementsUploads(List<DigitalObject> requirementsUploads) {
        this.requirementsUploads = requirementsUploads;
    }

    public DigitalObject getSelectedUpload() {
        return selectedUpload;
    }

    public void setSelectedUpload(DigitalObject selectedUpload) {
        this.selectedUpload = selectedUpload;
    }

    public DigitalObject getFileToAttach() {
        return fileToAttach;
    }

    public void setFileToAttach(DigitalObject fileToAttach) {
        this.fileToAttach = fileToAttach;
    }

    public void convertToNode(Object leaf) {
        Leaf l = (Leaf) leaf;
        l.getParent().convertToNode(l);
    }

    public void convertToLeaf(Object node) {
        Node n = (Node) node;
        n.getParent().convertToLeaf(n);
    }

    public String startExpert() {
        return requirementsExpert.enter();
    }
   
   

}
TOP

Related Classes of eu.planets_project.pp.plato.action.workflow.IdentifyRequirementsAction

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