| /******************************************************************************* |
| * Copyright (c) 2019 Christian Pontesegger and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v2.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Christian Pontesegger - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.ease.helpgenerator; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.StringReader; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.jar.Attributes; |
| import java.util.jar.Manifest; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.ease.helpgenerator.model.AbstractClassModel; |
| import org.eclipse.ease.helpgenerator.model.Category; |
| import org.eclipse.ease.helpgenerator.model.ModuleDefinition; |
| |
| public abstract class AbstractModuleDoclet { |
| |
| protected static ModuleDefinition findModule(String className, Set<ModuleDefinition> modules) { |
| for (final ModuleDefinition module : modules) { |
| if (className.equals(module.getClassName())) |
| return module; |
| } |
| |
| return null; |
| } |
| |
| public static String createHTMLFileName(final String moduleID) { |
| return "module_" + escape(moduleID) + ".html"; |
| } |
| |
| private static String escape(final String data) { |
| return data.replace(' ', '_').toLowerCase(); |
| } |
| |
| private final Map<String, List<String>> fParameters = new HashMap<>(); |
| private IReporter fReporter; |
| |
| private final LinkProvider fLinkProvider = new LinkProvider(); |
| |
| public boolean run() { |
| setReporter(createReporter()); |
| |
| try { |
| for (final ModuleDefinition module : getModules()) { |
| final AbstractClassModel classModel = getClassModel(module); |
| if (classModel != null) |
| createHTMLFile(module, classModel); |
| else |
| getReporter().report(IReporter.WARNING, "No class definition found for module \"" + module.getName() + "\""); |
| } |
| |
| if (hasModuleDefinitions()) { |
| // create module TOC files |
| final Set<String> tocFiles = createModuleTOCFiles(); |
| |
| // update plugin.xml |
| updatePluginXML(tocFiles); |
| |
| // update MANIFEST.MF |
| updateManifest(); |
| |
| // update build.properties |
| updateBuildProperties(); |
| } |
| |
| } catch (final Exception e) { |
| getReporter().report(IReporter.ERROR, e.getMessage()); |
| e.printStackTrace(); |
| return false; |
| } |
| |
| return !getReporter().hasErrors(); |
| } |
| |
| private boolean hasModuleDefinitions() throws Exception { |
| return !getModules().isEmpty() || !getCategories().isEmpty(); |
| } |
| |
| public void setParameter(String option, List<String> arguments) { |
| if (ModuleDoclet.OPTION_LINK.equals(option)) |
| registerLinks(arguments.get(0)); |
| |
| else if (ModuleDoclet.OPTION_LINK_OFFLINE.equals(option)) |
| registerOfflineLinks(arguments.get(0), arguments.get(1) + "/package-list"); |
| |
| else |
| fParameters.put(option, arguments); |
| } |
| |
| private void setReporter(IReporter reporter) { |
| fReporter = reporter; |
| } |
| |
| public IReporter getReporter() { |
| return fReporter; |
| } |
| |
| public Set<ModuleDefinition> getModules() throws Exception { |
| final Set<ModuleDefinition> registeredModules = new HashSet<>(); |
| |
| final IMemento document = getPluginDefinition(); |
| |
| for (final IMemento extensionNode : document.getChildren("extension")) { |
| if ("org.eclipse.ease.modules".equals(extensionNode.getString("point"))) { |
| for (final IMemento instanceNode : extensionNode.getChildren("module")) |
| registeredModules.add(new ModuleDefinition(instanceNode)); |
| } |
| } |
| |
| return registeredModules; |
| } |
| |
| public Set<Category> getCategories() { |
| final Set<Category> categories = new HashSet<>(); |
| |
| try { |
| final IMemento document = getPluginDefinition(); |
| |
| for (final IMemento extensionNode : document.getChildren("extension")) { |
| if ("org.eclipse.ease.modules".equals(extensionNode.getString("point"))) { |
| for (final IMemento instanceNode : extensionNode.getChildren("category")) |
| categories.add(new Category(instanceNode)); |
| } |
| } |
| } catch (final Exception e) { |
| } |
| |
| return categories; |
| } |
| |
| public IMemento getPluginDefinition() throws Exception { |
| final InputStreamReader reader = new InputStreamReader(new FileInputStream(getPluginOrFragmentFile())); |
| final IMemento root = XMLMemento.createReadRoot(reader); |
| reader.close(); |
| |
| return root; |
| } |
| |
| public File getProjectFile(String path, boolean shallExist) throws IOException { |
| final File pluginDefinition = getRootFolder().toPath().resolve(path).toFile(); |
| if ((pluginDefinition.exists()) || !shallExist) |
| return pluginDefinition; |
| |
| throw new IOException(path + " not found"); |
| } |
| |
| public File getPluginOrFragmentFile() throws IOException { |
| try { |
| return getProjectFile("plugin.xml", true); |
| } catch (final Exception e) { |
| } |
| |
| return getProjectFile("fragment.xml", true); |
| } |
| |
| public File getBuildPropertiesFile() throws IOException { |
| return getProjectFile("build.properties", true); |
| } |
| |
| public File getManifestFile() throws IOException { |
| return getProjectFile("META-INF/MANIFEST.MF", true); |
| } |
| |
| public File getRootFolder() throws IOException { |
| final List<String> parameter = fParameters.get(ModuleDoclet.OPTION_PROJECT_ROOT); |
| if ((parameter == null) || (parameter.size() != 1)) |
| throw new IOException("Root folder not found, use " + ModuleDoclet.OPTION_PROJECT_ROOT + " parameter"); |
| |
| final File rootFolder = new File(parameter.get(0)); |
| |
| if (!rootFolder.exists()) |
| throw new FileNotFoundException("Root folder \"" + parameter.get(0) + "\"does not exist"); |
| |
| if (!rootFolder.isDirectory()) |
| throw new IOException("Root folder is not a directory"); |
| |
| return rootFolder; |
| } |
| |
| public boolean failOnMissingDocs() { |
| final List<String> parameter = fParameters.get(ModuleDoclet.OPTION_FAIL_ON_MISSING_DOCS); |
| if ((parameter == null) || (parameter.size() != 1)) |
| return true; |
| |
| return Boolean.parseBoolean(parameter.get(0)); |
| } |
| |
| public boolean failOnHtmlErrors() { |
| final List<String> parameter = fParameters.get(ModuleDoclet.OPTION_FAIL_ON_HTML_ERRORS); |
| if ((parameter == null) || (parameter.size() != 1)) |
| return true; |
| |
| return Boolean.parseBoolean(parameter.get(0)); |
| } |
| |
| /** |
| * Verifies that the HTML content is well formed and correct. This guarantees that the code can be displayed in help hovers and code completion proposals. |
| * |
| * @throws Exception |
| * when content is not well formed |
| */ |
| public void verifyXmlContent(String content, String className) { |
| try { |
| // try to read content into an XMLMemento |
| XMLMemento.createReadRoot(new StringReader(content)); |
| } catch (final Exception e) { |
| getReporter().reportInvalidHtml("invalid file content for " + className + ":\t" + e.getMessage()); |
| |
| } |
| } |
| |
| public void createHTMLFile(ModuleDefinition module, final AbstractClassModel classModel) throws IOException { |
| fLinkProvider.setClassModel(classModel); |
| |
| final HtmlWriter htmlWriter = new HtmlWriter(module, classModel, fLinkProvider, getReporter()); |
| final String content = htmlWriter.createContents(); |
| |
| verifyXmlContent(content, classModel.getClassName()); |
| |
| if (!Files.exists(getProjectFile("help", false).toPath())) |
| Files.createDirectory(getProjectFile("help", false).toPath()); |
| |
| Files.write(getHtmlHelpFilePath(module.getId()), content.getBytes()); |
| } |
| |
| private Path getHtmlHelpFilePath(String moduleID) throws IOException { |
| return getProjectFile("help/" + "module_" + escape(moduleID) + ".html", false).toPath(); |
| } |
| |
| public void updateBuildProperties() throws IOException { |
| final File buildFile = getBuildPropertiesFile(); |
| |
| final Properties properties = new Properties(); |
| properties.load(new FileInputStream(buildFile)); |
| final String property = properties.getProperty("bin.includes"); |
| if (!property.contains("help/")) { |
| if (property.trim().isEmpty()) |
| properties.setProperty("bin.includes", "help/"); |
| else |
| properties.setProperty("bin.includes", "help/," + property.trim()); |
| |
| final FileOutputStream out = new FileOutputStream(buildFile); |
| properties.store(out, ""); |
| out.close(); |
| } |
| } |
| |
| public void updateManifest() throws IOException { |
| final File manifestFile = getManifestFile(); |
| |
| final Manifest manifest = new Manifest(); |
| manifest.read(new FileInputStream(manifestFile)); |
| |
| final Attributes mainAttributes = manifest.getMainAttributes(); |
| final String require = mainAttributes.getValue("Require-Bundle"); |
| |
| if ((require == null) || (require.isEmpty())) |
| mainAttributes.putValue("Require-Bundle", "org.eclipse.help;bundle-version=\"[3.5.0,4.0.0)\""); |
| |
| else if (!require.contains("org.eclipse.help")) |
| mainAttributes.putValue("Require-Bundle", "org.eclipse.help;bundle-version=\"[3.5.0,4.0.0)\"," + require); |
| |
| else |
| // manifest contains reference to org.eclipse.help, bail out |
| return; |
| |
| final FileOutputStream out = new FileOutputStream(manifestFile); |
| manifest.write(out); |
| out.close(); |
| } |
| |
| public Set<String> createModuleTOCFiles() throws Exception { |
| final Map<String, IMemento> tocDefinitions = new HashMap<>(); |
| |
| // create categories |
| for (final Category category : getCategories()) { |
| final XMLMemento memento = XMLMemento.createWriteRoot("toc"); |
| memento.putString("label", category.getName()); |
| memento.putString("link_to", category.getHelpLink()); |
| |
| final IMemento topicNode = memento.createChild("topic"); |
| topicNode.putString("label", category.getName()); |
| topicNode.putBoolean("sort", true); |
| |
| topicNode.createChild("anchor").putString("id", "modules_anchor"); |
| tocDefinitions.put(category.getFileName(), memento); |
| } |
| |
| // create modules |
| for (final ModuleDefinition module : getModules()) { |
| final String categoryID = module.getCategoryId(); |
| final String fileName = Category.createCategoryFileName(categoryID).replace("category_", "modules_"); |
| |
| IMemento memento; |
| if (tocDefinitions.containsKey(fileName)) |
| memento = tocDefinitions.get(fileName); |
| |
| else { |
| memento = XMLMemento.createWriteRoot("toc"); |
| memento.putString("label", "Modules"); |
| memento.putString("link_to", Category.createCategoryLink(categoryID)); |
| |
| tocDefinitions.put(fileName, memento); |
| } |
| |
| final IMemento topicNode = memento.createChild("topic"); |
| topicNode.putString("href", "help/" + createHTMLFileName(module.getId())); |
| topicNode.putString("label", module.getName()); |
| } |
| |
| for (final Entry<String, IMemento> entry : tocDefinitions.entrySet()) |
| Files.write(getProjectFile("help/" + entry.getKey(), false).toPath(), entry.getValue().toString().getBytes()); |
| |
| return tocDefinitions.keySet(); |
| } |
| |
| public void updatePluginXML(final Collection<String> tocs) throws Exception { |
| final HashSet<String> toDo = new HashSet<>(tocs); |
| |
| final IMemento memento = getPluginDefinition(); |
| for (final IMemento extensionNode : memento.getChildren("extension")) { |
| final String extensionPoint = extensionNode.getString("point"); |
| if ("org.eclipse.help.toc".equals(extensionPoint)) { |
| // a help topic is already registered |
| for (final IMemento tocNode : extensionNode.getChildren("toc")) { |
| final String tocLocation = tocNode.getString("file"); |
| if (tocLocation.length() > 5) |
| toDo.remove(tocLocation.substring(5)); |
| } |
| } |
| } |
| |
| for (final String fileLocation : toDo) { |
| // some TOCs not registered yet |
| final IMemento extensionNode = memento.createChild("extension"); |
| extensionNode.putString("point", "org.eclipse.help.toc"); |
| final IMemento tocNode = extensionNode.createChild("toc"); |
| tocNode.putString("file", "help/" + fileLocation); |
| tocNode.putBoolean("primary", false); |
| } |
| |
| if (!toDo.isEmpty()) { |
| final File pluginFile = getPluginOrFragmentFile(); |
| Files.write(pluginFile.toPath(), memento.toString().replace("
", "\n").getBytes()); |
| } |
| } |
| |
| public LinkProvider getLinkProvider() { |
| return fLinkProvider; |
| } |
| |
| protected void registerLinks(String url) { |
| registerOfflineLinks(url, url + "/package-list"); |
| } |
| |
| protected void registerOfflineLinks(String remoteUrl, String packageContentUrl) { |
| try { |
| try (InputStream packageData = new URL(packageContentUrl).openStream()) { |
| final List<String> packages = new BufferedReader(new InputStreamReader(packageData)).lines().collect(Collectors.toList()); |
| getLinkProvider().registerAddress(remoteUrl, packages); |
| } |
| |
| } catch (final MalformedURLException e) { |
| getReporter().report(IReporter.WARNING, "Cannot open location \"" + packageContentUrl + "\""); |
| } catch (final IOException e) { |
| getReporter().report(IReporter.WARNING, "Cannot read package data from \"" + packageContentUrl + "\""); |
| } |
| } |
| |
| protected abstract AbstractClassModel getClassModel(ModuleDefinition module); |
| |
| protected abstract IReporter createReporter(); |
| } |