/* Copyright 2013 Jonatan Jönsson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package se.softhouse.jargo;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.ImmutableList.copyOf;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.Math.max;
import static se.softhouse.common.strings.StringsUtil.NEWLINE;
import static se.softhouse.common.strings.StringsUtil.spaces;
import static se.softhouse.jargo.Argument.IS_INDEXED;
import static se.softhouse.jargo.Argument.IS_OF_VARIABLE_ARITY;
import static se.softhouse.jargo.Argument.IS_VISIBLE;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.annotation.concurrent.NotThreadSafe;
import se.softhouse.common.strings.Lines;
import se.softhouse.common.strings.StringBuilders;
import se.softhouse.jargo.internal.Texts.UsageTexts;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
/**
* Responsible for formatting usage texts for {@link CommandLineParser#usage()} and
* {@link Arguments#helpArgument(String, String...)}.
* Using it is often as simple as:
*
* <pre class="prettyprint">
* <code class="language-java">
* System.out.print(CommandLineParser.withArguments(someArguments).usage());
* </code>
* </pre>
*
* <pre>
* Sorts {@link Argument}s in the following order:
* <ol>
* <li>{@link ArgumentBuilder#names(String...) indexed arguments} without a {@link ArgumentBuilder#variableArity() variable arity}</li>
* <li>By their {@link ArgumentBuilder#names(String...) first name} in an alphabetical order</li>
* <li>The remaining {@link ArgumentBuilder#names(String...) indexed arguments} that are of {@link ArgumentBuilder#variableArity() variable arity}</li>
* </ol>
* </pre>
*/
@NotThreadSafe
public final class Usage implements Serializable
{
private static final long serialVersionUID = 1L;
/**
* Corresponds to usage for a single {@link Argument}
*/
@VisibleForTesting
static final class Row
{
private final StringBuilder nameColumn = new StringBuilder();
private final StringBuilder descriptionColumn = new StringBuilder();
@Override
public String toString()
{
return nameColumn.toString() + " " + descriptionColumn.toString();
}
}
private final transient Collection<Argument<?>> unfilteredArguments;
private transient String errorMessage = "";
private final transient Locale locale;
private final transient ProgramInformation program;
private final transient boolean forCommand;
// TODO(jontejj): try getting the correct value automatically, if not possible fall back to 80
private transient int columnWidth = 80;
private transient String fromSerializedUsage = null;
/**
* <pre>
* For:
* -l, --enable-logging Output debug information to standard out
* -p, --listen-port The port clients should connect to.
*
* This would be 20.
* </pre>
*/
private transient int indexOfDescriptionColumn;
private transient ImmutableList<Argument<?>> argumentsToPrint;
Usage(Collection<Argument<?>> arguments, Locale locale, ProgramInformation program, boolean forCommand)
{
this.unfilteredArguments = arguments;
this.locale = locale;
this.program = program;
this.forCommand = forCommand;
}
Usage(CommandLineParserInstance parser)
{
this.unfilteredArguments = parser.allArguments();
this.locale = parser.locale();
this.program = parser.programInformation();
this.forCommand = parser.isCommandParser();
}
private Usage(String fromSerializedUsage)
{
// All these are unused as the usage is already constructed
this(null, null, null, false);
this.fromSerializedUsage = fromSerializedUsage;
}
/**
* An optional errorMessage to print before any usage
*/
Usage withErrorMessage(String message)
{
this.errorMessage = checkNotNull(message);
return this;
}
/**
* Returns the usage text that's suitable to print on {@link System#out}.
*/
@Override
public String toString()
{
return usage();
}
private String usage()
{
if(fromSerializedUsage != null)
return fromSerializedUsage;
init();
StringBuilder builder = newStringBuilder();
printOn(builder);
return builder.toString();
}
/**
* Appends usage to {@code target}. An alternative to {@link #toString()} when the usage is very
* large and needs to be flushed from time to time.
*
* @throws IOException If an I/O error occurs while appending usage
*/
public void printOn(Appendable target) throws IOException
{
if(fromSerializedUsage != null)
{
target.append(fromSerializedUsage);
}
else
{
init();
appendUsageTo(target);
}
}
/**
* Appends usage to {@code target}, just like {@link #printOn(Appendable)} but for a
* {@link PrintStream} instead. The reason for this overload is that {@link Appendable} declares
* {@link IOException} to be thrown while {@link PrintStream} handles it internally through the
* use of the {@link PrintStream#checkError()} method.
*
* @param target a {@link PrintStream} such as {@link System#out} or {@link System#err}.
*/
public void printOn(PrintStream target)
{
try
{
printOn((Appendable) target);
}
catch(IOException impossible)
{
throw new AssertionError(impossible);
}
}
/**
* Appends usage to {@code target}, just like {@link #printOn(Appendable)} but for a
* {@link StringBuilder} instead
*/
public void printOn(StringBuilder target)
{
try
{
printOn((Appendable) target);
}
catch(IOException impossible)
{
throw new AssertionError(impossible);
}
}
private void appendUsageTo(Appendable builder) throws IOException
{
builder.append(errorMessage);
builder.append(header());
for(Argument<?> arg : argumentsToPrint)
{
Row forArgument = usageForArgument(arg);
appendRowTo(forArgument, builder);
}
}
private String header()
{
if(forCommand) // Commands get their header from their meta description
return hasArguments() ? NEWLINE : "";
String mainUsage = UsageTexts.USAGE_HEADER + program.programName();
if(hasArguments())
{
mainUsage += UsageTexts.ARGUMENT_INDICATOR;
}
mainUsage += NEWLINE + Lines.wrap(program.programDescription(), columnWidth, locale);
if(hasArguments())
{
mainUsage += NEWLINE + UsageTexts.ARGUMENT_HEADER + ":" + NEWLINE;
}
return mainUsage;
}
private static final int SPACES_BETWEEN_COLUMNS = 4;
private void init()
{
if(argumentsToPrint == null)
{
// The lack of synchronization is deliberate, repeated invocations will result in the
// same variables anyway
Collection<Argument<?>> visibleArguments = filter(unfilteredArguments, IS_VISIBLE);
this.argumentsToPrint = copyOf(sortedArguments(visibleArguments));
this.indexOfDescriptionColumn = determineLongestNameColumn() + SPACES_BETWEEN_COLUMNS;
}
}
private Iterable<Argument<?>> sortedArguments(Collection<Argument<?>> arguments)
{
Collection<Argument<?>> indexedArguments = filter(arguments, IS_INDEXED);
Iterable<Argument<?>> indexedWithoutVariableArity = filter(indexedArguments, not(IS_OF_VARIABLE_ARITY));
Iterable<Argument<?>> indexedWithVariableArity = filter(indexedArguments, IS_OF_VARIABLE_ARITY);
List<Argument<?>> sortedArgumentsByName = newArrayList(filter(arguments, not(IS_INDEXED)));
Collections.sort(sortedArgumentsByName, Argument.NAME_COMPARATOR);
return Iterables.concat(indexedWithoutVariableArity, sortedArgumentsByName, indexedWithVariableArity);
}
private int determineLongestNameColumn()
{
int longestNameSoFar = 0;
for(Argument<?> arg : argumentsToPrint)
{
longestNameSoFar = max(longestNameSoFar, lengthOfNameColumn(arg));
}
return Math.min(longestNameSoFar, maxNameColumnWidth());
}
private int lengthOfNameColumn(final Argument<?> argument)
{
int namesLength = 0;
for(String name : argument.names())
{
namesLength += name.length();
}
int separatorLength = max(0, UsageTexts.NAME_SEPARATOR.length() * (argument.names().size() - 1));
int metaLength = argument.metaDescriptionInLeftColumn().length();
return namesLength + separatorLength + metaLength;
}
/**
* A minimum of 1 third must be available to print descriptions on
*/
private int maxNameColumnWidth()
{
return columnWidth / 3 * 2;
}
private StringBuilder newStringBuilder()
{
// Two lines for each argument
return StringBuilders.withExpectedSize(2 * argumentsToPrint.size() * columnWidth);
}
private boolean hasArguments()
{
return !argumentsToPrint.isEmpty();
}
private static final Joiner NAME_JOINER = Joiner.on(UsageTexts.NAME_SEPARATOR);
/**
* <pre>
* Left column | Right column:
* name <meta> description of what the argument means [indicators]
* <meta>: description of valid values
* Default: default value
*
* For instance:
* -foo <integer> Foo something [Required]
* <integer>: 1 to 5
*
* -bar Bar something
* Default: 0
* </pre>
*/
private Row usageForArgument(final Argument<?> arg)
{
Row row = new Row();
NAME_JOINER.appendTo(row.nameColumn, arg.names());
row.nameColumn.append(arg.metaDescriptionInLeftColumn());
String description = arg.description();
if(!description.isEmpty())
{
row.descriptionColumn.append(Lines.wrap(description, indexOfDescriptionColumn, columnWidth, locale));
addIndicators(arg, row.descriptionColumn);
row.descriptionColumn.append(NEWLINE);
valueExplanation(arg, row.descriptionColumn);
}
else
{
valueExplanation(arg, row.descriptionColumn);
addIndicators(arg, row.descriptionColumn);
}
return row;
}
private <T> void addIndicators(final Argument<T> arg, StringBuilder target)
{
if(arg.isRequired())
{
target.append(UsageTexts.REQUIRED);
}
if(arg.isAllowedToRepeat())
{
target.append(UsageTexts.ALLOWS_REPETITIONS);
}
}
private <T> void valueExplanation(final Argument<T> arg, StringBuilder target)
{
String validValuesDescription = arg.descriptionOfValidValues(locale);
if(!validValuesDescription.isEmpty())
{
String meta = arg.metaDescriptionInRightColumn();
target.append(meta + ": ").append(validValuesDescription);
}
if(arg.isRequired())
return;
String descriptionOfDefaultValue = arg.defaultValueDescription(locale);
if(descriptionOfDefaultValue != null)
{
if(!validValuesDescription.isEmpty())
{
target.append(NEWLINE);
}
String spaces = spaces(UsageTexts.DEFAULT_VALUE_START.length());
descriptionOfDefaultValue = descriptionOfDefaultValue.replace(NEWLINE, NEWLINE + spaces);
target.append(UsageTexts.DEFAULT_VALUE_START).append(descriptionOfDefaultValue);
}
}
private void appendRowTo(Row row, Appendable target) throws IOException
{
StringBuilder nameColumn = Lines.wrap(row.nameColumn, indexOfDescriptionColumn, locale);
String descriptionColumn = row.descriptionColumn.toString();
Iterable<String> nameLines = Splitter.on(NEWLINE).split(nameColumn);
Iterator<String> descriptionLines = Splitter.on(NEWLINE).split(descriptionColumn).iterator();
for(String nameLine : nameLines)
{
target.append(nameLine);
if(descriptionLines.hasNext())
{
int lengthOfNameColumn = nameLine.length();
int paddingWidth = Math.max(1, indexOfDescriptionColumn - lengthOfNameColumn);
target.append(spaces(paddingWidth));
target.append(descriptionLines.next());
}
target.append(NEWLINE);
}
while(descriptionLines.hasNext())
{
target.append(spaces(indexOfDescriptionColumn));
target.append(descriptionLines.next());
target.append(NEWLINE);
}
}
private static final class SerializationProxy implements Serializable
{
/**
* @serial all arguments described. Constructed lazily when serialized.
*/
private final String serializedUsage;
private static final long serialVersionUID = 1L;
private SerializationProxy(Usage usage)
{
// TODO(jontejj): how to support calling withConsoleWidth() after serialization? Some
// kind of marker where the break iterator did something?
serializedUsage = usage.usage();
}
private Object readResolve()
{
return new Usage(serializedUsage);
}
}
private Object writeReplace()
{
return new SerializationProxy(this);
}
}