package gov.nasa.jpl.mbee.pma.analyses;
import com.nomagic.magicdraw.commandline.CommandLine;
import com.nomagic.magicdraw.core.Application;
import com.nomagic.magicdraw.core.Project;
import com.nomagic.magicdraw.core.project.ProjectDescriptor;
import com.nomagic.magicdraw.esi.EsiUtils;
import com.nomagic.magicdraw.teamwork2.ITeamworkService;
import com.nomagic.magicdraw.teamwork2.ServerLoginInfo;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Element;
import gov.nasa.jpl.mbee.mdk.api.MDKHelper;
import gov.nasa.jpl.mbee.mdk.api.MagicDrawHelper;
import gov.nasa.jpl.mbee.mdk.api.incubating.convert.Converters;
import gov.nasa.jpl.mbee.mdk.http.ServerException;
import gov.nasa.jpl.mbee.mdk.util.TicketUtils;
import gov.nasa.jpl.mbee.mdk.mms.actions.MMSLoginAction;
import gov.nasa.jpl.mbee.mdk.mms.sync.queue.OutputSyncRunner;
import gov.nasa.jpl.mbee.mdk.mms.sync.queue.Request;
import gov.nasa.jpl.mbee.mdk.options.MDKOptionsGroup;
import gov.nasa.jpl.mbee.mdk.util.Pair;
import javax.xml.bind.DatatypeConverter;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.rmi.RemoteException;
import java.util.*;
// TODO Redo in @donbot
public class AutomatedViewGeneration extends CommandLine {
// needed because CommandLine redirects it, and we want the output
private static final PrintStream stdout = System.out;
// cancel handler stuff
// cancel is set when the cancelHandler is triggered. it is used as a flag so the running
// program can discontinue normal flow, throw a cancel exception, and notify the
// waiting cancelHandler
// running is used to indicate whether we are in docweb scope or not
// this is needed because the cancel handler will trigger before we're fully loaded
// and then be stuck waiting for notification that never comes
private static boolean debug = false,
cancel = false,
running = false,
twLogin = false,
twLoaded = false;
private static byte error = 0;
private static int argIndex = 0,
applicationAccounts = 1;
private static String testRoot = "",
credentialsLocation = "",
teamworkUsername = "",
teamworkPassword = "",
teamworkServer = "",
teamworkPort = "",
teamworkProject = "",
teamworkBranchName = "master";
private static Project project;
private static final List<String> viewList = new ArrayList<>(),
messageLog = new ArrayList<>();
private static InterruptTrap cancelHandler;
private static final Object lock = new Object();
/*//////////////////////////////////////////////////////////////
*
* Execution methods
*
/*//////////////////////////////////////////////////////////////
@Override
protected byte execute() {
// send output back to stdout
System.setOut(AutomatedViewGeneration.stdout);
System.setErr(AutomatedViewGeneration.stdout);
// disable logJson, in case it's on, to make the logs not hideous
MDKOptionsGroup.getMDKOptions().setLogJson(false);
// start the cancel handler so we don't terminate in the middle of a view sync operation
// and so we can force logout if logged in to teamwork
cancelHandler = new InterruptTrap();
Runtime.getRuntime().addShutdownHook(cancelHandler);
running = true;
try {
String msg = "Performing automated view generation";
System.out.println(msg);
messageLog.add(msg);
// login TeamworkCloud, set MMS credentials
loginTeamwork();
// open project
loadTeamworkProject();
// generate views and commit images
generateViewsForDocList();
// logout in finally
} catch (Error err) {
error = 99;
System.out.println(err.toString());
err.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (twLoaded) {
// close project
System.out.println("[OPERATION] Closing open project");
Application.getInstance().getProjectsManager().closeProject();
}
if (twLogin) {
// logout
System.out.println("[OPERATION] Logging out of teamwork");
EsiUtils.getTeamworkService().logout();
}
try {
checkFailure();
} catch (IOException e) {
e.printStackTrace();
}
}
return error;
}
/*////////////////////////////////////////////////////////////////
*
* "User Operation" methods
*
/*////////////////////////////////////////////////////////////////
/**
* Logs in to teamwork using the initially loaded teamwork account. If that account
* is already in use, will load/generate and attempt to log in with additional sets
* of credentials, up to the limit specified in applicationAccounts.
*
* @throws FileNotFoundException missing credentialsLocation
* @throws UnsupportedEncodingException logMessage failures
* @throws InterruptedException cancel triggered and caught by cancel handler
* @throws IllegalAccessException access failure with loaded credentials
*/
private void loginTeamwork()
throws FileNotFoundException, UnsupportedEncodingException, InterruptedException, IllegalAccessException {
// disable all mdk popup warnings
MDKHelper.setPopupsDisabled(true);
String message = "[OPERATION] Logging in to Teamwork";
logMessage(message);
ITeamworkService twcService = EsiUtils.getTeamworkService();
for (int i = 1; i <= applicationAccounts; i++) {
try {
String appendage = (i == 1 ? "" : Integer.toString(i));
loadCredentials(appendage);
// LOG: credentials have loaded from credentialsLocation
} catch (IOException e) {
error = 100;
message = "[FAILURE] Unable to find credentials at location " + Paths.get(credentialsLocation).toAbsolutePath().toString();
logMessage(message);
throw new IllegalStateException(message, e);
}
if (i == 1) {
try {
reportStatus("running", debug);
} catch (IOException e) {
throw new IllegalAccessException("Automated View Generation failed - User " + teamworkUsername + " can not edit site (check Alfresco site membership).");
}
}
twcService.login(new ServerLoginInfo(teamworkServer + ":" + Integer.parseInt(teamworkPort),
teamworkUsername, teamworkPassword, true), true);
if (twcService.isConnected()) {
// setting MMS credentials after successful teamwork login since we don't know which account was used before hand
MDKHelper.setMMSLoginCredentials(teamworkUsername, teamworkPassword);
message = "Logged in to Teamwork as " + teamworkUsername + " on " + teamworkServer + ":" + teamworkPort;
logMessage(message);
// LOG: successfully logged in to the teamwork cloud server
break;
}
message = "Unable to log in to Teamwork as " + teamworkUsername + " on " + teamworkServer + ":" + teamworkPort;
logMessage(message);
// LOG: this user failed to log in
}
if (!twcService.isConnected()) {
error = 101;
message = "[FAILURE] Unable to log in to Teamwork as available account(s).";
logMessage(message);
throw new IllegalStateException(message);
}
twLogin = true;
checkCancel();
}
/**
* Loads the Teamwork project. Complains if it fails.
*
* @throws FileNotFoundException can't find teamwork project or branch
* @throws UnsupportedEncodingException logMessage failures
* @throws InterruptedException cancel triggered and caught by cancel handler
* @throws IllegalAccessException access failure with loaded credentials
* @throws RemoteException error getting the projectDescriptor back from the twUtil
*/
private void loadTeamworkProject()
throws FileNotFoundException, UnsupportedEncodingException, RemoteException, IllegalAccessException, InterruptedException {
String message;
ProjectDescriptor projectDescriptor;
if (teamworkProject.startsWith("PROJECT-")) {
teamworkProject = teamworkProject.substring(8);
}
int index = teamworkProject.indexOf("ID_");
if (index > 0) {
teamworkProject = teamworkProject.substring(index);
}
// get the descriptor of the project trunk
try {
projectDescriptor = EsiUtils.getTeamworkService().getProjectDescriptorById(teamworkProject);
} catch (Exception e) {
throw new RemoteException(e.getMessage());
}
// if trunk descriptor is null, error out and indicate projectId fail
if (projectDescriptor == null) {
message = "[FAILURE] Unable to find TeamworkCloud projectId " + teamworkProject;
logMessage(message);
error = 102;
throw new FileNotFoundException(message);
}
// if we need a branch, update the descriptor to the branch descriptor
if (!teamworkBranchName.isEmpty() && !teamworkBranchName.equals("master")) {
//TODO verify this with a TWC server with branches and whatever (need projectID and some config still)
projectDescriptor = EsiUtils.getDescriptorForBranch(projectDescriptor, teamworkBranchName);
// if updated projectDescriptor is now null, error out and indicate branch problem
if (projectDescriptor == null) {
message = "[FAILURE] Unable to find TeamworkCloud project branch " + projectDescriptor.getRepresentationString() + "/" + teamworkBranchName;
logMessage(message);
error = 102;
throw new FileNotFoundException(message);
}
}
// we have a valid project descriptor, so load the associated project
message = "[OPERATION] Loading TeamworkCloud project " + projectDescriptor.getRepresentationString();
logMessage(message);
Application.getInstance().getProjectsManager().loadProject(projectDescriptor, true);
// if not access to project, loaded project will be null, so error out
if (Application.getInstance().getProject() == null) {
message = "[FAILURE] User does not have access to " + teamworkProject;
logMessage(message);
error = 102;
throw new IllegalAccessException(message);
}
twLoaded = true;
project = Application.getInstance().getProject();
// move the stored message log into the MD notification window. This will mess up the time stamps, but will
// keep all of the messages in the same place
while (!messageLog.isEmpty()) {
Application.getInstance().getGUILog().log(messageLog.remove(0));
}
message = "Opened TeamworkCloud project";
logMessage(message);
// LOG: successfully opened the Teamwork project
checkCancel();
}
/**
* Generates views and commits images for each document / view in the docList
* sequentially. If an element is not found, skips generation and continues
* through list, and will throw an exception at the end.
*
* @throws FileNotFoundException one or more documents not found in project, or logMessage failure
* @throws InterruptedException cancel triggered and caught by cancel handler
* @throws UnsupportedEncodingException logMessage failure
*/
private void generateViewsForDocList()
throws FileNotFoundException, IllegalAccessException, InterruptedException, UnsupportedEncodingException {
if (!TicketUtils.isTicketSet(project)) {
TicketUtils.setUsernameAndPassword(teamworkUsername, teamworkPassword);
if (!MMSLoginAction.loginAction(project)) {
String message = "[FAILURE] User " + teamworkUsername + " failed to login to MMS.";
logMessage(message);
error = 103;
throw new IllegalAccessException("[FAILURE] Automated View Generation failed - User " + teamworkUsername + " can not log in to MMS server.");
}
}
String msg = "[OPERATION] Triggering view generation on MMS";
logMessage(msg);
boolean failedDocs = false;
for (String elementID : viewList) {
Element document = Converters.getIdToElementConverter().apply(elementID, Application.getInstance().getProject());
if (document == null) {
msg = "[ERROR] Unable to find element \"" + elementID + "\"";
logMessage(msg);
// LOG: the element which caused a failure and didn't generate
failedDocs = true;
}
else {
OutputSyncRunner.clearLastExceptionPair();
msg = "Generating views for \"" + document.getHumanName() + "\".";
logMessage(msg);
// LOG: the element which is being generated currently
MDKHelper.generateViews(document, true);
// wait is required for the auto-image commit, and it helps tie exceptions in output queue to their document
MDKHelper.mmsUploadWait();
if (OutputSyncRunner.getLastExceptionPair() != null) {
failedDocs = true;
Pair<Request, Exception> current = OutputSyncRunner.getLastExceptionPair();
Exception e = current.getValue();
if (e instanceof ServerException && ((ServerException) e).getCode() == 403) {
msg = "[ERROR] Unable to generate " + document.getHumanName() + ". User " + teamworkUsername + " does not have permission to write to the MMS in this branch.";
logMessage(msg);
}
else {
msg = "[ERROR] Unexpected error while generating " + document.getHumanName() + ". Reason: " + e.getMessage();
logMessage(msg);
}
}
}
}
// check for exceptions after running
if (failedDocs) {
error = 104;
throw new FileNotFoundException("[FAILURE] Automated View Generation Failed - Unable to find or write document(s)");
// LOG: AVG FAILED AT THIS POINT
}
checkCancel();
}
/*//////////////////////////////////////////////////////////////////
*
* Helper methods
*
/*//////////////////////////////////////////////////////////////////
/**
* parses arguments passed in from command line
*
* @param args Argument string array from the console
*/
@Override
protected void parseArgs(String[] args) {
// iteration of argIndex is handled by following code to account for
// variable length arguments with whitespace
for (argIndex = 0; argIndex < args.length; ) {
if (args[argIndex].startsWith("-")) {
switch (args[argIndex]) {
case "-crdlc":
credentialsLocation = buildArgString(args);
break;
case "-debug":
debug = true;
argIndex++;
break;
case "-doclist":
String csvDocumentList = buildArgString(args);
Collections.addAll(viewList, csvDocumentList.split(" && "));
System.out.println();
break;
case "-tstrt":
testRoot = buildArgString(args);
testRoot = testRoot + (testRoot.length() > 0 && testRoot.charAt(testRoot.length() - 1) == '/' ? "" : "/");
if (testRoot.equals("/")) {
testRoot = "";
}
break;
case "-twbrn":
teamworkBranchName = buildArgString(args);
break;
case "-twprj":
teamworkProject = buildArgString(args);
if (teamworkProject.startsWith("twcloud:/")) {
teamworkProject = teamworkProject.substring(9);
}
break;
case "-wkspc":
teamworkBranchName = buildArgString(args);
break;
default:
System.out.println("Invalid flag passed: " + argIndex + " " + args[argIndex++]);
}
}
else {
System.out.println("Invalid parameter passed: " + argIndex + " " + args[argIndex]);
}
argIndex++;
}
if (!Paths.get(credentialsLocation).toFile().exists()) {
credentialsLocation = testRoot + credentialsLocation;
}
}
private String buildArgString(String[] args) {
StringBuilder spacedArgument = new StringBuilder("");
while ((argIndex + 1) < args.length && !args[argIndex + 1].startsWith("-")) {
spacedArgument.append(args[++argIndex]);
spacedArgument.append(" ");
}
if (spacedArgument.length() > 0) {
spacedArgument.setLength(spacedArgument.length() - 1);
}
return spacedArgument.toString();
}
private void loadCredentials(String append) throws IOException {
Properties prop = new Properties();
try (InputStream input = new FileInputStream(credentialsLocation)) {
prop.load(input);
if (prop.containsKey("app.accounts")) {
try {
applicationAccounts = Integer.parseInt(prop.getProperty("app.accounts"));
} catch (NumberFormatException nfe) {
applicationAccounts = 1;
System.out.println("[WARNING] Unable to parse number specified for app.accounts. Using default.");
}
}
teamworkServer = prop.getProperty("tw.url");
if (teamworkServer.contains("//")) {
teamworkServer = teamworkServer.substring(teamworkServer.indexOf("//") + 2);
}
if (teamworkServer.lastIndexOf(':') != -1) {
teamworkServer = teamworkServer.substring(0, teamworkServer.lastIndexOf(':'));
}
teamworkPort = prop.getProperty("tw.port");
if (prop.containsKey("tw.user" + append)) {
teamworkUsername = prop.getProperty("tw.user" + append);
teamworkPassword = prop.getProperty("tw.pass" + append);
}
else {
teamworkUsername = prop.getProperty("tw.user") + append;
teamworkPassword = prop.getProperty("tw.pass") + append;
}
}
}
private void checkCancel() throws InterruptedException {
synchronized (lock) {
if (cancel) {
error = 127;
throw new InterruptedException("Cancel signal received.");
}
}
}
private void checkFailure() throws IOException {
synchronized (lock) {
running = false;
if (cancel) {
lock.notify();
}
else {
Runtime.getRuntime().removeShutdownHook(cancelHandler);
if (error == 0) {
reportStatus("completed", debug);
System.out.println("Automated View Generation completed without errors.\n");
}
else {
reportStatus("failed", debug);
System.out.println("Automated View Generation did not finish successfully. Operations were logged in MDNotificationWindowText.html.\n");
}
// System.exit(error);
}
}
}
private void logMessage(String msg) throws FileNotFoundException, UnsupportedEncodingException {
if (messageLog.size() > 0) {
if (!msg.isEmpty()) {
messageLog.add(msg);
System.out.println(msg);
}
exportMessageLog();
}
else {
if (!msg.isEmpty()) {
MagicDrawHelper.generalMessage(msg);
}
exportGUILog();
}
}
private void exportGUILog() throws FileNotFoundException, UnsupportedEncodingException {
String guiLog = Application.getInstance().getGUILog().getLoggedMessages();
try (PrintWriter writer = new PrintWriter(testRoot + "MDNotificationWindowText" + ".html", "UTF-8")) {
writer.println(guiLog);
}
}
private void exportMessageLog() throws FileNotFoundException, UnsupportedEncodingException {
String guiLog = generateLog();
try (PrintWriter writer = new PrintWriter(testRoot + "MDNotificationWindowText" + ".html", "UTF-8")) {
writer.println(guiLog);
}
}
private String generateLog() {
StringBuilder sb = new StringBuilder("");
sb.append("<html>\n")
.append("\t<head>\n")
.append("\t</head>\n")
.append("\n")
.append("\t<body>\n")
.append("\t\t<table cellpadding=\"0\" cellspacing=\"0\">\n");
for (String msg : messageLog) {
sb.append("\t\t\t<tr>\n").append("\t\t\t\t<td>\n")
.append("\t\t\t\t\t<div class=\"info\">\n")
.append("\t\t\t\t\t\t")
.append(msg)
.append("\n")
.append("\t\t\t\t\t</div>\n")
.append("\t\t\t\t</td>\n")
.append("\t\t\t</tr>\n");
}
sb.append("\n")
.append("\t\t</table>\n")
.append("\t</body>\n")
.append("</html>");
return sb.toString();
}
private void reportStatus(String status, boolean verbose) throws IOException {
System.out.println("Updating status: " + status);
Map<String, String> envvars = System.getenv();
String JOB_ID;
String MMS_SERVER;
if (!(envvars.containsKey("MMS_SERVER") && envvars.containsKey("JOB_ID"))) {
System.out.println("MMS_SERVER or JOB_ID not specified");
return;
}
else {
JOB_ID = envvars.get("JOB_ID");
MMS_SERVER = envvars.get("MMS_SERVER");
}
URL url = new URL(MMS_SERVER + "/alfresco/service/workspaces/" + teamworkBranchName + "/jobs");
String data = "{\"jobs\":[{\"sysmlid\":\"" + JOB_ID + "\", \"status\":\"" + status + "\"}]}";
byte[] postData = data.getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
String auth = teamworkUsername + ":" + teamworkPassword;
String encodedAuth = "Basic " + DatatypeConverter.printBase64Binary(auth.getBytes(StandardCharsets.UTF_8));
conn.setRequestProperty("Authorization", encodedAuth);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Content-Length", String.valueOf(postData.length));
conn.setDoOutput(true);
conn.getOutputStream().write(postData);
Reader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
for (int c; (c = in.read()) >= 0; ) {
if (verbose) {
System.out.print((char) c);
}
}
}
private class InterruptTrap extends Thread {
@Override
public void run() {
if (running) {
cancel = true;
try {
reportStatus("aborting", debug);
} catch (IOException e) {
e.printStackTrace();
}
synchronized (lock) {
System.setOut(AutomatedViewGeneration.stdout);
String msg = "Cancel received. Will complete current operation, logout, and terminate (max delay: 15min).";
try {
logMessage(msg);
} catch (FileNotFoundException | UnsupportedEncodingException e) {
e.printStackTrace();
}
try {
int mins = 15;
for (int i = 0; i < mins * 12; i++) {
if (!running) {
break;
}
lock.wait(5000);
}
} catch (InterruptedException ignored) {
}
}
try {
reportStatus("aborted", debug);
} catch (IOException e) {
e.printStackTrace();
}
Runtime.getRuntime().removeShutdownHook(cancelHandler);
Runtime.getRuntime().halt(error);
}
}
}
}