/*
* 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.common.tools.help;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ProcessBuilder.Redirect;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.tautua.markdownpapers.ast.Document;
import com.redhat.ceylon.common.OSUtil;
import com.redhat.ceylon.common.Versions;
import com.redhat.ceylon.common.tool.AnnotatedToolModel;
import com.redhat.ceylon.common.tool.ArgumentModel;
import com.redhat.ceylon.common.tool.Description;
import com.redhat.ceylon.common.tool.Hidden;
import com.redhat.ceylon.common.tool.Multiplicity;
import com.redhat.ceylon.common.tool.OptionModel;
import com.redhat.ceylon.common.tool.OptionModel.ArgumentType;
import com.redhat.ceylon.common.tool.PluginToolModel;
import com.redhat.ceylon.common.tool.RemainingSections;
import com.redhat.ceylon.common.tool.ScriptToolModel;
import com.redhat.ceylon.common.tool.SubtoolModel;
import com.redhat.ceylon.common.tool.Summary;
import com.redhat.ceylon.common.tool.ToolLoader;
import com.redhat.ceylon.common.tool.ToolModel;
import com.redhat.ceylon.common.tool.Tools;
import com.redhat.ceylon.common.tools.CeylonTool;
import com.redhat.ceylon.common.tools.help.Markdown.Section;
import com.redhat.ceylon.common.tools.help.model.DescribedSection;
import com.redhat.ceylon.common.tools.help.model.DescribedSection.Role;
import com.redhat.ceylon.common.tools.help.model.Doc;
import com.redhat.ceylon.common.tools.help.model.Option;
import com.redhat.ceylon.common.tools.help.model.OptionsSection;
import com.redhat.ceylon.common.tools.help.model.SubtoolVisitor;
import com.redhat.ceylon.common.tools.help.model.SummarySection;
import com.redhat.ceylon.common.tools.help.model.SynopsesSection;
import com.redhat.ceylon.common.tools.help.model.Synopsis;
public class DocBuilder {
protected ToolLoader toolLoader;
protected boolean includeHidden = false;
public DocBuilder(ToolLoader toolLoader) {
super();
this.toolLoader = toolLoader;
}
public boolean isIncludeHidden() {
return includeHidden;
}
public void setIncludeHidden(boolean includeHidden) {
this.includeHidden = includeHidden;
}
public Doc buildDoc(ToolModel<?> model, boolean specialRoot) {
checkModel(model);
boolean rootHack = specialRoot && (model instanceof AnnotatedToolModel) && CeylonTool.class.isAssignableFrom(((AnnotatedToolModel<?>)model).getToolClass());
Doc doc = new Doc();
doc.setVersion(Versions.CEYLON_VERSION);
doc.setToolModel(model);
doc.setInvocation(getCeylonInvocation(model));
doc.setSummary(buildSummary(model));
doc.setSynopses(rootHack ? buildRootSynopsis(model) : buildSynopsis(model));
doc.setDescription(rootHack ? buildRootDescription(model) : buildDescription(model));
doc.setOptions(buildOptions(model));
if (model.getSubtoolModel() != null) {
//doc.setSubcommands(buildSubcommands(model));
}
doc.setAdditionalSections(buildAdditionalSections(model));
return doc;
}
private void checkModel(ToolModel<?> model) {
new SubtoolVisitor(model) {
@Override
protected void visit(ToolModel<?> model, SubtoolModel<?> subtoolModel) {
if (model != root) {
if (getSummary(model) != null) {
System.err.println("@Summary not supported on subtools: " + model.getName());
}
if (!getSections(model).isEmpty()) {
System.err.println("@RemainingSections not supported on subtools: " + model.getName());
}
}
}
}.accept();
}
private SynopsesSection buildRootSynopsis(ToolModel<?> model) {
SynopsesSection synopsis = new SynopsesSection();
synopsis.setTitle(CeylonHelpToolMessages.msg("section.SYNOPSIS"));
List<Synopsis> synopsisList = new ArrayList<>();
{
Synopsis s1 = new Synopsis();
s1.setInvocation(Tools.progName());
OptionModel<Boolean> option = new OptionModel();
option.setLongName("version");
option.setArgumentType(ArgumentType.NOT_ALLOWED);
ArgumentModel<Boolean> argument = new ArgumentModel<>();
argument.setMultiplicity(Multiplicity._1);
argument.setType(Boolean.TYPE);
option.setArgument(argument);
s1.setOptionsAndArguments(Collections.singletonList(option));//model.getOption("version")));
synopsisList.add(s1);
}
{
Synopsis s2 = new Synopsis();
s2.setInvocation(Tools.progName());
ArrayList args = new ArrayList(model.getOptions());
args.remove(model.getOption("version"));
/*ArgumentModel<?> options = new ArgumentModel();
options.setMultiplicity(Multiplicity._0_OR_MORE);
options.setName("cey\u2011options");
args.add(options);*/
ArgumentModel<?> command = new ArgumentModel();
command.setMultiplicity(Multiplicity._1);
command.setName("command");
args.add(command);
ArgumentModel<?> commandOptions = new ArgumentModel();
commandOptions.setMultiplicity(Multiplicity._0_OR_MORE);
commandOptions.setName("command\u2011options");
args.add(commandOptions);
ArgumentModel<?> commandArgs = new ArgumentModel();
commandArgs.setMultiplicity(Multiplicity._0_OR_MORE);
commandArgs.setName("command\u2011args");
args.add(commandArgs);
s2.setOptionsAndArguments(args);
synopsisList.add(s2);
}
synopsis.setSynopses(synopsisList);
return synopsis;
}
private DescribedSection buildRootDescription(
ToolModel<?> rootModel) {
StringBuilder sb = new StringBuilder();
final String newline = "\n";
sb.append(newline);
sb.append(newline);
for (String toolName : toolLoader.getToolNames()) {
final ToolModel<?> model = toolLoader.loadToolModel(toolName);
if (model == null) {
throw new RuntimeException(toolName);
}
if (!model.isPorcelain() && !includeHidden) {
continue;
}
sb.append("* `").append(toolName).append("` - ");
String summary = getSummaryValue(model);
if (summary != null) {
sb.append(summary);
}
sb.append(newline);
sb.append(newline);
}
sb.append(newline);
sb.append(CeylonHelpToolMessages.getMoreInfo());
sb.append(newline);
sb.append(newline);
String both = getDescription(rootModel) + sb.toString();
DescribedSection description = buildDescription(rootModel, both);
return description;
}
public Doc buildDoc(ToolModel<?> model) {
return buildDoc(model, false);
}
private List<DescribedSection> buildAdditionalSections(ToolModel<?> model) {
List<DescribedSection> additionalSections = new ArrayList<DescribedSection>();
String sections = getSections(model);
if (sections != null && !sections.isEmpty()) {
Document doc = Markdown.markdown(sections);
List<Section> markdownSections = Markdown.extractSections(doc);
for (Markdown.Section sect : markdownSections) {
DescribedSection ds = new DescribedSection();
ds.setRole(Role.ADDITIONAL);
Document sectionDoc = sect.getDoc();
if (sect.getHeading() == null) {
// TODO Warn that there were no section headings
continue;
} else {
// Adjust the heading levels, so that the most prominent
// heading is H2
Markdown.adjustHeadings(sectionDoc, 2-sect.getHeading().getLevel());
}
ds.setTitle(sect.getHeading());
ds.setDescription(sectionDoc);
additionalSections.add(ds);
}
}
return additionalSections;
}
private OptionsSection buildOptions(ToolModel<?> model) {
if(!(model instanceof AnnotatedToolModel))
return null;
final HashMap<ToolModel<?>, OptionsSection> map = new HashMap<>();
new SubtoolVisitor(model) {
@Override
protected void visit(ToolModel<?> model,
SubtoolModel<?> subtoolModel) {
OptionsSection optionsSection = new OptionsSection();
map.put(model, optionsSection);
if (model==root) {
optionsSection.setTitle(
Markdown.markdown("##" + CeylonHelpToolMessages.msg("section.OPTIONS")));
} else {
optionsSection.setTitle(
Markdown.markdown("###" + CeylonHelpToolMessages.msg("section.OPTIONS.sub", model.getName())));
}
List<Option> options = new ArrayList<>();
for (OptionModel<?> opt : sortedOptions(model.getOptions())) {
Option option = new Option();
option.setOption(opt);
String descriptionMd = getOptionDescription(model, opt);
if (descriptionMd == null || descriptionMd.isEmpty()) {
descriptionMd = CeylonHelpToolMessages.msg("option.undocumented");
}
option.setDescription(Markdown.markdown(descriptionMd));
options.add(option);
}
optionsSection.setOptions(options);
if (model != root && !options.isEmpty()) {
OptionsSection parent = map.get(ancestors.lastElement().getModel());
ArrayList<OptionsSection> parentSubsections = new ArrayList<OptionsSection>(parent.getSubsections());
parentSubsections.add(optionsSection);
parent.setSubsections(parentSubsections);
}
}
}.accept();
return map.get(model);
}
private DescribedSection buildDescription(ToolModel<?> model) {
final HashMap<ToolModel<?>, DescribedSection> map = new HashMap<ToolModel<?>, DescribedSection>();
new SubtoolVisitor(model) {
@Override
protected void visit(ToolModel<?> model, SubtoolModel<?> subtoolModel) {
if (model == root) {
map.put(model, buildDescription(model, getDescription(model)));
} else if (model.getSubtoolModel() == null) {// leaf
DescribedSection section = new DescribedSection();
section.setRole(Role.DESCRIPTION);
StringBuilder sb = new StringBuilder();
for (SubtoolVisitor.ToolModelAndSubtoolModel subtool : ancestors.subList(1, ancestors.size())) {
sb.append(subtool.getModel().getName()).append(" ");
}
sb.append(model.getName());
section.setTitle(Markdown.markdown("###" + CeylonHelpToolMessages.msg("section.DESCRIPTION.sub", sb.toString())));
section.setDescription(Markdown.markdown(getDescription(model)));
section.setAbout(model);
List<DescribedSection> rootSubsections = new ArrayList<>(map.get(root).getSubsections());
rootSubsections.add(section);
map.get(root).setSubsections(rootSubsections);
}
}
}.accept();
return map.get(model);
}
private DescribedSection buildDescription(ToolModel<?> model, String description) {
DescribedSection section = null;
if (!description.isEmpty()) {
section = new DescribedSection();
section.setRole(Role.DESCRIPTION);
section.setTitle(
Markdown.markdown("## " + CeylonHelpToolMessages.msg("section.DESCRIPTION") + "\n"));
section.setDescription(Markdown.markdown(description));
}
return section;
}
private DescribedSection buildSubcommands(ToolModel<?> model) {
/*
DescribedSection section = null;
if (!description.isEmpty()) {
SubtoolModel<?> subtool = model.getSubtoolModel();
for (String toolName : subtool.getToolLoader().getToolNames()) {
ToolModel<Tool> subtoolModel = subtool.getToolLoader().loadToolModel(toolName);
}
/ *
* Here I need to build up the markdown something like as follows
*
The command `ceylon config` takes various subcommands
## SUBCOMMANDS
### `ceylon config foo`
summary
description
options
### `ceylon config bar baz`
summary
description
options
* /
section = new DescribedSection();
section.setRole(Role.SUBCOMMANDS);
section.setDescription(Markdown.markdown(
"##" + sectionsBundle.getString("section.SUBCOMMANDS") + "\n\n" +
description));
}
return section;*/
return null;
}
private SynopsesSection buildSynopsis(ToolModel<?> model) {
//Synopsis synopsis = out.startSynopsis(bundle.getString("section.SYNOPSIS"));
// TODO Make auto generated SYNOPSIS better -- we need to know which options
// form groups, or should we just have a @Synopses({@Synopsis(""), ...})
SynopsesSection synopsesSection = new SynopsesSection();
synopsesSection.setTitle(CeylonHelpToolMessages.msg("section.SYNOPSIS"));
final List<Synopsis> synopsisList = new ArrayList<>();
new SubtoolVisitor(model) {
@Override
protected void visit(ToolModel<?> model,
SubtoolModel<?> subtoolModel) {
if (model.getSubtoolModel() == null) {// a leaf
Synopsis synopsis = new Synopsis();
synopsis.setInvocation(getCeylonInvocationForSynopsis(root));
List<?> optionsAndArguments;
if (ancestors.isEmpty()) {
optionsAndArguments = optionsAndArguments(model);
} else {
optionsAndArguments = new ArrayList<>();
for (SubtoolVisitor.ToolModelAndSubtoolModel ancestor : ancestors) {
List subOptAndArgs = optionsAndArguments(ancestor.getModel());
if (ancestor.getModel() != root) {
// Don't treat the foo in `ceylon foo` as a subtool
subOptAndArgs.add(0, ancestor);
}
optionsAndArguments.addAll((List)subOptAndArgs);
}
List subOptAndArgs = optionsAndArguments(model);
subOptAndArgs.add(0, new SubtoolVisitor.ToolModelAndSubtoolModel(model, subtoolModel));
optionsAndArguments.addAll((List)subOptAndArgs);
}
synopsis.setOptionsAndArguments(optionsAndArguments);
synopsisList.add(synopsis);
}
}
}.accept();
synopsesSection.setSynopses(synopsisList);
return synopsesSection;
}
private <E> List<E> optionsAndArguments(ToolModel<?> model) {
List<E> optionsAndArguments = (List)sortedOptions(model.getOptions());
optionsAndArguments.addAll((List)model.getArguments());
return optionsAndArguments;
}
private boolean skipHiddenOption(OptionModel<?> option) {
return option.getArgument().getSetter().getAnnotation(Hidden.class) != null
&& !includeHidden;
}
private ArrayList<OptionModel<?>> sortedOptions(final Collection<OptionModel<?>> options2) {
ArrayList<OptionModel<?>> options = new ArrayList<OptionModel<?>>(options2);
for (Iterator<OptionModel<?>> iter = options.iterator(); iter.hasNext(); ) {
OptionModel<?> option = iter.next();
if (skipHiddenOption(option)) {
iter.remove();
}
}
Collections.sort(options, new Comparator<OptionModel<?>>() {
@Override
public int compare(OptionModel<?> o1, OptionModel<?> o2) {
return o1.getLongName().compareTo(o2.getLongName());
}
});
return options;
}
private SummarySection buildSummary(ToolModel<?> model) {
SummarySection summary = new SummarySection();
summary.setTitle(
Markdown.markdown("##" + CeylonHelpToolMessages.msg("section.NAME") + "\n"));
summary.setSummary(getSummaryValue(model));
return summary;
}
private String getName(ToolModel<?> model) {
return model.getName();
}
private String msg(ResourceBundle toolBundle, String key) {
if (toolBundle != null && toolBundle.containsKey(key)) {
String msg = toolBundle.getString(key);
if (msg != null) {
// Pass through a message format so that translators don't have to guess
// which things need doubled '' and which not.
return MessageFormat.format(msg, new Object[]{});
}
}
return "";
}
private String getSummaryValue(ToolModel<?> model) {
if(model instanceof ScriptToolModel){
return invokeScript((ScriptToolModel<?>) model, "--_print-summary");
}
if(model instanceof PluginToolModel){
return ((PluginToolModel<?>) model).getToolSummary();
}
ResourceBundle toolBundle = getToolBundle(model);
String msg = msg(toolBundle, "summary");
if (msg.isEmpty()) {
Summary summary = getSummary(model);
if (summary != null) {
msg = summary.value();
}
}
return msg;
}
private String invokeScript(ScriptToolModel<?> model, String arg) {
ProcessBuilder processBuilder;
if (OSUtil.isWindows()) {
processBuilder = new ProcessBuilder("cmd.exe", "/C", model.getScriptName(), arg);
} else {
processBuilder = new ProcessBuilder(model.getScriptName(), arg);
}
CeylonTool.setupScriptEnvironment(processBuilder, model.getScriptName());
processBuilder.redirectError(Redirect.INHERIT);
try {
Process process = processBuilder.start();
// no stdin to the tool
process.getOutputStream().close();
InputStream stream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
StringBuffer strbuf = new StringBuffer();
String line;
while((line = reader.readLine()) != null){
strbuf.append(line+"\n");
}
reader.close();
int exit = process.waitFor();
if(exit != 0)
return "";
return strbuf.toString();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}
private ResourceBundle getToolBundle(ToolModel<?> model) {
if(!(model instanceof AnnotatedToolModel))
return null;
AnnotatedToolModel<?> amodel = (AnnotatedToolModel<?>)model;
ResourceBundle toolBundle;
try {
toolBundle = ResourceBundle.getBundle(amodel.getToolClass().getName());
} catch (MissingResourceException e) {
toolBundle = null;
}
return toolBundle;
}
private Summary getSummary(ToolModel<?> model) {
if(!(model instanceof AnnotatedToolModel))
return null;
AnnotatedToolModel<?> amodel = (AnnotatedToolModel<?>)model;
return amodel.getToolClass().getAnnotation(Summary.class);
}
private String getDescription(ToolModel<?> model) {
if(model instanceof ScriptToolModel){
return invokeScript((ScriptToolModel<?>) model, "--_print-description");
}
AnnotatedToolModel<?> amodel = (AnnotatedToolModel<?>)model;
ResourceBundle toolBundle = getToolBundle(model);
String msg = msg(toolBundle, "description");
if (msg.isEmpty()) {
Description description = amodel.getToolClass().getAnnotation(Description.class);
if (description != null) {
msg = description.value();
}
}
return msg;
}
private String getSections(ToolModel<?> model) {
if(!(model instanceof AnnotatedToolModel))
return null;
AnnotatedToolModel<?> amodel = (AnnotatedToolModel<?>)model;
ResourceBundle toolBundle = getToolBundle(model);
String msg = msg(toolBundle, "sections.remaining");
if (msg.isEmpty()) {
RemainingSections sections = amodel.getToolClass().getAnnotation(RemainingSections.class);
if (sections != null) {
msg = sections.value();
}
}
return msg;
}
private String getOptionDescription(ToolModel<?> model, OptionModel<?> opt) {
ResourceBundle toolBundle = getToolBundle(model);
String msg = msg(toolBundle, "option."+opt.getLongName());
if (msg.isEmpty()) {
Description description = opt.getArgument().getSetter().getAnnotation(Description.class);
if (description != null) {
msg = description.value();
}
}
return msg;
}
private String getCeylonInvocation(ToolModel<?> model) {
return getName(model).isEmpty() ? Tools.progName(): Tools.progName() + " " + model.getName();
}
private String getCeylonInvocationForSynopsis(ToolModel<?> model) {
String ret = getCeylonInvocation(model);
if(model instanceof ScriptToolModel){
return ret + " " + invokeScript((ScriptToolModel<?>) model, "--_print-usage");
}
return ret;
}
}