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

com.github.raphcal.argparser.ArgumentParser Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2018 Raphaël Calabro .
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package com.github.raphcal.argparser;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Permet d'analyser les arguments donnés à un programme et d'afficher
 * la liste des arguments attendus.
 * 
 * @author Raphaël Calabro ([email protected])
 * @param  Type décrivant les arguments du programme.
 */
public class ArgumentParser {
	private static final int DEFAULT_LINE_LENGTH = 80;
	private static final int PREFIX_LENGTH = 9;
	
	private final Class clazz;
			
	private Map options;
	private List arguments;
	private int maxOptionNameLength;
	
	private int nonOptionalArgumentCount;
	
	private int lineLength = DEFAULT_LINE_LENGTH;
	
	/**
	 * Creates a new parser for the given argument list class.
	 *
	 * @param clazz Class containing field annotated with {@link Argument}
	 * and/or {@link Option} anotations.
	 */
	public ArgumentParser(Class clazz) {
		this.clazz = clazz;
		
		this.options = new LinkedHashMap();
		this.arguments = new ArrayList();
		final Map priorities = new HashMap();
		
		final Field[] fields = clazz.getDeclaredFields();
		for(final Field field : fields) {
			final ArgumentParserEntry entry = new ArgumentParserEntry(field, field.getType() != boolean.class);
			
			final Option option = field.getAnnotation(Option.class);
			if(option != null) {
				field.setAccessible(true);
				
				final String name = option.alias().isEmpty() ? field.getName() : option.alias();
				String shortName = "-" + name.charAt(0);
				if(options.containsKey(shortName)) {
					shortName = "-" + Character.toUpperCase(name.charAt(0));
				}
				if(options.containsKey(shortName)) {
					throw new IllegalArgumentException("Les options -" + name.charAt(0) + " et -" + 
							Character.toUpperCase(name.charAt(0)) + " sont déjà utilisés, choisissez un alias différent pour l'option '" + name +"'.");
				}
				
				options.put(shortName, entry);
				options.put("--" + name, entry);
				
				entry.setAlias(name);
				entry.setShortName(shortName);
				entry.setDescription(option.description());
				entry.setAutonomous(option.autonomous());
				
				if(name.length() > maxOptionNameLength) {
					maxOptionNameLength = name.length();
				}
			}
			
			final Argument argument = field.getAnnotation(Argument.class);
			if(argument != null) {
				field.setAccessible(true);
				arguments.add(entry);
				priorities.put(entry, argument.index());
				
				entry.setOptional(argument.optional());
				if (!argument.optional()) {
					nonOptionalArgumentCount++;
				}
			}
		}
		
		Collections.sort(arguments, new Comparator() {
			@Override
			public int compare(ArgumentParserEntry o1, ArgumentParserEntry o2) {
				return priorities.get(o1).compareTo(priorities.get(o2));
			}
		});
	}
	
	/**
	 * Parse the given program arguments and creates an instance of
	 * the argument list class.
	 * 

* If the number of required argument is not met, this method returns * null. * * @param args Program arguments. * @return A new instance of T or null if the * parsing can't be completed. */ public T parse(String[] args) { final T t = newInstance(clazz); int currentArgument = 0; boolean valid = true; boolean forceValid = false; boolean collection = false; for(int index = 0; index < args.length; index++) { final String arg = args[index]; ArgumentParserEntry entry = options.get(arg); if(entry != null) { if(entry.hasValue()) { index++; if(index < args.length) { set(t, entry, arg); } else { valid = false; } if(entry.isCollection()) { while(index < args.length) { if(options.get(args[index]) != null) { index--; break; } set(t, entry, args[index++]); } } } else { set(t, entry, true); } forceValid |= entry.isAutonomous(); } else if(currentArgument < arguments.size()) { entry = arguments.get(currentArgument); final Object value; if (entry.isEnumType()) { value = toEnumValue(entry, arg); if (value == null) { return null; } } else { value = arg; } set(t, entry, value); if(!entry.isCollection()) { currentArgument++; } else { collection = true; } } else { valid = false; } } if (collection) { currentArgument++; } final boolean validArgumentCount = currentArgument >= nonOptionalArgumentCount && currentArgument <= arguments.size(); if(forceValid || (valid && validArgumentCount)) { return t; } else { return null; } } /** * Change the line length for the usage. * * @param lineLength Line length of the terminal window. */ public void setLineLength(int lineLength) { this.lineLength = lineLength; } /** * Prints the usage to the given output stream. * * @param appName Name of the application. * @param outputStream Outputstream to use. * @throws IOException If an error occurs while printing the usage. */ public void printUsage(String appName, OutputStream outputStream) throws IOException { printUsage(appName, new OutputStreamWriter(outputStream, Charset.defaultCharset())); } /** * Prints the usage to the given writer. * * @param appName Name of the application. * @param writer Writer to use. * @throws IOException If an error occurs while printing the usage. */ public void printUsage(String appName, Writer writer) throws IOException { final String lineSeparator = System.getProperty("line.separator"); writer.write("Usage: java -jar "); writer.write(appName); if(!options.isEmpty()) { writer.write(" [options]"); } final ArrayList enums = new ArrayList(); for(final ArgumentParserEntry entry : arguments) { if (!entry.isOptional()) { writer.write(" <"); writer.write(entry.getField().getName()); writer.write(">"); } else { writer.write(" ["); writer.write(entry.getField().getName()); writer.write("]"); } if(entry.isCollection()) { writer.write(" [...]"); } if(entry.isEnumType()) { enums.add(entry); } } writer.write(lineSeparator); if(!enums.isEmpty()) { for(final ArgumentParserEntry entry : enums) { writer.write(capitalize(plurialize(entry.getField().getName()))); writer.write(lineSeparator); for (final Object constant : entry.getField().getType().getEnumConstants()) { final Enum enumConstant = (Enum)constant; writer.write(" "); writer.write(enumConstant.name().toLowerCase()); writer.write(lineSeparator); } } } if(!options.isEmpty()) { writer.write("Options"); writer.write(lineSeparator); final LinkedHashSet entries = new LinkedHashSet(); for(final Map.Entry entry : options.entrySet()) { entries.add(entry.getValue()); } for(final ArgumentParserEntry entry : entries) { final String name = entry.getAlias(); writer.write(" "); writer.write(entry.getShortName()); writer.write(", --" + name); writer.write(createSpace(maxOptionNameLength - name.length() + 1)); final String[] lines = cleanCut(entry.getDescription()); for(final String line : lines) { writer.write(line); writer.write(lineSeparator); } } } writer.flush(); } private I newInstance(Class clazz) { try { return clazz.newInstance(); } catch (InstantiationException ex) { Logger.getLogger(ArgumentParser.class.getName()).log(Level.SEVERE, "Impossible d'instancier '" + clazz +"'. Chargement des arguments impossible.", ex); } catch (IllegalAccessException ex) { Logger.getLogger(ArgumentParser.class.getName()).log(Level.SEVERE, "Impossible d'instancier '" + clazz +"'. Chargement des arguments impossible.", ex); } return null; } private void set(T t, ArgumentParserEntry entry, V value) { try { if(!entry.isCollection()) { entry.getField().set(t, value); } else { Object object = entry.getField().get(t); if(object == null) { object = newInstance(entry.getField().getType()); entry.getField().set(t, object); } if (object instanceof Collection) { final Collection collection = (Collection) object; collection.add(value); } } } catch (SecurityException ex) { Logger.getLogger(ArgumentParser.class.getName()).log(Level.SEVERE, "Accès non autorisé au champ '" + entry.getField().getName() + "'.", ex); } catch (IllegalArgumentException ex) { Logger.getLogger(ArgumentParser.class.getName()).log(Level.SEVERE, "Le champ '" + entry.getField().getName() + "' n'est pas compatible avec le type '" + value.getClass().getName() + "'.", ex); } catch (IllegalAccessException ex) { Logger.getLogger(ArgumentParser.class.getName()).log(Level.SEVERE, "Accès non autorisé au champ '" + entry.getField().getName() + "'.", ex); } } private String createSpace(int length) { final char[] chars = new char[length]; Arrays.fill(chars, ' '); return new String(chars); } private String[] cleanCut(String description) { final ArrayList lines = new ArrayList(); final StringBuilder stringBuilder = new StringBuilder(description); final int maxLength = lineLength - PREFIX_LENGTH - maxOptionNameLength; while(stringBuilder.length() > maxLength) { int cutIndex = stringBuilder.lastIndexOf(" ", maxLength); if(cutIndex == -1) { cutIndex = maxLength; } lines.add((lines.isEmpty() ? "" : createSpace(PREFIX_LENGTH + maxOptionNameLength)) + stringBuilder.substring(0, cutIndex)); stringBuilder.replace(0, cutIndex + 1, ""); } lines.add((lines.isEmpty() ? "" : createSpace(PREFIX_LENGTH + maxOptionNameLength)) + stringBuilder.toString()); return lines.toArray(new String[0]); } private String capitalize(final String source) { return Character.toUpperCase(source.charAt(0)) + source.substring(1); } private String plurialize(final String source) { if (source.endsWith("y")) { return source.substring(0, source.length() - 2) + "ies"; } else { return source + 's'; } } private > T toEnumValue(ArgumentParserEntry entry, String value) { @SuppressWarnings("unchecked") final Class enumType = (Class)entry.getField().getType(); try { return Enum.valueOf(enumType, value.toUpperCase()); } catch (IllegalArgumentException e) { // Ignored. } return null; } }