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

com.redhat.ceylon.tools.version.CeylonVersionTool Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
package com.redhat.ceylon.tools.version;

import java.io.Console;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.TokenRewriteStream;

import com.redhat.ceylon.common.config.DefaultToolOptions;
import com.redhat.ceylon.common.tool.Argument;
import com.redhat.ceylon.common.tool.CeylonBaseTool;
import com.redhat.ceylon.common.tool.Description;
import com.redhat.ceylon.common.tool.Option;
import com.redhat.ceylon.common.tool.OptionArgument;
import com.redhat.ceylon.common.tool.ParsedBy;
import com.redhat.ceylon.common.tool.RemainingSections;
import com.redhat.ceylon.common.tool.StandardArgumentParsers;
import com.redhat.ceylon.common.tool.Summary;
import com.redhat.ceylon.common.tools.CeylonTool;
import com.redhat.ceylon.common.tools.ModuleSpec;
import com.redhat.ceylon.compiler.typechecker.TypeChecker;
import com.redhat.ceylon.compiler.typechecker.TypeCheckerBuilder;
import com.redhat.ceylon.compiler.typechecker.context.PhasedUnits;
import com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer;
import com.redhat.ceylon.compiler.typechecker.parser.CeylonParser;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.ImportModule;
import com.redhat.ceylon.model.typechecker.model.Module;

@Summary("Shows and updates version numbers in module descriptors")
@Description(
          "If `--set` is present then update the module versions, "
        + "otherwise show the module versions."
        + "\n\n"
        + "If `--dependencies` is present then show the "
        + "versions of module imports of the given module(s)."
        + "\n\n"
        + "`` specifies the module names (excluding versions) of "
        + "the modules to show or whose versions should be updated. "
        + "If unspecified then all modules are shown/updated.\n\n"
        + "**Note:** Other modules may also be updated unless "
        + "the `--no-update-dependencies` option is used, even if they're not listed in ``\n\n"
        )
@RemainingSections(value=""
        + "## Examples"
        + "\n\n"
        + "Listing the versions of all the modules in the ceylon SDK:"
        + "\n\n"
        + "    ceylon version"
        + "\n\n"
        + "Listing the version of ceylon.collection, and modules that depend on it"
        + "\n\n"
        + "    ceylon version --dependencies ceylon.collection"
        + "\n\n"
        + "Updating the version of ceylon.collection, and the modules that depend on it"
        + "\n\n"
        + "    ceylon version --set 1.0.1 ceylon.collection")
public class CeylonVersionTool extends CeylonBaseTool {

    // TODO Allow --src to be a :-separated path
    
    private Appendable out = System.out;
    
    private String newVersion;
    
    private List modules;

    private List sourceFolders = DefaultToolOptions.getCompilerSourceDirs();
    
    private String encoding = System.getProperty("file.encoding");
    
    private boolean dependencies = false;

    private boolean noUpdateDependencies = false;

    private Confirm confirm = Confirm.all;
    
    public void setOut(Appendable out) {
        this.out = out;
    }
    
    @OptionArgument(longName="src", argumentName="dir")
    @ParsedBy(StandardArgumentParsers.PathArgumentParser.class)
    @Description("A directory containing Ceylon and/or Java source code (default: `./source`)")
    public void setSourceFolders(List sourceFolders) {
        this.sourceFolders  = sourceFolders;
    }
    
    @OptionArgument(longName="source", argumentName="dirs")
    @ParsedBy(StandardArgumentParsers.PathArgumentParser.class)
    @Description("An alias for `--src`" +
            " (default: `./source`)")
    public void setSource(List source) {
        setSourceFolders(source);
    }
    
    @OptionArgument
    @Description("The new version number to set."
            + "If unspecified then module versions are shown and not updated.")
    public void setSet(String newVersion) {
        this.newVersion = newVersion;
    }
    
    @OptionArgument(argumentName="charset")
    @Description("Used with `--set`, sets the encoding used for reading and writing the `module.ceylon` files " +
            "(default: platform-specific).")
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }
    
    @Option
    @Description("Do not update of the version in module imports of the "
            + "target module(s) in other modules in the given `--src` directories. "
            + "For example:\n\n"
            + "    ceylon version --set 1.1 ceylon.collection\n\n"
            + "would update the version of ceylon.collection to 1.1 and update "
            + "the module import version of all dependent modules in the given `--src` directories "
            + "which depended on `ceylon.collection` __even if those "
            + "modules are not listed as ``__.\n\n"
            + "Whereas:\n\n"
            + "    ceylon version --set 1.1 --no-update-dependencies ceylon.collection\n\n"
            + "would just update the version of `ceylon.collection` to 1.1, "
            + "leaving dependent modules depending on the old version.")
    public void setNoUpdateDependencies(boolean noUpdateDependencies) {
        this.noUpdateDependencies = noUpdateDependencies;
    }

    @Option
    @Description("Display modules who depend on the given module. Only used when displaying modules, not when setting a new version.")
    public void setDependencies(boolean dependencies) {
        this.dependencies = dependencies;
    }

    enum Confirm {
        none,
        all,
        dependencies
    }
    
    @OptionArgument(argumentName="option")
    @Description("Used with `--set`, determines which updates require confirmation.\n\n"
            + "* `--confirm=all` requires confirmation "
            + "on the console for each update performed.\n"
            + "* `--confirm=dependencies` means that confirmation is only "
            + "required when updating versions appearing in module imports; "
            + "module versions are updated without confirmation.\n"
            + "* `--confirm=none` prevents any confirmation.\n\n"
            + "(default: `all`).")
    public void setConfirm(Confirm confirm) {
        this.confirm = confirm;
    }
    
    @Argument(argumentName="modules", multiplicity="*")
    public void setModules(List modules) {
        setModuleSpecs(ModuleSpec.parseEachList(modules, ModuleSpec.Option.VERSION_PROHIBITED));
    }
    
    public void setModuleSpecs(List modules) {
        this.modules = modules;
    }

    @Override
    public void initialize(CeylonTool mainTool) {
    }
    
    @Override
    public void run() throws IOException, RecognitionException {
        // TODO if version is empty? Prompt? Or should --set have an optional argument? 
        TypeCheckerBuilder tcb = new TypeCheckerBuilder();
        for (File path: this.sourceFolders) {
            tcb.addSrcDirectory(applyCwd(path));
        }
        TypeChecker tc = tcb.getTypeChecker();
        PhasedUnits pus = tc.getPhasedUnits();
        pus.visitModules();
        
        ArrayList moduleList = new ArrayList(pus.getModuleSourceMapper().getCompiledModules());
        Collections.sort(moduleList, new Comparator() {
            @Override
            public int compare(Module m1, Module m2) {
                if (match(m1) && !match(m2)) {
                    return -1;
                } else if (!match(m1) && match(m2)) {
                    return +1;
                }
                int cmp = m1.getNameAsString().compareToIgnoreCase(m2.getNameAsString());
                if (cmp == 0) {
                    cmp = m1.getVersion().compareTo(m2.getVersion());
                }
                return cmp;
            }
        });
        // first update all module versions and remember which version we assigned to which module
        // because the user can update every individual version
        Map updatedModuleVersions = new HashMap();
        for (Module module : moduleList) {
            boolean isMatch = match(module);
            if (newVersion == null) {
                output(module, isMatch);
            } else if (isMatch) {
                if (!updateModuleVersion(module, updatedModuleVersions)) {
                    return;
                }
            }
        }
        // now do dependencies because we know which modules we did update
        if (newVersion != null && !noUpdateDependencies) {
            for (Module module : moduleList) {
                if (!updateModuleImports(module, updatedModuleVersions)) {
                    return;
                }
            }
        }
    }

    private boolean updateModuleVersion(Module module, Map updatedModuleVersions) throws IOException,
            RecognitionException {
        String moduleDescriptorPath = module.getUnit().getFullPath();
        CeylonLexer lexer = new CeylonLexer(new ANTLRFileStream(moduleDescriptorPath, encoding));
        TokenRewriteStream tokenStream = new TokenRewriteStream(lexer);
        CeylonParser parser = new CeylonParser(tokenStream);
        Tree.CompilationUnit cu = parser.compilationUnit();
        String v = this.confirm == Confirm.dependencies ? this.newVersion : confirm("update.module.version", module.getNameAsString(), module.getVersion(), this.newVersion);
        if (v == null) {
            return false;
        } else if (!v.isEmpty()) {
            // record the new version for this module
            updatedModuleVersions.put(module.getNameAsString(), v);
            updateModuleVersion(moduleDescriptorPath, tokenStream, cu, v);
        }
        return true;
    }

    private boolean updateModuleImports(Module module, Map updatedModuleVersions) throws IOException,
            RecognitionException {
        String moduleDescriptorPath = module.getUnit().getFullPath();
        CeylonLexer lexer = new CeylonLexer(new ANTLRFileStream(moduleDescriptorPath, encoding));
        TokenRewriteStream tokenStream = new TokenRewriteStream(lexer);
        CeylonParser parser = new CeylonParser(tokenStream);
        Tree.CompilationUnit cu = parser.compilationUnit();
        List moduleImports = findUpdatedImport(cu, updatedModuleVersions);
        for (Tree.ImportModule moduleImport : moduleImports) {
            String importedModuleName = getModuleName(moduleImport);
            String newVersion = updatedModuleVersions.get(importedModuleName);
            if(newVersion == null)
                newVersion = this.newVersion;
            String v = confirm("update.dependency.version", importedModuleName, module.getNameAsString(), module.getVersion(), newVersion);
            if (v == null) {
                return false;
            } else if (!v.isEmpty()) {
                updateImportVersion(moduleDescriptorPath, tokenStream, moduleImport, v);
            }
        }
        return true;
    }

    private void output(Module module, boolean isMatch) throws IOException,
            RecognitionException {
        if (isMatch) {
            outputVersion(module);
        } else if (dependencies) {
            String moduleDescriptorPath = module.getUnit().getFullPath();
            CeylonLexer lexer = new CeylonLexer(new ANTLRFileStream(moduleDescriptorPath, encoding));
            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
            CeylonParser parser = new CeylonParser(tokenStream);
            Tree.CompilationUnit cu = parser.compilationUnit();
            List moduleImports = findImport(cu);
            for (Tree.ImportModule moduleImport : moduleImports) {
                outputDependency(module, moduleImport);
            }
        }
    }
    private void outputVersion(Module module) throws IOException {
        out.append(CeylonVersionMessages.msg("output.module", 
                module.getNameAsString(), module.getVersion()))
            .append(System.lineSeparator());
    }
    private void outputDependency(Module module, ImportModule moduleImport) throws IOException {
        String version = moduleImport.getVersion().getText();
        version = version.substring(1, version.length()-1);
        out.append(CeylonVersionMessages.msg("output.dependency", 
                module.getNameAsString(), module.getVersion(),
                getModuleName(moduleImport), version))
            .append(System.lineSeparator());
        
    }
    
    private boolean match(Module module) {
        return match(module.getNameAsString());
    }
    
    private boolean match(String moduleName) {
        if(this.modules == null
                || this.modules.isEmpty())
            return true;
        for(ModuleSpec spec : this.modules){
            if(spec.getName().equals(moduleName))
                return true;
        }
        return false;
    }
    private void updateImportVersion(String moduleDescriptorPath, TokenRewriteStream tokenStream,
            Tree.ImportModule moduleImport, String version) 
                throws IOException {
        tokenStream.replace(moduleImport.getVersion().getToken(), "\"" + version + "\"");
        write(moduleDescriptorPath, tokenStream);
    }
    private void updateModuleVersion(String moduleDescriptorPath, TokenRewriteStream tokenStream,
            Tree.CompilationUnit cu, String version) 
                throws IOException {
        tokenStream.replace(cu.getModuleDescriptors().get(0).getVersion().getToken(), "\"" + version + "\"");
        write(moduleDescriptorPath, tokenStream);
    }

    private void write(String moduleDescriptorPath, TokenRewriteStream tokenStream) throws IOException,
            UnsupportedEncodingException, FileNotFoundException {
        // Write to a temp file in the same directory, and then atomically rename
        // so if anything goes wrong we've not destroyed the original file
        File target = new File(moduleDescriptorPath);
        File temp = File.createTempFile("ceylon-module-", ".tmp", target.getParentFile());
        try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(temp), encoding)) {
            writer.append(tokenStream.toString());
        } catch (IOException e) {
            temp.delete();
        }
        Files.move(temp.toPath(), target.toPath(), 
                StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
    }
    
    private List findImport(Tree.CompilationUnit cu) {
        List dependsOnTarget = new LinkedList();
        for (Tree.ImportModule importModule : cu.getModuleDescriptors().get(0).getImportModuleList().getImportModules()) {
            String name = getModuleName(importModule);
            if (match(name)) {
                dependsOnTarget.add(importModule);
            }
        }
        return dependsOnTarget;
    }

    private List findUpdatedImport(Tree.CompilationUnit cu, Map updatedModules) {
        List dependsOnTarget = new LinkedList();
        for (Tree.ImportModule importModule : cu.getModuleDescriptors().get(0).getImportModuleList().getImportModules()) {
            String name = getModuleName(importModule);
            if (updatedModules.containsKey(name)) {
                dependsOnTarget.add(importModule);
            }
        }
        return dependsOnTarget;
    }

    private String getModuleName(Tree.ImportModule importModule) {
        String name;
        if (importModule.getQuotedLiteral() != null) {
            name = importModule.getQuotedLiteral().getText();
            name = name.substring(1, name.length()-1);
        } else {
            StringBuilder sb = new StringBuilder();
            for (Tree.Identifier namePart : importModule.getImportPath().getIdentifiers()) {
                sb.append(namePart.getText()).append('.');
            }
            if (sb.length() > 0) {
                sb.setLength(sb.length()-1);
            }
            name = sb.toString();
        }
        return name;
    }

    /**
     * Issues a confirmation
     * @param prompt
     * @return null means quit, empty string means no, anything else is the version string to use.
     * @throws IOException 
     */
    private String confirm(String msgKey, Object... args) throws IOException {
        if (confirm == Confirm.none) {
            return this.newVersion;
        }
        String version = this.newVersion;
        Console console = System.console();
        while (true) {
            prompt: while (true) {
                // XXX Ugly: Need to replace the new version within the args array 
                args[args.length-1] = version;
                console.printf("%s", CeylonVersionMessages.msg(msgKey, args));
                String ch = console.readLine();
                if (ch.equals(CeylonVersionMessages.msg("mnemonic.yes"))) {
                    return version;
                } else if (ch.equals(CeylonVersionMessages.msg("mnemonic.help"))) {
                    out.append(CeylonVersionMessages.msg("help")).append(System.lineSeparator());
                    continue prompt;
                } else if (ch.equals(CeylonVersionMessages.msg("mnemonic.quit"))) {
                    return null;
                } else if (ch.equals(CeylonVersionMessages.msg("mnemonic.all"))) {
                    this.confirm = Confirm.none;
                    return version;
                } else if (ch.equals(CeylonVersionMessages.msg("mnemonic.no"))) {
                    return "";
                } else if (ch.equals(CeylonVersionMessages.msg("mnemonic.edit"))) {
                    break prompt;
                } else {
                    continue prompt;
                }
            }
            console.printf(CeylonVersionMessages.msg("prompt.version"));
            version = console.readLine();
        }
    }

    

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy