All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kttdevelopment.jcore.Workflow Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2021 Katsute
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package com.kttdevelopment.jcore;

import java.util.*;
import java.util.function.Supplier;

/**
 * The workflow class replicates the functionality of GitHub workflow commands.
 *
 * @author Katsute
 * @since 1.0.0
 * @version 1.0.0
 */
public abstract class Workflow {

    private Workflow(){ }

    // ----- variables ---------------

    /**
     * Hides a certain phrase from the logs. Alias for {@link #setSecret(String)}.
     *
     * @param mask the phrase to hide
     *
     * @see #setSecret(String)
     * @since 1.0.0
     */
    public static void addMask(final String mask){
        setSecret(mask);
    }

    /**
     * Hides a certain phrase from the logs.
     *
     * @param secret the phrase to hide
     *
     * @see #addMask(String)
     * @since 1.0.0
     */
    public static void setSecret(final String secret){
        issueCommand("add-mask", secret);
    }

    /**
     * Returns a specified workflow input or null.
     *
     * @param name name of input
     *
     * @return value of input or null
     *
     * @see #getInput(String, boolean)
     * @see #getInput(String, boolean, boolean) 
     * @since 1.0.0
     */
    public static String getInput(final String name){
        return getInput(name, false, true);
    }

    /**
     * Returns a specified workflow input or null.
     *
     * @param name name of input
     * @param required if true, a {@link NullPointerException} will be thrown if the value is null
     * @return value of input
     *
     * @see #getInput(String)
     * @see #getInput(String, boolean, boolean)
     * @since 1.0.0
     */
    public static String getInput(final String name, final boolean required){
        return getInput(name, required, true);
    }

    /**
     * Returns a specified workflow input or null.
     *
     * @param name name of input
     * @param required if true, a {@link NullPointerException} will be thrown if the value is null
     * @param trimWhitespace whether to trim the value or not, true by default
     * @return value of input
     *
     * @see #getInput(String)
     * @see #getInput(String, boolean)
     * @since 1.0.0
     */
    public static String getInput(final String name, final boolean required, final boolean trimWhitespace){
        final String input = name != null ? "INPUT_" + name.replace(' ', '_').toUpperCase() : "";
        final String value = System.getenv(input);
        if(required && value == null)
            throw new NullPointerException("Input '" + name + "' is required and not supplied");
        else
            return value != null
                ? trimWhitespace
                    ? value.trim()
                    : value
                : null;
    }

    /**
     * Returns a specified workflow input split into lines.
     *
     * @param name name of input
     * @return value of input in lines
     *
     * @see #getMultilineInput(String, boolean)
     * @see #getMultilineInput(String, boolean, boolean)
     * @since 1.0.0
     */
    public static String[] getMultilineInput(final String name){
        return getMultilineInput(name, false, true);
    }

    /**
     * Returns a specified workflow input split into lines.
     *
     * @param name name of input
     * @param required if true, a {@link NullPointerException} will be thrown if the value is null
     * @return value of input in lines
     *
     * @see #getMultilineInput(String)
     * @see #getMultilineInput(String, boolean, boolean)
     * @since 1.0.0
     */
    public static String[] getMultilineInput(final String name, final boolean required){
        return getMultilineInput(name, required, true);
    }

    /**
     * Returns a specified workflow input split into lines.
     *
     * @param name name of input
     * @param required if true, a {@link NullPointerException} will be thrown if the value is null
     * @param trimWhitespace whether to trim the whole value or not, true by default
     * @return value of input in lines
     *
     * @see #getMultilineInput(String)
     * @see #getMultilineInput(String, boolean)
     * @since 1.0.0
     */
    public static String[] getMultilineInput(final String name, final boolean required, final boolean trimWhitespace){
        final String input = getInput(name, required, trimWhitespace);
        return input == null
            ? new String[0]
            : Arrays.stream(input
                .split("\\n"))
                .filter(s -> !s.isEmpty())
                .toArray(String[]::new);
    }

    /**
     * Returns the value of a workflow input as a boolean.
     *
     * @param name name of input
     * @return value of input
     *
     * @see #getBooleanInput(String, boolean)
     * @see #getBooleanInput(String, boolean, boolean)
     * @since 1.0.0
     */
    public static boolean getBooleanInput(final String name){
        return getBooleanInput(name, false, true);
    }

    /**
     * Returns the value of a workflow input as a boolean.
     *
     * @param name name of input
     * @param required if true, a {@link NullPointerException} will be thrown if the value is null
     * @return value of input
     *
     * @see #getBooleanInput(String)
     * @see #getBooleanInput(String, boolean, boolean)
     * @since 1.0.0
     */
    public static boolean getBooleanInput(final String name, final boolean required){
        return getBooleanInput(name, required, true);
    }

    /**
     * Returns the value of a workflow input as a boolean.
     *
     * @param name name of input
     * @param required if true, a {@link NullPointerException} will be thrown if the value is null
     * @param trimWhitespace whether to trim the value or not, true by default
     * @return value of input
     *
     * @see #getBooleanInput(String)
     * @see #getBooleanInput(String, boolean)
     * @since 1.0.0
     */
    public static boolean getBooleanInput(final String name, final boolean required, final boolean trimWhitespace){
        final String input = getInput(name, required, trimWhitespace);

        if(input != null)
            if(input.equalsIgnoreCase("true"))
                return true;
            else if(input.equalsIgnoreCase("false"))
                return false;
            else
                throw new IllegalArgumentException("Input '" + name + "' is not a boolean type");
        else
            return false;
    }

    /**
     * Sets the workflow step output.
     *
     * @param name output key name
     * @param value output value
     *
     * @since 1.0.0
     */
    public static void setOutput(final String name, final Object value){
        issueCommand("set-output", new LinkedHashMap(){{
            put("name", name);
        }}, value);
    }

    /**
     * Toggles command echo. This does not disable commands.
     *
     * @param enabled whether commands are echoed or not
     *
     * @since 1.0.0
     */
    public static void setCommandEcho(final boolean enabled){
        issueCommand("echo", enabled ? "on" : "off");
    }

    // ----- results ---------------

    /**
     * Prints an error message and sets the error code to 1.
     *
     * @param error error message
     *
     * @see #error(String)
     * @see #error(Throwable)
     * @see #errorSupplier(String)
     * @see #throwError(Throwable)
     * @since 1.0.0
     */
    public static void setFailed(final String error){
        final Throwable throwable = new Throwable();
        error(Arrays.copyOfRange(throwable.getStackTrace(), 1, throwable.getStackTrace().length), error);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.exit(1)));
    }

    // ----- logging commands ---------------

    /**
     * Prints a message.
     *
     * @param message message to print
     *
     * @since 1.0.0
     */
    public static void info(final String message){
        System.out.println(message);
    }

    /**
     * Returns if the runner is in debug mode.
     *
     * @return debug status
     *
     * @since 1.0.0
     */
    public static boolean isDebug(){
        return "1".equals(System.getenv("RUNNER_DEBUG"));
    }

    /**
     * Prints a debug message.
     *
     * @param debug message to print
     *
     * @since 1.0.0
     */
    public static void debug(final String debug){
        issueCommand("debug", debug);
    }

    /**
     * Prints a warning message.
     *
     * @param warning message to print
     *
     * @see #warning(Throwable)
     * @see #warningSupplier(String)
     * @see #throwWarning(Throwable)
     * @since 1.0.0
     */
    public static void warning(final String warning){
        final Throwable throwable = new Throwable();
        warning(Arrays.copyOfRange(throwable.getStackTrace(), 1, throwable.getStackTrace().length), warning);
    }

    /**
     * Prints a warning message.
     *
     * @param throwable throwable
     *
     * @see #warning(String)
     * @see #warningSupplier(String)
     * @see #throwWarning(Throwable)
     * @since 1.0.0
     */
    public static void warning(final Throwable throwable){
        warning(throwable.getStackTrace(), throwable.getMessage());
    }

    /**
     * Prints a warning message and rethrows the exception.
     *
     * @param throwable throwable
     * @param  type of throwable
     * @throws T throwable
     *
     * @see #warning(String)
     * @see #warning(Throwable)
     * @see #warningSupplier(String)
     * @since 1.0.0
     */
    public static  void throwWarning(final T throwable) throws T{
        warning(throwable);
        throw throwable;
    }

    /**
     * Creates a supplier that returns a warning message. Prints warning if running on CI.
     *
     * @param warning message to print
     * @return warning message
     *
     * @see #warning(String)
     * @see #warning(Throwable)
     * @see #throwWarning(Throwable)
     * @since 1.0.0
     */
    public static Supplier warningSupplier(final String warning){
        final Throwable throwable = new Throwable();
        return () -> {
            if(CI)
                warning(Arrays.copyOfRange(throwable.getStackTrace(), 1, throwable.getStackTrace().length), warning);
            return warning;
        };
    }

    private static void warning(final StackTraceElement[] trace, final String message){
        issueCommand("warning", new LinkedHashMap(){{
            put("file", getFile(trace[0]));
            put("line", trace[0].getLineNumber());
            put("col", 1);
        }}, getTraceMessage(trace, message));
    }

    /**
     * Prints an error message.
     *
     * @param error message to print
     *
     * @see #error(Throwable)
     * @see #errorSupplier(String)
     * @see #throwError(Throwable)
     * @see #setFailed(String)
     * @since 1.0.0
     */
    public static void error(final String error){
        final Throwable throwable = new Throwable();
        error(Arrays.copyOfRange(throwable.getStackTrace(), 1, throwable.getStackTrace().length), error);
    }

    /**
     * Prints an error message.
     *
     * @param throwable throwable
     *
     * @see #error(String)
     * @see #errorSupplier(String)
     * @see #throwError(Throwable)
     * @see #setFailed(String)
     * @since 1.0.0
     */
    public static void error(final Throwable throwable){
        error(throwable.getStackTrace(), throwable.getMessage());
    }

    /**
     * Prints an error message and rethrows the exception.
     *
     * @param throwable throwable
     * @param  type of throwable
     * @throws T throwable
     *
     * @see #error(String)
     * @see #error(Throwable)
     * @see #errorSupplier(String)
     * @see #setFailed(String)
     * @since 1.0.0
     */
    public static  void throwError(final T throwable) throws T{
        error(throwable);
        throw throwable;
    }

    /**
     * Creates a supplier that returns an error message. Prints error if running on CI.
     *
     * @param error message to print
     * @return error message
     *
     * @see #error(String)
     * @see #error(Throwable)
     * @see #throwError(Throwable)
     * @see #setFailed(String)
     * @since 1.0.0
     */
    public static Supplier errorSupplier(final String error){
        final Throwable throwable = new Throwable();
        return () -> {
            if(CI)
                error(Arrays.copyOfRange(throwable.getStackTrace(), 1, throwable.getStackTrace().length), error);
            return error;
        };
    }

    private static void error(final StackTraceElement[] trace, final String message){
        issueCommand("error", new LinkedHashMap(){{
            put("file", getFile(trace[0]));
            put("line", trace[0].getLineNumber());
            put("col", 1);
        }}, getTraceMessage(trace, message));
    }

    /**
     * Starts a group.
     *
     * @param name name of group
     *
     * @see #startGroup(String, Runnable)
     * @see #endGroup()
     * @since 1.0.0
     */
    public static void startGroup(final String name){
        issueCommand("group", name);
    }

    /**
     * Starts a group in a {@link Runnable} and ends it after it finishes.
     *
     * @param name name of group
     * @param runnable runnable
     *
     * @see #startGroup(String)
     * @since 1.0.0
     */
    public static void startGroup(final String name, final Runnable runnable){
        startGroup(name);
        try{
            runnable.run();
        }finally{
            endGroup();
        }
    }

    /**
     * Ends the currently opened group.
     *
     * @see #startGroup(String)
     * @since 1.0.0
     */
    @SuppressWarnings("SpellCheckingInspection")
    public static void endGroup(){
        issueCommand("endgroup");
    }

    // ----- state ---------------

    /**
     * Saves a state.
     *
     * @param name name of state
     * @param value state value
     *
     * @see #getState(String)
     * @since 1.0.0
     */
    public static void saveState(final String name, final Object value){
        issueCommand("save-state", new LinkedHashMap(){{
            put("name", name);
        }}, value);
    }

    /**
     * Retrieves a state.
     *
     * @param state name of state
     * @return state value
     *
     * @see #saveState(String, Object)
     * @since 1.0.0
     */
    public static String getState(final String state){
        return System.getenv("STATE_" + state);
    }

    // ----- commands ---------------

    /**
     * Stops workflow commands until a token is passed.
     *
     * @param token token to restart commands
     *
     * @see #startCommand(String)
     * @since 1.0.0
     */
    public static void stopCommand(final String token){
        issueCommand("stop-commands", token);
    }

    /**
     * Starts workflow commands.
     *
     * @param token token used to stop commands
     *
     * @see #stopCommand(String)
     * @since 1.0.0
     */
    public static void startCommand(final String token){
        issueCommand(token);
    }

    /**
     * Adds a matcher.
     *
     * @param matcher path to matcher json
     *
     * @see #removeMatcher(String)
     * @since 1.0.0
     */
    public static void addMatcher(final String matcher){
        issueCommand("add-matcher", null, matcher);
    }

    /**
     * Removes a matcher.
     *
     * @param owner the owner of the matcher as specified in the json
     *
     * @see #addMatcher(String)
     * @since 1.0.0
     */
    public static void removeMatcher(final String owner){
        issueCommand("remove-matcher", new LinkedHashMap(){{
            put("owner", owner);
        }}, null);
    }

    // ----- utility ---------------

    private static final String workspace   = System.getenv("GITHUB_WORKSPACE");
    private static final String repository  = System.getenv("GITHUB_REPOSITORY");
    private static final String SHA         = System.getenv("GITHUB_SHA");

    private static final boolean CI = "true".equals(System.getenv("CI"));

    @SuppressWarnings("ConstantConditions")
    private static String getFile(final StackTraceElement traceElement){
        return Workflow.class.getClassLoader().getResource(traceElement.getClassName().replace('.', '/') + ".class")
            .getPath()
            .replaceFirst(workspace != null ? workspace : "", "")
            .replaceFirst("^/", "")
            .replaceFirst("target/test-classes", "src/test/java")
            .replaceFirst("target/classes", "src/main/java")
            .replaceAll("class$", "java");
    }

    private static String getTraceMessage(final StackTraceElement[] stacktrace, final String message){
        final StackTraceElement cause = stacktrace[0];
        final StringBuilder output = new StringBuilder();
        if(CI)
            output
                .append("https://github.com/")
                .append(repository).append('/').append("blob").append('/')
                .append(SHA).append('/');
        output
            .append(getFile(cause))
            .append("#L").append(cause.getLineNumber());
        if(message != null)
            output.append(" : ").append(message);
        output.append('\n');

        boolean first = true;
        for(final StackTraceElement stackTraceElement : stacktrace){
            if(!first)
                output.append("\n\t").append("at").append(' ');
            first = false;
            output.append(stackTraceElement.toString());
        }

        return output.toString();
    }

    // ----- command ---------------

    private static void issueCommand(final String command){
        issueCommand(command, null, null);
    }

    private static void issueCommand(final String command, final String message){
        issueCommand(command, null, message);
    }

    private static void issueCommand(final String command, final Map properties, final Object message){
        System.out.println(toCommand(command, properties, message));
    }

    private static final String commandString = "::";

    private static String toCommand(final String command, final Map properties, final Object message){
        final StringBuilder commandString = new StringBuilder(Workflow.commandString + (command != null ? command : "missing.command"));

        if(properties != null && !properties.isEmpty()){
            commandString.append(' ');
            boolean first = true;
            for(final String key : properties.keySet()){
                final Object value = properties.get(key);
                if(value != null){
                    if(first)
                        first = false;
                    else
                        commandString.append(',');
                    commandString
                        .append(key)
                        .append('=')
                        .append(
                            toCommandValue(value)
                                .replaceAll("%", "%25")
                                .replaceAll("\\r", "%0D")
                                .replaceAll("\\n", "%0A")
                                .replaceAll(":", "%3A")
                                .replaceAll(",", "%2C")
                        );
                }
            }
        }
        commandString.append(Workflow.commandString);
        commandString.append(
            message != null
                ? toCommandValue(message)
                    .replaceAll("%", "%25")
                    .replaceAll("\\r", "%0D")
                    .replaceAll("\\n", "%0A")
                : ""
        );

        return commandString.toString();
    }

    private static String toCommandValue(final Object obj){
        if(obj == null)
            return "";
        else if(obj instanceof String)
            return (String) obj;
        else
            return obj.toString();
        // todo: obj to json string
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy