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

com.redhat.ceylon.ceylondoc.CeylonDocTool Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
/*
 * Copyright Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the authors tag. All rights reserved.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License version 2.
 * 
 * This particular file is subject to the "Classpath" exception as provided in the 
 * LICENSE file that accompanied this code.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * 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 distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */

package com.redhat.ceylon.ceylondoc;

import static com.redhat.ceylon.ceylondoc.Util.join;

import java.awt.Desktop;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.redhat.ceylon.ceylondoc.Util.ReferenceableComparatorByName;
import com.redhat.ceylon.cmr.api.ArtifactContext;
import com.redhat.ceylon.cmr.api.RepositoryManager;
import com.redhat.ceylon.cmr.ceylon.OutputRepoUsingTool;
import com.redhat.ceylon.common.Constants;
import com.redhat.ceylon.common.FileUtil;
import com.redhat.ceylon.common.config.CeylonConfig;
import com.redhat.ceylon.common.config.DefaultToolOptions;
import com.redhat.ceylon.common.log.Logger;
import com.redhat.ceylon.common.tool.Argument;
import com.redhat.ceylon.common.tool.Description;
import com.redhat.ceylon.common.tool.Hidden;
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.common.tools.ModuleWildcardsHelper;
import com.redhat.ceylon.compiler.java.loader.SourceDeclarationVisitor;
import com.redhat.ceylon.compiler.typechecker.TypeChecker;
import com.redhat.ceylon.compiler.typechecker.TypeCheckerBuilder;
import com.redhat.ceylon.compiler.typechecker.analyzer.ModuleSourceMapper;
import com.redhat.ceylon.compiler.typechecker.context.Context;
import com.redhat.ceylon.compiler.typechecker.context.PhasedUnit;
import com.redhat.ceylon.compiler.typechecker.tree.Node;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.CompilationUnit;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.Expression;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.ModuleDescriptor;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.PackageDescriptor;
import com.redhat.ceylon.compiler.typechecker.tree.Visitor;
import com.redhat.ceylon.compiler.typechecker.tree.Walker;
import com.redhat.ceylon.compiler.typechecker.util.ModuleManagerFactory;
import com.redhat.ceylon.model.loader.AbstractModelLoader;
import com.redhat.ceylon.model.typechecker.model.Annotation;
import com.redhat.ceylon.model.typechecker.model.Class;
import com.redhat.ceylon.model.typechecker.model.ClassOrInterface;
import com.redhat.ceylon.model.typechecker.model.Declaration;
import com.redhat.ceylon.model.typechecker.model.Element;
import com.redhat.ceylon.model.typechecker.model.Function;
import com.redhat.ceylon.model.typechecker.model.FunctionOrValue;
import com.redhat.ceylon.model.typechecker.model.Interface;
import com.redhat.ceylon.model.typechecker.model.Module;
import com.redhat.ceylon.model.typechecker.model.NothingType;
import com.redhat.ceylon.model.typechecker.model.Package;
import com.redhat.ceylon.model.typechecker.model.Parameter;
import com.redhat.ceylon.model.typechecker.model.Referenceable;
import com.redhat.ceylon.model.typechecker.model.Scope;
import com.redhat.ceylon.model.typechecker.model.Type;
import com.redhat.ceylon.model.typechecker.model.TypeAlias;
import com.redhat.ceylon.model.typechecker.model.TypeDeclaration;
import com.redhat.ceylon.model.typechecker.model.TypeParameter;
import com.redhat.ceylon.model.typechecker.model.Unit;
import com.redhat.ceylon.model.typechecker.model.Value;
import com.redhat.ceylon.model.typechecker.util.ModuleManager;

@Summary("Generates Ceylon API documentation from Ceylon source files")
@Description("The default module repositories are `modules` and `" +
        Constants.REPO_URL_CEYLON+"`, and the default source directory is `source`. " +
        "The default output module repository is `modules`." +
        "\n\n"+
        "The `` are the names (with an optional version) of the modules " +
        "to compile the documentation of." +
        "\n\n"+
        "The documentation compiler searches for compilation units belonging " +
        "to the specified modules in the specified source directories and in " +
        "source archives in the specified module repositories. For each " +
        "specified module, the compiler generates a set of XHTML pages in the " +
        "module documentation directory (the module-doc directory) of the " +
        "specified output module repository." +
        "\n\n" +
        "The compiler searches for source in the following locations:" +
        "\n\n" +
        "* source archives in the specified repositories, and\n" +
        "* module directories in the specified source directories." +
        "\n\n" +
        "If no version identifier is specified for a module, the module is " +
        "assumed to exist in a source directory.")
@RemainingSections(
        "## EXAMPLE\n" +
        "\n" +
        "The following would compile the `org.hibernate` module source code found in " +
        "the `~/projects/hibernate/src` directory to the " +
        "repository `~/projects/hibernate/build`:\n" +
        "\n" +
        "    ceylon doc org.hibernate/3.0.0.beta \\\n"+
        "        --src ~/projects/hibernate/src \\\n"+
        "        --out ~/projects/hibernate/build" +
        "\n\n" +
        OutputRepoUsingTool.DOCSECTION_REPOSITORIES)
public class CeylonDocTool extends OutputRepoUsingTool {

    private static final String OPTION_SECTION = "doctool.";
    private static final String OPTION_HEADER = OPTION_SECTION + "header";
    private static final String OPTION_FOOTER = OPTION_SECTION + "footer";
    private static final String OPTION_NON_SHARED = OPTION_SECTION + "non-shared";
    private static final String OPTION_SOURCE_CODE = OPTION_SECTION + "source-code";
    private static final String OPTION_IGNORE_MISSING_DOC = OPTION_SECTION + "ignore-missing-doc";
    private static final String OPTION_IGNORE_MISSING_THROWS = OPTION_SECTION + "ignore-missing-throws";
    private static final String OPTION_IGNORE_BROKEN_LINK = OPTION_SECTION + "ignore-broken-link";
    private static final String OPTION_LINK = OPTION_SECTION + "link";
    private static final String OPTION_RESOURCE_FOLDER = OPTION_SECTION + "resource-folder";

    private String encoding;
    private String header;
    private String footer;
    private boolean includeNonShared;
    private boolean includeSourceCode;
    private boolean ignoreMissingDoc;
    private boolean ignoreMissingThrows;
    private boolean ignoreBrokenLink;
    private boolean browse;
    private boolean haltOnError = true;
    private boolean bootstrapCeylon;
    private String resourceFolder;
    private List sourceFolders = DefaultToolOptions.getCompilerSourceDirs();
    private List docFolders = DefaultToolOptions.getCompilerDocDirs();
    private List moduleSpecs = Arrays.asList("*");
    private List links = new LinkedList();
    
    private TypeChecker typeChecker;
    private Module currentModule;
    private File tempDestDir;
    private final List phasedUnits = new LinkedList();
    private final List modules = new LinkedList();
    private final List compiledClasses = new LinkedList();
    private final Map> subclasses = new IdentityHashMap>();
    private final Map> satisfyingClassesOrInterfaces = new IdentityHashMap>();
    private final Map> annotationConstructors = new IdentityHashMap>();
    private final Map modelUnitMap = new IdentityHashMap();
    private final Map modelNodeMap = new IdentityHashMap();
    private final Map parameterUnitMap = new IdentityHashMap();
    private final Map parameterNodeMap = new IdentityHashMap();
    private final Map moduleUrlAvailabilityCache = new HashMap();
    private RepositoryManager outputRepositoryManager;

    public CeylonDocTool() {
        super(CeylondMessages.RESOURCE_BUNDLE);

        CeylonConfig config = CeylonConfig.get();
        
        header = config.getOption(OPTION_HEADER);
        footer = config.getOption(OPTION_FOOTER);
        includeNonShared = config.getBoolOption(OPTION_NON_SHARED, false);
        includeSourceCode = config.getBoolOption(OPTION_SOURCE_CODE, false);
        ignoreMissingDoc = config.getBoolOption(OPTION_IGNORE_MISSING_DOC, false);
        ignoreMissingThrows = config.getBoolOption(OPTION_IGNORE_MISSING_THROWS, false);
        ignoreBrokenLink = config.getBoolOption(OPTION_IGNORE_BROKEN_LINK, false);
        resourceFolder = config.getOption(OPTION_RESOURCE_FOLDER, ".resources");

        String[] linkValues = config.getOptionValues(OPTION_LINK);
        if (linkValues != null) {
            setLinks(Arrays.asList(linkValues));
        }
        
        log = new CeylondLogger();
    }

    @OptionArgument(argumentName="encoding")
    @Description("Sets the encoding used for reading source files (default: platform-specific)")
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public String getEncoding(){
        return encoding;
    }
    
    @OptionArgument(argumentName="header")
    @Description("Sets the header text to be placed at the top of each page.")
    public void setHeader(String header) {
        this.header = header;
    }
    
    public String getHeader() {
        return header;
    }
    
    @OptionArgument(argumentName="footer")
    @Description("Sets the footer text to be placed at the bottom of each page.")
    public void setFooter(String footer) {
        this.footer = footer;
    }
    
    public String getFooter() {
        return footer;
    }

    @Option(longName="non-shared")
    @Description("Includes documentation for package-private declarations.")
    public void setIncludeNonShared(boolean includeNonShared) {
        this.includeNonShared = includeNonShared;
    }

    public boolean isIncludeNonShared() {
        return includeNonShared;
    }

    @Option(longName="source-code")
    @Description("Includes source code in the generated documentation.")
    public void setIncludeSourceCode(boolean includeSourceCode) {
        this.includeSourceCode = includeSourceCode;
    }

    public boolean isIncludeSourceCode() {
        return includeSourceCode;
    }

    @Option(longName = "ignore-missing-doc")
    @Description("Do not print warnings about missing documentation.")
    public void setIgnoreMissingDoc(boolean ignoreMissingDoc) {
        this.ignoreMissingDoc = ignoreMissingDoc;
    }

    @Option(longName = "ignore-missing-throws")
    @Description("Do not print warnings about missing throws annotation.")
    public void setIgnoreMissingThrows(boolean ignoreMissingThrows) {
        this.ignoreMissingThrows = ignoreMissingThrows;
    }

    @Option(longName = "ignore-broken-link")
    @Description("Do not print warnings about broken links.")
    public void setIgnoreBrokenLink(boolean ignoreBrokenLink) {
        this.ignoreBrokenLink = ignoreBrokenLink;
    }

    @Hidden
    @Option(longName = "bootstrap-ceylon")
    @Description("Is used when documenting the Ceylon language module.")
    public void setBootstrapCeylon(boolean bootstrapCeylon) {
        this.bootstrapCeylon = bootstrapCeylon;
    }

    public void setHaltOnError(boolean haltOnError) {
        this.haltOnError = haltOnError;
    }

    @OptionArgument(longName="source", argumentName="dirs")
    @ParsedBy(StandardArgumentParsers.PathArgumentParser.class)
    @Description("An alias for `--src` (default: `./source`)")
    public void setSource(List source) {
        setSourceFolders(source);
    }

    @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="doc", argumentName="dirs")
    @ParsedBy(StandardArgumentParsers.PathArgumentParser.class)
    @Description("A directory containing your module documentation (default: `./doc`)")
    public void setDocFolders(List docFolders) {
        this.docFolders = docFolders;
    }

    @Argument(argumentName="modules", multiplicity="*")
    public void setModuleSpecs(List moduleSpecs) {
        this.moduleSpecs = moduleSpecs;
    }
    
    public List getLinks() {
        return links;
    }
    
    @OptionArgument(longName="link", argumentName="dir-or-url")
    @Description("The URL or path of a module repository containing " +
            "documentation for external dependencies." +
    		"\n\n" +
    		"The URL must use one of the supported protocols " +
    		"(http://, https:// or file://) or be a path to a directory. " +
            "The argument can start with a module name prefix, " +
            "separated from the URL by a `=` character, so that only " +
            "those external modules " +
            "whose name begins with the prefix will be linked using that URL.\n" +
            "Can be specified multiple times." +
            "\n\n" +
            "Examples:\n" +
            "\n" +
            "    --link "+Constants.REPO_URL_CEYLON+"\n" +
            "    --link ceylon.math="+Constants.REPO_URL_CEYLON+"\n"+
            "    --link com.example=http://example.com/ceylondoc/")
    public void setLinks(List linkArgs) {
        this.links = new ArrayList();
        if( linkArgs != null ) {
            for(String link : linkArgs) {
                links.add(validateLink(link));
            }
        }
    }

    private String validateLink(String link) {
        String[] linkParts = LinkRenderer.divideToPatternAndUrl(link);
        String moduleNamePattern = linkParts[0];
        String moduleRepoUrl = linkParts[1];

        if (!LinkRenderer.isHttpProtocol(moduleRepoUrl) && !LinkRenderer.isFileProtocol(moduleRepoUrl)) {
            File moduleRepoFile = new File(moduleRepoUrl);
            if (moduleRepoFile.exists() && moduleRepoFile.isDirectory()) {
                moduleRepoUrl = moduleRepoFile.toURI().toString();
            } else if (moduleNamePattern == null) {
                throw new IllegalArgumentException(CeylondMessages.msg("error.unexpectedLink", link));
            }
        }

        return moduleNamePattern != null ? (moduleNamePattern + "=" + moduleRepoUrl) : moduleRepoUrl;
    }

    @Option(longName="offline")
    @Description("Enables offline mode that will prevent the module loader from connecting to remote repositories.")
    public void setOffline(boolean offline) {
        this.offline = offline;
    }

    @Option(longName="browse")
    @Description("Open module documentation in browser.")
    public void setBrowse(boolean browse) {
        this.browse = browse;
    }
    
    @OptionArgument(longName="resource-folder", argumentName="dir")
    @Description("A directory name, where the documentation resources (css, js, ...) will be placed (default: .resources)")
    public void setResourceFolder(String resourceFolder) {
        this.resourceFolder = resourceFolder;
    }

    public List getCompiledClasses() {
        return compiledClasses;
    }

    public String getOut() {
        return out;
    }

    @Override
    protected List getSourceDirs() {
        return sourceFolders;
    }

    @Override
    public void initialize(CeylonTool mainTool) {
        TypeCheckerBuilder builder = new TypeCheckerBuilder();
        for(File src : sourceFolders){
            builder.addSrcDirectory(src);
        }
        
        // set up the artifact repository
        RepositoryManager repository = getRepositoryManager();
        
        builder.setRepositoryManager(repository);
        
        // make a destination repo
        outputRepositoryManager = getOutputRepositoryManager();

        // create the actual list of modules to process
        List srcs = FileUtil.applyCwd(cwd, sourceFolders);
        List expandedModules = ModuleWildcardsHelper.expandWildcards(srcs , moduleSpecs, null);
        final List modules = ModuleSpec.parseEachList(expandedModules);
        
        // we need to plug in the module manager which can load from .cars
        builder.moduleManagerFactory(new ModuleManagerFactory(){
            @Override
            public ModuleManager createModuleManager(Context context) {
                return new CeylonDocModuleManager(CeylonDocTool.this, context, modules, outputRepositoryManager, bootstrapCeylon, log);
            }

            @Override
            public ModuleSourceMapper createModuleManagerUtil(Context context, ModuleManager moduleManager) {
                return new CeylonDocModuleSourceMapper(context, (CeylonDocModuleManager) moduleManager, CeylonDocTool.this);
            }
        });
        
        // only parse what we asked for
        List moduleFilters = new LinkedList();
        for(ModuleSpec spec : modules){
            moduleFilters.add(spec.getName());
            if(spec.getName().equals(Module.LANGUAGE_MODULE_NAME) && !bootstrapCeylon){
                throw new CeylondException("error.languageModuleBootstrapOptionMissing");
            }
        }
        builder.setModuleFilters(moduleFilters);
        String fileEncoding  = getEncoding();
        if (fileEncoding == null) {
            fileEncoding = CeylonConfig.get(DefaultToolOptions.DEFAULTS_ENCODING);
        }
        if (fileEncoding != null) {
            builder.encoding(fileEncoding);
        }
        
        typeChecker = builder.getTypeChecker();
        // collect all units we are typechecking
        initTypeCheckedUnits(typeChecker);
        typeChecker.process();
        if (haltOnError && typeChecker.getErrors() > 0) {
            throw new CeylondException("error.failedParsing", new Object[] { typeChecker.getErrors() }, null);
        }
        
        initModules(modules);
        initPhasedUnits();
    }

    private void initTypeCheckedUnits(TypeChecker typeChecker) {
        for(PhasedUnit unit : typeChecker.getPhasedUnits().getPhasedUnits()){
            // obtain the unit container path
            final String pkgName = Util.getUnitPackageName(unit); 
            unit.getCompilationUnit().visit(new SourceDeclarationVisitor(){
                @Override
                public void loadFromSource(com.redhat.ceylon.compiler.typechecker.tree.Tree.Declaration decl) {
                    compiledClasses.add(Util.getQuotedFQN(pkgName, decl));
                }

                @Override
                public void loadFromSource(ModuleDescriptor that) {
                    // don't think we care about these
                }

                @Override
                public void loadFromSource(PackageDescriptor that) {
                    // don't think we care about these
                }
            });
        }
    }

    private void initModules(List moduleSpecs) {
        for (ModuleSpec moduleSpec : moduleSpecs) {
            Module foundModule = null;
            for (Module module : typeChecker.getContext().getModules().getListOfModules()) {
                if (module.getNameAsString().equals(moduleSpec.getName())) {
                    if (!moduleSpec.isVersioned() || moduleSpec.getVersion().equals(module.getVersion()))
                        foundModule = module;
                }
            }
            if (foundModule != null) {
                modules.add(foundModule);
            }
            else if (moduleSpec.isVersioned()) {
                throw new RuntimeException(CeylondMessages.msg("error.cantFindModule", moduleSpec.getName(),
                        moduleSpec.getVersion()));
            }
            else {
                throw new RuntimeException(CeylondMessages.msg("error.cantFindModuleNoVersion", moduleSpec.getName()));
            }
        }
    }
    
    private void initPhasedUnits() {
        for (PhasedUnit pu : typeChecker.getPhasedUnits().getPhasedUnits()) {
            if (modules.contains(pu.getUnit().getPackage().getModule())) {
                phasedUnits.add(pu);
            }
        }
    }
    
    public String getFileName(TypeDeclaration type) {
        // we need postfix, because objects can have same file name like classes/interfaces in not case-sensitive file systems
        String postfix;
        if (type instanceof Class && type.isAnonymous()) {
            postfix = ".object";
        } else {
            postfix = ".type";
        }

        List names = new LinkedList();
        Scope scope = type;
        while (scope instanceof TypeDeclaration) {
            names.add(0, ((TypeDeclaration) scope).getName());
            scope = scope.getContainer();
        }

        return join(".", names) + postfix + ".html";
    }

    private File getFolder(Package pkg) {
        Module module = pkg.getModule();
        List unprefixedName;
        if(module.isDefault())
            unprefixedName = pkg.getName();
        else{
            // remove the leading module name part
            unprefixedName = pkg.getName().subList(module.getName().size(), pkg.getName().size());
        }
        File dir = new File(getApiOutputFolder(module), join("/", unprefixedName));
        if(shouldInclude(module))
            FileUtil.mkdirs(dir);
        return dir;
    }

    private File getFolder(TypeDeclaration type) {
        return getFolder(getPackage(type));
    }

    private File getApiOutputFolder(Module module) {
        return getOutputFolder(module, "api");
    }

    private File getDocOutputFolder(Module module) {
        return getOutputFolder(module, "doc");
    }

    private File getOutputFolder(Module module, String subDir) {
        File folder = new File(com.redhat.ceylon.compiler.java.util.Util.getModulePath(tempDestDir, module), "module-doc");
        if (subDir != null) {
            folder = new File(folder, subDir);
        }
        if (shouldInclude(module)) {
            FileUtil.mkdirs(folder);
        }
        return folder;
    }

    private File getObjectFile(Object modPgkOrDecl) throws IOException {
        final File file;
        if (modPgkOrDecl instanceof TypeDeclaration) {
            TypeDeclaration type = (TypeDeclaration)modPgkOrDecl;
            String filename = getFileName(type);
            file = new File(getFolder(type), filename);
        } else if (modPgkOrDecl instanceof Module) {
            String filename = "index.html";
            file = new File(getApiOutputFolder((Module)modPgkOrDecl), filename);
        } else if (modPgkOrDecl instanceof Package) {
            String filename = "index.html";
            file = new File(getFolder((Package)modPgkOrDecl), filename);
        } else {
            throw new RuntimeException(CeylondMessages.msg("error.unexpected", modPgkOrDecl));
        }
        return file.getCanonicalFile();
    }

    @Override
    public void run() throws Exception {
        // make a temp dest folder
        tempDestDir = Files.createTempDirectory("ceylon-doc-").toFile();
        try {
            // create the documentation
            makeDoc();
        } finally {
            FileUtil.deleteQuietly(tempDestDir);
        }
    }
    
    private void makeDoc() throws IOException {
        buildNodesMaps();
        if (includeSourceCode) {
            copySourceFiles();
        }

        collectSubclasses();
        collectAnnotationConstructors();

        // document every module
        boolean documentedOne = false;
        for(Module module : modules){
            if (isEmpty(module)) {
                log.warning(CeylondMessages.msg("warn.moduleHasNoDeclaration", module.getNameAsString()));
            } else {
                documentedOne = true;
            }
                
            documentModule(module);
            
            ArtifactContext artifactDocs = new ArtifactContext(module.getNameAsString(), module.getVersion(), ArtifactContext.DOCS);
            
            // find all doc folders to copy
            File outputDocFolder = getDocOutputFolder(module);
            for (File docFolder : docFolders) {
                File moduleDocFolder = new File(docFolder, join("/", module.getName()));
                if (moduleDocFolder.exists()) {
                    FileUtil.copyAll(moduleDocFolder, outputDocFolder);
                }
            }

            repositoryRemoveArtifact(outputRepositoryManager, artifactDocs);
            
            repositoryPutArtifact(outputRepositoryManager, artifactDocs, getOutputFolder(module, null));
        }
        if (!documentedOne) {
            log.warning(CeylondMessages.msg("warn.couldNotFindAnyDeclaration"));
        }

        if (browse) {
            for(Module module : modules) {
                if (isEmpty(module)) {
                    continue;
                }
                ArtifactContext docArtifact = new ArtifactContext(module.getNameAsString(), module.getVersion(), ArtifactContext.DOCS);
                File docFolder = outputRepositoryManager.getArtifact(docArtifact);
                File docIndex = new File(docFolder, "api/index.html");
                if (docIndex.isFile()) {
                    try {
                        Desktop.getDesktop().browse(docIndex.toURI());
                    } catch (Exception e) {
                        log.error(CeylondMessages.msg("error.unableBrowseModuleDoc", docIndex.toURI()));
                    }
                }
            }
        }
    }

    private void repositoryRemoveArtifact(RepositoryManager outputRepository, ArtifactContext artifactContext) {
        try {
            outputRepository.removeArtifact(artifactContext);
        } catch (Exception e) {
            throw new CeylondException("error.failedRemoveArtifact", new Object[] { artifactContext, e.getLocalizedMessage() }, e);
        }
    }

    private void repositoryPutArtifact(RepositoryManager outputRepository, ArtifactContext artifactContext, File content) {
        try {
            outputRepository.putArtifact(artifactContext, content);
        } catch (Exception e) {
            throw new CeylondException("error.failedWriteArtifact", new Object[] { artifactContext, e.getLocalizedMessage() }, e);
        }
    }
    
    private boolean isEmpty(Module module) {
        for(Package pkg : getPackages(module))
            if(!pkg.getMembers().isEmpty())
                return false;
        return true;
    }

    private void documentModule(Module module) throws IOException {
        try {
            currentModule = module;
            clearModuleUrlAvailabilityCache();
            
            doc(module);
            makeApiIndex(module);
            makeIndex(module);
            makeSearch(module);
            
            File resourcesDir = getResourcesDir(module);
            copyResource("resources/ceylondoc.css", new File(resourcesDir, "ceylondoc.css"));
            copyResource("resources/ceylondoc.js", new File(resourcesDir, "ceylondoc.js"));
            
            copyResource("resources/bootstrap.min.css", new File(resourcesDir, "bootstrap.min.css"));
            copyResource("resources/bootstrap.min.js", new File(resourcesDir, "bootstrap.min.js"));
            copyResource("resources/jquery-1.8.2.min.js", new File(resourcesDir, "jquery-1.8.2.min.js"));
            
            copyResource("resources/ceylon.css", new File(resourcesDir, "ceylon.css"));
            copyResource("resources/rainbow.min.js", new File(resourcesDir, "rainbow.min.js"));
            copyResource("resources/rainbow.linenumbers.js", new File(resourcesDir, "rainbow.linenumbers.js"));
            copyResource("resources/ceylon.js", new File(resourcesDir, "ceylon.js"));
            
            copyResource("resources/favicon.ico", new File(resourcesDir, "favicon.ico"));
            copyResource("resources/ceylondoc-logo.png", new File(resourcesDir, "ceylondoc-logo.png"));
            copyResource("resources/ceylondoc-icons.png", new File(resourcesDir, "ceylondoc-icons.png"));
            copyResource("resources/NOTICE.txt", new File(getApiOutputFolder(module), "NOTICE.txt"));
        }
        finally {
            currentModule = null;
        }
    }
    
    private void clearModuleUrlAvailabilityCache() {
        String[] moduleUrls = moduleUrlAvailabilityCache.keySet().toArray(new String[] {});
        for (String moduleUrl : moduleUrls) {
            if (LinkRenderer.isFileProtocol(moduleUrl)) {
                moduleUrlAvailabilityCache.remove(moduleUrl);
            }
        }
    }

    private void collectSubclasses() throws IOException {
        for (Module module : modules) {
            for (Package pkg : getPackages(module)) {
                for (Declaration decl : pkg.getMembers()) {
                    if(!shouldInclude(decl)) {
                        continue;
                    }
                    if (decl instanceof ClassOrInterface) {
                        ClassOrInterface c = (ClassOrInterface) decl;                    
                        // subclasses map
                        if (c instanceof Class) {
                            Type superclass = c.getExtendedType();                    
                            if (superclass != null) {
                                TypeDeclaration superdec = superclass.getDeclaration();
                                if (subclasses.get(superdec) == null) {
                                    subclasses.put(superdec, new ArrayList());
                                }
                                subclasses.get(superdec).add((Class) c);
                            }
                        }

                        List satisfiedTypes = new ArrayList(c.getSatisfiedTypes());                     
                        if (satisfiedTypes != null && satisfiedTypes.isEmpty() == false) {
                            // satisfying classes or interfaces map
                            for (Type satisfiedType : satisfiedTypes) {
                                TypeDeclaration superdec = satisfiedType.getDeclaration();
                                if (satisfyingClassesOrInterfaces.get(superdec) ==  null) {
                                    satisfyingClassesOrInterfaces.put(superdec, new ArrayList());
                                }
                                satisfyingClassesOrInterfaces.get(superdec).add(c);
                            }
                        }
                    }
                }
            }
        }
    }
    
    private void collectAnnotationConstructors() {
        for (Module module : modules) {
            for (Package pkg : getPackages(module)) {
                for (Declaration decl : pkg.getMembers()) {
                    if (decl instanceof Function && decl.isAnnotation() && shouldInclude(decl)) {
                        Function annotationCtor = (Function) decl;
                        TypeDeclaration annotationType = annotationCtor.getTypeDeclaration();
                        List annotationConstructorList = annotationConstructors.get(annotationType);
                        if (annotationConstructorList == null) {
                            annotationConstructorList = new ArrayList();
                            annotationConstructors.put(annotationType, annotationConstructorList);
                        }
                        annotationConstructorList.add(annotationCtor);
                    }
                }
            }
        }
    }
    
    private Writer openWriter(File file) throws IOException {
        return new OutputStreamWriter(new FileOutputStream(file), "UTF-8"); 
    }
    
    private void makeSearch(Module module) throws IOException {
        Writer writer = openWriter(new File(getApiOutputFolder(module), "search.html"));
        try {
            new Search(module, this, writer).generate();
        } finally {
            writer.close();
        }
    }

    private void buildNodesMaps() {
        for (final PhasedUnit pu : phasedUnits) {
            CompilationUnit cu = pu.getCompilationUnit();
            Walker.walkCompilationUnit(new Visitor() {
                public void visit(Tree.Declaration that) {
                    modelUnitMap.put(that.getDeclarationModel(), pu);
                    modelNodeMap.put(that.getDeclarationModel(), that);
                    super.visit(that);
                }
                public void visit(Tree.ObjectDefinition that) {
                    if( that.getDeclarationModel() != null && that.getDeclarationModel().getTypeDeclaration() != null ) {
                        TypeDeclaration typeDecl = that.getDeclarationModel().getTypeDeclaration();
                        modelUnitMap.put(typeDecl, pu);
                        modelNodeMap.put(typeDecl, that);
                    }
                    super.visit(that);
                }
                public void visit(Tree.PackageDescriptor that) {
                    if (that.getImportPath() != null && that.getImportPath().getModel() != null) {
                        Referenceable model = that.getImportPath().getModel();
                        modelUnitMap.put(model, pu);
                        modelNodeMap.put(model, that);
                    }
                    super.visit(that);
                }
                public void visit(Tree.ModuleDescriptor that) {
                    if (that.getImportPath() != null && that.getImportPath().getModel() != null) {
                        Referenceable model = that.getImportPath().getModel();
                        modelUnitMap.put(model, pu);
                        modelNodeMap.put(model, that);
                    }
                    super.visit(that);
                }
                public void visit(Tree.SpecifierStatement that) {
                    modelUnitMap.put(that.getDeclaration(), pu);
                    modelNodeMap.put(that.getDeclaration(), that);
                    super.visit(that);
                }
                public void visit(Tree.Parameter param) {
                    parameterUnitMap.put(param.getParameterModel(), pu);
                    parameterNodeMap.put(param.getParameterModel(), param);
                    super.visit(param);
                }
            }, cu);
        }
    }

    private void copySourceFiles() throws FileNotFoundException, IOException {
        for (PhasedUnit pu : phasedUnits) {
            Package pkg = pu.getUnit().getPackage();
            if (!shouldInclude(pkg)) {
                continue;
            }

            File file = new File(getFolder(pu.getPackage()), pu.getUnitFile().getName()+".html");
            File dir = file.getParentFile();
            if (!dir.exists() && !FileUtil.mkdirs(dir)) {
                throw new IOException(CeylondMessages.msg("error.couldNotCreateDirectory", file));
            }
            Writer writer = openWriter(file);
            try {
                Markup markup = new Markup(writer);
                markup.write("");
                markup.open("html xmlns='http://www.w3.org/1999/xhtml'");
                markup.open("head");
                markup.tag("meta charset='UTF-8'");
                markup.around("title", pu.getUnit().getFilename());
                markup.tag("link href='" + getResourceUrl(pkg, "favicon.ico") + "' rel='shortcut icon'");
                markup.tag("link href='" + getResourceUrl(pkg, "ceylon.css") + "' rel='stylesheet' type='text/css'");
                markup.tag("link href='" + getResourceUrl(pkg, "ceylondoc.css") + "' rel='stylesheet' type='text/css'");
                markup.tag("link href='//fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'");
                
                markup.open("script type='text/javascript'");
                markup.write("var resourceBaseUrl = '" + getResourceUrl(pkg, "") + "'");
                markup.close("script");
                
                markup.around("script src='" + getResourceUrl(pkg, "jquery-1.8.2.min.js") + "' type='text/javascript'");
                markup.around("script src='" + getResourceUrl(pkg, "rainbow.min.js") + "' type='text/javascript'");
                markup.around("script src='" + getResourceUrl(pkg, "rainbow.linenumbers.js") + "' type='text/javascript'");
                markup.around("script src='" + getResourceUrl(pkg, "ceylon.js") + "' type='text/javascript'");
                markup.around("script src='" + getResourceUrl(pkg, "ceylondoc.js") + "' type='text/javascript'"); 
                markup.close("head");
                markup.open("body", "pre data-language='ceylon' style='font-family: Inconsolata, Monaco, Courier, monospace'");
                // XXX source char encoding
                BufferedReader input = new BufferedReader(new InputStreamReader(pu.getUnitFile().getInputStream()));
                try{
                    String line = input.readLine();
                    while (line != null) {
                        markup.text(line, "\n");
                        line = input.readLine();
                    }
                } finally {
                    input.close();
                }
                markup.close("pre", "body", "html");
            } finally {
                writer.close();
            }
        }
    }

    private void doc(Module module) throws IOException {
        Writer rootWriter = openWriter(getObjectFile(module));
        try {
            ModuleDoc moduleDoc = new ModuleDoc(this, rootWriter, module);
            moduleDoc.generate();
            for (Package pkg : getPackages(module)) {
                if(pkg.getMembers().isEmpty()){
                    continue;
                }
                // document the package
                if (!isRootPackage(module, pkg)) {
                    Writer packageWriter = openWriter(getObjectFile(pkg));
                    try {
                        new PackageDoc(this, packageWriter, pkg).generate();
                    } finally {
                        packageWriter.close();
                    }
                }
                // document its members
                for (Declaration decl : pkg.getMembers()) {
                    doc(decl);
                }
                
                if (pkg.getNameAsString().equals(AbstractModelLoader.CEYLON_LANGUAGE)) {
                    docNothingType(pkg);
                }
            }
        } finally {
            rootWriter.close();
        }
        
    }

    private void docNothingType(Package pkg) throws IOException {
        final Annotation nothingDoc = new Annotation();
        nothingDoc.setName("doc");
        nothingDoc.addPositionalArgment(
                "The special type _Nothing_ represents: \n" +
                " - the intersection of all types, or, equivalently \n" +
                " - the empty set \n" +
                "\n" +
                "_Nothing_ is assignable to all other types, but has no instances. \n" +
                "A reference to a member of an expression of type _Nothing_ is always an error, since there can never be a receiving instance. \n" +
                "_Nothing_ is considered to belong to the module _ceylon.language_. However, it cannot be defined within the language. \n" +
                "\n" +
                "Because of the restrictions imposed by Ceylon's mixin inheritance model: \n" +
                "- If X and Y are classes, and X is not a subclass of Y, and Y is not a subclass of X, then the intersection type X&Y is equivalent to _Nothing_. \n" +
                "- If X is an interface, the intersection type X&Nothing is equivalent to _Nothing_. \n" +
                "- If X<T> is invariant in its type parameter T, and the distinct types A and B do not involve type parameters, then X<A>&X<B> is equivalent to _Nothing_. \n");
        
        NothingType nothingType = new NothingType(pkg.getUnit()) {
            @Override
            public List getAnnotations() {
                return Collections.singletonList(nothingDoc);
            }
        };
        
        doc(nothingType);
    }

    private void makeIndex(Module module) throws IOException {
        File dir = getResourcesDir(module);
        Writer writer = openWriter(new File(dir, "index.js"));
        try {
            new IndexDoc(this, writer, module).generate();
        } finally {
            writer.close();
        }
    }
    
    private void makeApiIndex(Module module) throws IOException {
        Writer writer = openWriter(new File(getApiOutputFolder(module), "api-index.html"));
        try {
            new IndexApiDoc(this, writer, module).generate();
        } finally {
            writer.close();
        }
    }

    private File getResourcesDir(Module module) throws IOException {
        File dir = new File(getApiOutputFolder(module), resourceFolder);
        if (!dir.exists() && !FileUtil.mkdirs(dir)) {
            throw new IOException();
        }
        return dir;
    }
    
    /**
     * Determines whether the given package is the 'root package' (i.e. has the 
     * same fully qualified name as) of the given module.
     * @param module
     * @param pkg
     * @return
     */
    protected boolean isRootPackage(Module module, Package pkg) {
        if(module.isDefault())
            return pkg.getNameAsString().isEmpty();
        return pkg.getNameAsString().equals(module.getNameAsString());
    }

    private void copyResource(String path, File file) throws IOException {
        File dir = file.getParentFile();
        if (!dir.exists()
                && !FileUtil.mkdirs(dir)) {
            throw new IOException();
        }
        try (InputStream resource = getClass().getResourceAsStream(path)) {
            copy(resource, file);
        }
    }

    private void copy(InputStream resource, File file)
            throws FileNotFoundException, IOException {
        try (OutputStream os = new FileOutputStream(file)) {
            byte[] buf = new byte[1024];
            int read;
            while ((read = resource.read(buf)) > -1) {
                os.write(buf, 0, read);
            }
            os.flush();
        }
    }

    public void doc(Declaration decl) throws IOException {
        if (decl instanceof TypeDeclaration) {
            if (shouldInclude(decl)) {
                Writer writer = openWriter(getObjectFile(decl));
                try {
                    new ClassDoc(this, writer, (TypeDeclaration) decl).generate();
                } finally {
                    writer.close();
                }
            }
        }
    }

    protected Package getPackage(Declaration decl) {
        Scope scope = decl.getContainer();
        while (!(scope instanceof Package)) {
            scope = scope.getContainer();
        }
        return (Package)scope;
    }

    protected Module getModule(Object modPkgOrDecl) {
        if (modPkgOrDecl instanceof Module) {
            return (Module)modPkgOrDecl;
        } else if (modPkgOrDecl instanceof Package) {
            return ((Package)modPkgOrDecl).getModule();
        } else if (modPkgOrDecl instanceof Declaration) {
            return getPackage((Declaration)modPkgOrDecl).getModule();
        }
        throw new RuntimeException();
    }
    
    List getPackages(Module module) {
        List packages = new ArrayList();
        for (Package pkg : module.getPackages()) {
            if (shouldInclude(pkg)) {
                packages.add(pkg);
            }
        }
        Collections.sort(packages, ReferenceableComparatorByName.INSTANCE);
        return packages;
    }

    protected boolean shouldInclude(Declaration decl) {
        if (!includeNonShared && !decl.isShared()) {
            return false;
        }
        if (decl.isNativeImplementation()) {
            return false;
        }
        return true;
    }
    
    protected boolean shouldInclude(Package pkg) {
        return (includeNonShared || pkg.isShared()) && pkg.getMembers().size() > 0;
    }
    
    protected boolean shouldInclude(Module module){
        return modules.contains(module);
    }

    /**
     * Returns the absolute URI of the page for the given thing
     * @param obj (Module, Package, Declaration etc)
     * @throws IOException 
     */
    private URI getAbsoluteObjectUrl(Object obj) throws IOException {
        File f = getObjectFile(obj);
        if (f == null) {
            throw new RuntimeException(CeylondMessages.msg("error.noPage", obj));
        }
        return f.toURI();
    }
    
    /**
     * Gets the base URL
     * @return Gets the base URL
     */
    private URI getBaseUrl(Module module) throws IOException {
        return getApiOutputFolder(module).getCanonicalFile().toURI();
    }
    
    /**
     * Generates a relative URL such that:
     * 
     *   uri1.resolve(relativize(url1, url2)).equals(uri2);
     * 
* @param uri * @param uri2 * @return A URL suitable for a link from a page at uri to a page at uri2 * @throws IOException */ private URI relativize(Module module, URI uri, URI uri2) throws IOException { if (!uri.isAbsolute()) { throw new IllegalArgumentException(CeylondMessages.msg("error.expectedUriToBeAbsolute", uri)); } if (!uri2.isAbsolute()) { throw new IllegalArgumentException(CeylondMessages.msg("error.expectedUriToBeAbsolute", uri2)); } URI baseUrl = getBaseUrl(module); StringBuilder sb = new StringBuilder(); URI r = uri; if (!r.equals(baseUrl)) { r = uri.resolve(URI.create(sb.toString())); if (!r.equals(baseUrl)) { r = uri; } } while (!r.equals(baseUrl)) { sb.append("../"); r = uri.resolve(URI.create(sb.toString())); } URI result = URI.create(sb.toString() + baseUrl.relativize(uri2)); if (result.isAbsolute()) { // FIXME: this throws in some cases even for absolute URIs, not sure why //throw new RuntimeException("Result not absolute: "+result); } if (!uri.resolve(result).equals(uri2)) { throw new RuntimeException(CeylondMessages.msg("error.failedUriRelativize", uri, uri2, result)); } return result; } protected String getObjectUrl(Object from, Object to) throws IOException { return getObjectUrl(from, to, true); } protected String getObjectUrl(Object from, Object to, boolean withFragment) throws IOException { Module module = getModule(from); URI fromUrl = getAbsoluteObjectUrl(from); URI toUrl = getAbsoluteObjectUrl(to); String result = relativize(module, fromUrl, toUrl).toString(); if (withFragment && to instanceof Package && isRootPackage(module, (Package)to)) { result += "#section-package"; } return result; } protected String getResourceUrl(Object from, String to) throws IOException { Module module = getModule(from); URI fromUrl = getAbsoluteObjectUrl(from); URI toUrl = getBaseUrl(module).resolve(resourceFolder + "/" + to); String result = relativize(module, fromUrl, toUrl).toString(); return result; } /** * Gets a URL for the source file containing the given thing * @param from Where the link is relative to * @param modPkgOrDecl e.g. Module, Package or Declaration * @return A (relative) URL, or null if no source file exists (e.g. for a * package or a module without a descriptor) * @throws IOException */ protected String getSrcUrl(Object from, Object modPkgOrDecl) throws IOException { URI fromUrl = getAbsoluteObjectUrl(from); Module module = getModule(from); String filename; File folder; if (modPkgOrDecl instanceof Element) { Unit unit = ((Element)modPkgOrDecl).getUnit(); filename = unit.getFilename(); folder = getFolder(unit.getPackage()); } else if (modPkgOrDecl instanceof Package) { filename = "package.ceylon"; folder = getFolder((Package)modPkgOrDecl); } else if (modPkgOrDecl instanceof Module) { Module moduleDecl = (Module)modPkgOrDecl; folder = getApiOutputFolder(moduleDecl); filename = Constants.MODULE_DESCRIPTOR; } else { throw new RuntimeException(CeylondMessages.msg("error.unexpected", modPkgOrDecl)); } File srcFile = new File(folder, filename + ".html").getCanonicalFile(); String result; if (srcFile.exists()) { URI url = srcFile.toURI(); result = relativize(module, fromUrl, url).toString(); } else { result = null; } return result; } protected PhasedUnit getUnit(Referenceable referenceable) { return modelUnitMap.get(referenceable); } protected Node getNode(Referenceable referenceable) { return modelNodeMap.get(referenceable); } protected PhasedUnit getParameterUnit(Parameter parameter) { return parameterUnitMap.get(parameter); } protected Node getParameterNode(Parameter parameter) { return parameterNodeMap.get(parameter); } protected List getAnnotationConstructors(TypeDeclaration klass) { return annotationConstructors.get(klass); } protected List getSatisfyingClassesOrInterfaces(TypeDeclaration klass) { return satisfyingClassesOrInterfaces.get(klass); } protected List getSubclasses(TypeDeclaration klass) { return subclasses.get(klass); } protected Map getModuleUrlAvailabilityCache() { return moduleUrlAvailabilityCache; } /** * Returns the starting and ending line number of the given declaration * @param decl The declaration * @return [start, end] */ protected int[] getDeclarationSrcLocation(Declaration decl) { Node node = modelNodeMap.get(decl); if (node == null) { return null; } else { return new int[] { node.getToken().getLine(), node.getEndToken().getLine() }; } } protected Module getCurrentModule() { return currentModule; } public List getDocumentedModules(){ return modules; } protected TypeChecker getTypeChecker() { return typeChecker; } protected Logger getLogger() { return log; } protected void warningMissingDoc(String name, Referenceable scope) { if (!ignoreMissingDoc) { log.warning(CeylondMessages.msg("warn.missingDoc", name, getPosition(getNode(scope)))); } } protected void warningBrokenLink(String docLinkText, Tree.DocLink docLink, Referenceable scope) { if (!ignoreBrokenLink) { log.warning(CeylondMessages.msg("warn.brokenLink", docLinkText, getWhere(scope), getPosition(docLink))); } } protected void warningSetterDoc(String name, Declaration scope) { log.warning(CeylondMessages.msg("warn.setterDoc", name, getPosition(getNode(scope)))); } protected void warningMissingThrows(Declaration d) { if (ignoreMissingThrows) { return; } final Scope scope = d.getScope(); final PhasedUnit unit = getUnit(d); final Node node = getNode(d); if (scope == null || unit == null || unit.getUnit() == null || node == null || !(d instanceof FunctionOrValue)) { return; } List documentedExceptions = new ArrayList(); for (Annotation annotation : d.getAnnotations()) { if (annotation.getName().equals("throws")) { String exceptionName = annotation.getPositionalArguments().get(0); Declaration exceptionDecl = scope.getMemberOrParameter(unit.getUnit(), exceptionName, null, false); if (exceptionDecl instanceof TypeDeclaration) { documentedExceptions.add(((TypeDeclaration) exceptionDecl).getType()); } } } final List thrownExceptions = new ArrayList(); node.visitChildren(new Visitor() { @Override public void visit(Tree.Throw that) { Expression expression = that.getExpression(); if (expression != null) { thrownExceptions.add(expression.getTypeModel()); } else { thrownExceptions.add(unit.getUnit().getExceptionType()); } } @Override public void visit(Tree.Declaration that) { // the end of searching } }); for (Type thrownException : thrownExceptions) { boolean isDocumented = false; for (Type documentedException : documentedExceptions) { if (thrownException.isSubtypeOf(documentedException)) { isDocumented = true; break; } } if (!isDocumented) { log.warning(CeylondMessages.msg("warn.missingThrows", thrownException.asString(), getWhere(d), getPosition(getNode(d)))); } } } private String getWhere(Referenceable scope) { String where = ""; if (scope instanceof Module) { where += "module "; } else if (scope instanceof Package) { where += "package "; } else if (scope instanceof Class) { where += "class "; } else if (scope instanceof Interface) { where += "interface "; } else if (scope instanceof TypeAlias) { where += "type alias "; } else if (scope instanceof TypeParameter) { where += "type parameter "; } else if (scope instanceof Function) { if (((Function) scope).isToplevel()) { where += "function "; } else { where += "method "; } } else if (scope instanceof Value) { if (((Value) scope).isToplevel()) { where += "value "; } else if (((Value) scope).isParameter()) { where += "parameter "; } else { where += "attribute "; } } if( scope instanceof Declaration ) { where += ((Declaration) scope).getQualifiedNameString(); } else { where += scope.getNameAsString(); } return where; } private String getPosition(Node node) { if (node != null && node.getToken() != null && node.getUnit() != null && node.getUnit().getFilename() != null) { return "(" + node.getUnit().getFilename() + ":" + node.getToken().getLine() + ")"; } return ""; } public ModuleSourceMapper getModuleSourceMapper() { return typeChecker.getPhasedUnits().getModuleSourceMapper(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy