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

org.praxislive.ide.project.DefaultPraxisProject Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2024 Neil C Smith.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3 only, as
 * published by the Free Software Foundation.
 *
 * This code 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
 * version 3 for more details.
 *
 * You should have received a copy of the GNU General Public License version 3
 * along with this work; if not, see http://www.gnu.org/licenses/
 *
 *
 * Please visit https://www.praxislive.org if you need additional information or
 * have any questions.
 */
package org.praxislive.ide.project;

import java.beans.PropertyChangeEvent;
import org.praxislive.ide.project.ui.PraxisCustomizerProvider;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.lang.model.SourceVersion;
import javax.swing.Icon;
import org.netbeans.api.java.classpath.*;
import org.praxislive.ide.core.api.Callback;
import org.praxislive.ide.project.api.ExecutionLevel;
import org.praxislive.ide.project.api.PraxisProject;
import org.praxislive.ide.project.ui.PraxisLogicalViewProvider;
import org.praxislive.ide.project.ui.ProjectDialogManager;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.spi.java.classpath.ClassPathProvider;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.netbeans.spi.project.ActionProvider;
import org.netbeans.spi.project.ProjectState;
import org.netbeans.spi.project.support.LookupProviderSupport;
import org.netbeans.spi.project.ui.PrivilegedTemplates;
import org.netbeans.spi.project.ui.ProjectOpenedHook;
import org.netbeans.spi.project.ui.support.UILookupMergerSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
import org.praxislive.core.Value;
import org.praxislive.core.types.PArray;
import org.praxislive.core.types.PResource;
import org.praxislive.ide.core.api.AbstractTask;
import org.praxislive.ide.core.api.SerialTasks;
import org.praxislive.ide.core.api.Task;
import org.praxislive.ide.project.api.ExecutionElement;
import org.praxislive.ide.project.spi.ElementHandler;
import org.praxislive.ide.project.spi.LineHandler;
import org.praxislive.project.ProjectElement;
import org.praxislive.project.ProjectModel;

/**
 *
 */
@NbBundle.Messages({
    "# {0} - required Java release",
    "PraxisProject.javaVersionError=This project requires Java {0}",
    "ERR_elementContinueBuild=Continue building project?",
    "ERR_elementContinueRun=Continue running project?",
    "# {0} - path or command",
    "ERR_elementExecution=Error executing {0}"
})
public class DefaultPraxisProject implements PraxisProject {

    public final static String LIBS_PATH = "config/libs/";
    final static String LIBS_COMMAND = "libraries {\n  " + LIBS_PATH + "*.jar\n}";

    public static final int MIN_JAVA_VERSION = 21;
    public static final int MAX_JAVA_VERSION;

    static {
        int max = SourceVersion.latest().ordinal();
        MAX_JAVA_VERSION = max < MIN_JAVA_VERSION ? MIN_JAVA_VERSION : max;
    }

    private final static RequestProcessor RP = new RequestProcessor(PraxisProject.class);
    private final static LinkedHashSet REGISTRY
            = new LinkedHashSet<>();

    private final FileObject directory;
    private final FileObject projectFile;
    private final ProjectState state;
    private final HubManager hubManager;
    private final ProjectPropertiesImpl properties;
    private final PropertiesListener propsListener;
    private final Lookup lookup;
    private final Set executedHandlers;

    private boolean actionsEnabled;
    private List libPath;
    private ClassPath libsCP;
    private ClassPath compileCP;
    private TaskExec activeExec;

    DefaultPraxisProject(FileObject directory, FileObject projectFile, ProjectState state)
            throws IOException {
        this.directory = directory;
        this.projectFile = projectFile;
        this.state = state;
        hubManager = new HubManager(this);
        properties = parseProjectFile(projectFile);
        propsListener = new PropertiesListener();
        properties.addPropertyChangeListener(propsListener);
        Lookup base = Lookups.fixed(
                this,
                properties,
                new Info(),
                new ActionImpl(),
                new ProjectOpenedHookImpl(),
                state,
                new PraxisCustomizerProvider(this),
                new PraxisLogicalViewProvider(this),
                new BaseTemplates(),
                new ClassPathImpl(),
                UILookupMergerSupport.createPrivilegedTemplatesMerger()
        );

        base = new ProxyLookup(base, hubManager.getLookup());
        this.lookup = LookupProviderSupport.createCompositeLookup(base, LOOKUP_PATH);
        executedHandlers = new HashSet<>();
        actionsEnabled = true;
        libPath = List.of();
        compileCP = CoreClassPathRegistry.getInstance().getCompileClasspath();
    }

    private ProjectPropertiesImpl parseProjectFile(FileObject projectFile) {
        ProjectPropertiesImpl props = new ProjectPropertiesImpl(this);
        try {
            ProjectModel model = ProjectModel.parse(directory.toURI(), projectFile.asText());
            List config = model.setupElements().stream()
                    .map(this::fromModelElement)
                    .filter(e -> e != null)
                    .toList();
            List build = model.buildElements().stream()
                    .map(this::fromModelElement)
                    .filter(e -> e != null)
                    .toList();
            List run = model.runElements().stream()
                    .map(this::fromModelElement)
                    .filter(e -> e != null)
                    .toList();
            props.initElements(Map.of(
                    ExecutionLevel.CONFIGURE, config,
                    ExecutionLevel.BUILD, build,
                    ExecutionLevel.RUN, run));
        } catch (Exception ex) {
            Exceptions.printStackTrace(ex);
        }
        return props;
    }

    @Override
    public FileObject getProjectDirectory() {
        return directory;
    }

    @Override
    public Lookup getLookup() {
        return lookup;
    }

    public void save() throws IOException {
        ProjectModel.Builder builder = ProjectModel.builder();
        builder.context(directory.toURI());
        Map> elements = properties.elements();
        elements.get(ExecutionLevel.CONFIGURE)
                .forEach(e -> builder.setupElement(toModelElement(e)));
        elements.get(ExecutionLevel.BUILD)
                .forEach(e -> builder.buildElement(toModelElement(e)));
        elements.get(ExecutionLevel.RUN)
                .forEach(e -> builder.runElement(toModelElement(e)));
        String script = builder.build().writeToString();
        Files.writeString(FileUtil.toPath(projectFile), script);
    }

    public boolean isActive() {
        return hubManager.getState() == HubManager.State.Running;
    }

    public static List activeProjects() {
        // extra check required?
        REGISTRY.removeIf(p -> !p.isActive());
        return new ArrayList<>(REGISTRY);
    }

    private void execute(ExecutionLevel level) {

        if (properties.getJavaRelease() > MAX_JAVA_VERSION) {
            ProjectDialogManager.get(this).reportError(
                    Bundle.PraxisProject_javaVersionError(properties.getJavaRelease()));
            return;
        }

        List tasks = new ArrayList<>();

        if (!isActive()) {
            executedHandlers.clear();
            tasks.add(hubManager.createStartupTask());
        }

        var elements = properties.elements();
        elements.get(ExecutionLevel.CONFIGURE).forEach(e -> {
            if (!executedHandlers.contains(e.handler())) {
                tasks.add(new ElementTask(ExecutionLevel.CONFIGURE, e));
            }
        });
        if (level == ExecutionLevel.BUILD || level == ExecutionLevel.RUN) {
            elements.get(ExecutionLevel.BUILD).forEach(e -> {
                if (!executedHandlers.contains(e.handler())) {
                    tasks.add(new ElementTask(ExecutionLevel.BUILD, e));
                }
            });
        }
        if (level == ExecutionLevel.RUN) {
            elements.get(ExecutionLevel.RUN).forEach(e -> {
                tasks.add(new ElementTask(ExecutionLevel.RUN, e));
            });
        }

        actionsEnabled = false;
        activeExec = new TaskExec(tasks);
        var execState = activeExec.execute();
        if (execState == Task.State.RUNNING) {
            activeExec.addPropertyChangeListener(e -> {
                actionsEnabled = true;
                activeExec = null;
                if (isActive()) {
                    REGISTRY.add(this);
                }
            });
        } else {
            actionsEnabled = true;
            activeExec = null;
            if (isActive()) {
                REGISTRY.add(this);
            }
        }

    }

    private void clean() {
        if (activeExec != null) {
            activeExec.cancel();
        }
        List tasks = List.of(hubManager.createShutdownTask());
        activeExec = new TaskExec(tasks);
        actionsEnabled = false;
        var execState = activeExec.execute();
        if (execState == Task.State.RUNNING) {
            activeExec.addPropertyChangeListener(e -> {
                actionsEnabled = true;
                activeExec = null;
                REGISTRY.removeIf(p -> !p.isActive());
            });
        } else {
            actionsEnabled = true;
            activeExec = null;
            REGISTRY.removeIf(p -> !p.isActive());
        }

    }

    void updateLibs(PArray newLibs, PArray newLibsPath) {
        clearLibs();
        properties.updateLibraries(newLibs);
        libPath = List.copyOf(buildLibList(newLibsPath));
        libsCP = buildLibsClasspath(libPath);
        if (libsCP != null) {
            compileCP = ClassPathSupport.createProxyClassPath(libsCP,
                    CoreClassPathRegistry.getInstance().getCompileClasspath());
            GlobalPathRegistry.getDefault().register(ClassPath.COMPILE, new ClassPath[]{libsCP});
        }
    }

    private void clearLibs() {
        if (libsCP != null) {
            GlobalPathRegistry.getDefault().unregister(ClassPath.COMPILE, new ClassPath[]{libsCP});
        }
        libPath = List.of();
        libsCP = null;
        compileCP = CoreClassPathRegistry.getInstance().getCompileClasspath();
    }

    private List buildLibList(PArray path) {
        return path.stream()
                .flatMap(v -> PResource.from(v).stream())
                .map(PResource::value)
                .filter(uri -> "file".equals(uri.getScheme()))
                .collect(Collectors.toList());
    }

    private ClassPath buildLibsClasspath(List path) {
        try {
            return ClassPathSupport.createClassPath(
                    path.stream()
                            .map(File::new)
                            .map(FileUtil::urlForArchiveOrDir)
                            .toArray(URL[]::new)
            );
        } catch (Exception ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }

    private ExecutionElement fromModelElement(ProjectElement element) {
        try {
            if (element instanceof ProjectElement.File fileElement) {
                return ExecutionElement.forFile(FileUtil.toFileObject(Path.of(fileElement.file())));
            } else if (element instanceof ProjectElement.Line lineElement) {
                return ExecutionElement.forLine(lineElement.line());
            }
        } catch (Exception ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }

    private ProjectElement toModelElement(ExecutionEntry entry) {
        ExecutionElement element = entry.element();
        ElementHandler handler = entry.handler();
        if (element instanceof ExecutionElement.File fileElement) {
            return ProjectElement.file(fileElement.file().toURI());
        } else if (element instanceof ExecutionElement.Line lineElement) {
            String line = lineElement.line();
            if (handler instanceof LineHandler lineHandler) {
                line = lineHandler.rewrite(line);
            }
            return ProjectElement.line(line);
        }
        throw new IllegalArgumentException();
    }

    private class ClassPathImpl implements ClassPathProvider {

        @Override
        public ClassPath findClassPath(FileObject file, String type) {
            switch (type) {
                case ClassPath.BOOT:
                case JavaClassPathConstants.MODULE_BOOT_PATH:
                    return CoreClassPathRegistry.getInstance().getBootClasspath();
                case ClassPath.COMPILE:
                    return compileCP;
                default:
                    return null;
            }
        }

    }

    private class Info implements ProjectInformation {

        @Override
        public String getName() {
            return directory.getName();
        }

        @Override
        public String getDisplayName() {
            return directory.getName();
        }

        @Override
        public Icon getIcon() {
            return ImageUtilities.loadImageIcon("org/praxislive/ide/project/resources/pxp16.png", false);
        }

        @Override
        public Project getProject() {
            return DefaultPraxisProject.this;
        }

        @Override
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            // no op
        }

        @Override
        public void removePropertyChangeListener(PropertyChangeListener listener) {
            // no op
        }
    }

    private class ProjectOpenedHookImpl extends ProjectOpenedHook {

        @Override
        protected void projectOpened() {
        }

        @Override
        protected void projectClosed() {
            clearLibs();
        }

    }

    private class BaseTemplates implements PrivilegedTemplates {

        @Override
        public String[] getPrivilegedTemplates() {
            return new String[]{
                "Templates/Other/Folder",
                "Templates/Other/org-netbeans-modules-project-ui-NewFileIterator-folderIterator"
            };
        }
    }

    private class PropertiesListener implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            state.markModified();
            RP.schedule(new Runnable() {

                @Override
                public void run() {
                    try {
                        ProjectManager.getDefault().saveProject(DefaultPraxisProject.this);
                    } catch (IOException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
            }, 500, TimeUnit.MILLISECONDS);
        }
    }

    private class ActionImpl implements ActionProvider {

        @Override
        public String[] getSupportedActions() {
            return new String[]{
                ActionProvider.COMMAND_RUN,
                ActionProvider.COMMAND_BUILD,
                ActionProvider.COMMAND_CLEAN
            };
        }

        @Override
        public void invokeAction(String command, Lookup context) throws IllegalArgumentException {

            if (ActionProvider.COMMAND_RUN.equals(command)) {
                execute(ExecutionLevel.RUN);
            } else if (ActionProvider.COMMAND_BUILD.equals(command)) {
                execute(ExecutionLevel.BUILD);
            } else if (ActionProvider.COMMAND_CLEAN.equals(command)) {
                clean();
            } else {
                throw new IllegalArgumentException();
            }
        }

        @Override
        public boolean isActionEnabled(String command, Lookup context) throws IllegalArgumentException {

            if (ActionProvider.COMMAND_CLEAN.equals(command)) {
                return isActive();
            } else {
                return actionsEnabled;
            }

        }
    }

    private class TaskExec extends SerialTasks {

        private final Map> warnings;
        private final ProgressHandle progress;
        private final int count;

        private TaskExec(List tasks) {
            super(tasks);
            warnings = new LinkedHashMap<>();
            progress = ProgressHandle.createHandle("Executing...", this);
            progress.setInitialDelay(0);
            count = tasks.size();
        }

        @Override
        protected void beforeExecute() {
            progress.start(count);
        }

        @Override
        protected void beforeTask(Task task) {
            task.description().ifPresentOrElse(
                    d -> progress.progress(d, count - remaining()),
                    () -> progress.progress(count - remaining()));
        }

        @Override
        protected void afterTask(Task task) {
            var log = task.log();
            if (!log.isEmpty()) {
                warnings.put(task, List.copyOf(log));
            }
        }

        @Override
        protected void afterExecute() {
            progress.finish();
            if (!warnings.isEmpty()) {
                ProjectDialogManager.get(DefaultPraxisProject.this)
                        .reportWarnings(warnings);
            }
        }

    }

    private class ElementTask extends AbstractTask {

        private final ExecutionLevel level;
        private final ExecutionElement element;
        private final ElementHandler handler;

        private ElementTask(ExecutionLevel level, ExecutionEntry entry) {
            this.level = level;
            this.element = entry.element();
            this.handler = entry.handler();
        }

        @Override
        protected void handleExecute() throws Exception {
            if (level != ExecutionLevel.RUN) {
                executedHandlers.add(handler);
            }
            handler.process(Callback.create(result -> {
                if (result.isError()) {
                    if (continueOnError(result.args())) {
                        updateState(State.COMPLETED);
                    } else {
                        updateState(State.ERROR);
                    }
                } else {
                    updateState(State.COMPLETED);
                }
            }));
        }

        @Override
        public Optional description() {
            if (element instanceof ExecutionElement.File) {
                var msg = FileUtil.getRelativePath(getProjectDirectory(),
                        ((ExecutionElement.File) element).file())
                        + " [" + level + "]";
                return Optional.of(msg);
            } else {
                return Optional.empty();
            }
        }

        @Override
        public List log() {
            return handler.warnings();
        }

        private boolean continueOnError(List args) {
            String pathOrCmd;
            if (element instanceof ExecutionElement.File) {
                var file = ((ExecutionElement.File) element).file();
                var path = FileUtil.getRelativePath(getProjectDirectory(), file);
                if (path == null) {
                    path = file.getPath();
                }
                pathOrCmd = path;
            } else if (element instanceof ExecutionElement.Line) {
                var cmd = ((ExecutionElement.Line) element).line();
                if (handler instanceof LineHandler) {
                    cmd = ((LineHandler) handler).rewrite(cmd)
                            .lines().limit(5).collect(Collectors.joining("\n"));
                }
                pathOrCmd = cmd;
            } else {
                pathOrCmd = "???"; // should never get here!
            }
            String extra = null;
            if (!args.isEmpty()) {
                extra = args.get(0).toString().lines().limit(5).collect(Collectors.joining("\n"));
            }
            String message = Bundle.ERR_elementExecution(pathOrCmd);
            if (extra != null) {
                message += "\n\n";
                message += extra;
            }
            String title = level == ExecutionLevel.RUN
                    ? Bundle.ERR_elementContinueRun()
                    : Bundle.ERR_elementContinueBuild();
            return ProjectDialogManager.get(DefaultPraxisProject.this)
                    .confirmOnError(title, message);
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy