@SuppressWarnings("serial")
public class ControllerValidationException extends Exception {
	
	/**
	 * Default controller. Message is requested
	 * @param message Message that will be displayed in the UI
	 */
	public ControllerValidationException(String message) { 
		super(message);
	}
	
	/**
	 * Return the message as a JSON string
	 * @return the message as a JSON string
	 */
	public String getMessageAsJSON(){
		return "{\"validationErrorMessage\":\"" + this.getMessage() + "\"}";
	}
} public class LastScriptVersionLabel {
	private Long id;
	private String label;
	
	public LastScriptVersionLabel(Long id, String label) {
		super();
		this.id = id;
		this.label = label;
	}

	public Long getId() {
		return id;
	}

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

	public String getLabel() {
		return label;
	}

	public void setLabel(String label) {
		this.label = label;
	}

} @Entity
public class Project {

    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    @Size(min = 1, max = 33)
    private String name;

    @JsonIgnore
    @OneToMany(cascade=CascadeType.ALL, mappedBy = "project")
    private List<Run> runs;

    @JsonIgnore
    @OneToMany(cascade=CascadeType.ALL, mappedBy = "project")
    private List<Script> scripts;
   
    private String description;

    public Long getId() {
        return id;
    }

	/**
	 * Unique key used by JPA for persistence (automatically generated)
	 * @param id	Unique key
	 */
    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    /**
     * Set the name of the project, displayed on the UI
     * @param name Name of the project
     */
    public void setName(String name) {
        this.name = name;
    }

    public List<Run> getRuns() {
        return runs;
    }

    /**
     * Set all the runs for this project
     * @param runs A list of runs
     */
    public void setRuns(List<Run> runs) {
        this.runs = runs;
    }

    public List<Script> getScripts() {
        return scripts;
    }

    /**
     * Set all the scripts for this project
     * @param runs A list of scripts
     */
    public void setScripts(List<Script> scripts) {
        this.scripts = scripts;
    }

	public String getDescription() {
		return description;
	}

    /**
     * Set project description
     * @param description A description of this project 
     */
	public void setDescription(String description) {
		this.description = description;
	}
       
} @Entity
public class Run {

	@Id
	@GeneratedValue
	private Long id;

	@NotNull
	@Size(min = 1, max = 33)
	private String label;

	@ManyToOne
	private ScriptVersion scriptVersion;
	
	private boolean launched = false;
	
	private boolean running = false;
	
	private Date startDate;
	
	private Date endDate;
	
	private String comment;
	
	@OneToMany(cascade=CascadeType.ALL)
	private List<ScriptVariable> customScriptVariables = new ArrayList<ScriptVariable>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "projectId")
    private Project project;
	
	@Lob
	@Column( length = 1000000 )
	@Type(type="text")
	private String processOutput;
	
	@OneToMany(mappedBy="run", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
	private List<Sample> samples;
	
	private SampleDataBackendTypeEnum sampleDataBackendType = SampleDataBackendTypeEnum.DEFAULT;
	
	/**
	 * @return true if the run has been launched. It may be running or not.
	 */
	public boolean isLaunched() {
		return launched;
	}

	/**
	 * Set to true to indicate a Run has been launched
	 * @param launched	if the run has been launched
	 */
	public void setLaunched(boolean launched) {
		this.launched = launched;
	}

	/**
	 * @return true if the run is currently running
	 */
	public boolean isRunning() {
		return running;
	}

	/**
	 * @param running If true, then the run is currently running	
	 */
	public void setRunning(boolean running) {
		this.running = running;
	}

	public Long getId() {
		return id;
	}

	/**
	 * Set the unique key used by JPA for persistence (automatically generated)
	 * @param id	Unique key
	 */
	public void setId(Long id) {
		this.id = id;
	}

	public String getLabel() {
		return label;
	}

	/**
	 * Set the label that will be displayed in UI for this run
	 * @param label	label of the run
	 */
	public void setLabel(String label) {
		this.label = label;
	}

	public ScriptVersion getScriptVersion() {
		return scriptVersion;
	}

	/**
	 * A run is associated to a specific version of a script. Specific variables values for a run are linked to the script version
	 * @param scriptVersion	Version of the script for this run
	 */
	public void setScriptVersion(ScriptVersion scriptVersion) {
		this.scriptVersion = scriptVersion;
	}

	public Date getStartDate() {
		return startDate;
	}

	/**
	 * Set the date/time the run has been launched
	 * @param startDate	Date the run has been launched
	 */
	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}

	public Date getEndDate() {
		return endDate;
	}

	/**
	 * The date the run end (automatically or aborted)
	 * @param endDate	Date the run has ended
	 */
	public void setEndDate(Date endDate) {
		this.endDate = endDate;
	}

	public String getProcessOutput() {
		return processOutput;
	}

	/**
	 * Set the output of the injector (jMeter or Gatling for example). May be useful to know the status of underlaying injectors
	 * @param processOutput	Raw output of injectors
	 */
	public void setProcessOutput(String processOutput) {
		this.processOutput = processOutput;
	}
	
	public List<Sample> getSamples() {
		return samples;
	}

	/**
	 * A list of all samples generated during the Run.
	 * @param samples	A list of samples
	 */
	public void setSamples(List<Sample> samples) {
		this.samples = samples;
	}

	public List<ScriptVariable> getCustomScriptVariables() {
		return customScriptVariables;
	}

	/**
	 * Script variable that have been updated for this run. CP only stores custom variable for a Run. Other values will be taken from the default value for a variable
	 * @param customScriptVariables	List of custom variables
	 */
	public void setCustomScriptVariables(List<ScriptVariable> customScriptVariables) {
		this.customScriptVariables = customScriptVariables;
	}

    public Project getProject() {
        return project;
    }

    /**
     * Set the project a Run belongs to. A run cannot belong to multiple projects
     * @param project	The parent project
     */
    public void setProject(Project project) {
        this.project = project;
    }

	public String getComment() {
		return comment;
	}

	/**
	 * Set the "free text" comment added by the user to describe platforms, environment, circumstances... of the run
	 * @param comment	Free text for comment
	 */
	public void setComment(String comment) {
		this.comment = comment;
	}

	/**
	 * Type of backend used to store this run sample results data
	 * @return
	 */
	public SampleDataBackendTypeEnum getSampleDataBackendType() {
		return sampleDataBackendType;
	}

	public void setSampleDataBackendType(SampleDataBackendTypeEnum sampleDataStorageTypeEnum) {
		this.sampleDataBackendType = sampleDataStorageTypeEnum;
	}
} @Entity
public class Script{

	@Id
	@GeneratedValue
	private Long id;

	/**
	 * Label of the script
	 */
	private String label;

    /**
     * Short description for the script
     */
    private String description;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "projectId")
    private Project project;

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
    @OrderColumn(name="INDEX")
    private List<ScriptVersion> versions = new ArrayList<ScriptVersion>();

    /**
     * Reference to the kind of sampler associated to this script
     */
    private String samplerUID;
    
	public Long getId() {
		return id;
	}

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

	public String getLabel() {
		return label;
	}

	public void setLabel(String label) {
		this.label = label;
	}

    public Project getProject() {
        return project;
    }

    public void setProject(Project project) {
        this.project = project;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<ScriptVersion> getVersions() {
        return versions;
    }

    public void setVersions(List<ScriptVersion> versions) {
        this.versions = versions;
    }

	public String getSamplerUID() {
		return samplerUID;
	}

	public void setSamplerUID(String samplerUID) {
		this.samplerUID = samplerUID;
	}
} @Entity
public class ScriptVersion {

    @Id
    @GeneratedValue
    private Long id;

    /**
     * Number of the version
     */
    @OrderColumn
    private Long number;

    /**
     * Short description for this version
     */
    private String description;

    /**
     * Set of variables for this script
     */
    @OneToMany(fetch = FetchType.EAGER, cascade= CascadeType.ALL)
    private List<ScriptVariableSet> scriptVariableSets;

    @Lob
    @Column( length = 100000000 )
    @Type(type="text")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "scriptId")
    private Script script;

    public Long getId() {
        return id;
    }

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

    public Long getNumber() {
        return number;
    }

    public void setNumber(Long number) {
        this.number = number;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<ScriptVariableSet> getScriptVariableSets() {
        return scriptVariableSets;
    }

    public void setScriptVariableSets(List<ScriptVariableSet> scriptVariableSets) {
        this.scriptVariableSets = scriptVariableSets;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
    
    public Script getScript() {
		return script;
	}
    
    public void setScript(Script script) {
		this.script = script;
	}
} public interface ProjectRepository extends PagingAndSortingRepository<Project, Long> {
	
	@Query(value="SELECT DISTINCT s.project FROM Script s")
	List<Project> findProjectsWithScript();
} public interface RunRepository extends PagingAndSortingRepository<Run, Long> {
	/**
	 * Get all run associated with a specific Script
	 * @param id	Id of the script
	 * @return	A list of runs associated with this script
	 */
	List<Run> findByScriptVersionScriptId(Long id);
	
	/**
	 * Get all run associated with a specific ScriptVersion
	 * @param id	Id of the script version
	 * @return	A list of runs associated with this script version
	 */
	List<Run> findByScriptVersionId(Long id);
	
	/**
	 * Get all run for a project ordered by start date (reverse)
	 * @param id	Id of the project
	 * @return	A list of runs associated with this project
	 */	
    List<Run> findByProjectIdOrderByStartDateDesc(Long id);
    
	/**
	 * Get all run currently running
	 * @param running	Running or not
	 * @return	A list of runs on current running state
	 */	    
    List<Run> findByRunning(boolean running);
    
    /**
     * Force fetching of the project to avoid lazy loading exception in threads
     * FIXME : manage JPA session correctly in threads
     * @param id
     * @return
     */
    @Query("from Run r left join fetch r.project p where r.id = ?1")
    Run forceProjectFetch(Long id);
} public interface ScriptRepository extends PagingAndSortingRepository<Script, Long> {

} public interface ScriptVersionRepository extends PagingAndSortingRepository<ScriptVersion, Long> {
	
	/**
	 * Get all script versions of a script, order by their number (new versions first) 
	 * @param id	Id of the script
	 * @return	All script versions for this run, new versions first
	 */
	List<ScriptVersion> findByScriptIdOrderByNumberDesc(Long id);
} public interface Sampler {
	/**
	 * Human readable of the sampler 
	 * @return The name for this sampler
	 */
	public String getName();
	
	/**
	 * Arbitratry unique ID for this sampler. Will never be displayed, but we be used to identify the kind of sampler used for a run 
	 * @return The UID of this sampler
	 */
	public String getUID();
	
	/**
	 * Launcher for this sample
	 * @return The launched of this sampler
	 */
	public SamplerLauncher getLauncher();
	
	/**
	 * The script processor analyze script to extract variables and is used to inject custom variables values in a definitive script
	 * before being launched
	 * @return The script processor for this sampler
	 */
	public SamplerScriptProcessor getScriptProcessor();
	
	/**
	 * Get the file extension for a script. This allows to build a complete filename when downloading the script
	 * @return the script file common extension (".jmx" for example)
	 */
	public String getScriptFileExtension();
} @Service
public class SamplerService implements InitializingBean{
	
	private Map<String,Sampler> samplers;

	@Resource
	private JMeterSampler jMeterSampler;	
	
	public Collection<Sampler> getSamplers() {
		return samplers.values();
	}
	
	public void setSamplers(Collection<Sampler> samplers) {
		if(this.samplers != null) 
			this.samplers.clear();
		else
			 this.samplers = new HashMap<>();
		for (Sampler sampler : samplers) {
			this.samplers.put(sampler.getUID(), sampler);
		}
	}
	
	public Sampler getSamplerByUID(String UID){
		return samplers.get(UID);
	}

	@Override
	public void afterPropertiesSet() {
		this.samplers = new HashMap<>();
		this.samplers.put(jMeterSampler.getUID(), jMeterSampler);
	}
	
} @Service
public class ScriptService {

    @Resource
    private SamplerService samplerService;

    @Resource
    private ScriptRepository scriptRepository;

    @Resource
    private ScriptVersionRepository scriptVersionRepository;

    // Add a new script to the repository
    public void addScript(Script script) {

        // get the associated sampler
        Sampler sampler = samplerService.getSamplerByUID(script.getSamplerUID());

        // Extract variables from Script file
        List<ScriptVariableSet> scriptVariableSets = sampler.getScriptProcessor().getVariableSets(script.getVersions().get(0).getContent());
        if (scriptVariableSets != null)
            script.getVersions().get(0).setScriptVariableSets(scriptVariableSets);

        // Saving script to repository
        scriptRepository.save(script);
    }

    // Add a new script version to the repository
    public void addScriptVersion(ScriptVersion scriptVersion) {

        // get the associated sampler
        Sampler sampler = samplerService.getSamplerByUID(scriptVersion.getScript().getSamplerUID());

        // Extract variables from Script file
        List<ScriptVariableSet> scriptVariableSets = sampler.getScriptProcessor().getVariableSets(scriptVersion.getContent());
        if (scriptVariableSets != null)
            scriptVersion.setScriptVariableSets(scriptVariableSets);

        // Saving script version to repository
        scriptVersionRepository.save(scriptVersion);
    }

    // Add a new script to the repository
    public Script addScript(Project project, String samplerUID, String label, String description, String content) {
        Script script = new Script();
        script.setLabel(label);
        script.setSamplerUID(samplerUID);
        script.setDescription(description);
        script.setProject(project);
        ScriptVersion scriptVersion = new ScriptVersion();
        scriptVersion.setDescription("First version");
        scriptVersion.setNumber(1L);
        scriptVersion.setContent(content);
        scriptVersion.setScript(script);
        script.getVersions().add(0, scriptVersion);

        addScript(script);

        return script;
    }

    // Add a new script version to the repository
    public ScriptVersion addScriptVersion(Script script, String description, String content) {
        ScriptVersion scriptVersion = new ScriptVersion();
        scriptVersion.setDescription(description);
        scriptVersion.setContent(content);
        List<ScriptVersion> scriptVersions = script.getVersions();
        long maxScriptVersion = -1;
        for (ScriptVersion version : scriptVersions) {
            if (maxScriptVersion < version.getNumber()) maxScriptVersion = version.getNumber() + 1;
        }
        scriptVersion.setNumber(maxScriptVersion);
        scriptVersion.setScript(script);
        script.getVersions().add(0, scriptVersion);
        addScriptVersion(scriptVersion);
        return scriptVersion;
    }

    public void deleteScriptVersion(long scriptVersionId) {
        scriptVersionRepository.findById(scriptVersionId).ifPresent(
				scriptVersion -> {
                    Script script = scriptVersion.getScript();
                    for (ScriptVersion version : script.getVersions()) {
                        if (version.getId() == scriptVersionId) {
                            script.getVersions().remove(version);
                            break;
                        }
                    }
                    scriptRepository.save(script);
                });
    }


}
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.centralperf.controller.exception.ControllerValidationException;
import org.centralperf.model.LastScriptVersionLabel;
import org.centralperf.model.dao.Project;
import org.centralperf.model.dao.Run;
import org.centralperf.model.dao.Script;
import org.centralperf.model.dao.ScriptVersion;
import org.centralperf.repository.ProjectRepository;
import org.centralperf.repository.RunRepository;
import org.centralperf.repository.ScriptRepository;
import org.centralperf.repository.ScriptVersionRepository;
import org.centralperf.sampler.api.Sampler;
import org.centralperf.service.SamplerService;
import org.centralperf.service.ScriptService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
/*
 * Copyright (C) 2014  The Central Perf authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.centralperf.controller;




/**
 * The Script controller handle user interactions with the scripts and their versions
 *  
 * @since 1.0
 * 
 */
@Controller
@SessionAttributes
public class ScriptController extends BaseController{
	
	@Resource
	private ScriptRepository scriptRepository;

	@Resource
	private ScriptVersionRepository scriptVersionRepository;
	
	@Resource
	private ScriptService scriptService;

	@Resource
	private SamplerService samplerService;	
	
	@Resource
	private RunRepository runRepository;

    @Resource
    private ProjectRepository projectRepository;

	private static final Logger log = LoggerFactory.getLogger(ScriptController.class);

	/**
	 * Generic Exception handler for the whole controller
	 * @param exception	The exception raised
	 * @return The message of the exception
	 */
    @ExceptionHandler(Throwable.class)
    public @ResponseBody String handleValidationFailure(Throwable exception) {
        return exception.getMessage();
    }
    
    /**
     * Add a new script to a project
     * @param projectId	ID of the project
     * @param script	Script to add (from HTML form binding)
     * @param file	Uploaded file of the script (JMX or other)
     * @return Redirection to the script detail page
     */
    @RequestMapping(value = "/project/{projectId}/script/new", method = RequestMethod.POST)
    public String addScript(
                            @PathVariable("projectId") Long projectId,
    						@ModelAttribute("script") Script script,
                            @RequestParam("jmxFile") MultipartFile file) {
    	String scriptContent = null;

		// Get the script File
		try {
			scriptContent = new String(file.getBytes());
		} catch (IOException e) {
		}
		Script newScript = scriptService.addScript(script.getProject(), script.getSamplerUID(), script.getLabel(), script.getDescription(), scriptContent);
        return "redirect:/project/" + projectId + "/script/" + newScript.getId() + "/detail";
    }
    
    /**
     * Add a version to a script
     * @param projectId	ID of the project
     * @param scriptId	ID of the script
     * @param scriptVersion	Script version to add (from HTML form binding)
     * @param file Uploaded file of the script (JMX or other)
     * @return Redirection to the script detail page
     */
    @RequestMapping(value = "/project/{projectId}/script/{scriptId}/version/new", method = RequestMethod.POST)
    public String addScriptVersion(
    					@PathVariable("projectId") Long projectId, 
    					@PathVariable("scriptId") Long scriptId,
    					@ModelAttribute("scriptVersion") ScriptVersion scriptVersion,
    					@RequestParam("scriptFile") MultipartFile file) throws ControllerValidationException {
    	String scriptContent = null;

		// Get the script File
		try {
			scriptContent = new String(file.getBytes());
		} catch (IOException e) {
		}
		Script script = scriptRepository.findById(scriptId).orElseThrow(() -> new ControllerValidationException(String.format("Script with id %s does not exists", scriptId)));
		ScriptVersion newScriptVersion = scriptService.addScriptVersion(script, scriptVersion.getDescription(), scriptContent);
        return "redirect:/project/" + projectId + "/script/" + scriptId + "/detail#" + newScriptVersion.getId();
    }    
    
    /**
     * Display all script for a project
     * @param projectId	ID of the project
     * @param model	Model prepared for the script listing view
     * @return Name of the scripts listing view
     */
    @RequestMapping("/project/{projectId}/script")
    public String showProjectScripts(@PathVariable("projectId") Long projectId, Model model) throws ControllerValidationException {
		Project project = projectRepository.findById(projectId).orElseThrow(() -> new ControllerValidationException(String.format("Project with id %s does not exists", projectId)));