urls = findResources(script.getName());
+
+ // Handle search failure
+ Assert.notNull(urls, "Unexpected error looking for '" + script.getName() + "'");
+
+ // Handle the search being OK but the file simply not being present
+ Assert.notEmpty(urls, "Script '" + script + "' not found on disk or in classpath");
+ Assert.isTrue(urls.size() == 1, "More than one '" + script + "' was found in the classpath; unable to continue");
+ try {
+ return urls.iterator().next().openStream();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ /**
+ * Execute the single line from a script.
+ *
+ * This method can be overridden by sub-classes to pre-process script lines.
+ */
+ protected boolean executeScriptLine(final String line) {
+ return executeCommand(line);
+ }
+
+ public boolean executeCommand(String line) {
+ // Another command was attempted
+ setShellStatus(ShellStatus.Status.PARSING);
+
+ final ExecutionStrategy executionStrategy = getExecutionStrategy();
+ boolean flashedMessage = false;
+ while (executionStrategy == null || !executionStrategy.isReadyForCommands()) {
+ // Wait
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ignore) {}
+ if (!flashedMessage) {
+ flash(Level.INFO, "Please wait - still loading", MY_SLOT);
+ flashedMessage = true;
+ }
+ }
+ if (flashedMessage) {
+ flash(Level.INFO, "", MY_SLOT);
+ }
+
+ ParseResult parseResult = null;
+ try {
+ // We support simple block comments; ie a single pair per line
+ if (!inBlockComment && line.contains("/*") && line.contains("*/")) {
+ blockCommentBegin();
+ String lhs = line.substring(0, line.lastIndexOf("/*"));
+ if (line.contains("*/")) {
+ line = lhs + line.substring(line.lastIndexOf("*/") + 2);
+ blockCommentFinish();
+ } else {
+ line = lhs;
+ }
+ }
+ if (inBlockComment) {
+ if (!line.contains("*/")) {
+ return true;
+ }
+ blockCommentFinish();
+ line = line.substring(line.lastIndexOf("*/") + 2);
+ }
+ // We also support inline comments (but only at start of line, otherwise valid
+ // command options like http://www.helloworld.com will fail as per ROO-517)
+ if (!inBlockComment && (line.trim().startsWith("//") || line.trim().startsWith("#"))) { // # support in ROO-1116
+ line = "";
+ }
+ // Convert any TAB characters to whitespace (ROO-527)
+ line = line.replace('\t', ' ');
+ if ("".equals(line.trim())) {
+ setShellStatus(Status.EXECUTION_SUCCESS);
+ return true;
+ }
+ parseResult = getParser().parse(line);
+ if (parseResult == null) {
+ return false;
+ }
+
+ setShellStatus(Status.EXECUTING);
+ Object result = executionStrategy.execute(parseResult);
+ setShellStatus(Status.EXECUTION_RESULT_PROCESSING);
+ if (result != null) {
+ if (result instanceof ExitShellRequest) {
+ exitShellRequest = (ExitShellRequest) result;
+ // Give ProcessManager a chance to close down its threads before the overall OSGi framework is terminated (ROO-1938)
+ executionStrategy.terminate();
+ } else if (result instanceof Iterable>) {
+ for (Object o : (Iterable>) result) {
+ logger.info(o.toString());
+ }
+ } else {
+ logger.info(result.toString());
+ }
+ }
+
+ logCommandIfRequired(line, true);
+ setShellStatus(Status.EXECUTION_SUCCESS, line, parseResult);
+ return true;
+ } catch (RuntimeException e) {
+ setShellStatus(Status.EXECUTION_FAILED, line, parseResult);
+ // We rely on execution strategy to log it
+ try {
+ logCommandIfRequired(line, false);
+ } catch (Exception ignored) {}
+ return false;
+ } finally {
+ setShellStatus(Status.USER_INPUT);
+ }
+ }
+
+ /**
+ * Allows a subclass to log the execution of a well-formed command. This is invoked after a command
+ * has completed, and indicates whether the command returned normally or returned an exception. Note
+ * that attempted commands that are not well-formed (eg they are missing a mandatory argument) will
+ * never be presented to this method, as the command execution is never actually attempted in those
+ * cases. This method is only invoked if an attempt is made to execute a particular command.
+ *
+ *
+ * Implementations should consider specially handling the "script" commands, and also
+ * indicating whether a command was successful or not. Implementations that wish to behave
+ * consistently with other {@link AbstractShell} subclasses are encouraged to simply override
+ * {@link #logCommandToOutput(String)} instead, and only override this method if you actually
+ * need to fine-tune the output logic.
+ *
+ * @param line the parsed line (any comments have been removed; never null)
+ * @param successful if the command was successful or not
+ */
+ protected void logCommandIfRequired(final String line, final boolean successful) {
+ if (line.startsWith("script")) {
+ logCommandToOutput((successful ? "// " : "// [failed] ") + line);
+ } else {
+ logCommandToOutput((successful ? "" : "// [failed] ") + line);
+ }
+ }
+
+ /**
+ * Allows a subclass to actually write the resulting logged command to some form of output. This
+ * frees subclasses from needing to implement the logic within {@link #logCommandIfRequired(String, boolean)}.
+ *
+ *
+ * Implementations should invoke {@link #getExitShellRequest()} to monitor any attempts to exit the shell and
+ * release resources such as output log files.
+ *
+ * @param processedLine the line that should be appended to some type of output (excluding the \n character)
+ */
+ protected void logCommandToOutput(final String processedLine) {}
+
+ /**
+ * Base implementation of the {@link Shell#setPromptPath(String)} method, designed for simple shell
+ * implementations. Advanced implementations (eg those that support ANSI codes etc) will likely want
+ * to override this method and set the {@link #shellPrompt} variable directly.
+ *
+ * @param path to set (can be null or empty; must NOT be formatted in any special way eg ANSI codes)
+ */
+ public void setPromptPath(final String path) {
+ if (path == null || "".equals(path)) {
+ shellPrompt = ROO_PROMPT;
+ } else {
+ shellPrompt = path + " " + ROO_PROMPT;
+ }
+ }
+
+ /**
+ * Default implementation of {@link Shell#setPromptPath(String, boolean))} method to satisfy STS compatibility.
+ *
+ * @param path to set (can be null or empty)
+ * @param overrideStyle
+ */
+ public void setPromptPath(String path, boolean overrideStyle) {
+ setPromptPath(path);
+ }
+
+ public ExitShellRequest getExitShellRequest() {
+ return exitShellRequest;
+ }
+
+ @CliCommand(value = { "//", ";" }, help = "Inline comment markers (start of line only)")
+ public void inlineComment() {}
+
+ @CliCommand(value = { "/*" }, help = "Start of block comment")
+ public void blockCommentBegin() {
+ Assert.isTrue(!inBlockComment, "Cannot open a new block comment when one already active");
+ inBlockComment = true;
+ }
+
+ @CliCommand(value = { "*/" }, help = "End of block comment")
+ public void blockCommentFinish() {
+ Assert.isTrue(inBlockComment, "Cannot close a block comment when it has not been opened");
+ inBlockComment = false;
+ }
+
+ @CliCommand(value = { "system properties" }, help = "Shows the shell's properties")
+ public String props() {
+ final Set data = new TreeSet(); // For repeatability
+ for (final Entry entry : System.getProperties().entrySet()) {
+ data.add(entry.getKey() + " = " + entry.getValue());
+ }
+
+ return StringUtils.collectionToDelimitedString(data, LINE_SEPARATOR) + LINE_SEPARATOR;
+ }
+
+ @CliCommand(value = { "date" }, help = "Displays the local date and time")
+ public String date() {
+ return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date());
+ }
+
+ @CliCommand(value = { "flash test" }, help = "Tests message flashing")
+ public void flashCustom() throws Exception {
+ flash(Level.FINE, "Hello world", "a");
+ Thread.sleep(150);
+ flash(Level.FINE, "Short world", "a");
+ Thread.sleep(150);
+ flash(Level.FINE, "Small", "a");
+ Thread.sleep(150);
+ flash(Level.FINE, "Downloading xyz", "b");
+ Thread.sleep(150);
+ flash(Level.FINE, "", "a");
+ Thread.sleep(150);
+ flash(Level.FINE, "Downloaded xyz", "b");
+ Thread.sleep(150);
+ flash(Level.FINE, "System online", "c");
+ Thread.sleep(150);
+ flash(Level.FINE, "System ready", "c");
+ Thread.sleep(150);
+ flash(Level.FINE, "System farewell", "c");
+ Thread.sleep(150);
+ flash(Level.FINE, "", "c");
+ Thread.sleep(150);
+ flash(Level.FINE, "", "b");
+ }
+
+ @CliCommand(value = { "version" }, help = "Displays shell version")
+ public String version(@CliOption(key = "", help = "Special version flags") final String extra) {
+ StringBuilder sb = new StringBuilder();
+
+ if ("jaime".equals(extra)) {
+ sb.append(" /\\ /l").append(LINE_SEPARATOR);
+ sb.append(" ((.Y(!").append(LINE_SEPARATOR);
+ sb.append(" \\ |/").append(LINE_SEPARATOR);
+ sb.append(" / 6~6,").append(LINE_SEPARATOR);
+ sb.append(" \\ _ +-.").append(LINE_SEPARATOR);
+ sb.append(" \\`-=--^-' \\").append(LINE_SEPARATOR);
+ sb.append(" \\ \\ |\\--------------------------+").append(LINE_SEPARATOR);
+ sb.append(" _/ \\ | Thanks for loading Roo! |").append(LINE_SEPARATOR);
+ sb.append(" ( . Y +---------------------------+").append(LINE_SEPARATOR);
+ sb.append(" /\"\\ `---^--v---.").append(LINE_SEPARATOR);
+ sb.append(" / _ `---\"T~~\\/~\\/").append(LINE_SEPARATOR);
+ sb.append(" / \" ~\\. !").append(LINE_SEPARATOR);
+ sb.append(" _ Y Y.~~~ /'").append(LINE_SEPARATOR);
+ sb.append(" Y^| | | Roo 7").append(LINE_SEPARATOR);
+ sb.append(" | l | / . /'").append(LINE_SEPARATOR);
+ sb.append(" | `L | Y .^/ ~T").append(LINE_SEPARATOR);
+ sb.append(" | l ! | |/ | | ____ ____ ____").append(LINE_SEPARATOR);
+ sb.append(" | .`\\/' | Y | ! / __ \\/ __ \\/ __ \\").append(LINE_SEPARATOR);
+ sb.append(" l \"~ j l j L______ / /_/ / / / / / / /").append(LINE_SEPARATOR);
+ sb.append(" \\,____{ __\"\" ~ __ ,\\_,\\_ / _, _/ /_/ / /_/ /").append(LINE_SEPARATOR);
+ sb.append(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /_/ |_|\\____/\\____/").append(" ").append(versionInfo()).append(LINE_SEPARATOR);
+ return sb.toString();
+ }
+
+ sb.append(" ____ ____ ____ ").append(LINE_SEPARATOR);
+ sb.append(" / __ \\/ __ \\/ __ \\ ").append(LINE_SEPARATOR);
+ sb.append(" / /_/ / / / / / / / ").append(LINE_SEPARATOR);
+ sb.append(" / _, _/ /_/ / /_/ / ").append(LINE_SEPARATOR);
+ sb.append("/_/ |_|\\____/\\____/ ").append(" ").append(versionInfo()).append(LINE_SEPARATOR);
+ sb.append(LINE_SEPARATOR);
+
+ return sb.toString();
+ }
+
+ public static String versionInfo() {
+ // Try to determine the bundle version
+ String bundleVersion = null;
+ String gitCommitHash = null;
+ JarFile jarFile = null;
+ try {
+ URL classContainer = AbstractShell.class.getProtectionDomain().getCodeSource().getLocation();
+ if (classContainer.toString().endsWith(".jar")) {
+ // Attempt to obtain the "Bundle-Version" version from the manifest
+ jarFile = new JarFile(new File(classContainer.toURI()), false);
+ ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF");
+ Manifest manifest = new Manifest(jarFile.getInputStream(manifestEntry));
+ bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version");
+ gitCommitHash = manifest.getMainAttributes().getValue("Git-Commit-Hash");
+ }
+ } catch (IOException ignoreAndMoveOn) {
+ } catch (URISyntaxException ignoreAndMoveOn) {
+ } finally {
+ IOUtils.closeQuietly(jarFile);
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ if (bundleVersion != null) {
+ sb.append(bundleVersion);
+ }
+
+ if (gitCommitHash != null && gitCommitHash.length() > 7) {
+ if (sb.length() > 0) {
+ sb.append(" "); // to separate from version
+ }
+ sb.append("[rev ");
+ sb.append(gitCommitHash.substring(0,7));
+ sb.append("]");
+ }
+
+ if (sb.length() == 0) {
+ sb.append("UNKNOWN VERSION");
+ }
+
+ return sb.toString();
+ }
+
+ public String getShellPrompt() {
+ return shellPrompt;
+ }
+
+ /**
+ * Obtains the home directory for the current shell instance.
+ *
+ *
+ * Note: calls the {@link #getHomeAsString()} method to allow subclasses to provide the home directory location as
+ * string using different environment-specific strategies.
+ *
+ *
+ * If the path indicated by {@link #getHomeAsString()} exists and refers to a directory, that directory
+ * is returned.
+ *
+ *
+ * If the path indicated by {@link #getHomeAsString()} exists and refers to a file, an exception is thrown.
+ *
+ *
+ * If the path indicated by {@link #getHomeAsString()} does not exist, it will be created as a directory.
+ * If this fails, an exception will be thrown.
+ *
+ * @return the home directory for the current shell instance (which is guaranteed to exist and be a directory)
+ */
+ public File getHome() {
+ String rooHome = getHomeAsString();
+ File f = new File(rooHome);
+ Assert.isTrue(!f.exists() || (f.exists() && f.isDirectory()), "Path '" + f.getAbsolutePath() + "' must be a directory, or it must not exist");
+ if (!f.exists()) {
+ f.mkdirs();
+ }
+ Assert.isTrue(f.exists() && f.isDirectory(), "Path '" + f.getAbsolutePath() + "' is not a directory; please specify roo.home system property correctly");
+ return f;
+ }
+
+ /**
+ * Simple implementation of {@link #flash(Level, String, String)} that simply displays the message via the logger. It is
+ * strongly recommended shell implementations override this method with a more effective approach.
+ */
+ public void flash(final Level level, final String message, final String slot) {
+ Assert.notNull(level, "Level is required for a flash message");
+ Assert.notNull(message, "Message is required for a flash message");
+ Assert.hasText(slot, "Slot name must be specified for a flash message");
+ if (!("".equals(message))) {
+ logger.log(level, message);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java b/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java
new file mode 100644
index 00000000..207c2934
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java
@@ -0,0 +1,37 @@
+package org.springframework.roo.shell;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a method that can indicate whether a particular command is presently
+ * available or not.
+ *
+ *
+ * This annotation must only be applied to a public no-argument method that returns primitive boolean.
+ * The method should be inexpensive to evaluate, as this method can be called very
+ * frequently. If expensive operations are necessary to compute command availability,
+ * it is suggested the method return a boolean field that is maintained using the observer
+ * pattern.
+ *
+ *
+ * It is possible that a particular availability method might be able to represent the
+ * availability status of multiple commands. As such, an availability indicator annotation
+ * will indicate the commands that it applies to. If a specific command has multiple
+ * aliases (ie by using an array for {@link CliCommand#value()}), only one of the commands
+ * need to be specified in the {@link CliAvailabilityIndicator} annotation.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface CliAvailabilityIndicator {
+
+ /**
+ * @return the name of the command or commands that this availability indicator represents
+ */
+ String[] value();
+}
diff --git a/src/main/java/org/springframework/roo/shell/CliCommand.java b/src/main/java/org/springframework/roo/shell/CliCommand.java
new file mode 100644
index 00000000..67c091b5
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/CliCommand.java
@@ -0,0 +1,22 @@
+package org.springframework.roo.shell;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface CliCommand {
+
+ /**
+ * @return one or more strings which must serve as the start of a particular command in order to match this method
+ * (these must be unique within the entire application; if not unique, behaviour is not specified)
+ */
+ String[] value();
+
+ /**
+ * @return a help message for this command (the default is a blank String, which means there is no help)
+ */
+ String help() default "";
+}
diff --git a/src/main/java/org/springframework/roo/shell/CliOption.java b/src/main/java/org/springframework/roo/shell/CliOption.java
new file mode 100644
index 00000000..8b370ce9
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/CliOption.java
@@ -0,0 +1,63 @@
+package org.springframework.roo.shell;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface CliOption {
+
+ /**
+ * @return if true, the user cannot specify this option and it is provided by the shell infrastructure
+ * (defaults to false)
+ */
+ boolean systemProvided() default false;
+
+ /**
+ * @return the name of the option, which must be unique within this {@link CliCommand} (an empty String may
+ * be given, which would denote this option is the default for the command)
+ */
+ String[] key();
+
+ /**
+ * @return true if this option must be specified one way or the other by the user (defaults to false)
+ */
+ boolean mandatory() default false;
+
+ /**
+ * @return the default value to use if this option is unspecified by the user (defaults to __NULL__, which causes null to
+ * be presented to any non-primitive parameter)
+ */
+ String unspecifiedDefaultValue() default "__NULL__";
+
+ /**
+ * @return the default value to use if this option is included by the user, but they didn't specify an
+ * actual value (most commonly used for flags; defaults to __NULL__, which causes null to
+ * be presented to any non-primitive parameter)
+ */
+ String specifiedDefaultValue() default "__NULL__";
+
+ /**
+ * Returns a string providing context-specific information (e.g. a comma-delimited
+ * set of keywords) to the {@link Converter} that handles the annotated parameter's type.
+ *
+ * For example, if a method parameter "thing" of type "Thing" is annotated as
+ * follows:
+ *
@CliOption(..., optionContext = "foo,bar", ...) Thing thing
+ * ... then the {@link Converter} that converts the text entered by the user
+ * into an instance of Thing will be passed "foo,bar" as the value of the
+ * optionContext parameter in its public methods. This allows
+ * the behaviour of that Converter to be individually customised for each
+ * {@link CliOption} of each {@link CliCommand}.
+ *
+ * @return a non-null string (can be empty)
+ */
+ String optionContext() default "";
+
+ /**
+ * @return a help message for this option (the default is a blank String, which means there is no help)
+ */
+ String help() default "";
+}
diff --git a/src/main/java/org/springframework/roo/shell/CliOptionContext.java b/src/main/java/org/springframework/roo/shell/CliOptionContext.java
new file mode 100644
index 00000000..895eeb4a
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/CliOptionContext.java
@@ -0,0 +1,40 @@
+package org.springframework.roo.shell;
+
+/**
+ * Utility methods relating to shell option contexts
+ */
+public final class CliOptionContext {
+
+ // Class fields
+ private static ThreadLocal optionContextHolder = new ThreadLocal();
+
+ /**
+ * Returns the option context for the current thread.
+ *
+ * @return null if none has been set
+ */
+ public static String getOptionContext() {
+ return optionContextHolder.get();
+ }
+
+ /**
+ * Stores the given option context for the current thread.
+ *
+ * @param optionContext the option context to store
+ */
+ public static void setOptionContext(final String optionContext) {
+ optionContextHolder.set(optionContext);
+ }
+
+ /**
+ * Resets the option context for the current thread.
+ */
+ public static void resetOptionContext() {
+ optionContextHolder.remove();
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private CliOptionContext() {}
+}
diff --git a/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java b/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java
new file mode 100644
index 00000000..c0496c47
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java
@@ -0,0 +1,27 @@
+package org.springframework.roo.shell;
+
+/**
+ * Utility methods relating to shell simple parser contexts.
+ */
+public final class CliSimpleParserContext {
+
+ // Class fields
+ private static ThreadLocal simpleParserContextHolder = new ThreadLocal();
+
+ public static Parser getSimpleParserContext() {
+ return simpleParserContextHolder.get();
+ }
+
+ public static void setSimpleParserContext(final SimpleParser simpleParserContext) {
+ simpleParserContextHolder.set(simpleParserContext);
+ }
+
+ public static void resetSimpleParserContext() {
+ simpleParserContextHolder.remove();
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private CliSimpleParserContext() {}
+}
diff --git a/src/main/java/org/springframework/roo/shell/CommandMarker.java b/src/main/java/org/springframework/roo/shell/CommandMarker.java
new file mode 100644
index 00000000..81bb3adc
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/CommandMarker.java
@@ -0,0 +1,6 @@
+package org.springframework.roo.shell;
+
+/**
+ * Marker interface indicating a provider of one or more shell commands.
+ */
+public interface CommandMarker {}
diff --git a/src/main/java/org/springframework/roo/shell/Completion.java b/src/main/java/org/springframework/roo/shell/Completion.java
new file mode 100644
index 00000000..eb1e3994
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/Completion.java
@@ -0,0 +1,92 @@
+package org.springframework.roo.shell;
+
+import org.springframework.roo.support.util.AnsiEscapeCode;
+import org.springframework.roo.support.util.StringUtils;
+
+public class Completion {
+
+ // Fields
+ private final int order;
+ private final String formattedValue;
+ private final String heading;
+ private final String value;
+
+ /**
+ * Constructor
+ *
+ * @param value
+ */
+ public Completion(final String value) {
+ this(value, value, null, 0);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param value
+ * @param formattedValue
+ * @param heading
+ * @param order
+ */
+ public Completion(final String value, final String formattedValue, String heading, final int order) {
+ this.formattedValue = formattedValue;
+ this.order = order;
+ this.value = value;
+ if (StringUtils.hasText(heading)) {
+ heading = AnsiEscapeCode.decorate(heading, AnsiEscapeCode.UNDERSCORE, AnsiEscapeCode.FG_GREEN);
+ }
+ this.heading = heading;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getFormattedValue() {
+ return formattedValue;
+ }
+
+ public String getHeading() {
+ return heading;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public String toString() {
+ return order + ". " + heading + " - " + value;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final Completion that = (Completion) o;
+ if (formattedValue != null ? !formattedValue.equals(that.formattedValue) : that.formattedValue != null) {
+ return false;
+ }
+ if (heading != null ? !heading.equals(that.heading) : that.heading != null) {
+ return false;
+ }
+ if (value != null ? !value.equals(that.value) : that.value != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = value != null ? value.hashCode() : 0;
+ result = 31 * result + (formattedValue != null ? formattedValue.hashCode() : 0);
+ result = 31 * result + (heading != null ? heading.hashCode() : 0);
+ return result;
+ }
+}
+
diff --git a/src/main/java/org/springframework/roo/shell/Converter.java b/src/main/java/org/springframework/roo/shell/Converter.java
new file mode 100644
index 00000000..944deef9
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/Converter.java
@@ -0,0 +1,58 @@
+package org.springframework.roo.shell;
+
+import java.util.List;
+
+/**
+ * Converts between Strings (as displayed by and entered via the shell) and Java objects
+ *
+ * @author Ben Alex
+ * @param the type being converted to/from
+ */
+public interface Converter {
+
+ /**
+ * Indicates whether this converter supports the given type in the given option context
+ *
+ * @param type the type being checked
+ * @param optionContext a non-null string that customises the
+ * behaviour of this converter for a given {@link CliOption} of a given
+ * {@link CliCommand}; the contents will have special meaning to this
+ * converter (e.g. be a comma-separated list of keywords known to this
+ * converter)
+ * @return see above
+ */
+ boolean supports(Class> type, String optionContext);
+
+ /**
+ * Converts from the given String value to type T
+ *
+ * @param value the value to convert
+ * @param targetType the type being converted to; can't be null
+ * @param optionContext a non-null string that customises the
+ * behaviour of this converter for a given {@link CliOption} of a given
+ * {@link CliCommand}; the contents will have special meaning to this
+ * converter (e.g. be a comma-separated list of keywords known to this
+ * converter)
+ * @return see above
+ * @throws RuntimeException if the given value could not be converted
+ */
+ T convertFromText(String value, Class> targetType, String optionContext);
+
+ /**
+ * Populates the given list with the possible completions
+ *
+ * @param completions the list to populate; can't be null
+ * @param targetType the type of parameter for which a string is being entered
+ * @param existingData what the user has typed so far
+ * @param optionContext a non-null string that customises the
+ * behaviour of this converter for a given {@link CliOption} of a given
+ * {@link CliCommand}; the contents will have special meaning to this
+ * converter (e.g. be a comma-separated list of keywords known to this
+ * converter)
+ * @param target
+ * @return true if all the added completions are complete
+ * values, or false if the user can press TAB to add further
+ * information to some or all of them
+ */
+ boolean getAllPossibleValues(List completions, Class> targetType, String existingData, String optionContext, MethodTarget target);
+}
diff --git a/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java b/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java
new file mode 100644
index 00000000..39957cdb
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java
@@ -0,0 +1,39 @@
+package org.springframework.roo.shell;
+
+/**
+ * Strategy interface to permit the controlled execution of methods.
+ *
+ *
+ * This interface is used to enable a {@link Shell} to execute methods in a consistent, system-wide
+ * manner. A typical use case is to ensure user interface commands are not executed concurrently
+ * when other background threads are performing certain operations.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ *
+ */
+public interface ExecutionStrategy {
+
+ /**
+ * Executes the method indicated by the {@link ParseResult}.
+ *
+ * @param parseResult that should be executed (never presented as null)
+ * @return an object which will be rendered by the {@link Shell} implementation (may return null)
+ * @throws RuntimeException which is handled by the {@link Shell} implementation
+ */
+ Object execute(ParseResult parseResult) throws RuntimeException;
+
+ /**
+ * Indicates commands are able to be presented. This generally means all important
+ * system startup activities have completed.
+ *
+ * @return whether commands can be presented for processing at this time
+ */
+ boolean isReadyForCommands();
+
+ /**
+ * Indicates the execution runtime should be terminated. This allows it to cleanup before returning
+ * control flow to the caller. Necessary for clean shutdowns.
+ */
+ void terminate();
+}
diff --git a/src/main/java/org/springframework/roo/shell/ExitShellRequest.java b/src/main/java/org/springframework/roo/shell/ExitShellRequest.java
new file mode 100644
index 00000000..08746dc3
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/ExitShellRequest.java
@@ -0,0 +1,29 @@
+package org.springframework.roo.shell;
+
+/**
+ * An immutable representation of a request to exit the shell.
+ *
+ *
+ * Implementations of the shell are free to handle these requests in whatever
+ * way they wish. Callers should not expect an exit request to be completed.
+ *
+ * @author Ben Alex
+ */
+public class ExitShellRequest {
+
+ // Constants
+ public static final ExitShellRequest NORMAL_EXIT = new ExitShellRequest(0);
+ public static final ExitShellRequest FATAL_EXIT = new ExitShellRequest(1);
+ public static final ExitShellRequest JVM_TERMINATED_EXIT = new ExitShellRequest(99); // Ensure 99 is maintained in o.s.r.bootstrap.Main as it's the default for a null roo.exit code
+
+ // Fields
+ private final int exitCode;
+
+ private ExitShellRequest(final int exitCode) {
+ this.exitCode = exitCode;
+ }
+
+ public int getExitCode() {
+ return exitCode;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/MethodTarget.java b/src/main/java/org/springframework/roo/shell/MethodTarget.java
new file mode 100644
index 00000000..959e317b
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/MethodTarget.java
@@ -0,0 +1,110 @@
+package org.springframework.roo.shell;
+
+import java.lang.reflect.Method;
+
+import org.springframework.roo.support.style.ToStringCreator;
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.ObjectUtils;
+import org.springframework.roo.support.util.StringUtils;
+
+/**
+ * A method that can be executed via a shell command.
+ *
+ * Immutable since 1.2.0.
+ *
+ * @author Ben Alex
+ */
+public class MethodTarget {
+
+ // Fields
+ private final Method method;
+ private final Object target;
+ private final String remainingBuffer;
+ private final String key;
+
+ /**
+ * Constructor for a null remainingBuffer and key
+ *
+ * @param method the method to invoke (required)
+ * @param target the object on which the method is to be invoked (required)
+ * @since 1.2.0
+ */
+ public MethodTarget(final Method method, final Object target) {
+ this(method, target, null, null);
+ }
+
+ /**
+ * Constructor that allows all fields to be set
+ *
+ * @param method the method to invoke (required)
+ * @param target the object on which the method is to be invoked (required)
+ * @param remainingBuffer can be blank
+ * @param key can be blank
+ * @since 1.2.0
+ */
+ public MethodTarget(final Method method, final Object target, final String remainingBuffer, final String key) {
+ Assert.notNull(method, "Method is required");
+ Assert.notNull(target, "Target is required");
+ this.key = StringUtils.trimToEmpty(key);
+ this.method = method;
+ this.remainingBuffer = StringUtils.trimToEmpty(remainingBuffer);
+ this.target = target;
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof MethodTarget)) {
+ return false;
+ }
+ final MethodTarget otherMethodTarget = (MethodTarget) other;
+ return this.method.equals(otherMethodTarget.getMethod()) && this.target.equals(otherMethodTarget.getTarget());
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.nullSafeHashCode(method, target);
+ }
+
+ @Override
+ public final String toString() {
+ final ToStringCreator tsc = new ToStringCreator(this);
+ tsc.append("target", target);
+ tsc.append("method", method);
+ tsc.append("remainingBuffer", remainingBuffer);
+ tsc.append("key", key);
+ return tsc.toString();
+ }
+
+ /**
+ * @since 1.2.0
+ */
+ public String getKey() {
+ return this.key;
+ }
+
+ /**
+ * @return a non-null method
+ * @since 1.2.0
+ */
+ public Method getMethod() {
+ return this.method;
+ }
+
+ /**
+ * @since 1.2.0
+ */
+ public String getRemainingBuffer() {
+ return this.remainingBuffer;
+ }
+
+ /**
+ * @return a non-null Object
+ * @since 1.2.0
+ */
+ public Object getTarget() {
+ return this.target;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java b/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java
new file mode 100644
index 00000000..eb4900a8
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java
@@ -0,0 +1,178 @@
+package org.springframework.roo.shell;
+
+import java.util.Comparator;
+
+/**
+ * NaturalOrderComparator.java -- Perform natural order comparisons of strings in Java.
+ * Copyright (C) 2003 by Pierre-Luc Paour
+ * Based on the C version by Martin Pool, of which this is more or less a straight conversion.
+ * Copyright (C) 2000 by Martin Pool
+ *
+ * This software is provided as-is, without any express or implied
+ * warranty. In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ * claim that you wrote the original software. If you use this software
+ * in a product, an acknowledgement in the product documentation would be
+ * appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ * misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+public class NaturalOrderComparator implements Comparator {
+
+ /**
+ * Returns the character at the given position of the given string;
+ * equivalent to {@link String#charAt(int)}, but handles overly large
+ * indices.
+ *
+ * @param s the string to read (can't be null)
+ * @param i the index at which to read (zero-based)
+ * @return 0 if the given index is beyond the end of the string
+ */
+ static char charAt(final String s, final int i) {
+ if (i >= s.length()) {
+ return 0;
+ }
+ return s.charAt(i);
+ }
+
+ /**
+ * Indicates whether the given character is whitespace
+ *
+ * @param c the character to check
+ * @return see above
+ */
+ public static boolean isSpace(final char c) {
+ switch (c) {
+ case ' ':
+ return true;
+ case '\n':
+ return true;
+ case '\t':
+ return true;
+ case '\f':
+ return true;
+ case '\r':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ int compareRight(final String a, final String b) {
+ int bias = 0;
+ int ia = 0;
+ int ib = 0;
+
+ // The longest run of digits wins. That aside, the greatest
+ // value wins, but we can't know that it will until we've scanned
+ // both numbers to know that they have the same magnitude, so we
+ // remember it in BIAS.
+ for (; ; ia++, ib++) {
+ char ca = charAt(a, ia);
+ char cb = charAt(b, ib);
+
+ if (!Character.isDigit(ca) && !Character.isDigit(cb)) {
+ return bias;
+ } else if (!Character.isDigit(ca)) {
+ return -1;
+ } else if (!Character.isDigit(cb)) {
+ return +1;
+ } else if (ca < cb) {
+ if (bias == 0) {
+ bias = -1;
+ }
+ } else if (ca > cb) {
+ if (bias == 0)
+ bias = +1;
+ } else if (ca == 0 && cb == 0) {
+ return bias;
+ }
+ }
+ }
+
+ protected String stringify(final E object) {
+ return object.toString();
+ }
+
+ public int compare(final E o1, final E o2) {
+ if (o1 == null && o2 == null) {
+ return 1;
+ }
+
+ if (o1 == null) {
+ return 1;
+ }
+
+ if (o2 == null) {
+ return -1;
+ }
+
+ String a = stringify(o1);
+ String b = stringify(o2);
+
+ int ia = 0, ib = 0;
+ int nza = 0, nzb = 0;
+ char ca, cb;
+ int result;
+
+ while (true) {
+ // Only count the number of zeroes leading the last number compared
+ nza = nzb = 0;
+
+ ca = charAt(a, ia);
+ cb = charAt(b, ib);
+
+ // Skip over leading spaces or zeros
+ while (isSpace(ca) || ca == '0') {
+ if (ca == '0') {
+ nza++;
+ } else {
+ // Only count consecutive zeroes
+ nza = 0;
+ }
+
+ ca = charAt(a, ++ia);
+ }
+
+ while (isSpace(cb) || cb == '0') {
+ if (cb == '0') {
+ nzb++;
+ } else {
+ // Only count consecutive zeroes
+ nzb = 0;
+ }
+
+ cb = charAt(b, ++ib);
+ }
+
+ // Process run of digits
+ if (Character.isDigit(ca) && Character.isDigit(cb)) {
+ if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0) {
+ return result;
+ }
+ }
+
+ if (ca == 0 && cb == 0) {
+ // The strings compare the same. Perhaps the caller
+ // will want to call strcmp to break the tie.
+ return nza - nzb;
+ }
+
+ if (ca < cb) {
+ return -1;
+ } else if (ca > cb) {
+ return +1;
+ }
+
+ ++ia;
+ ++ib;
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/ParseResult.java b/src/main/java/org/springframework/roo/shell/ParseResult.java
new file mode 100644
index 00000000..7492780b
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/ParseResult.java
@@ -0,0 +1,92 @@
+package org.springframework.roo.shell;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+import org.springframework.roo.support.style.ToStringCreator;
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.StringUtils;
+
+/**
+ * Immutable representation of the outcome of parsing a given shell line.
+ *
+ *
+ * Note that contained objects (the instance and the arguments) may be mutable, as the shell infrastructure
+ * has no way of restricting which methods can be the target of CLI commands and nor the arguments
+ * they will accept via the {@link Converter} infrastructure.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class ParseResult {
+
+ // Fields
+ private final Method method;
+ private final Object instance;
+ private final Object[] arguments; // May be null if no arguments needed
+
+ public ParseResult(final Method method, final Object instance, final Object[] arguments) {
+ Assert.notNull(method, "Method required");
+ Assert.notNull(instance, "Instance required");
+ int length = arguments == null ? 0 : arguments.length;
+ Assert.isTrue(method.getParameterTypes().length == length, "Required " + method.getParameterTypes().length + " arguments, but received " + length);
+ this.method = method;
+ this.instance = instance;
+ this.arguments = arguments;
+ }
+
+ public Method getMethod() {
+ return method;
+ }
+
+ public Object getInstance() {
+ return instance;
+ }
+
+ public Object[] getArguments() {
+ return arguments;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(arguments);
+ result = prime * result + ((instance == null) ? 0 : instance.hashCode());
+ result = prime * result + ((method == null) ? 0 : method.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ParseResult other = (ParseResult) obj;
+ if (!Arrays.equals(arguments, other.arguments))
+ return false;
+ if (instance == null) {
+ if (other.instance != null)
+ return false;
+ } else if (!instance.equals(other.instance))
+ return false;
+ if (method == null) {
+ if (other.method != null)
+ return false;
+ } else if (!method.equals(other.method))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ ToStringCreator tsc = new ToStringCreator(this);
+ tsc.append("method", method);
+ tsc.append("instance", instance);
+ tsc.append("arguments", StringUtils.arrayToCommaDelimitedString(arguments));
+ return tsc.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/Parser.java b/src/main/java/org/springframework/roo/shell/Parser.java
new file mode 100644
index 00000000..d22a7e0d
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/Parser.java
@@ -0,0 +1,35 @@
+package org.springframework.roo.shell;
+
+import java.util.List;
+
+/**
+ * Interface for {@link SimpleParser}.
+ *
+ * @author Ben Alex
+ * @author Alan Stewart
+ * @since 1.0
+ */
+public interface Parser {
+
+ ParseResult parse(String buffer);
+
+ /**
+ * Populates a list of completion candidates. This method is required for backward compatibility for STS versions up to 2.8.0.
+ *
+ * @param buffer
+ * @param cursor
+ * @param candidates
+ * @return
+ */
+ int complete(String buffer, int cursor, List candidates);
+
+ /**
+ * Populates a list of completion candidates.
+ *
+ * @param buffer
+ * @param cursor
+ * @param candidates
+ * @return
+ */
+ int completeAdvanced(String buffer, int cursor, List candidates);
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/ParserUtils.java b/src/main/java/org/springframework/roo/shell/ParserUtils.java
new file mode 100644
index 00000000..fbf483a3
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/ParserUtils.java
@@ -0,0 +1,181 @@
+package org.springframework.roo.shell;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.roo.support.util.Assert;
+
+/**
+ * Utilities for parsing.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class ParserUtils {
+
+ private ParserUtils() {}
+
+ /**
+ * Converts a particular buffer into a tokenized structure.
+ *
+ *
+ * Properly treats double quotes (") as option delimiters.
+ *
+ *
+ * Expects option names to be preceded by a single or double dash. We call this an "option marker".
+ *
+ *
+ * Treats spaces as the default option tokenizer.
+ *
+ *
+ * Any token without an option marker is considered the default. The default is returned in the Map as an element with an empty string key (""). There can only be a single default.
+ *
+ * @param remainingBuffer to tokenize
+ * @return a Map where keys are the option names (minus any dashes) and values are the option values (any double-quotes are removed)
+ */
+ public static Map tokenize(final String remainingBuffer) {
+ Assert.notNull(remainingBuffer, "Remaining buffer cannot be null, although it can be empty");
+ Map result = new LinkedHashMap();
+ StringBuilder currentOption = new StringBuilder();
+ StringBuilder currentValue = new StringBuilder();
+ boolean inQuotes = false;
+
+ // Verify correct number of double quotes are present
+ int count = 0;
+ for (char c : remainingBuffer.toCharArray()) {
+ if ('"' == c) {
+ count++;
+ }
+ }
+ Assert.isTrue(count % 2 == 0, "Cannot have an unbalanced number of quotation marks");
+
+ if ("".equals(remainingBuffer.trim())) {
+ // They've not specified anything, so exit now
+ return result;
+ }
+
+ String[] split = remainingBuffer.split(" ");
+ for (int i = 0; i < split.length; i++) {
+ String currentToken = split[i];
+
+ if (currentToken.startsWith("\"") && currentToken.endsWith("\"") && currentToken.length() > 1) {
+ String tokenLessDelimiters = currentToken.substring(1, currentToken.length() - 1);
+ currentValue.append(tokenLessDelimiters);
+
+ // Store this token
+ store(result, currentOption, currentValue);
+ currentOption = new StringBuilder();
+ currentValue = new StringBuilder();
+ continue;
+ }
+
+ if (inQuotes) {
+ // We're only interested in this token series ending
+ if (currentToken.endsWith("\"")) {
+ String tokenLessDelimiters = currentToken.substring(0, currentToken.length() - 1);
+ currentValue.append(" ").append(tokenLessDelimiters);
+ inQuotes = false;
+
+ // Store this now-ended token series
+ store(result, currentOption, currentValue);
+ currentOption = new StringBuilder();
+ currentValue = new StringBuilder();
+ } else {
+ // The current token series has not ended
+ currentValue.append(" ").append(currentToken);
+ }
+ continue;
+ }
+
+ if (currentToken.startsWith("\"")) {
+ // We're about to start a new delimited token
+ String tokenLessDelimiters = currentToken.substring(1);
+ currentValue.append(tokenLessDelimiters);
+ inQuotes = true;
+ continue;
+ }
+
+ if (currentToken.trim().equals("")) {
+ // It's simply empty, so ignore it (ROO-23)
+ continue;
+ }
+
+ if (currentToken.startsWith("--")) {
+ // We're about to start a new option marker
+ // First strip all of the - or -- or however many there are
+ int lastIndex = currentToken.lastIndexOf("-");
+ String tokenLessDelimiters = currentToken.substring(lastIndex + 1);
+ currentOption.append(tokenLessDelimiters);
+
+ // Store this token if it's the last one, or the next token starts with a "-"
+ if (i + 1 == split.length) {
+ // We're at the end of the tokens, so store this one and stop processing
+ store(result, currentOption, currentValue);
+ break;
+ }
+
+ if (split[i + 1].startsWith("-")) {
+ // A new token is being started next iteration, so store this one now
+ store(result, currentOption, currentValue);
+ currentOption = new StringBuilder();
+ currentValue = new StringBuilder();
+ }
+
+ continue;
+ }
+
+ // We must be in a standard token
+
+ // If the standard token has no option name, we allow it to contain unquoted spaces
+ if (currentOption.length() == 0) {
+ if (currentValue.length() > 0) {
+ // Existing content, so add a space first
+ currentValue.append(" ");
+ }
+ currentValue.append(currentToken);
+
+ // Store this token if it's the last one, or the next token starts with a "-"
+ if (i + 1 == split.length) {
+ // We're at the end of the tokens, so store this one and stop processing
+ store(result, currentOption, currentValue);
+ break;
+ }
+
+ if (split[i + 1].startsWith("--")) {
+ // A new token is being started next iteration, so store this one now
+ store(result, currentOption, currentValue);
+ currentOption = new StringBuilder();
+ currentValue = new StringBuilder();
+ }
+
+ continue;
+ }
+
+ // This is an ordinary token, so store it now
+ currentValue.append(currentToken);
+ store(result, currentOption, currentValue);
+ currentOption = new StringBuilder();
+ currentValue = new StringBuilder();
+ }
+
+ // Strip out an empty default option, if it was returned (ROO-379)
+ if (result.containsKey("") && result.get("").trim().equals("")) {
+ result.remove("");
+ }
+
+ return result;
+ }
+
+ private static void store(final Map results, final StringBuilder currentOption, final StringBuilder currentValue) {
+ if (currentOption.length() > 0) {
+ // There is an option marker
+ String option = currentOption.toString();
+ Assert.isTrue(!results.containsKey(option), "You cannot specify option '" + option + "' more than once in a single command");
+ results.put(option, currentValue.toString());
+ } else {
+ // There was no option marker, so verify this isn't the first
+ Assert.isTrue(!results.containsKey(""), "You cannot add more than one default option ('" + currentValue.toString() + "') in a single command");
+ results.put("", currentValue.toString());
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/Shell.java b/src/main/java/org/springframework/roo/shell/Shell.java
new file mode 100644
index 00000000..dd3c6e04
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/Shell.java
@@ -0,0 +1,98 @@
+package org.springframework.roo.shell;
+
+import java.io.File;
+import java.util.logging.Level;
+
+import org.springframework.roo.shell.event.ShellStatusProvider;
+
+/**
+ * Specifies the contract for an interactive shell.
+ *
+ *
+ * Any interactive shell class which implements these methods can be launched by the roo-bootstrap mechanism.
+ *
+ *
+ * It is envisaged implementations will be provided for JLine initially, with possible implementations for
+ * Eclipse in the future.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public interface Shell extends ShellStatusProvider, ShellPromptAccessor {
+
+ /**
+ * The slot name to use with {@link #flash(Level, String, String)} if a caller wishes to modify the window title.
+ * This may not be supported by all operating system shells. It is provided on a best-effort basis only.
+ */
+ String WINDOW_TITLE_SLOT = "WINDOW_TITLE_SLOT";
+
+ /**
+ * Presents a console prompt and allows the user to interact with the shell. The shell should not return
+ * to the caller until the user has finished their session (by way of a "quit" or similar command).
+ */
+ void promptLoop();
+
+ /**
+ * @return null if no exit was requested, otherwise the last exit code indicated to the shell to use
+ */
+ ExitShellRequest getExitShellRequest();
+
+ /**
+ * Runs the specified command. Control will return to the caller after the command is run.
+ *
+ * @param line to execute (required)
+ * @return true if the command was successful, false if there was an exception
+ */
+ boolean executeCommand(String line);
+
+ /**
+ * Indicates the shell should switch into a lower-level development mode. The exact meaning varies by
+ * shell implementation.
+ *
+ * @param developmentMode true if development mode should be enabled, false otherwise
+ */
+ void setDevelopmentMode(boolean developmentMode);
+
+ /**
+ * Displays a progress notification to the user. This notification will ideally be displayed in a
+ * consistent screen location by the shell implementation.
+ *
+ *
+ * An implementation may allow multiple messages to be displayed concurrently. So an implementation can
+ * determine when a flash message replaces a previous flash message, callers should allocate a unique
+ * "slot" name for their messages. It is suggested the class name of the caller be used. This way a
+ * slot will be updated without conflicting with flash message sequences from other slots.
+ *
+ *
+ * Passing an empty string in as the "message" indicates the slot should be cleared.
+ *
+ *
+ * An implementation need not necessarily use the level or slot concepts. They are expected to be
+ * used in most cases, though.
+ *
+ * @param level the importance of the message (cannot be null)
+ * @param message to display (cannot be null, but may be empty)
+ * @param slot the identification slot for the message (cannot be null or empty)
+ */
+ void flash(Level level, String message, String slot);
+
+ boolean isDevelopmentMode();
+
+ /**
+ * Changes the "path" displayed in the shell prompt. An implementation will ensure this path is
+ * included on the screen, taking care to merge it with the product name and handle any special
+ * formatting requirements (such as ANSI, if supported by the implementation).
+ *
+ * @param path to set (can be null or empty; must NOT be formatted in any special way eg ANSI codes)
+ */
+ void setPromptPath(String path);
+
+ void setPromptPath(String path, boolean overrideStyle);
+
+ /**
+ * Returns the home directory of the current running shell instance
+ *
+ * @return the home directory of the current shell instance
+ */
+ File getHome();
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java b/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java
new file mode 100644
index 00000000..fc7f597b
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java
@@ -0,0 +1,16 @@
+package org.springframework.roo.shell;
+
+/**
+ * Obtains the prompt used by a {@link Shell}.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public interface ShellPromptAccessor {
+
+ /**
+ * @return the shell prompt (never null; the result may include special characters such as ANSI
+ * escape codes if the implementation is using them)
+ */
+ String getShellPrompt();
+}
diff --git a/src/main/java/org/springframework/roo/shell/SimpleParser.java b/src/main/java/org/springframework/roo/shell/SimpleParser.java
new file mode 100644
index 00000000..84015c17
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/SimpleParser.java
@@ -0,0 +1,1077 @@
+package org.springframework.roo.shell;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.transform.Transformer;
+
+import org.springframework.roo.support.logging.HandlerUtils;
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.CollectionUtils;
+import org.springframework.roo.support.util.ExceptionUtils;
+import org.springframework.roo.support.util.FileCopyUtils;
+import org.springframework.roo.support.util.StringUtils;
+import org.springframework.roo.support.util.XmlElementBuilder;
+import org.springframework.roo.support.util.XmlUtils;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/**
+ * Default implementation of {@link Parser}.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class SimpleParser implements Parser {
+
+ // Constants
+ private static final Logger LOGGER = HandlerUtils.getLogger(SimpleParser.class);
+ private static final Comparator COMPARATOR = new NaturalOrderComparator();
+
+ // Fields
+ private final Object mutex = new Object();
+ private final Set> converters = new HashSet>();
+ private final Set commands = new HashSet();
+ private final Map availabilityIndicators = new HashMap();
+
+ private MethodTarget getAvailabilityIndicator(final String command) {
+ return availabilityIndicators.get(command);
+ }
+
+ public ParseResult parse(final String rawInput) {
+ synchronized (mutex) {
+ Assert.notNull(rawInput, "Raw input required");
+ final String input = normalise(rawInput);
+
+ // Locate the applicable targets which match this buffer
+ final Collection matchingTargets = locateTargets(input, true, true);
+ if (matchingTargets.isEmpty()) {
+ // Before we just give up, let's see if we can offer a more informative message to the user
+ // by seeing the command is simply unavailable at this point in time
+ CollectionUtils.populate(matchingTargets, locateTargets(input, true, false));
+ if (matchingTargets.isEmpty()) {
+ commandNotFound(LOGGER, input);
+ } else {
+ LOGGER.warning("Command '" + input + "' was found but is not currently available (type 'help' then ENTER to learn about this command)");
+ }
+ return null;
+ }
+ if (matchingTargets.size() > 1) {
+ LOGGER.warning("Ambigious command '" + input + "' (for assistance press " + AbstractShell.completionKeys + " or type \"hint\" then hit ENTER)");
+ return null;
+ }
+ MethodTarget methodTarget = matchingTargets.iterator().next();
+
+ // Argument conversion time
+ Annotation[][] parameterAnnotations = methodTarget.getMethod().getParameterAnnotations();
+ if (parameterAnnotations.length == 0) {
+ // No args
+ return new ParseResult(methodTarget.getMethod(), methodTarget.getTarget(), null);
+ }
+
+ // Oh well, we need to convert some arguments
+ final List arguments = new ArrayList(methodTarget.getMethod().getParameterTypes().length);
+
+ // Attempt to parse
+ Map options = null;
+ try {
+ options = ParserUtils.tokenize(methodTarget.getRemainingBuffer());
+ } catch (IllegalArgumentException e) {
+ LOGGER.warning(ExceptionUtils.extractRootCause(e).getMessage());
+ return null;
+ }
+
+ final Set cliOptions = getCliOptions(parameterAnnotations);
+ for (CliOption cliOption : cliOptions) {
+ Class> requiredType = methodTarget.getMethod().getParameterTypes()[arguments.size()];
+
+ if (cliOption.systemProvided()) {
+ Object result;
+ if (SimpleParser.class.isAssignableFrom(requiredType)) {
+ result = this;
+ } else {
+ LOGGER.warning("Parameter type '" + requiredType + "' is not system provided");
+ return null;
+ }
+ arguments.add(result);
+ continue;
+ }
+
+ // Obtain the value the user specified, taking care to ensure they only specified it via a single alias
+ String value = null;
+ String sourcedFrom = null;
+ for (String possibleKey : cliOption.key()) {
+ if (options.containsKey(possibleKey)) {
+ if (sourcedFrom != null) {
+ LOGGER.warning("You cannot specify option '" + possibleKey + "' when you have also specified '" + sourcedFrom + "' in the same command");
+ return null;
+ }
+ sourcedFrom = possibleKey;
+ value = options.get(possibleKey);
+ }
+ }
+
+ // Ensure the user specified a value if the value is mandatory
+ if (StringUtils.isBlank(value) && cliOption.mandatory()) {
+ if ("".equals(cliOption.key()[0])) {
+ StringBuilder message = new StringBuilder("You must specify a default option ");
+ if (cliOption.key().length > 1) {
+ message.append("(otherwise known as option '").append(cliOption.key()[1]).append("') ");
+ }
+ message.append("for this command");
+ LOGGER.warning(message.toString());
+ } else {
+ LOGGER.warning("You must specify option '" + cliOption.key()[0] + "' for this command");
+ }
+ return null;
+ }
+
+ // Accept a default if the user specified the option, but didn't provide a value
+ if ("".equals(value)) {
+ value = cliOption.specifiedDefaultValue();
+ }
+
+ // Accept a default if the user didn't specify the option at all
+ if (value == null) {
+ value = cliOption.unspecifiedDefaultValue();
+ }
+
+ // Special token that denotes a null value is sought (useful for default values)
+ if ("__NULL__".equals(value)) {
+ if (requiredType.isPrimitive()) {
+ LOGGER.warning("Nulls cannot be presented to primitive type " + requiredType.getSimpleName() + " for option '" + StringUtils.arrayToCommaDelimitedString(cliOption.key()) + "'");
+ return null;
+ }
+ arguments.add(null);
+ continue;
+ }
+
+ // Now we're ready to perform a conversion
+ try {
+ CliOptionContext.setOptionContext(cliOption.optionContext());
+ CliSimpleParserContext.setSimpleParserContext(this);
+ Object result;
+ Converter> c = null;
+ for (Converter> candidate : converters) {
+ if (candidate.supports(requiredType, cliOption.optionContext())) {
+ // Found a usable converter
+ c = candidate;
+ break;
+ }
+ }
+ if (c == null) {
+ throw new IllegalStateException("TODO: Add basic type conversion");
+ // TODO Fall back to a normal SimpleTypeConverter and attempt conversion
+ // SimpleTypeConverter simpleTypeConverter = new SimpleTypeConverter();
+ // result = simpleTypeConverter.convertIfNecessary(value, requiredType, mp);
+ }
+
+ // Use the converter
+ result = c.convertFromText(value, requiredType, cliOption.optionContext());
+
+ // If the option has been specified to be mandatory then the result should never be null
+ if (result == null && cliOption.mandatory()) {
+ throw new IllegalStateException();
+ }
+ arguments.add(result);
+ } catch (RuntimeException e) {
+ LOGGER.warning(e.getClass().getName() + ": Failed to convert '" + value + "' to type " + requiredType.getSimpleName() + " for option '" + StringUtils.arrayToCommaDelimitedString(cliOption.key()) + "'");
+ if (StringUtils.hasText(e.getMessage())) {
+ LOGGER.warning(e.getMessage());
+ }
+ return null;
+ } finally {
+ CliOptionContext.resetOptionContext();
+ CliSimpleParserContext.resetSimpleParserContext();
+ }
+ }
+
+ // Check for options specified by the user but are unavailable for the command
+ Set unavailableOptions = getSpecifiedUnavailableOptions(cliOptions, options);
+ if (!unavailableOptions.isEmpty()) {
+ StringBuilder message = new StringBuilder();
+ if (unavailableOptions.size() == 1) {
+ message.append("Option '").append(unavailableOptions.iterator().next()).append("' is not available for this command. ");
+ } else {
+ message.append("Options ").append(StringUtils.collectionToDelimitedString(unavailableOptions, ", ", "'", "'")).append(" are not available for this command. ");
+ }
+ message.append("Use tab assist or the \"help\" command to see the legal options");
+ LOGGER.warning(message.toString());
+ return null;
+ }
+
+ return new ParseResult(methodTarget.getMethod(), methodTarget.getTarget(), arguments.toArray());
+ }
+ }
+
+ /**
+ * Normalises the given raw user input string ready for parsing
+ *
+ * @param rawInput the string to normalise; can't be null
+ * @return a non-null string
+ */
+ String normalise(final String rawInput) {
+ // Replace all multiple spaces with a single space and then trim
+ return rawInput.replaceAll(" +", " ").trim();
+ }
+
+ private Set getSpecifiedUnavailableOptions(final Set cliOptions, final Map options) {
+ Set cliOptionKeySet = new LinkedHashSet();
+ for (CliOption cliOption : cliOptions) {
+ for (String key : cliOption.key()) {
+ cliOptionKeySet.add(key.toLowerCase());
+ }
+ }
+ Set unavailableOptions = new LinkedHashSet();
+ for (String suppliedOption : options.keySet()) {
+ if (!cliOptionKeySet.contains(suppliedOption.toLowerCase())) {
+ unavailableOptions.add(suppliedOption);
+ }
+ }
+ return unavailableOptions;
+ }
+
+ private Set getCliOptions(final Annotation[][] parameterAnnotations) {
+ Set cliOptions = new LinkedHashSet();
+ for (Annotation[] annotations : parameterAnnotations) {
+ for (Annotation annotation : annotations) {
+ if (annotation instanceof CliOption) {
+ CliOption cliOption = (CliOption) annotation;
+ cliOptions.add(cliOption);
+ }
+ }
+ }
+ return cliOptions;
+ }
+
+ protected void commandNotFound(final Logger logger, final String buffer) {
+ logger.warning("Command '" + buffer + "' not found (for assistance press " + AbstractShell.completionKeys + " or type \"hint\" then hit ENTER)");
+ }
+
+ private Collection locateTargets(final String buffer, final boolean strictMatching, final boolean checkAvailabilityIndicators) {
+ Assert.notNull(buffer, "Buffer required");
+ final Collection result = new HashSet();
+
+ // The reflection could certainly be optimised, but it's good enough for now (and cached reflection
+ // is unlikely to be noticeable to a human being using the CLI)
+ for (final CommandMarker command : commands) {
+ for (final Method method : command.getClass().getMethods()) {
+ CliCommand cmd = method.getAnnotation(CliCommand.class);
+ if (cmd != null) {
+ // We have a @CliCommand.
+ if (checkAvailabilityIndicators) {
+ // Decide if this @CliCommand is available at this moment
+ Boolean available = null;
+ for (String value : cmd.value()) {
+ MethodTarget mt = getAvailabilityIndicator(value);
+ if (mt != null) {
+ Assert.isNull(available, "More than one availability indicator is defined for '" + method.toGenericString() + "'");
+ try {
+ available = (Boolean) mt.getMethod().invoke(mt.getTarget());
+ // We should "break" here, but we loop over all to ensure no conflicting availability indicators are defined
+ } catch (Exception e) {
+ available = false;
+ }
+ }
+ }
+ // Skip this @CliCommand if it's not available
+ if (available != null && !available) {
+ continue;
+ }
+ }
+
+ for (String value : cmd.value()) {
+ String remainingBuffer = isMatch(buffer, value, strictMatching);
+ if (remainingBuffer != null) {
+ result.add(new MethodTarget(method, command, remainingBuffer, value));
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ static String isMatch(final String buffer, final String command, final boolean strictMatching) {
+ if ("".equals(buffer.trim())) {
+ return "";
+ }
+ String[] commandWords = StringUtils.delimitedListToStringArray(command, " ");
+ int lastCommandWordUsed = 0;
+ Assert.notEmpty(commandWords, "Command required");
+
+ String bufferToReturn = null;
+ String lastWord = null;
+
+ next_buffer_loop: for (int bufferIndex = 0; bufferIndex < buffer.length(); bufferIndex++) {
+ String bufferSoFarIncludingThis = buffer.substring(0, bufferIndex + 1);
+ String bufferRemaining = buffer.substring(bufferIndex + 1);
+
+ int bufferLastIndexOfWord = bufferSoFarIncludingThis.lastIndexOf(" ");
+ String wordSoFarIncludingThis = bufferSoFarIncludingThis;
+ if (bufferLastIndexOfWord != -1) {
+ wordSoFarIncludingThis = bufferSoFarIncludingThis.substring(bufferLastIndexOfWord);
+ }
+
+ if (wordSoFarIncludingThis.equals(" ") || bufferIndex == buffer.length() - 1) {
+ if (bufferIndex == buffer.length() - 1 && !"".equals(wordSoFarIncludingThis.trim())) {
+ lastWord = wordSoFarIncludingThis.trim();
+ }
+
+ // At end of word or buffer. Let's see if a word matched or not
+ for (int candidate = lastCommandWordUsed; candidate < commandWords.length; candidate++) {
+ if (lastWord != null && lastWord.length() > 0 && commandWords[candidate].startsWith(lastWord)) {
+ if (bufferToReturn == null) {
+ // This is the first match, so ensure the intended match really represents the start of a command and not a later word within it
+ if (lastCommandWordUsed == 0 && candidate > 0) {
+ // This is not a valid match
+ break next_buffer_loop;
+ }
+ }
+
+ if (bufferToReturn != null) {
+ // We already matched something earlier, so ensure we didn't skip any word
+ if (candidate != lastCommandWordUsed + 1) {
+ // User has skipped a word
+ bufferToReturn = null;
+ break next_buffer_loop;
+ }
+ }
+
+ bufferToReturn = bufferRemaining;
+ lastCommandWordUsed = candidate;
+ if (candidate + 1 == commandWords.length) {
+ // This was a match for the final word in the command, so abort
+ break next_buffer_loop;
+ }
+ // There are more words left to potentially match, so continue
+ continue next_buffer_loop;
+ }
+ }
+
+ // This word is unrecognised as part of a command, so abort
+ bufferToReturn = null;
+ break next_buffer_loop;
+ }
+
+ lastWord = wordSoFarIncludingThis.trim();
+ }
+
+ // We only consider it a match if ALL words were actually used
+ if (bufferToReturn != null) {
+ if (!strictMatching || lastCommandWordUsed + 1 == commandWords.length) {
+ return bufferToReturn;
+ }
+ }
+
+ return null; // Not a match
+ }
+
+ public int complete(String buffer, int cursor, final List candidates) {
+ final List completions = new ArrayList();
+ int result = completeAdvanced(buffer, cursor, completions);
+ for (final Completion completion : completions) {
+ candidates.add(completion.getValue());
+ }
+ return result;
+ }
+
+ public int completeAdvanced(String buffer, int cursor, final List candidates) {
+ synchronized (mutex) {
+ Assert.notNull(buffer, "Buffer required");
+ Assert.notNull(candidates, "Candidates list required");
+
+ // Remove all spaces from beginning of command
+ while (buffer.startsWith(" ")) {
+ buffer = buffer.replaceFirst("^ ", "");
+ cursor--;
+ }
+
+ // Replace all multiple spaces with a single space
+ while (buffer.contains(" ")) {
+ buffer = StringUtils.replaceFirst(buffer, " ", " ");
+ cursor--;
+ }
+
+ // Begin by only including the portion of the buffer represented to the present cursor position
+ String translated = buffer.substring(0, cursor);
+
+ // Start by locating a method that matches
+ final Collection targets = locateTargets(translated, false, true);
+ SortedSet results = new TreeSet(COMPARATOR);
+
+ if (targets.isEmpty()) {
+ // Nothing matches the buffer they've presented
+ return cursor;
+ }
+ if (targets.size() > 1) {
+ // Assist them locate a particular target
+ for (MethodTarget target : targets) {
+ // Calculate the correct starting position
+ int startAt = translated.length();
+
+ // Only add the first word of each target
+ int stopAt = target.getKey().indexOf(" ", startAt);
+ if (stopAt == -1) {
+ stopAt = target.getKey().length();
+ }
+
+ results.add(new Completion(target.getKey().substring(0, stopAt) + " "));
+ }
+ candidates.addAll(results);
+ return 0;
+ }
+
+ // There is a single target of this method, so provide completion services for it
+ MethodTarget methodTarget = targets.iterator().next();
+
+ // Identify the command we're working with
+ CliCommand cmd = methodTarget.getMethod().getAnnotation(CliCommand.class);
+ Assert.notNull(cmd, "CliCommand unavailable for '" + methodTarget.getMethod().toGenericString() + "'");
+
+ // Make a reasonable attempt at parsing the remainingBuffer
+ Map options;
+ try {
+ options = ParserUtils.tokenize(methodTarget.getRemainingBuffer());
+ } catch (IllegalArgumentException ex) {
+ // Assume any IllegalArgumentException is due to a quotation mark mismatch
+ candidates.add(new Completion(translated + "\""));
+ return 0;
+ }
+
+ // Lookup arguments for this target
+ Annotation[][] parameterAnnotations = methodTarget.getMethod().getParameterAnnotations();
+
+ // If there aren't any parameters for the method, at least ensure they have typed the command properly
+ if (parameterAnnotations.length == 0) {
+ for (String value : cmd.value()) {
+ if (buffer.startsWith(value) || value.startsWith(buffer)) {
+ results.add(new Completion(value)); // no space at the end, as there's no need to continue the command further
+ }
+ }
+ candidates.addAll(results);
+ return 0;
+ }
+
+ // If they haven't specified any parameters yet, at least verify the command name is fully completed
+ if (options.isEmpty()) {
+ for (String value : cmd.value()) {
+ if (value.startsWith(buffer)) {
+ // They are potentially trying to type this command
+ // We only need provide completion, though, if they failed to specify it fully
+ if (!buffer.startsWith(value)) {
+ // They failed to specify the command fully
+ results.add(new Completion(value + " "));
+ }
+ }
+ }
+
+ // Only quit right now if they have to finish specifying the command name
+ if (results.size() > 0) {
+ candidates.addAll(results);
+ return 0;
+ }
+ }
+
+ // To get this far, we know there are arguments required for this CliCommand, and they specified a valid command name
+
+ // Record all the CliOptions applicable to this command
+ List cliOptions = new ArrayList();
+ for (Annotation[] annotations : parameterAnnotations) {
+ CliOption cliOption = null;
+ for (Annotation a : annotations) {
+ if (a instanceof CliOption) {
+ cliOption = (CliOption) a;
+ }
+ }
+ Assert.notNull(cliOption, "CliOption not found for parameter '" + Arrays.toString(annotations) + "'");
+ cliOptions.add(cliOption);
+ }
+
+ // Make a list of all CliOptions they've already included or are system-provided
+ List alreadySpecified = new ArrayList();
+ for (CliOption option : cliOptions) {
+ for (String value : option.key()) {
+ if (options.containsKey(value)) {
+ alreadySpecified.add(option);
+ break;
+ }
+ }
+ if (option.systemProvided()) {
+ alreadySpecified.add(option);
+ }
+ }
+
+ // Make a list of all CliOptions they have not provided
+ List unspecified = new ArrayList(cliOptions);
+ unspecified.removeAll(alreadySpecified);
+
+ // Determine whether they're presently editing an option key or an option value
+ // (and if possible, the full or partial name of the said option key being edited)
+ String lastOptionKey = null;
+ String lastOptionValue = null;
+
+ // The last item in the options map is *always* the option key they're editing (will never be null)
+ if (options.size() > 0) {
+ lastOptionKey = new ArrayList(options.keySet()).get(options.keySet().size() - 1);
+ lastOptionValue = options.get(lastOptionKey);
+ }
+
+ // Handle if they are trying to find out the available option keys; always present option keys in order
+ // of their declaration on the method signature, thus we can stop when mandatory options are filled in
+ if (methodTarget.getRemainingBuffer().endsWith("--")) {
+ boolean showAllRemaining = true;
+ for (CliOption include : unspecified) {
+ if (include.mandatory()) {
+ showAllRemaining = false;
+ break;
+ }
+ }
+
+ for (CliOption include : unspecified) {
+ for (String value : include.key()) {
+ if (!"".equals(value)) {
+ results.add(new Completion(translated + value + " "));
+ }
+ }
+ if (!showAllRemaining) {
+ break;
+ }
+ }
+ candidates.addAll(results);
+ return 0;
+ }
+
+ // Handle suggesting an option key if they haven't got one presently specified (or they've completed a full option key/value pair)
+ if (lastOptionKey == null || (!"".equals(lastOptionKey) && !"".equals(lastOptionValue) && translated.endsWith(" "))) {
+ // We have either NEVER specified an option key/value pair
+ // OR we have specified a full option key/value pair
+
+ // Let's list some other options the user might want to try (naturally skip the "" option, as that's the default)
+ for (CliOption include : unspecified) {
+ for (String value : include.key()) {
+ // Manually determine if this non-mandatory but unspecifiedDefaultValue=* requiring option is able to be bound
+ if (!include.mandatory() && "*".equals(include.unspecifiedDefaultValue()) && !"".equals(value)) {
+ try {
+ for (Converter> candidate : converters) {
+ // Find the target parameter
+ Class> paramType = null;
+ int index = -1;
+ for (Annotation[] a : methodTarget.getMethod().getParameterAnnotations()) {
+ index++;
+ for (Annotation an : a) {
+ if (an instanceof CliOption) {
+ if (an.equals(include)) {
+ // Found the parameter, so store it
+ paramType = methodTarget.getMethod().getParameterTypes()[index];
+ break;
+ }
+ }
+ }
+ }
+ if (paramType != null && candidate.supports(paramType, include.optionContext())) {
+ // Try to invoke this usable converter
+ candidate.convertFromText("*", paramType, include.optionContext());
+ // If we got this far, the converter is happy with "*" so we need not bother the user with entering the data in themselves
+ break;
+ }
+ }
+ } catch (RuntimeException notYetReady) {
+ if (translated.endsWith(" ")) {
+ results.add(new Completion(translated + "--" + value + " "));
+ } else {
+ results.add(new Completion(translated + " --" + value + " "));
+ }
+ continue;
+ }
+ }
+
+ // Handle normal mandatory options
+ if (!"".equals(value) && include.mandatory()) {
+ if (translated.endsWith(" ")) {
+ results.add(new Completion(translated + "--" + value + " "));
+ } else {
+ results.add(new Completion(translated + " --" + value + " "));
+ }
+ }
+ }
+ }
+
+ // Only abort at this point if we have some suggestions; otherwise we might want to try to complete the "" option
+ if (results.size() > 0) {
+ candidates.addAll(results);
+ return 0;
+ }
+ }
+
+ // Handle completing the option key they're presently typing
+ if ((lastOptionValue == null || "".equals(lastOptionValue)) && !translated.endsWith(" ")) {
+ // Given we haven't got an option value of any form, and there's no space at the buffer end, we must still be typing an option key
+
+ for (CliOption option : cliOptions) {
+ for (String value : option.key()) {
+ if (value != null && lastOptionKey != null && value.regionMatches(true, 0, lastOptionKey, 0, lastOptionKey.length())) {
+ String completionValue = translated.substring(0, (translated.length() - lastOptionKey.length())) + value + " ";
+ results.add(new Completion(completionValue));
+ }
+ }
+ }
+ candidates.addAll(results);
+ return 0;
+ }
+
+ // To be here, we are NOT typing an option key (or we might be, and there are no further option keys left)
+ if (lastOptionKey != null && !"".equals(lastOptionKey)) {
+ // Lookup the relevant CliOption that applies to this lastOptionKey
+ // We do this via the parameter type
+ Class>[] parameterTypes = methodTarget.getMethod().getParameterTypes();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ CliOption option = cliOptions.get(i);
+ Class> parameterType = parameterTypes[i];
+
+ for (String key : option.key()) {
+ if (key.equals(lastOptionKey)) {
+ List allValues = new ArrayList();
+ String suffix = " ";
+
+ // Let's use a Converter if one is available
+ for (Converter> candidate : converters) {
+ if (candidate.supports(parameterType, option.optionContext())) {
+ // Found a usable converter
+ boolean addSpace = candidate.getAllPossibleValues(allValues, parameterType, lastOptionValue, option.optionContext(), methodTarget);
+ if (!addSpace) {
+ suffix = "";
+ }
+ break;
+ }
+ }
+
+ if (allValues.isEmpty()) {
+ // Doesn't appear to be a custom Converter, so let's go and provide defaults for simple types
+
+ // Provide some simple options for common types
+ if (Boolean.class.isAssignableFrom(parameterType) || Boolean.TYPE.isAssignableFrom(parameterType)) {
+ allValues.add(new Completion("true"));
+ allValues.add(new Completion("false"));
+ }
+
+ if (Number.class.isAssignableFrom(parameterType)) {
+ allValues.add(new Completion("0"));
+ allValues.add(new Completion("1"));
+ allValues.add(new Completion("2"));
+ allValues.add(new Completion("3"));
+ allValues.add(new Completion("4"));
+ allValues.add(new Completion("5"));
+ allValues.add(new Completion("6"));
+ allValues.add(new Completion("7"));
+ allValues.add(new Completion("8"));
+ allValues.add(new Completion("9"));
+ }
+ }
+
+ String prefix = "";
+ if (!translated.endsWith(" ")) {
+ prefix = " ";
+ }
+
+ // Only include in the candidates those results which are compatible with the present buffer
+ for (Completion currentValue : allValues) {
+ // We only provide a suggestion if the lastOptionValue == ""
+ if (StringUtils.isBlank(lastOptionValue)) {
+ // We should add the result, as they haven't typed anything yet
+ results.add(new Completion(prefix + currentValue.getValue() + suffix, currentValue.getFormattedValue(), currentValue.getHeading(), currentValue.getOrder()));
+ } else {
+ // Only add the result **if** what they've typed is compatible *AND* they haven't already typed it in full
+ if (currentValue.getValue().toLowerCase().startsWith(lastOptionValue.toLowerCase()) && !lastOptionValue.equalsIgnoreCase(currentValue.getValue()) && lastOptionValue.length() < currentValue.getValue().length()) {
+ results.add(new Completion(prefix + currentValue.getValue() + suffix, currentValue.getFormattedValue(), currentValue.getHeading(), currentValue.getOrder()));
+ }
+ }
+ }
+
+ // ROO-389: give inline options given there's multiple choices available and we want to help the user
+ StringBuilder help = new StringBuilder();
+ help.append(StringUtils.LINE_SEPARATOR);
+ help.append(option.mandatory() ? "required --" : "optional --");
+ if ("".equals(option.help())) {
+ help.append(lastOptionKey).append(": ").append("No help available");
+ } else {
+ help.append(lastOptionKey).append(": ").append(option.help());
+ }
+ if (option.specifiedDefaultValue().equals(option.unspecifiedDefaultValue())) {
+ if (option.specifiedDefaultValue().equals("__NULL__")) {
+ help.append("; no default value");
+ } else {
+ help.append("; default: '").append(option.specifiedDefaultValue()).append("'");
+ }
+ } else {
+ if (!"".equals(option.specifiedDefaultValue()) && !"__NULL__".equals(option.specifiedDefaultValue())) {
+ help.append("; default if option present: '").append(option.specifiedDefaultValue()).append("'");
+ }
+ if (!"".equals(option.unspecifiedDefaultValue()) && !"__NULL__".equals(option.unspecifiedDefaultValue())) {
+ help.append("; default if option not present: '").append(option.unspecifiedDefaultValue()).append("'");
+ }
+ }
+ LOGGER.info(help.toString());
+
+ if (results.size() == 1) {
+ String suggestion = results.iterator().next().getValue().trim();
+ if (suggestion.equals(lastOptionValue)) {
+ // They have pressed TAB in the default value, and the default value has already been provided as an explicit option
+ return 0;
+ }
+ }
+
+ if (results.size() > 0) {
+ candidates.addAll(results);
+ // Values presented from the last space onwards
+ if (translated.endsWith(" ")) {
+ return translated.lastIndexOf(" ") + 1;
+ }
+ return translated.trim().lastIndexOf(" ");
+ }
+ return 0;
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+ }
+
+ public void helpReferenceGuide() {
+ synchronized (mutex) {
+ File f = new File(".");
+ File[] existing = f.listFiles(new FileFilter() {
+ public boolean accept(final File pathname) {
+ return pathname.getName().startsWith("appendix_");
+ }
+ });
+ for (File e : existing) {
+ e.delete();
+ }
+
+ // Compute the sections we'll be outputting, and get them into a nice order
+ SortedMap sections = new TreeMap(COMPARATOR);
+ next_target: for (Object target : commands) {
+ Method[] methods = target.getClass().getMethods();
+ for (Method m : methods) {
+ CliCommand cmd = m.getAnnotation(CliCommand.class);
+ if (cmd != null) {
+ String sectionName = target.getClass().getSimpleName();
+ Pattern p = Pattern.compile("[A-Z][^A-Z]*");
+ Matcher matcher = p.matcher(sectionName);
+ StringBuilder string = new StringBuilder();
+ while (matcher.find()) {
+ string.append(matcher.group()).append(" ");
+ }
+ sectionName = string.toString().trim();
+ if (sections.containsKey(sectionName)) {
+ throw new IllegalStateException("Section name '" + sectionName + "' not unique");
+ }
+ sections.put(sectionName, target);
+ continue next_target;
+ }
+ }
+ }
+
+ // Build each section of the appendix
+ DocumentBuilder builder = XmlUtils.getDocumentBuilder();
+ Document document = builder.newDocument();
+ List builtSections = new ArrayList();
+
+ for (final Entry entry : sections.entrySet()) {
+ final String section = entry.getKey();
+ final Object target = entry.getValue();
+ SortedMap individualCommands = new TreeMap(COMPARATOR);
+
+ Method[] methods = target.getClass().getMethods();
+ for (Method m : methods) {
+ CliCommand cmd = m.getAnnotation(CliCommand.class);
+ if (cmd != null) {
+ StringBuilder cmdSyntax = new StringBuilder();
+ cmdSyntax.append(cmd.value()[0]);
+
+ // Build the syntax list
+
+ // Store the order options appear
+ List optionKeys = new ArrayList();
+ // key: option key, value: help text
+ Map optionDetails = new HashMap();
+ for (Annotation[] ann : m.getParameterAnnotations()) {
+ for (Annotation a : ann) {
+ if (a instanceof CliOption) {
+ CliOption option = (CliOption) a;
+ // Figure out which key we want to use (use first non-empty string, or make it "(default)" if needed)
+ String key = option.key()[0];
+ if ("".equals(key)) {
+ for (String otherKey : option.key()) {
+ if (!"".equals(otherKey)) {
+ key = otherKey;
+ break;
+ }
+ }
+ if ("".equals(key)) {
+ key = "[default]";
+ }
+ }
+
+ StringBuilder help = new StringBuilder();
+ if ("".equals(option.help())) {
+ help.append("No help available");
+ } else {
+ help.append(option.help());
+ }
+ if (option.specifiedDefaultValue().equals(option.unspecifiedDefaultValue())) {
+ if (option.specifiedDefaultValue().equals("__NULL__")) {
+ help.append("; no default value");
+ } else {
+ help.append("; default: '").append(option.specifiedDefaultValue()).append("'");
+ }
+ } else {
+ if (!"".equals(option.specifiedDefaultValue()) && !"__NULL__".equals(option.specifiedDefaultValue())) {
+ help.append("; default if option present: '").append(option.specifiedDefaultValue()).append("'");
+ }
+ if (!"".equals(option.unspecifiedDefaultValue()) && !"__NULL__".equals(option.unspecifiedDefaultValue())) {
+ help.append("; default if option not present: '").append(option.unspecifiedDefaultValue()).append("'");
+ }
+ }
+ help.append(option.mandatory() ? " (mandatory) " : "");
+
+ // Store details for later
+ key = "--" + key;
+ optionKeys.add(key);
+ optionDetails.put(key, help.toString());
+
+ // Include it in the mandatory syntax
+ if (option.mandatory()) {
+ cmdSyntax.append(" ").append(key);
+ }
+ }
+ }
+ }
+
+ // Make a variable list element
+ Element variableListElement = document.createElement("variablelist");
+ boolean anyVars = false;
+ for (String optionKey : optionKeys) {
+ anyVars = true;
+ String help = optionDetails.get(optionKey);
+ variableListElement.appendChild(new XmlElementBuilder("varlistentry", document).addChild(new XmlElementBuilder("term", document).setText(optionKey).build()).addChild(new XmlElementBuilder("listitem", document).addChild(new XmlElementBuilder("para", document).setText(help).build()).build()).build());
+ }
+
+ if (!anyVars) {
+ variableListElement = new XmlElementBuilder("para", document).setText("This command does not accept any options.").build();
+ }
+
+ // Now we've figured out the options, store this individual command
+ CDATASection progList = document.createCDATASection(cmdSyntax.toString());
+ String safeName = cmd.value()[0].replace("\\", "BCK").replace("/", "FWD").replace("*", "ASX");
+ Element element = new XmlElementBuilder("section", document).addAttribute("xml:id", "command-index-" + safeName.toLowerCase().replace(' ', '-')).addChild(new XmlElementBuilder("title", document).setText(cmd.value()[0]).build()).addChild(new XmlElementBuilder("para", document).setText(cmd.help()).build()).addChild(new XmlElementBuilder("programlisting", document).addChild(progList).build()).addChild(variableListElement).build();
+
+ individualCommands.put(cmdSyntax.toString(), element);
+ }
+ }
+
+ Element topSection = document.createElement("section");
+ topSection.setAttribute("xml:id", "command-index-" + section.toLowerCase().replace(' ', '-'));
+ topSection.appendChild(new XmlElementBuilder("title", document).setText(section).build());
+ topSection.appendChild(new XmlElementBuilder("para", document).setText(section + " are contained in " + target.getClass().getName() + ".").build());
+
+ for (final Element value : individualCommands.values()) {
+ topSection.appendChild(value);
+ }
+
+ builtSections.add(topSection);
+ }
+
+ Element appendix = document.createElement("appendix");
+ appendix.setAttribute("xmlns", "http://docbook.org/ns/docbook");
+ appendix.setAttribute("version", "5.0");
+ appendix.setAttribute("xml:id", "command-index");
+ appendix.appendChild(new XmlElementBuilder("title", document).setText("Command Index").build());
+ appendix.appendChild(new XmlElementBuilder("para", document).setText("This appendix was automatically built from Roo " + AbstractShell.versionInfo() + ".").build());
+ appendix.appendChild(new XmlElementBuilder("para", document).setText("Commands are listed in alphabetic order, and are shown in monospaced font with any mandatory options you must specify when using the command. Most commands accept a large number of options, and all of the possible options for each command are presented in this appendix.").build());
+
+ for (Element section : builtSections) {
+ appendix.appendChild(section);
+ }
+ document.appendChild(appendix);
+
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+
+ Transformer transformer = XmlUtils.createIndentingTransformer();
+ // Causes an "Error reported by XML parser: Multiple notations were used which had the name 'linespecific', but which were not determined to be duplicates." when creating the DocBook
+ // transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//OASIS//DTD DocBook XML V4.5//EN");
+ // transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd");
+
+ XmlUtils.writeXml(transformer, byteArrayOutputStream, document);
+ try {
+ File output = new File(f, "appendix-command-index.xml");
+ FileCopyUtils.copy(byteArrayOutputStream.toByteArray(), output);
+ } catch (IOException ioe) {
+ throw new IllegalStateException(ioe);
+ }
+ }
+ }
+
+ public void obtainHelp(@CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for") String buffer) {
+ synchronized (mutex) {
+ if (buffer == null) {
+ buffer = "";
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ // Figure out if there's a single command we can offer help for
+ final Collection matchingTargets = locateTargets(buffer, false, false);
+ if (matchingTargets.size() == 1) {
+ // Single command help
+ MethodTarget methodTarget = matchingTargets.iterator().next();
+
+ // Argument conversion time
+ Annotation[][] parameterAnnotations = methodTarget.getMethod().getParameterAnnotations();
+ if (parameterAnnotations.length > 0) {
+ // Offer specified help
+ CliCommand cmd = methodTarget.getMethod().getAnnotation(CliCommand.class);
+ Assert.notNull(cmd, "CliCommand not found");
+
+ for (String value : cmd.value()) {
+ sb.append("Keyword: ").append(value).append(StringUtils.LINE_SEPARATOR);
+ }
+
+ sb.append("Description: ").append(cmd.help()).append(StringUtils.LINE_SEPARATOR);
+
+ for (Annotation[] annotations : parameterAnnotations) {
+ CliOption cliOption = null;
+ for (Annotation a : annotations) {
+ if (a instanceof CliOption) {
+ cliOption = (CliOption) a;
+
+ for (String key : cliOption.key()) {
+ if ("".equals(key)) {
+ key = "** default **";
+ }
+ sb.append(" Keyword: ").append(key).append(StringUtils.LINE_SEPARATOR);
+ }
+
+ sb.append(" Help: ").append(cliOption.help()).append(StringUtils.LINE_SEPARATOR);
+ sb.append(" Mandatory: ").append(cliOption.mandatory()).append(StringUtils.LINE_SEPARATOR);
+ sb.append(" Default if specified: '").append(cliOption.specifiedDefaultValue()).append("'").append(StringUtils.LINE_SEPARATOR);
+ sb.append(" Default if unspecified: '").append(cliOption.unspecifiedDefaultValue()).append("'").append(StringUtils.LINE_SEPARATOR);
+ sb.append(StringUtils.LINE_SEPARATOR);
+ }
+
+ }
+ Assert.notNull(cliOption, "CliOption not found for parameter '" + Arrays.toString(annotations) + "'");
+ }
+ }
+ // Only a single argument, so default to the normal help operation
+ }
+
+ SortedSet result = new TreeSet(COMPARATOR);
+ for (MethodTarget mt : matchingTargets) {
+ CliCommand cmd = mt.getMethod().getAnnotation(CliCommand.class);
+ if (cmd != null) {
+ for (String value : cmd.value()) {
+ if ("".equals(cmd.help())) {
+ result.add("* " + value);
+ } else {
+ result.add("* " + value + " - " + cmd.help());
+ }
+ }
+ }
+ }
+
+ for (String s : result) {
+ sb.append(s).append(StringUtils.LINE_SEPARATOR);
+ }
+
+ LOGGER.info(sb.toString());
+ LOGGER.warning("** Type 'hint' (without the quotes) and hit ENTER for step-by-step guidance **" + StringUtils.LINE_SEPARATOR);
+ }
+ }
+
+ public Set getEveryCommand() {
+ synchronized (mutex) {
+ SortedSet result = new TreeSet(COMPARATOR);
+ for (Object o : commands) {
+ Method[] methods = o.getClass().getMethods();
+ for (Method m : methods) {
+ CliCommand cmd = m.getAnnotation(CliCommand.class);
+ if (cmd != null) {
+ result.addAll(Arrays.asList(cmd.value()));
+ }
+ }
+ }
+ return result;
+ }
+ }
+
+ public final void add(final CommandMarker command) {
+ synchronized (mutex) {
+ commands.add(command);
+ for (final Method method : command.getClass().getMethods()) {
+ CliAvailabilityIndicator availability = method.getAnnotation(CliAvailabilityIndicator.class);
+ if (availability != null) {
+ Assert.isTrue(method.getParameterTypes().length == 0, "CliAvailabilityIndicator is only legal for 0 parameter methods (" + method.toGenericString() + ")");
+ Assert.isTrue(method.getReturnType().equals(Boolean.TYPE), "CliAvailabilityIndicator is only legal for primitive boolean return types (" + method.toGenericString() + ")");
+ for (String cmd : availability.value()) {
+ Assert.isTrue(!availabilityIndicators.containsKey(cmd), "Cannot specify an availability indicator for '" + cmd + "' more than once");
+ availabilityIndicators.put(cmd, new MethodTarget(method, command));
+ }
+ }
+ }
+ }
+ }
+
+ public final void remove(final CommandMarker command) {
+ synchronized (mutex) {
+ commands.remove(command);
+ for (Method m : command.getClass().getMethods()) {
+ CliAvailabilityIndicator availability = m.getAnnotation(CliAvailabilityIndicator.class);
+ if (availability != null) {
+ for (String cmd : availability.value()) {
+ availabilityIndicators.remove(cmd);
+ }
+ }
+ }
+ }
+ }
+
+ public final void add(final Converter> converter) {
+ synchronized (mutex) {
+ converters.add(converter);
+ }
+ }
+
+ public final void remove(final Converter> converter) {
+ synchronized (mutex) {
+ converters.remove(converter);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java b/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java
new file mode 100644
index 00000000..b8dc4a9e
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java
@@ -0,0 +1,41 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+import org.springframework.roo.shell.SimpleParser;
+
+/**
+ * Available commands converter.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class AvailableCommandsConverter implements Converter {
+
+ public String convertFromText(final String text, final Class> requiredType, final String optionContext) {
+ return text;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return String.class.isAssignableFrom(requiredType) && "availableCommands".equals(optionContext);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ if (target.getTarget() instanceof SimpleParser) {
+ SimpleParser cmd = (SimpleParser) target.getTarget();
+
+ // Only include the first word of each command
+ for (String s : cmd.getEveryCommand()) {
+ if (s.contains(" ")) {
+ completions.add(new Completion(s.substring(0, s.indexOf(" "))));
+ } else {
+ completions.add(new Completion(s));
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java b/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java
new file mode 100644
index 00000000..be606d43
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java
@@ -0,0 +1,29 @@
+package org.springframework.roo.shell.converters;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link BigDecimal}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class BigDecimalConverter implements Converter {
+
+ public BigDecimal convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new BigDecimal(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return BigDecimal.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java b/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java
new file mode 100644
index 00000000..2ddb6c8f
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java
@@ -0,0 +1,29 @@
+package org.springframework.roo.shell.converters;
+
+import java.math.BigInteger;
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link BigInteger}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class BigIntegerConverter implements Converter {
+
+ public BigInteger convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new BigInteger(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return BigInteger.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java b/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java
new file mode 100644
index 00000000..972106a9
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java
@@ -0,0 +1,40 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Boolean}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class BooleanConverter implements Converter {
+
+ public Boolean convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ if ("true".equalsIgnoreCase(value) || "1".equals(value) || "yes".equalsIgnoreCase(value)) {
+ return true;
+ } else if ("false".equalsIgnoreCase(value) || "0".equals(value) || "no".equalsIgnoreCase(value)) {
+ return false;
+ } else {
+ throw new IllegalArgumentException("Cannot convert " + value + " to type Boolean.");
+ }
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ completions.add(new Completion("true"));
+ completions.add(new Completion("false"));
+ completions.add(new Completion("yes"));
+ completions.add(new Completion("no"));
+ completions.add(new Completion("1"));
+ completions.add(new Completion("0"));
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Boolean.class.isAssignableFrom(requiredType) || boolean.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java b/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java
new file mode 100644
index 00000000..085f1059
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Character}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class CharacterConverter implements Converter {
+
+ public Character convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return value.charAt(0);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Character.class.isAssignableFrom(requiredType) || char.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/DateConverter.java b/src/main/java/org/springframework/roo/shell/converters/DateConverter.java
new file mode 100644
index 00000000..e30ce63a
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/DateConverter.java
@@ -0,0 +1,47 @@
+package org.springframework.roo.shell.converters;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Date}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class DateConverter implements Converter {
+
+ // Fields
+ private final DateFormat dateFormat;
+
+ public DateConverter() {
+ this.dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, Locale.getDefault());
+ }
+
+ public DateConverter(final DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public Date convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ try {
+ return dateFormat.parse(value);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse date: " + e.getMessage());
+ }
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Date.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java b/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java
new file mode 100644
index 00000000..c3fa7869
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Double}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class DoubleConverter implements Converter {
+
+ public Double convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new Double(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Double.class.isAssignableFrom(requiredType) || double.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java b/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java
new file mode 100644
index 00000000..10227e9e
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java
@@ -0,0 +1,38 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Enum}.
+ *
+ * @author Ben Alex
+ * @author Alan Stewart
+ * @since 1.0
+ */
+@SuppressWarnings("all")
+public class EnumConverter implements Converter {
+
+ public Enum convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ Class enumClass = (Class) requiredType;
+ return Enum.valueOf(enumClass, value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ Class enumClass = (Class) requiredType;
+ for (Enum enumValue : enumClass.getEnumConstants()) {
+ String candidate = enumValue.name();
+ if ("".equals(existingData) || candidate.startsWith(existingData) || existingData.startsWith(candidate) || candidate.toUpperCase().startsWith(existingData.toUpperCase()) || existingData.toUpperCase().startsWith(candidate.toUpperCase())) {
+ completions.add(new Completion(candidate));
+ }
+ }
+ return true;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Enum.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/FileConverter.java b/src/main/java/org/springframework/roo/shell/converters/FileConverter.java
new file mode 100644
index 00000000..ba5c6f9c
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/FileConverter.java
@@ -0,0 +1,127 @@
+package org.springframework.roo.shell.converters;
+
+import java.io.File;
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.FileUtils;
+
+/**
+ * {@link Converter} for {@link File}.
+ *
+ * @author Stefan Schmidt
+ * @author Roman Kuzmik
+ * @author Ben Alex
+ * @since 1.0
+ */
+public abstract class FileConverter implements Converter {
+
+ private static final String HOME_DIRECTORY_SYMBOL = "~";
+ // Constants
+ private static final String home = System.getProperty("user.home");
+
+ // Fields
+
+ /**
+ * @return the "current working directory" this {@link FileConverter} should use if the user fails to provide
+ * an explicit directory in their input (required)
+ */
+ protected abstract File getWorkingDirectory();
+
+ public File convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new File(convertUserInputIntoAFullyQualifiedPath(value));
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String originalUserInput, final String optionContext, final MethodTarget target) {
+ String adjustedUserInput = convertUserInputIntoAFullyQualifiedPath(originalUserInput);
+
+ String directoryData = adjustedUserInput.substring(0, adjustedUserInput.lastIndexOf(File.separator) + 1);
+ adjustedUserInput = adjustedUserInput.substring(adjustedUserInput.lastIndexOf(File.separator) + 1);
+
+ populate(completions, adjustedUserInput, originalUserInput, directoryData);
+
+ return false;
+ }
+
+ protected void populate(final List completions, final String adjustedUserInput, final String originalUserInput, final String directoryData) {
+ File directory = new File(directoryData);
+
+ if (!directory.isDirectory()) {
+ return;
+ }
+
+ for (File file : directory.listFiles()) {
+ if (adjustedUserInput == null || adjustedUserInput.length() == 0 ||
+ file.getName().toLowerCase().startsWith(adjustedUserInput.toLowerCase())) {
+
+ String completion = "";
+ if (directoryData.length() > 0)
+ completion += directoryData;
+ completion += file.getName();
+
+ completion = convertCompletionBackIntoUserInputStyle(originalUserInput, completion);
+
+ if (file.isDirectory()) {
+ completions.add(new Completion(completion + File.separator));
+ } else {
+ completions.add(new Completion(completion));
+ }
+ }
+ }
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return File.class.isAssignableFrom(requiredType);
+ }
+
+ private String convertCompletionBackIntoUserInputStyle(final String originalUserInput, final String completion) {
+ if (FileUtils.denotesAbsolutePath(originalUserInput)) {
+ // Input was originally as a fully-qualified path, so we just keep the completion in that form
+ return completion;
+ }
+ if (originalUserInput.startsWith(HOME_DIRECTORY_SYMBOL)) {
+ // Input originally started with this symbol, so replace the user's home directory with it again
+ Assert.notNull(home, "Home directory could not be determined from system properties");
+ return HOME_DIRECTORY_SYMBOL + completion.substring(home.length());
+ }
+ // The path was working directory specific, so strip the working directory given the user never typed it
+ return completion.substring(getWorkingDirectoryAsString().length());
+ }
+
+ /**
+ * If the user input starts with a tilde character (~), replace the tilde character with the
+ * user's home directory. If the user input does not start with a tilde, simply return the original
+ * user input without any changes if the input specifies an absolute path, or return an absolute path
+ * based on the working directory if the input specifies a relative path.
+ *
+ * @param userInput the user input, which may commence with a tilde (required)
+ * @return a string that is guaranteed to no longer contain a tilde as the first character (never null)
+ */
+ private String convertUserInputIntoAFullyQualifiedPath(final String userInput) {
+ if (FileUtils.denotesAbsolutePath(userInput)) {
+ // Input is already in a fully-qualified path form
+ return userInput;
+ }
+ if (userInput.startsWith(HOME_DIRECTORY_SYMBOL)) {
+ // Replace this symbol with the user's actual home directory
+ Assert.notNull(home, "Home directory could not be determined from system properties");
+ if (userInput.length() > 1) {
+ return home + userInput.substring(1);
+ }
+ }
+ // The path is working directory specific, so prepend the working directory
+ String fullPath = getWorkingDirectoryAsString() + userInput;
+ return fullPath;
+ }
+
+ private String getWorkingDirectoryAsString() {
+ try {
+ return getWorkingDirectory().getCanonicalPath() + File.separator;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java b/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java
new file mode 100644
index 00000000..684e5d33
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Float}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class FloatConverter implements Converter {
+
+ public Float convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new Float(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Float.class.isAssignableFrom(requiredType) || float.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java b/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java
new file mode 100644
index 00000000..0723c597
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Integer}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class IntegerConverter implements Converter {
+
+ public Integer convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new Integer(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Integer.class.isAssignableFrom(requiredType) || int.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java b/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java
new file mode 100644
index 00000000..5680d366
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java
@@ -0,0 +1,39 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Locale}. Supports locales
+ * with ISO-639 (ie 'en') or a combination of ISO-639 and
+ * ISO-3166 (ie 'en_AU').
+ *
+ * @author Stefan Schmidt
+ * @since 1.1
+ */
+public class LocaleConverter implements Converter {
+
+ public Locale convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ if (value.length() == 2) {
+ // In case only a simpele ISO-639 code is provided we use that code also for the country (ie 'de_DE')
+ return new Locale(value, value.toUpperCase());
+ } else if (value.length() == 5) {
+ String[] split = value.split("_");
+ return new Locale(split[0], split[1]);
+ } else {
+ return null;
+ }
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Locale.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/LongConverter.java b/src/main/java/org/springframework/roo/shell/converters/LongConverter.java
new file mode 100644
index 00000000..795fafff
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/LongConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Long}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class LongConverter implements Converter {
+
+ public Long convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new Long(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Long.class.isAssignableFrom(requiredType) || long.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java b/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java
new file mode 100644
index 00000000..f3eadd05
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link Short}.
+ *
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class ShortConverter implements Converter {
+
+ public Short convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return new Short(value);
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return Short.class.isAssignableFrom(requiredType) || short.class.isAssignableFrom(requiredType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java
new file mode 100644
index 00000000..b99e9ced
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java
@@ -0,0 +1,17 @@
+package org.springframework.roo.shell.converters;
+
+import org.springframework.roo.shell.Converter;
+
+/**
+ * Interface for adding and removing classes that provide static fields which should
+ * be made available via a {@link Converter}.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public interface StaticFieldConverter extends Converter {
+
+ void add(Class> clazz);
+
+ void remove(Class> clazz);
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java
new file mode 100644
index 00000000..fe475a03
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java
@@ -0,0 +1,90 @@
+package org.springframework.roo.shell.converters;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.StringUtils;
+
+/**
+ * A simple {@link Converter} for those classes which provide public static fields to represent possible
+ * textual values.
+ *
+ * @author Stefan Schmidt
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class StaticFieldConverterImpl implements StaticFieldConverter {
+
+ // Fields
+ private final Map,Map> fields = new HashMap,Map>();
+
+ public void add(final Class> clazz) {
+ Assert.notNull(clazz, "A class to provide conversion services is required");
+ Assert.isNull(fields.get(clazz), "Class '" + clazz + "' is already registered for completion services");
+ Map ffields = new HashMap();
+ for (Field field : clazz.getFields()) {
+ int modifier = field.getModifiers();
+ if (Modifier.isStatic(modifier) && Modifier.isPublic(modifier)) {
+ ffields.put(field.getName(), field);
+ }
+ }
+ Assert.notEmpty(ffields, "Zero public static fields accessible in '" + clazz + "'");
+ fields.put(clazz, ffields);
+ }
+
+ public void remove(final Class> clazz) {
+ Assert.notNull(clazz, "A class that was providing conversion services is required");
+ fields.remove(clazz);
+ }
+
+ public Object convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ if (StringUtils.isBlank(value)) {
+ return null;
+ }
+ Map ffields = fields.get(requiredType);
+ if (ffields == null) {
+ return null;
+ }
+ Field f = ffields.get(value);
+ if (f == null) {
+ // Fallback to case insensitive search
+ for (Field candidate : ffields.values()) {
+ if (candidate.getName().equalsIgnoreCase(value)) {
+ f = candidate;
+ break;
+ }
+ }
+ if (f == null) {
+ // Still not found, despite a case-insensitive search
+ return null;
+ }
+ }
+ try {
+ return f.get(null);
+ } catch (Exception ex) {
+ throw new IllegalStateException("Unable to acquire field '" + value + "' from '" + requiredType.getName() + "'", ex);
+ }
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ Map ffields = fields.get(requiredType);
+ if (ffields == null) {
+ return true;
+ }
+ for (String field : ffields.keySet()) {
+ completions.add(new Completion(field));
+ }
+ return true;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return fields.get(requiredType) != null;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/converters/StringConverter.java b/src/main/java/org/springframework/roo/shell/converters/StringConverter.java
new file mode 100644
index 00000000..7b1fc0b9
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/converters/StringConverter.java
@@ -0,0 +1,28 @@
+package org.springframework.roo.shell.converters;
+
+import java.util.List;
+
+import org.springframework.roo.shell.Completion;
+import org.springframework.roo.shell.Converter;
+import org.springframework.roo.shell.MethodTarget;
+
+/**
+ * {@link Converter} for {@link String}.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class StringConverter implements Converter {
+
+ public String convertFromText(final String value, final Class> requiredType, final String optionContext) {
+ return value;
+ }
+
+ public boolean getAllPossibleValues(final List completions, final Class> requiredType, final String existingData, final String optionContext, final MethodTarget target) {
+ return false;
+ }
+
+ public boolean supports(final Class> requiredType, final String optionContext) {
+ return String.class.isAssignableFrom(requiredType) && (optionContext == null || !optionContext.contains("disable-string-converter"));
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java b/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java
new file mode 100644
index 00000000..cdc9056e
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java
@@ -0,0 +1,67 @@
+package org.springframework.roo.shell.event;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.springframework.roo.shell.ParseResult;
+import org.springframework.roo.shell.event.ShellStatus.Status;
+import org.springframework.roo.support.util.Assert;
+
+/**
+ * Provides a convenience superclass for those shells wishing to publish status messages.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public abstract class AbstractShellStatusPublisher implements ShellStatusProvider {
+
+ // Fields
+ protected Set shellStatusListeners = new CopyOnWriteArraySet();
+ protected ShellStatus shellStatus = new ShellStatus(Status.STARTING);
+
+ public final void addShellStatusListener(final ShellStatusListener shellStatusListener) {
+ Assert.notNull(shellStatusListener, "Status listener required");
+ synchronized (shellStatus) {
+ shellStatusListeners.add(shellStatusListener);
+ }
+ }
+
+ public final void removeShellStatusListener(final ShellStatusListener shellStatusListener) {
+ Assert.notNull(shellStatusListener, "Status listener required");
+ synchronized (shellStatus) {
+ shellStatusListeners.remove(shellStatusListener);
+ }
+ }
+
+ public final ShellStatus getShellStatus() {
+ synchronized (shellStatus) {
+ return shellStatus;
+ }
+ }
+
+ protected void setShellStatus(final Status shellStatus) {
+ setShellStatus(shellStatus, null, null);
+ }
+
+ protected void setShellStatus(final Status shellStatus, final String msg, final ParseResult parseResult) {
+ Assert.notNull(shellStatus, "Shell status required");
+
+ synchronized (this.shellStatus) {
+ ShellStatus st;
+ if (msg == null || msg.length() == 0) {
+ st = new ShellStatus(shellStatus);
+ } else {
+ st = new ShellStatus(shellStatus, msg, parseResult);
+ }
+
+ if (this.shellStatus.equals(st)) {
+ return;
+ }
+
+ for (ShellStatusListener listener : shellStatusListeners) {
+ listener.onShellStatusChange(this.shellStatus, st);
+ }
+ this.shellStatus = st;
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/event/ShellStatus.java b/src/main/java/org/springframework/roo/shell/event/ShellStatus.java
new file mode 100644
index 00000000..0e160c7e
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/event/ShellStatus.java
@@ -0,0 +1,95 @@
+package org.springframework.roo.shell.event;
+
+import org.springframework.roo.shell.ParseResult;
+
+/**
+ * Represents the different states that a shell can legally be in.
+ *
+ *
+ * There is no "shut down" state because the shell would have been terminated by
+ * that stage and potentially garbage collected. There is no guarantee that a
+ * shell implementation will necessarily publish every state.
+ *
+ * @author Ben Alex
+ * @author Stefan Schmidt
+ * @since 1.0
+ */
+public class ShellStatus {
+
+ // Fields
+ private final Status status;
+ private String message = "";
+ private ParseResult parseResult;
+
+ public enum Status {
+ STARTING,
+ STARTED,
+ USER_INPUT,
+ PARSING,
+ EXECUTING,
+ EXECUTION_RESULT_PROCESSING,
+ EXECUTION_SUCCESS,
+ EXECUTION_FAILED,
+ SHUTTING_DOWN
+ }
+
+ ShellStatus(final Status status) {
+ this.status = status;
+ }
+
+ ShellStatus(final Status status, final String msg, final ParseResult parseResult) {
+ this.status = status;
+ this.message = msg;
+ this.parseResult = parseResult;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public final ParseResult getParseResult() {
+ return parseResult;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((message == null) ? 0 : message.hashCode());
+ result = prime * result
+ + ((parseResult == null) ? 0 : parseResult.hashCode());
+ result = prime * result + ((status == null) ? 0 : status.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ShellStatus other = (ShellStatus) obj;
+ if (message == null) {
+ if (other.message != null)
+ return false;
+ } else if (!message.equals(other.message))
+ return false;
+ if (parseResult == null) {
+ if (other.parseResult != null)
+ return false;
+ } else if (!parseResult.equals(other.parseResult))
+ return false;
+ if (status == null) {
+ if (other.status != null)
+ return false;
+ } else if (!status.equals(other.status))
+ return false;
+ return true;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java b/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java
new file mode 100644
index 00000000..8acee67b
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java
@@ -0,0 +1,18 @@
+package org.springframework.roo.shell.event;
+
+/**
+ * Implemented by classes that wish to be notified of shell status changes.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public interface ShellStatusListener {
+
+ /**
+ * Invoked by the shell to report a new status.
+ *
+ * @param oldStatus the old status
+ * @param newStatus the new status
+ */
+ void onShellStatusChange(ShellStatus oldStatus, ShellStatus newStatus);
+}
diff --git a/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java b/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java
new file mode 100644
index 00000000..660d7490
--- /dev/null
+++ b/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java
@@ -0,0 +1,48 @@
+package org.springframework.roo.shell.event;
+
+/**
+ * Implemented by shells that support the publication of shell status changes.
+ *
+ *
+ * Implementations are not required to provide any guarantees with respect to the order
+ * in which notifications are delivered to listeners.
+ *
+ *
+ * Implementations must permit modification of the listener list, even while delivering
+ * event notifications to listeners. However, listeners do not receive any guarantee that
+ * their addition or removal from the listener list will be effective or not for any event
+ * notification that is currently proceeding.
+ *
+ *
+ * Implementations must ensure that status notifications are only delivered when an actual
+ * change has taken place.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public interface ShellStatusProvider {
+
+ /**
+ * Registers a new status listener.
+ *
+ * @param shellStatusListener to register (cannot be null)
+ */
+ void addShellStatusListener(ShellStatusListener shellStatusListener);
+
+ /**
+ * Removes an existing status listener.
+ *
+ *
+ * If the presented status listener is not found, the method returns without exception.
+ *
+ * @param shellStatusListener to remove (cannot be null)
+ */
+ void removeShellStatusListener(ShellStatusListener shellStatusListener);
+
+ /**
+ * Returns the current shell status.
+ *
+ * @return the current status (never null)
+ */
+ ShellStatus getShellStatus();
+}
diff --git a/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java b/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java
new file mode 100644
index 00000000..accb678a
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java
@@ -0,0 +1,219 @@
+package org.springframework.roo.support.ant;
+
+import java.util.Map;
+
+/**
+ * Package-protected helper class for {@link AntPathMatcher}. Tests whether or not a string matches against a pattern.
+ * The pattern may contain special characters: '*' means zero or more characters '?' means one and only one
+ * character, '{' and '}' indicate a uri template pattern
+ *
+ * @author Arjen Poutsma
+ * @since 3.0
+ */
+class AntPatchStringMatcher {
+
+ // Fields
+ private final char[] patArr;
+ private final char[] strArr;
+ private int patIdxStart = 0;
+ private int patIdxEnd;
+ private int strIdxStart = 0;
+ private int strIdxEnd;
+ private char ch;
+ private final Map uriTemplateVariables;
+
+ /** Constructs a new instance of the AntPatchStringMatcher. */
+ AntPatchStringMatcher(final String pattern, final String str, final Map uriTemplateVariables) {
+ patArr = pattern.toCharArray();
+ strArr = str.toCharArray();
+ this.uriTemplateVariables = uriTemplateVariables;
+ patIdxEnd = patArr.length - 1;
+ strIdxEnd = strArr.length - 1;
+ }
+
+ private void addTemplateVariable(final int curlyIdxStart, final int curlyIdxEnd, final int valIdxStart, final int valIdxEnd) {
+ if (uriTemplateVariables != null) {
+ String varName = new String(patArr, curlyIdxStart + 1, curlyIdxEnd - curlyIdxStart - 1);
+ String varValue = new String(strArr, valIdxStart, valIdxEnd - valIdxStart + 1);
+ uriTemplateVariables.put(varName, varValue);
+ }
+ }
+
+ /**
+ * Main entry point.
+ *
+ * @return true if the string matches against the pattern, or false otherwise.
+ */
+ boolean matchStrings() {
+ if (shortcutPossible()) {
+ return doShortcut();
+ }
+ if (patternContainsOnlyStar()) {
+ return true;
+ }
+ if (patternContainsOneTemplateVariable()) {
+ addTemplateVariable(0, patIdxEnd, 0, strIdxEnd);
+ return true;
+ }
+ if (!matchBeforeFirstStarOrCurly()) {
+ return false;
+ }
+ if (allCharsUsed()) {
+ return onlyStarsLeft();
+ }
+ if (!matchAfterLastStarOrCurly()) {
+ return false;
+ }
+ if (allCharsUsed()) {
+ return onlyStarsLeft();
+ }
+ // Process pattern between stars. padIdxStart and patIdxEnd point always to a '*'.
+ while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
+ int patIdxTmp;
+ if (patArr[patIdxStart] == '{') {
+ patIdxTmp = findClosingCurly();
+ addTemplateVariable(patIdxStart, patIdxTmp, strIdxStart, strIdxEnd);
+ patIdxStart = patIdxTmp + 1;
+ strIdxStart = strIdxEnd + 1;
+ continue;
+ }
+ patIdxTmp = findNextStarOrCurly();
+ if (consecutiveStars(patIdxTmp)) {
+ continue;
+ }
+ // Find the pattern between padIdxStart & padIdxTmp in str between strIdxStart & strIdxEnd
+ int patLength = (patIdxTmp - patIdxStart - 1);
+ int strLength = (strIdxEnd - strIdxStart + 1);
+ int foundIdx = -1;
+ strLoop:
+ for (int i = 0; i <= strLength - patLength; i++) {
+ for (int j = 0; j < patLength; j++) {
+ ch = patArr[patIdxStart + j + 1];
+ if (ch != '?') {
+ if (ch != strArr[strIdxStart + i + j]) {
+ continue strLoop;
+ }
+ }
+ }
+
+ foundIdx = strIdxStart + i;
+ break;
+ }
+
+ if (foundIdx == -1) {
+ return false;
+ }
+
+ patIdxStart = patIdxTmp;
+ strIdxStart = foundIdx + patLength;
+ }
+
+ return onlyStarsLeft();
+ }
+
+ private boolean consecutiveStars(final int patIdxTmp) {
+ if (patIdxTmp == patIdxStart + 1 && patArr[patIdxStart] == '*' && patArr[patIdxTmp] == '*') {
+ // Two stars next to each other, skip the first one.
+ patIdxStart++;
+ return true;
+ }
+ return false;
+ }
+
+ private int findNextStarOrCurly() {
+ for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
+ if (patArr[i] == '*' || patArr[i] == '{') {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private int findClosingCurly() {
+ for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
+ if (patArr[i] == '}') {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private boolean onlyStarsLeft() {
+ for (int i = patIdxStart; i <= patIdxEnd; i++) {
+ if (patArr[i] != '*') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean allCharsUsed() {
+ return strIdxStart > strIdxEnd;
+ }
+
+ private boolean shortcutPossible() {
+ for (char ch : patArr) {
+ if (ch == '*' || ch == '{' || ch == '}') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean doShortcut() {
+ if (patIdxEnd != strIdxEnd) {
+ return false; // Pattern and string do not have the same size
+ }
+ for (int i = 0; i <= patIdxEnd; i++) {
+ ch = patArr[i];
+ if (ch != '?') {
+ if (ch != strArr[i]) {
+ return false;// Character mismatch
+ }
+ }
+ }
+ return true; // String matches against pattern
+ }
+
+ private boolean patternContainsOnlyStar() {
+ return (patIdxEnd == 0 && patArr[0] == '*');
+ }
+
+ private boolean patternContainsOneTemplateVariable() {
+ if ((patIdxEnd >= 2 && patArr[0] == '{' && patArr[patIdxEnd] == '}')) {
+ for (int i = 1; i < patIdxEnd; i++) {
+ if (patArr[i] == '}') {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean matchBeforeFirstStarOrCurly() {
+ while ((ch = patArr[patIdxStart]) != '*' && ch != '{' && strIdxStart <= strIdxEnd) {
+ if (ch != '?') {
+ if (ch != strArr[strIdxStart]) {
+ return false;
+ }
+ }
+ patIdxStart++;
+ strIdxStart++;
+ }
+ return true;
+ }
+
+ private boolean matchAfterLastStarOrCurly() {
+ while ((ch = patArr[patIdxEnd]) != '*' && ch != '}' && strIdxStart <= strIdxEnd) {
+ if (ch != '?') {
+ if (ch != strArr[strIdxEnd]) {
+ return false;
+ }
+ }
+ patIdxEnd--;
+ strIdxEnd--;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java b/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java
new file mode 100644
index 00000000..23148a01
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java
@@ -0,0 +1,243 @@
+package org.springframework.roo.support.ant;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.StringUtils;
+
+/**
+ * PathMatcher implementation for Ant-style path patterns.
+ * Examples are provided below.
+ *
+ * @author Alef Arendsen
+ * @author Juergen Hoeller
+ * @author Rob Harrop
+ * @since 16.07.2003
+ */
+public class AntPathMatcher implements PathMatcher {
+
+ /** Default path separator: "/" */
+ public static final String DEFAULT_PATH_SEPARATOR = "/";
+
+ private String pathSeparator = DEFAULT_PATH_SEPARATOR;
+
+ /**
+ * Set the path separator to use for pattern parsing.
+ * Default is "/", as in Ant.
+ */
+ public void setPathSeparator(final String pathSeparator) {
+ this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
+ }
+
+ public boolean isPattern(final String path) {
+ return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
+ }
+
+ public boolean match(final String pattern, final String path) {
+ return doMatch(pattern, path, true, null);
+ }
+
+ public boolean matchStart(final String pattern, final String path) {
+ return doMatch(pattern, path, false, null);
+ }
+
+ /**
+ * Actually match the given path against the given pattern.
+ * @param pattern the pattern to match against
+ * @param path the path String to test
+ * @param fullMatch whether a full pattern match is required
+ * (else a pattern match as far as the given base path goes is sufficient)
+ * @return true if the supplied path matched,
+ * false if it didn't
+ */
+ protected boolean doMatch(final String pattern, final String path, final boolean fullMatch, final Map uriTemplateVariables) {
+ if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
+ return false;
+ }
+
+ String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
+ String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);
+
+ int pattIdxStart = 0;
+ int pattIdxEnd = pattDirs.length - 1;
+ int pathIdxStart = 0;
+ int pathIdxEnd = pathDirs.length - 1;
+
+ // Match all elements up to the first **
+ while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+ String patDir = pattDirs[pattIdxStart];
+ if ("**".equals(patDir)) {
+ break;
+ }
+ if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
+ return false;
+ }
+ pattIdxStart++;
+ pathIdxStart++;
+ }
+
+ if (pathIdxStart > pathIdxEnd) {
+ // Path is exhausted, only match if rest of pattern is * or **'s
+ if (pattIdxStart > pattIdxEnd) {
+ return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator));
+ }
+ if (!fullMatch) {
+ return true;
+ }
+ if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
+ return true;
+ }
+ for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+ if (!pattDirs[i].equals("**")) {
+ return false;
+ }
+ }
+ return true;
+ } else if (pattIdxStart > pattIdxEnd) {
+ // String not exhausted, but pattern is. Failure.
+ return false;
+ } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
+ // Path start definitely matches due to "**" part in pattern.
+ return true;
+ }
+
+ // Up to last '**'
+ while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+ String patDir = pattDirs[pattIdxEnd];
+ if (patDir.equals("**")) {
+ break;
+ }
+ if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
+ return false;
+ }
+ pattIdxEnd--;
+ pathIdxEnd--;
+ }
+ if (pathIdxStart > pathIdxEnd) {
+ // String is exhausted
+ for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+ if (!pattDirs[i].equals("**")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+ int patIdxTmp = -1;
+ for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
+ if (pattDirs[i].equals("**")) {
+ patIdxTmp = i;
+ break;
+ }
+ }
+ if (patIdxTmp == pattIdxStart + 1) {
+ // '**/**' situation, so skip one
+ pattIdxStart++;
+ continue;
+ }
+ // Find the pattern between padIdxStart & padIdxTmp in str between strIdxStart & strIdxEnd
+ int patLength = (patIdxTmp - pattIdxStart - 1);
+ int strLength = (pathIdxEnd - pathIdxStart + 1);
+ int foundIdx = -1;
+
+ strLoop: for (int i = 0; i <= strLength - patLength; i++) {
+ for (int j = 0; j < patLength; j++) {
+ String subPat = pattDirs[pattIdxStart + j + 1];
+ String subStr = pathDirs[pathIdxStart + i + j];
+ if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
+ continue strLoop;
+ }
+ }
+ foundIdx = pathIdxStart + i;
+ break;
+ }
+
+ if (foundIdx == -1) {
+ return false;
+ }
+
+ pattIdxStart = patIdxTmp;
+ pathIdxStart = foundIdx + patLength;
+ }
+
+ for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+ if (!pattDirs[i].equals("**")) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Tests whether or not a string matches against a pattern.
+ * The pattern may contain two special characters:
+ * '*' means zero or more characters
+ * '?' means one and only one character
+ * @param pattern pattern to match against.
+ * Must not be null.
+ * @param str string which must be matched against the pattern.
+ * Must not be null.
+ * @return true if the string matches against the
+ * pattern, or false otherwise.
+ */
+ private boolean matchStrings(final String pattern, final String str, final Map uriTemplateVariables) {
+ AntPatchStringMatcher matcher = new AntPatchStringMatcher(pattern, str, uriTemplateVariables);
+ return matcher.matchStrings();
+ }
+
+ /**
+ * Given a pattern and a full path, determine the pattern-mapped part.
+ * For example:
+ *
+ * '/docs/cvs/commit.html' and '/docs/cvs/commit.html -> ''
+ * '/docs/*' and '/docs/cvs/commit -> 'cvs/commit'
+ * '/docs/cvs/*.html' and '/docs/cvs/commit.html -> 'commit.html'
+ * '/docs/**' and '/docs/cvs/commit -> 'cvs/commit'
+ * '/docs/**\/*.html' and '/docs/cvs/commit.html -> 'cvs/commit.html'
+ * '/*.html' and '/docs/cvs/commit.html -> 'docs/cvs/commit.html'
+ * '*.html' and '/docs/cvs/commit.html -> '/docs/cvs/commit.html'
+ * '*' and '/docs/cvs/commit.html -> '/docs/cvs/commit.html'
+ *
+ * Assumes that {@link #match} returns true for 'pattern'
+ * and 'path', but does not enforce this.
+ */
+ public String extractPathWithinPattern(final String pattern, final String path) {
+ String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
+ String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator);
+
+ StringBuilder builder = new StringBuilder();
+
+ // Add any path parts that have a wildcarded pattern part.
+ int puts = 0;
+ for (int i = 0; i < patternParts.length; i++) {
+ String patternPart = patternParts[i];
+ if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {
+ if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) {
+ builder.append(this.pathSeparator);
+ }
+ builder.append(pathParts[i]);
+ puts++;
+ }
+ }
+
+ // Append any trailing path parts.
+ for (int i = patternParts.length; i < pathParts.length; i++) {
+ if (puts > 0 || i > 0) {
+ builder.append(this.pathSeparator);
+ }
+ builder.append(pathParts[i]);
+ }
+
+ return builder.toString();
+ }
+
+ public Map extractUriTemplateVariables(final String pattern, final String path) {
+ Map variables = new LinkedHashMap();
+ boolean result = doMatch(pattern, path, true, variables);
+ Assert.state(result, "Pattern \"" + pattern + "\" is not a match for \"" + path + "\"");
+ return variables;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/ant/PathMatcher.java b/src/main/java/org/springframework/roo/support/ant/PathMatcher.java
new file mode 100644
index 00000000..22e795ac
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/ant/PathMatcher.java
@@ -0,0 +1,84 @@
+package org.springframework.roo.support.ant;
+
+import java.util.Map;
+
+/**
+ * Strategy interface for String-based path matching.
+ *
+ * The default implementation is {@link AntPathMatcher}, supporting the
+ * Ant-style pattern syntax.
+ *
+ * @author Juergen Hoeller
+ * @since 1.2.0
+ * @see AntPathMatcher
+ */
+public interface PathMatcher {
+
+ /**
+ * Does the given path represent a pattern that can be matched
+ * by an implementation of this interface?
+ *
If the return value is false, then the {@link #match}
+ * method does not have to be used because direct equality comparisons
+ * on the static path Strings will lead to the same result.
+ * @param path the path String to check
+ * @return true if the given path represents a pattern
+ */
+ boolean isPattern(String path);
+
+ /**
+ * Match the given path against the given pattern,
+ * according to this PathMatcher's matching strategy.
+ * @param pattern the pattern to match against
+ * @param path the path String to test
+ * @return true if the supplied path matched,
+ * false if it didn't
+ */
+ boolean match(String pattern, String path);
+
+ /**
+ * Match the given path against the corresponding part of the given
+ * pattern, according to this PathMatcher's matching strategy.
+ *
Determines whether the pattern at least matches as far as the given base
+ * path goes, assuming that a full path may then match as well.
+ * @param pattern the pattern to match against
+ * @param path the path String to test
+ * @return true if the supplied path matched,
+ * false if it didn't
+ */
+ boolean matchStart(String pattern, String path);
+
+ /**
+ * Given a pattern and a full path, determine the pattern-mapped part.
+ *
This method is supposed to find out which part of the path is matched
+ * dynamically through an actual pattern, that is, it strips off a statically
+ * defined leading path from the given full path, returning only the actually
+ * pattern-matched part of the path.
+ *
For example: For "myroot/*.html" as pattern and "myroot/myfile.html"
+ * as full path, this method should return "myfile.html". The detailed
+ * determination rules are specified to this PathMatcher's matching strategy.
+ *
A simple implementation may return the given full path as-is in case
+ * of an actual pattern, and the empty String in case of the pattern not
+ * containing any dynamic parts (i.e. the pattern parameter being
+ * a static path that wouldn't qualify as an actual {@link #isPattern pattern}).
+ * A sophisticated implementation will differentiate between the static parts
+ * and the dynamic parts of the given path pattern.
+ * @param pattern the path pattern
+ * @param path the full path to introspect
+ * @return the pattern-mapped part of the given path
+ * (never null)
+ */
+ String extractPathWithinPattern(String pattern, String path);
+
+ /**
+ * Given a pattern and a full path, extract the URI template variables. URI template
+ * variables are expressed through curly brackets ('{' and '}').
+ *
+ *
For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will
+ * return a map containing "hotel"->"1".
+ *
+ * @param pattern the path pattern, possibly containing URI templates
+ * @param path the full path to extract template variables from
+ * @return a map, containing variable names as keys; variables values as values
+ */
+ Map extractUriTemplateVariables(String pattern, String path);
+}
diff --git a/src/main/java/org/springframework/roo/support/api/AddOnSearch.java b/src/main/java/org/springframework/roo/support/api/AddOnSearch.java
new file mode 100644
index 00000000..972c7591
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/api/AddOnSearch.java
@@ -0,0 +1,38 @@
+package org.springframework.roo.support.api;
+
+import java.util.logging.Logger;
+
+/**
+ * Interface defining an add-on search service.
+ *
+ *
+ * This interface is included in the support module because several of Roo's core
+ * infrastructure modules require add-on search capabilities.
+ *
+ * @author Ben Alex
+ * @author Stefan Schmidt
+ * @since 1.1.1
+ */
+public interface AddOnSearch {
+
+ /**
+ * Search all add-ons presently known this Roo instance, including add-ons which have
+ * not been downloaded or installed by the user.
+ *
+ *
+ * Information is optionally emitted to the console via {@link Logger#info}.
+ *
+ * @param showFeedback if false will never output any messages to the console (required)
+ * @param searchTerms comma separated list of search terms (required)
+ * @param refresh attempt a fresh download of roobot.xml (optional)
+ * @param linesPerResult maximum number of lines per add-on (optional)
+ * @param maxResults maximum number of results to display (optional)
+ * @param trustedOnly display only trusted add-ons in search results (optional)
+ * @param compatibleOnly display only compatible add-ons in search results (optional)
+ * @param communityOnly display only community-provided add-ons in search results (optional)
+ * @param requiresCommand display only add-ons which offer the specified command (optional)
+ * @return the total number of matches found, even if only some of these are displayed due to maxResults
+ * (or null if the add-on list is unavailable for some reason, eg network problems etc)
+ */
+ Integer searchAddOns(boolean showFeedback, String searchTerms, boolean refresh, int linesPerResult, int maxResults, boolean trustedOnly, boolean compatibleOnly, boolean communityOnly, String requiresCommand);
+}
diff --git a/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java b/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java
new file mode 100644
index 00000000..0c30bb0d
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java
@@ -0,0 +1,128 @@
+package org.springframework.roo.support.logging;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+import org.springframework.roo.support.util.Assert;
+
+/**
+ * Defers the publication of JDK {@link LogRecord} instances until a target {@link Handler} is registered.
+ *
+ *
+ * This class is useful if a target {@link Handler} cannot be instantiated before {@link LogRecord} instances are being
+ * published. This may be the case if the target {@link Handler} requires the establishment of complex publication
+ * infrastructure such as a GUI, message queue, IoC container and the establishment of that infrastructure may produce
+ * log messages that should ultimately be delivered to the target {@link Handler}.
+ *
+ *
+ * In recognition that sometimes the target {@link Handler} may never be registered (perhaps due to failures configuring
+ * its supporting infrastructure), this class supports a fallback mode. When in fallback mode, a fallback {@link Handler}
+ * will receive all previous and future {@link LogRecord} instances. Fallback mode is automatically triggered if a
+ * {@link LogRecord} is published at the fallback {@link Level}. Fallback mode is also triggered if the {@link #flush()}
+ * or {@link #close()} method is involved and the target {@link Handler} has never been registered.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public class DeferredLogHandler extends Handler {
+
+ // Fields
+ private final List logRecords = Collections.synchronizedList(new ArrayList());
+ private final Handler fallbackHandler;
+ private final Level fallbackPushLevel;
+ private boolean fallbackMode = false;
+ private Handler targetHandler;
+
+ /**
+ * Creates an instance that will publish all recorded {@link LogRecord} instances to the specified fallback
+ * {@link Handler} if an event of the specified {@link Level} is received.
+ *
+ * @param fallbackHandler to publish events to (mandatory)
+ * @param fallbackPushLevel the level which will trigger an event publication (mandatory)
+ */
+ public DeferredLogHandler(final Handler fallbackHandler, final Level fallbackPushLevel) {
+ Assert.notNull(fallbackHandler, "Fallback handler required");
+ Assert.notNull(fallbackPushLevel, "Fallback push level required");
+ this.fallbackHandler = fallbackHandler;
+ this.fallbackPushLevel = fallbackPushLevel;
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ if (targetHandler == null) {
+ fallbackMode = true;
+ }
+ if (fallbackMode) {
+ publishLogRecordsTo(fallbackHandler);
+ fallbackHandler.close();
+ return;
+ }
+ targetHandler.close();
+ }
+
+ @Override
+ public void flush() {
+ if (targetHandler == null) {
+ fallbackMode = true;
+ }
+ if (fallbackMode) {
+ publishLogRecordsTo(fallbackHandler);
+ fallbackHandler.flush();
+ return;
+ }
+ targetHandler.flush();
+ }
+
+ /**
+ * Stores the log record internally.
+ */
+ @Override
+ public void publish(final LogRecord record) {
+ if (!isLoggable(record)) {
+ return;
+ }
+ if (fallbackMode) {
+ fallbackHandler.publish(record);
+ return;
+ }
+ if (targetHandler != null) {
+ targetHandler.publish(record);
+ return;
+ }
+ synchronized (logRecords) {
+ logRecords.add(record);
+ }
+ if (!fallbackMode && record.getLevel().intValue() >= fallbackPushLevel.intValue()) {
+ fallbackMode = true;
+ publishLogRecordsTo(fallbackHandler);
+ }
+ }
+
+ /**
+ * @return the target {@link Handler}, or null if there is no target {@link Handler} defined so far
+ */
+ public Handler getTargetHandler() {
+ return targetHandler;
+ }
+
+ public void setTargetHandler(final Handler targetHandler) {
+ Assert.notNull(targetHandler, "Must specify a target handler");
+ this.targetHandler = targetHandler;
+ if (!fallbackMode) {
+ publishLogRecordsTo(this.targetHandler);
+ }
+ }
+
+ private void publishLogRecordsTo(final Handler destination) {
+ synchronized (logRecords) {
+ for (LogRecord record : logRecords) {
+ destination.publish(record);
+ }
+ logRecords.clear();
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java b/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java
new file mode 100644
index 00000000..f7c6067c
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java
@@ -0,0 +1,148 @@
+package org.springframework.roo.support.logging;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.StringUtils;
+
+/**
+ * Utility methods for dealing with {@link Handler} objects.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ *
+ */
+public abstract class HandlerUtils {
+
+ /**
+ * Obtains a {@link Logger} that guarantees to set the {@link Level}
+ * to {@link Level#FINE} if it is part of org.springframework.roo.
+ * Unfortunately this is needed due to a regression in JDK 1.6.0_18
+ * as per issue ROO-539.
+ *
+ * @param clazz to retrieve the logger for (required)
+ * @return the logger, which will at least of {@link Level#FINE} if no level was specified
+ */
+ public static Logger getLogger(final Class> clazz) {
+ Assert.notNull(clazz, "Class required");
+ Logger logger = Logger.getLogger(clazz.getName());
+ if (logger.getLevel() == null && clazz.getName().startsWith("org.springframework.roo")) {
+ logger.setLevel(Level.FINE);
+ }
+ return logger;
+ }
+
+ /**
+ * Replaces each {@link Handler} defined against the presented {@link Logger} with {@link DeferredLogHandler}.
+ *
+ *
+ * This is useful for ensuring any {@link Handler} defaults defined by the user are preserved and treated as the
+ * {@link DeferredLogHandler} "fallback" {@link Handler} if the indicated severity {@link Level} is encountered.
+ *
+ *
+ * This method will create a {@link ConsoleHandler} if the presented {@link Logger} has no current {@link Handler}.
+ *
+ * @param logger to introspect and replace the {@link Handler}s for (required)
+ * @param fallbackSeverity to trigger fallback mode (required)
+ * @return the number of {@link DeferredLogHandler}s now registered against the {@link Logger} (guaranteed to be 1 or above)
+ */
+ public static int wrapWithDeferredLogHandler(final Logger logger, final Level fallbackSeverity) {
+ Assert.notNull(logger, "Logger is required");
+ Assert.notNull(fallbackSeverity, "Fallback severity is required");
+
+ List newHandlers = new ArrayList();
+
+ // Create DeferredLogHandlers for each Handler in presented Logger
+ Handler[] handlers = logger.getHandlers();
+ if (handlers != null && handlers.length > 0) {
+ for (Handler h : handlers) {
+ logger.removeHandler(h);
+ newHandlers.add(new DeferredLogHandler(h, fallbackSeverity));
+ }
+ }
+
+ // Create a default DeferredLogHandler if no Handler was defined in the presented Logger
+ if (newHandlers.isEmpty()) {
+ ConsoleHandler consoleHandler = new ConsoleHandler();
+ consoleHandler.setFormatter(new Formatter() {
+ @Override
+ public String format(final LogRecord record) {
+ return record.getMessage() + StringUtils.LINE_SEPARATOR;
+ }
+ });
+ newHandlers.add(new DeferredLogHandler(consoleHandler, fallbackSeverity));
+ }
+
+ // Add the new DeferredLogHandlers to the presented Logger
+ for (DeferredLogHandler h : newHandlers) {
+ logger.addHandler(h);
+ }
+
+ return newHandlers.size();
+ }
+
+ /**
+ * Registers the presented target {@link Handler} against any {@link DeferredLogHandler} encountered in the presented
+ * {@link Logger}.
+ *
+ *
+ * Generally this method is used on {@link Logger} instances that have previously been presented to the
+ * {@link #wrapWithDeferredLogHandler(Logger, Level)} method.
+ *
+ *
+ * The method will return a count of how many {@link DeferredLogHandler} instances it detected. Note that no
+ * attempt is made to distinguish between instances already possessing the intended target {@link Handler}
+ * or those already possessing any target {@link Handler} at all. This method always overwrites the target
+ * {@link Handler} and the returned count represents how many overwrites took place.
+ *
+ * @param logger to introspect for {@link DeferredLogHandler} instances (required)
+ * @param target to set as the target {@link Handler}
+ * @return number of {@link DeferredLogHandler} instances detected and updated (may be 0 if none found)
+ */
+ public static int registerTargetHandler(final Logger logger, final Handler target) {
+ Assert.notNull(logger, "Logger is required");
+ Assert.notNull(target, "Target handler is required");
+
+ int replaced = 0;
+ Handler[] handlers = logger.getHandlers();
+ if (handlers != null && handlers.length > 0) {
+ for (Handler h : handlers) {
+ if (h instanceof DeferredLogHandler) {
+ replaced++;
+ DeferredLogHandler defLogger = (DeferredLogHandler) h;
+ defLogger.setTargetHandler(target);
+ }
+ }
+ }
+
+ return replaced;
+ }
+
+ /**
+ * Forces all {@link Handler} instances registered in the presented {@link Logger} to be flushed.
+ *
+ * @param logger to flush (required)
+ * @return the number of {@link Handler}s flushed (may be 0 or above)
+ */
+ public static int flushAllHandlers(final Logger logger) {
+ Assert.notNull(logger, "Logger is required");
+
+ int flushed = 0;
+ Handler[] handlers = logger.getHandlers();
+ if (handlers != null && handlers.length > 0) {
+ for (Handler h : handlers) {
+ flushed++;
+ h.flush();
+ }
+ }
+
+ return flushed;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java b/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java
new file mode 100644
index 00000000..a808eb60
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java
@@ -0,0 +1,75 @@
+package org.springframework.roo.support.logging;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.IOUtils;
+
+/**
+ * Wraps an {@link OutputStream} and automatically passes each line to the {@link Logger}
+ * when {@link OutputStream#flush()} or {@link OutputStream#close()} is called.
+ *
+ * @author Ben Alex
+ * @since 1.1
+ */
+public class LoggingOutputStream extends OutputStream {
+
+ // Constants
+ protected static final Logger LOGGER = HandlerUtils.getLogger(LoggingOutputStream.class);
+
+ // Fields
+ private final Level level;
+ private String sourceClassName = LoggingOutputStream.class.getName();
+ private int count;
+ private ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ /**
+ * Constructor
+ *
+ * @param level the level at which to log (required)
+ */
+ public LoggingOutputStream(final Level level) {
+ Assert.notNull(level, "A logging level is required");
+ this.level = level;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ baos.write(b);
+ count++;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (count > 0) {
+ String msg = new String(baos.toByteArray());
+ LogRecord record = new LogRecord(level, msg);
+ record.setSourceClassName(sourceClassName);
+ try {
+ LOGGER.log(record);
+ } finally {
+ count = 0;
+ IOUtils.closeQuietly(baos);
+ baos = new ByteArrayOutputStream();
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ flush();
+ }
+
+ public String getSourceClassName() {
+ return sourceClassName;
+ }
+
+ public void setSourceClassName(final String sourceClassName) {
+ this.sourceClassName = sourceClassName;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/style/DefaultToStringStyler.java b/src/main/java/org/springframework/roo/support/style/DefaultToStringStyler.java
new file mode 100644
index 00000000..c67e5392
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/style/DefaultToStringStyler.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.style;
+
+import org.springframework.roo.support.util.Assert;
+import org.springframework.roo.support.util.ClassUtils;
+import org.springframework.roo.support.util.ObjectUtils;
+
+/**
+ * Spring's default toString() styler.
+ *
+ *
This class is used by {@link ToStringCreator} to style toString()
+ * output in a consistent manner according to Spring conventions.
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @since 1.2.2
+ */
+public class DefaultToStringStyler implements ToStringStyler {
+
+ // Fields
+ private final ValueStyler valueStyler;
+
+ /**
+ * Create a new DefaultToStringStyler.
+ * @param valueStyler the ValueStyler to use
+ */
+ public DefaultToStringStyler(final ValueStyler valueStyler) {
+ Assert.notNull(valueStyler, "ValueStyler must not be null");
+ this.valueStyler = valueStyler;
+ }
+
+ /**
+ * Return the ValueStyler used by this ToStringStyler.
+ */
+ protected final ValueStyler getValueStyler() {
+ return this.valueStyler;
+ }
+
+ public void styleStart(final StringBuilder buffer, final Object obj) {
+ if (!obj.getClass().isArray()) {
+ buffer.append('[').append(ClassUtils.getShortName(obj.getClass()));
+ styleIdentityHashCode(buffer, obj);
+ }
+ else {
+ buffer.append('[');
+ styleIdentityHashCode(buffer, obj);
+ buffer.append(' ');
+ styleValue(buffer, obj);
+ }
+ }
+
+ private void styleIdentityHashCode(final StringBuilder buffer, final Object obj) {
+ buffer.append('@');
+ buffer.append(ObjectUtils.getIdentityHexString(obj));
+ }
+
+ public void styleEnd(final StringBuilder buffer, final Object o) {
+ buffer.append(']');
+ }
+
+ public void styleField(final StringBuilder buffer, final String fieldName, final Object value) {
+ styleFieldStart(buffer, fieldName);
+ styleValue(buffer, value);
+ styleFieldEnd(buffer, fieldName);
+ }
+
+ protected void styleFieldStart(final StringBuilder buffer, final String fieldName) {
+ buffer.append(' ').append(fieldName).append(" = ");
+ }
+
+ protected void styleFieldEnd(final StringBuilder buffer, final String fieldName) {
+ }
+
+ public void styleValue(final StringBuilder buffer, final Object value) {
+ buffer.append(this.valueStyler.style(value));
+ }
+
+ public void styleFieldSeparator(final StringBuilder buffer) {
+ buffer.append(',');
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/style/DefaultValueStyler.java b/src/main/java/org/springframework/roo/support/style/DefaultValueStyler.java
new file mode 100644
index 00000000..3e256283
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/style/DefaultValueStyler.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.style;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.roo.support.util.ClassUtils;
+import org.springframework.roo.support.util.ObjectUtils;
+
+/**
+ * Converts objects to String form, generally for debugging purposes,
+ * using Spring's toString styling conventions.
+ *
+ *
Uses the reflective visitor pattern underneath the hood to nicely
+ * encapsulate styling algorithms for each type of styled object.
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @since 1.2.2
+ */
+public class DefaultValueStyler implements ValueStyler {
+ private static final String EMPTY = "[empty]";
+ private static final String NULL = "[null]";
+ private static final String COLLECTION = "collection";
+ private static final String SET = "set";
+ private static final String LIST = "list";
+ private static final String MAP = "map";
+ private static final String ARRAY = "array";
+
+ public String style(final Object value) {
+ if (value == null) {
+ return NULL;
+ } else if (value instanceof String) {
+ return "\'" + value + "\'";
+ } else if (value instanceof Class>) {
+ return ClassUtils.getShortName((Class>) value);
+ } else if (value instanceof Method) {
+ Method method = (Method) value;
+ return method.getName() + "@" + ClassUtils.getShortName(method.getDeclaringClass());
+ } else if (value instanceof Map, ?>) {
+ return style((Map, ?>) value);
+ } else if (value instanceof Map.Entry, ?>) {
+ return style((Map.Entry, ?>) value);
+ } else if (value instanceof Collection>) {
+ return style((Collection>) value);
+ } else if (value.getClass().isArray()) {
+ return styleArray(ObjectUtils.toObjectArray(value));
+ } else {
+ return String.valueOf(value);
+ }
+ }
+
+ private String style(final Map, ?> value) {
+ StringBuilder result = new StringBuilder(value.size() * 8 + 16);
+ result.append(MAP + "[");
+ for (Iterator> it = value.entrySet().iterator(); it.hasNext();) {
+ Map.Entry, ?> entry = (Map.Entry, ?>) it.next();
+ result.append(style(entry));
+ if (it.hasNext()) {
+ result.append(',').append(' ');
+ }
+ }
+ if (value.isEmpty()) {
+ result.append(EMPTY);
+ }
+ result.append("]");
+ return result.toString();
+ }
+
+ private String style(final Map.Entry, ?> value) {
+ return style(value.getKey()) + " -> " + style(value.getValue());
+ }
+
+ private String style(final Collection> value) {
+ StringBuilder result = new StringBuilder(value.size() * 8 + 16);
+ result.append(getCollectionTypeString(value)).append('[');
+ for (Iterator> i = value.iterator(); i.hasNext();) {
+ result.append(style(i.next()));
+ if (i.hasNext()) {
+ result.append(',').append(' ');
+ }
+ }
+ if (value.isEmpty()) {
+ result.append(EMPTY);
+ }
+ result.append("]");
+ return result.toString();
+ }
+
+ private String getCollectionTypeString(final Collection> value) {
+ if (value instanceof List>) {
+ return LIST;
+ } else if (value instanceof Set>) {
+ return SET;
+ } else {
+ return COLLECTION;
+ }
+ }
+
+ private String styleArray(final Object[] array) {
+ StringBuilder result = new StringBuilder(array.length * 8 + 16);
+ result.append(ARRAY + "<").append(ClassUtils.getShortName(array.getClass().getComponentType())).append(">[");
+ for (int i = 0; i < array.length - 1; i++) {
+ result.append(style(array[i]));
+ result.append(',').append(' ');
+ }
+ if (array.length > 0) {
+ result.append(style(array[array.length - 1]));
+ } else {
+ result.append(EMPTY);
+ }
+ result.append("]");
+ return result.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/style/StylerUtils.java b/src/main/java/org/springframework/roo/support/style/StylerUtils.java
new file mode 100644
index 00000000..449bc108
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/style/StylerUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.style;
+
+/**
+ * Simple utility class to allow for convenient access to value
+ * styling logic, mainly to support descriptive logging messages.
+ *
+ *
For more sophisticated needs, use the {@link ValueStyler} abstraction
+ * directly. This class simply uses a shared {@link DefaultValueStyler}
+ * instance underneath.
+ *
+ * @author Keith Donald
+ * @since 1.2.2
+ * @see ValueStyler
+ * @see DefaultValueStyler
+ */
+public abstract class StylerUtils {
+
+ /**
+ * Default ValueStyler instance used by the style method.
+ * Also available for the {@link ToStringCreator} class in this package.
+ */
+ static final ValueStyler DEFAULT_VALUE_STYLER = new DefaultValueStyler();
+
+ /**
+ * Style the specified value according to default conventions.
+ * @param value the Object value to style
+ * @return the styled String
+ * @see DefaultValueStyler
+ */
+ public static String style(final Object value) {
+ return DEFAULT_VALUE_STYLER.style(value);
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/style/ToStringCreator.java b/src/main/java/org/springframework/roo/support/style/ToStringCreator.java
new file mode 100644
index 00000000..0f58209e
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/style/ToStringCreator.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.style;
+
+import org.springframework.roo.support.util.Assert;
+
+/**
+ * Utility class that builds pretty-printing toString() methods
+ * with pluggable styling conventions. By default, ToStringCreator adheres
+ * to Spring's toString() styling conventions.
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @since 1.2.2
+ */
+public class ToStringCreator {
+
+ /**
+ * Default ToStringStyler instance used by this ToStringCreator.
+ */
+ private static final ToStringStyler DEFAULT_TO_STRING_STYLER = new DefaultToStringStyler(StylerUtils.DEFAULT_VALUE_STYLER);
+
+ // Fields
+ private final StringBuilder buffer = new StringBuilder(512);
+ private final ToStringStyler styler;
+ private final Object object;
+
+ private boolean styledFirstField;
+
+ /**
+ * Create a ToStringCreator for the given object.
+ *
+ * @param obj the object to be stringified
+ */
+ public ToStringCreator(final Object obj) {
+ this(obj, (ToStringStyler) null);
+ }
+
+ /**
+ * Create a ToStringCreator for the given object, using the provided style.
+ *
+ * @param obj the object to be stringified
+ * @param styler the ValueStyler encapsulating pretty-print instructions
+ */
+ public ToStringCreator(final Object obj, final ValueStyler styler) {
+ this(obj, new DefaultToStringStyler(styler != null ? styler : StylerUtils.DEFAULT_VALUE_STYLER));
+ }
+
+ /**
+ * Create a ToStringCreator for the given object, using the provided style.
+ *
+ * @param obj the object to be stringified
+ * @param styler the ToStringStyler encapsulating pretty-print instructions
+ */
+ public ToStringCreator(final Object obj, final ToStringStyler styler) {
+ Assert.notNull(obj, "The object to be styled must not be null");
+ this.object = obj;
+ this.styler = (styler != null ? styler : DEFAULT_TO_STRING_STYLER);
+ this.styler.styleStart(this.buffer, this.object);
+ }
+
+ /**
+ * Append a byte field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final byte value) {
+ return append(fieldName, Byte.valueOf(value));
+ }
+
+ /**
+ * Append a short field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final short value) {
+ return append(fieldName, Short.valueOf(value));
+ }
+
+ /**
+ * Append a integer field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final int value) {
+ return append(fieldName, Integer.valueOf(value));
+ }
+
+ /**
+ * Append a long field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final long value) {
+ return append(fieldName, Long.valueOf(value));
+ }
+
+ /**
+ * Append a float field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final float value) {
+ return append(fieldName, new Float(value));
+ }
+
+ /**
+ * Append a double field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final double value) {
+ return append(fieldName, new Double(value));
+ }
+
+ /**
+ * Append a boolean field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final boolean value) {
+ return append(fieldName, Boolean.valueOf(value));
+ }
+
+ /**
+ * Append a field value.
+ *
+ * @param fieldName the name of the field, usually the member variable name
+ * @param value the field value; can be null
+ * @return this, to support call-chaining
+ */
+ public ToStringCreator append(final String fieldName, final Object value) {
+ printFieldSeparatorIfNecessary();
+ this.styler.styleField(this.buffer, fieldName, value);
+ return this;
+ }
+
+ private void printFieldSeparatorIfNecessary() {
+ if (this.styledFirstField) {
+ this.styler.styleFieldSeparator(this.buffer);
+ }
+ else {
+ this.styledFirstField = true;
+ }
+ }
+
+ /**
+ * Append the provided value.
+ *
+ * @param value The value to append
+ * @return this, to support call-chaining.
+ */
+ public ToStringCreator append(final Object value) {
+ this.styler.styleValue(this.buffer, value);
+ return this;
+ }
+
+ /**
+ * Return the String representation that this ToStringCreator built.
+ */
+ @Override
+ public String toString() {
+ this.styler.styleEnd(this.buffer, this.object);
+ return this.buffer.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/style/ToStringStyler.java b/src/main/java/org/springframework/roo/support/style/ToStringStyler.java
new file mode 100644
index 00000000..c1f999e6
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/style/ToStringStyler.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.style;
+
+/**
+ * A strategy interface for pretty-printing toString() methods.
+ * Encapsulates the print algorithms; some other object such as a builder
+ * should provide the workflow.
+ *
+ * @author Keith Donald
+ * @since 1.2.2
+ */
+public interface ToStringStyler {
+
+ /**
+ * Style a toString()'ed object before its fields are styled.
+ *
+ * @param buffer the buffer to print to
+ * @param obj the object to style; can be null
+ */
+ void styleStart(StringBuilder buffer, Object obj);
+
+ /**
+ * Style a toString()'ed object after it's fields are styled.
+ *
+ * @param buffer the buffer to print to
+ * @param obj the object to style; can be null
+ */
+ void styleEnd(StringBuilder buffer, Object obj);
+
+ /**
+ * Style a field value as a string.
+ *
+ * @param buffer the buffer to print to
+ * @param fieldName the he name of the field
+ * @param value the field value; can be null
+ */
+ void styleField(StringBuilder buffer, String fieldName, Object value);
+
+ /**
+ * Style the given value.
+ *
+ * @param buffer the buffer to print to
+ * @param value the field value; can be null
+ */
+ void styleValue(StringBuilder buffer, Object value);
+
+ /**
+ * Style the field separator.
+ *
+ * @param buffer buffer to print to
+ */
+ void styleFieldSeparator(StringBuilder buffer);
+}
diff --git a/src/main/java/org/springframework/roo/support/style/ValueStyler.java b/src/main/java/org/springframework/roo/support/style/ValueStyler.java
new file mode 100644
index 00000000..4ec3aca3
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/style/ValueStyler.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.style;
+
+/**
+ * Strategy that encapsulates value String styling algorithms
+ * according to Spring conventions.
+ *
+ * @author Keith Donald
+ * @since 1.2.2
+ */
+public interface ValueStyler {
+
+ /**
+ * Style the given value, returning a String representation.
+ * @param value the Object value to style
+ * @return the styled String
+ */
+ String style(Object value);
+}
diff --git a/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java b/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java
new file mode 100644
index 00000000..e84d68ad
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java
@@ -0,0 +1,69 @@
+package org.springframework.roo.support.util;
+
+/**
+ * ANSI escape codes supported by JLine
+ *
+ * @author Andrew Swan
+ * @since 1.2.0
+ */
+public enum AnsiEscapeCode {
+
+ // These int literals are non-public constants in ANSIBuffer.ANSICodes
+ BLINK(5),
+ BOLD(1),
+ CONCEALED(8),
+ FG_BLACK(30),
+ FG_BLUE(34),
+ FG_CYAN(36),
+ FG_GREEN(32),
+ FG_MAGENTA(35),
+ FG_RED(31),
+ FG_YELLOW(33),
+ FG_WHITE(37),
+ OFF(0),
+ REVERSE(7),
+ UNDERSCORE(4);
+
+ // Constant for the escape character
+ private static final boolean ANSI_SUPPORTED = Boolean.getBoolean("roo.console.ansi");
+ private static final char ESC = 27;
+
+ /**
+ * Decorates the given text with the given escape codes (turning them off
+ * afterwards)
+ *
+ * @param text the text to decorate; can be null
+ * @param codes
+ * @return null if null is passed
+ */
+ public static String decorate(final String text, final AnsiEscapeCode... codes) {
+ if (text == null || "".equals(text)) {
+ return text;
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ if (ANSI_SUPPORTED) {
+ for (final AnsiEscapeCode code : codes) {
+ sb.append(code.code);
+ }
+ }
+ sb.append(text);
+ if (codes != null && codes.length > 0 && ANSI_SUPPORTED) {
+ sb.append(OFF.code);
+ }
+ return sb.toString();
+ }
+
+ // Fields
+ final String code;
+
+ /**
+ * Constructor
+ *
+ * @param code the numeric ANSI escape code
+ */
+ private AnsiEscapeCode(final int code) {
+ // Copied from the method ANSIBuffer.ANSICodes#attrib(int)
+ this.code = ESC + "[" + code + "m";
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/Assert.java b/src/main/java/org/springframework/roo/support/util/Assert.java
new file mode 100644
index 00000000..695eae4c
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/Assert.java
@@ -0,0 +1,397 @@
+package org.springframework.roo.support.util;
+
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Assertion utility class that assists in validating arguments.
+ * Useful for identifying programmer errors early and clearly at runtime.
+ *
+ *
For example, if the contract of a public method states it does not
+ * allow null arguments, Assert can be used to validate that
+ * contract. Doing this clearly indicates a contract violation when it
+ * occurs and protects the class's invariants.
+ *
+ *
Typically used to validate method arguments rather than configuration
+ * properties, to check for cases that are usually programmer errors rather than
+ * configuration errors. In contrast to config initialization code, there is
+ * usally no point in falling back to defaults in such methods.
+ *
+ *
This class is similar to JUnit's assertion library. If an argument value is
+ * deemed invalid, an {@link IllegalArgumentException} is thrown (typically).
+ * For example:
+ *
+ *
+ * Assert.notNull(clazz, "The class must not be null");
+ * Assert.isTrue(i > 0, "The value must be greater than zero");
+ *
+ * Mainly for internal use within the framework; consider Jakarta's Commons Lang
+ * >= 2.0 for a more comprehensive suite of assertion utilities.
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @author Colin Sampaleanu
+ * @author Rob Harrop
+ * @since 1.1.2
+ */
+public abstract class Assert {
+
+ /**
+ * Assert a boolean expression, throwing IllegalArgumentException
+ * if the test result is false.
+ * Assert.isTrue(i > 0, "The value must be greater than zero");
+ * @param expression a boolean expression
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if expression is false
+ */
+ public static void isTrue(final boolean expression, final String message) {
+ if (!expression) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert a boolean expression, throwing IllegalArgumentException
+ * if the test result is false.
+ * Assert.isTrue(i > 0);
+ * @param expression a boolean expression
+ * @throws IllegalArgumentException if expression is false
+ */
+ public static void isTrue(final boolean expression) {
+ isTrue(expression, "[Assertion failed] - this expression must be true");
+ }
+
+ /**
+ * Assert that an object is null .
+ * Assert.isNull(value, "The value must be null");
+ * @param object the object to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object is not null
+ */
+ public static void isNull(final Object object, final String message) {
+ if (object != null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that an object is null .
+ * Assert.isNull(value);
+ * @param object the object to check
+ * @throws IllegalArgumentException if the object is not null
+ */
+ public static void isNull(final Object object) {
+ isNull(object, "[Assertion failed] - the object argument must be null");
+ }
+
+ /**
+ * Assert that an object is not null .
+ * Assert.notNull(clazz, "The class must not be null");
+ * @param object the object to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object is null
+ */
+ public static void notNull(final Object object, final String message) {
+ if (object == null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that an object is not null .
+ * Assert.notNull(clazz);
+ * @param object the object to check
+ * @throws IllegalArgumentException if the object is null
+ */
+ public static void notNull(final Object object) {
+ notNull(object, "[Assertion failed] - this argument is required; it must not be null");
+ }
+
+ /**
+ * Assert that the given String is not empty; that is,
+ * it must not be null and not the empty String.
+ * Assert.hasLength(name, "Name must not be empty");
+ * @param text the String to check
+ * @param message the exception message to use if the assertion fails
+ * @see StringUtils#hasLength
+ */
+ public static void hasLength(final String text, final String message) {
+ if (!StringUtils.hasLength(text)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that the given String is not empty; that is,
+ * it must not be null and not the empty String.
+ * Assert.hasLength(name);
+ * @param text the String to check
+ * @see StringUtils#hasLength
+ */
+ public static void hasLength(final String text) {
+ hasLength(text,
+ "[Assertion failed] - this String argument must have length; it must not be null or empty");
+ }
+
+ /**
+ * Assert that the given String has valid text content; that is, it must not
+ * be null and must contain at least one non-whitespace character.
+ * Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check
+ * @param message the exception message to use if the assertion fails
+ * @see StringUtils#hasText
+ */
+ public static void hasText(final String text, final String message) {
+ if (StringUtils.isBlank(text)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that the given String has valid text content; that is, it must not
+ * be null and must contain at least one non-whitespace character.
+ * Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check
+ * @see StringUtils#hasText
+ */
+ public static void hasText(final String text) {
+ hasText(text,
+ "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank");
+ }
+
+ /**
+ * Assert that the given text does not contain the given substring.
+ * Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * @param textToSearch the text to search
+ * @param substring the substring to find within the text
+ * @param message the exception message to use if the assertion fails
+ */
+ public static void doesNotContain(final String textToSearch, final String substring, final String message) {
+ if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) &&
+ textToSearch.indexOf(substring) != -1) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that the given text does not contain the given substring.
+ * Assert.doesNotContain(name, "rod");
+ * @param textToSearch the text to search
+ * @param substring the substring to find within the text
+ */
+ public static void doesNotContain(final String textToSearch, final String substring) {
+ doesNotContain(textToSearch, substring,
+ "[Assertion failed] - this String argument must not contain the substring [" + substring + "]");
+ }
+
+ /**
+ * Assert that an array has elements; that is, it must not be
+ * null and must have at least one element.
+ * Assert.notEmpty(array, "The array must have elements");
+ * @param array the array to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object array is null or has no elements
+ */
+ public static void notEmpty(final Object[] array, final String message) {
+ if (ObjectUtils.isEmpty(array)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that an array has elements; that is, it must not be
+ * null and must have at least one element.
+ * Assert.notEmpty(array);
+ * @param array the array to check
+ * @throws IllegalArgumentException if the object array is null or has no elements
+ */
+ public static void notEmpty(final Object[] array) {
+ notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element");
+ }
+
+ /**
+ * Assert that an array has no null elements.
+ * Note: Does not complain if the array is empty!
+ * Assert.noNullElements(array, "The array must have non-null elements");
+ * @param array the array to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object array contains a null element
+ */
+ public static void noNullElements(final Object[] array, final String message) {
+ if (array != null) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i] == null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+ }
+ }
+
+ /**
+ * Assert that an array has no null elements.
+ * Note: Does not complain if the array is empty!
+ * Assert.noNullElements(array);
+ * @param array the array to check
+ * @throws IllegalArgumentException if the object array contains a null element
+ */
+ public static void noNullElements(final Object[] array) {
+ noNullElements(array, "[Assertion failed] - this array must not contain any null elements");
+ }
+
+ /**
+ * Assert that a collection has elements; that is, it must not be
+ * null and must have at least one element.
+ * Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the collection is null or has no elements
+ */
+ public static void notEmpty(final Collection> collection, final String message) {
+ if (CollectionUtils.isEmpty(collection)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that a collection has elements; that is, it must not be
+ * null and must have at least one element.
+ * Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check
+ * @throws IllegalArgumentException if the collection is null or has no elements
+ */
+ public static void notEmpty(final Collection> collection) {
+ notEmpty(collection,
+ "[Assertion failed] - this collection must not be empty: it must contain at least 1 element");
+ }
+
+ /**
+ * Assert that a Map has entries; that is, it must not be null
+ * and must have at least one entry.
+ * Assert.notEmpty(map, "Map must have entries");
+ * @param map the map to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the map is null or has no entries
+ */
+ public static void notEmpty(final Map, ?> map, final String message) {
+ if (CollectionUtils.isEmpty(map)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that a Map has entries; that is, it must not be null
+ * and must have at least one entry.
+ * Assert.notEmpty(map);
+ * @param map the map to check
+ * @throws IllegalArgumentException if the map is null or has no entries
+ */
+ public static void notEmpty(final Map, ?> map) {
+ notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry");
+ }
+
+ /**
+ * Assert that the provided object is an instance of the provided class.
+ * Assert.instanceOf(Foo.class, foo);
+ * @param clazz the required class
+ * @param obj the object to check
+ * @throws IllegalArgumentException if the object is not an instance of clazz
+ * @see Class#isInstance
+ */
+ public static void isInstanceOf(final Class> clazz, final Object obj) {
+ isInstanceOf(clazz, obj, "");
+ }
+
+ /**
+ * Assert that the provided object is an instance of the provided class.
+ * Assert.instanceOf(Foo.class, foo);
+ * @param type the type to check against
+ * @param obj the object to check
+ * @param message a message which will be prepended to the message produced by
+ * the function itself, and which may be used to provide context. It should
+ * normally end in a ": " or ". " so that the function generate message looks
+ * ok when prepended to it.
+ * @throws IllegalArgumentException if the object is not an instance of clazz
+ * @see Class#isInstance
+ */
+ public static void isInstanceOf(final Class> type, final Object obj, final String message) {
+ notNull(type, "Type to check against must not be null");
+ if (!type.isInstance(obj)) {
+ throw new IllegalArgumentException(message +
+ "Object of class [" + (obj != null ? obj.getClass().getName() : "null") +
+ "] must be an instance of " + type);
+ }
+ }
+
+ /**
+ * Assert that superType.isAssignableFrom(subType) is true.
+ * Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check
+ * @param subType the sub type to check
+ * @throws IllegalArgumentException if the classes are not assignable
+ */
+ public static void isAssignable(final Class> superType, final Class> subType) {
+ isAssignable(superType, subType, "");
+ }
+
+ /**
+ * Assert that superType.isAssignableFrom(subType) is true.
+ * Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check against
+ * @param subType the sub type to check
+ * @param message a message which will be prepended to the message produced by
+ * the function itself, and which may be used to provide context. It should
+ * normally end in a ": " or ". " so that the function generate message looks
+ * ok when prepended to it.
+ * @throws IllegalArgumentException if the classes are not assignable
+ */
+ public static void isAssignable(final Class> superType, final Class> subType, final String message) {
+ notNull(superType, "Type to check against must not be null");
+ if (subType == null || !superType.isAssignableFrom(subType)) {
+ throw new IllegalArgumentException(message + subType + " is not assignable to " + superType);
+ }
+ }
+
+ /**
+ * Assert a boolean expression, throwing IllegalStateException
+ * if the test result is false. Call isTrue if you wish to
+ * throw IllegalArgumentException on an assertion failure.
+ * Assert.state(id == null, "The id property must not already be initialized");
+ * @param expression a boolean expression
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalStateException if expression is false
+ */
+ public static void state(final boolean expression, final String message) {
+ if (!expression) {
+ throw new IllegalStateException(message);
+ }
+ }
+
+ /**
+ * Assert a boolean expression, throwing {@link IllegalStateException}
+ * if the test result is false.
+ * Call {@link #isTrue(boolean)} if you wish to
+ * throw {@link IllegalArgumentException} on an assertion failure.
+ *
Assert.state(id == null);
+ * @param expression a boolean expression
+ * @throws IllegalStateException if the supplied expression is false
+ */
+ public static void state(final boolean expression) {
+ state(expression, "[Assertion failed] - this state invariant must be true");
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/Base64.java b/src/main/java/org/springframework/roo/support/util/Base64.java
new file mode 100644
index 00000000..83f27fae
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/Base64.java
@@ -0,0 +1,1882 @@
+package org.springframework.roo.support.util;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Encodes and decodes to and from Base64 notation.
+ * Homepage: http://iharder.net/base64 .
+ *
+ * Example:
+ *
+ * String encoded = Base64.encode(myByteArray);
+ *
+ * byte[] myByteArray = Base64.decode(encoded);
+ *
+ * The options parameter, which appears in a few places, is used to pass
+ * several pieces of information to the encoder. In the "higher level" methods such as
+ * encodeBytes(bytes, options) the options parameter can be used to indicate such
+ * things as first gzipping the bytes before encoding them, not inserting linefeeds,
+ * and encoding using the URL-safe and Ordered dialects.
+ *
+ * Note, according to RFC3548 ,
+ * Section 2.1, implementations should not add line feeds unless explicitly told
+ * to do so. I've got Base64 set to this behavior now, although earlier versions
+ * broke lines by default.
+ *
+ * The constants defined in Base64 can be OR-ed together to combine options, so you
+ * might make a call like this:
+ *
+ * String encoded = Base64.encodeBytes(mybytes, Base64.GZIP | Base64.DO_BREAK_LINES);
+ * to compress the data before encoding it and then making the output have newline characters.
+ * Also...
+ * String encoded = Base64.encodeBytes(crazyString.getBytes());
+ *
+ *
+ *
+ *
+ * Change Log:
+ *
+ *
+ * v2.3.7 - Fixed subtle bug when base 64 input stream contained the
+ * value 01111111, which is an invalid base 64 character but should not
+ * throw an ArrayIndexOutOfBoundsException either. Led to discovery of
+ * mishandling (or potential for better handling) of other bad input
+ * characters. You should now get an IOException if you try decoding
+ * something that has bad characters in it.
+ * v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded
+ * string ended in the last column; the buffer was not properly shrunk and
+ * contained an extra (null) byte that made it into the string.
+ * v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size
+ * was wrong for files of size 31, 34, and 37 bytes.
+ * v2.3.4 - Fixed bug when working with gzipped streams whereby flushing
+ * the Base64.OutputStream closed the Base64 encoding (by padding with equals
+ * signs) too soon. Also added an option to suppress the automatic decoding
+ * of gzipped streams. Also added experimental support for specifying a
+ * class loader when using the
+ * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)}
+ * method.
+ * v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java
+ * footprint with its CharEncoders and so forth. Fixed some javadocs that were
+ * inconsistent. Removed imports and specified things like IOException
+ * explicitly inline.
+ * v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the
+ * final encoded data will be so that the code doesn't have to create two output
+ * arrays: an oversized initial one and then a final, exact-sized one. Big win
+ * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not
+ * using the gzip options which uses a different mechanism with streams and stuff).
+ * v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some
+ * similar helper methods to be more efficient with memory by not returning a
+ * String but just a byte array.
+ * v2.3 - This is not a drop-in replacement! This is two years of comments
+ * and bug fixes queued up and finally executed. Thanks to everyone who sent
+ * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else.
+ * Much bad coding was cleaned up including throwing exceptions where necessary
+ * instead of returning null values or something similar. Here are some changes
+ * that may affect you:
+ *
+ * Does not break lines, by default. This is to keep in compliance with
+ * RFC3548 .
+ * Throws exceptions instead of returning null values. Because some operations
+ * (especially those that may permit the GZIP option) use IO streams, there
+ * is a possiblity of an IOException being thrown. After some discussion and
+ * thought, I've changed the behavior of the methods to throw IOExceptions
+ * rather than return null if ever there's an error. I think this is more
+ * appropriate, though it will require some changes to your code. Sorry,
+ * it should have been done this way to begin with.
+ * Removed all references to System.out, System.err, and the like.
+ * Shame on me. All I can say is sorry they were ever there.
+ * Throws NullPointerExceptions and IllegalArgumentExceptions as needed
+ * such as when passed arrays are null or offsets are invalid.
+ * Cleaned up as much javadoc as I could to avoid any javadoc warnings.
+ * This was especially annoying before for people who were thorough in their
+ * own projects and then had gobs of javadoc warnings on this file.
+ *
+ * v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug
+ * when using very small files (~< 40 bytes).
+ * v2.2 - Added some helper methods for encoding/decoding directly from
+ * one file to the next. Also added a main() method to support command line
+ * encoding/decoding from one file to the next. Also added these Base64 dialects:
+ *
+ * The default is RFC3548 format.
+ * Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates
+ * URL and file name friendly format as described in Section 4 of RFC3548.
+ * http://www.faqs.org/rfcs/rfc3548.html
+ * Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates
+ * URL and file name friendly format that preserves lexical ordering as described
+ * in http://www.faqs.org/qa/rfcc-1940.html
+ *
+ * Special thanks to Jim Kellerman at http://www.powerset.com/
+ * for contributing the new Base64 dialects.
+ *
+ *
+ * v2.1 - Cleaned up javadoc comments and unused variables and methods. Added
+ * some convenience methods for reading and writing to and from files.
+ * v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
+ * with other encodings (like EBCDIC).
+ * v2.0.1 - Fixed an error when decoding a single byte, that is, when the
+ * encoded data was a single byte.
+ * v2.0 - I got rid of methods that used booleans to set options.
+ * Now everything is more consolidated and cleaner. The code now detects
+ * when data that's being decoded is gzip-compressed and will decompress it
+ * automatically. Generally things are cleaner. You'll probably have to
+ * change some method calls that you were making to support the new
+ * options format (int s that you "OR" together).
+ * v1.5.1 - Fixed bug when decompressing and decoding to a
+ * byte[] using decode(String s, boolean gzipCompressed) .
+ * Added the ability to "suspend" encoding in the Output Stream so
+ * you can turn on and off the encoding if you need to embed base64
+ * data in an otherwise "normal" stream (like an XML file).
+ * v1.5 - Output stream pases on flush() command but doesn't do anything itself.
+ * This helps when using GZIP streams.
+ * Added the ability to GZip-compress objects before encoding them.
+ * v1.4 - Added helper methods to read/write files.
+ * v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
+ * v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
+ * where last buffer being read, if not completely full, was not returned.
+ * v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
+ * v1.3.3 - Fixed I/O streams which were totally messed up.
+ *
+ *
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit http://iharder.net/base64
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.3.7
+ */
+public class Base64 {
+
+/* ******** P U B L I C F I E L D S ******** */
+
+ /** No options specified. Value is zero. */
+ public final static int NO_OPTIONS = 0;
+
+ /** Specify encoding in first bit. Value is one. */
+ public final static int ENCODE = 1;
+
+ /** Specify decoding in first bit. Value is zero. */
+ public final static int DECODE = 0;
+
+ /** Specify that data should be gzip-compressed in second bit. Value is two. */
+ public final static int GZIP = 2;
+
+ /** Specify that gzipped data should not be automatically gunzipped. */
+ public final static int DONT_GUNZIP = 4;
+
+ /** Do break lines when encoding. Value is 8. */
+ public final static int DO_BREAK_LINES = 8;
+
+ /**
+ * Encode using Base64-like encoding that is URL- and Filename-safe as described
+ * in Section 4 of RFC3548:
+ * http://www.faqs.org/rfcs/rfc3548.html .
+ * It is important to note that data encoded this way is not officially valid Base64,
+ * or at the very least should not be called Base64 without also specifying that is
+ * was encoded using the URL- and Filename-safe dialect.
+ */
+ public final static int URL_SAFE = 16;
+
+ /**
+ * Encode using the special "ordered" dialect of Base64 described here:
+ * http://www.faqs.org/qa/rfcc-1940.html .
+ */
+ public final static int ORDERED = 32;
+
+/* ******** P R I V A T E F I E L D S ******** */
+
+ /** Maximum line length (76) of Base64 output. */
+ private final static int MAX_LINE_LENGTH = 76;
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte)'=';
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte)'\n';
+
+ /** Preferred encoding. */
+ private final static String PREFERRED_ENCODING = "US-ASCII";
+
+ private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
+ private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
+
+/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
+
+ /** The 64 valid Base64 values. */
+ /* Host platform me be something funny like EBCDIC, so we hardcode these values. */
+ private final static byte[] _STANDARD_ALPHABET = {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
+ };
+
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ */
+ private final static byte[] _STANDARD_DECODABET = {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9,-9,-9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
+ 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
+ -9,-9,-9,-9,-9,-9, // Decimal 91 - 96
+ 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
+ 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
+ -9,-9,-9,-9,-9 // Decimal 123 - 127
+ ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ };
+
+
+/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
+ * http://www.faqs.org/rfcs/rfc3548.html .
+ * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash."
+ */
+ private final static byte[] _URL_SAFE_ALPHABET = {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_'
+ };
+
+ /**
+ * Used in decoding URL- and Filename-safe dialects of Base64.
+ */
+ private final static byte[] _URL_SAFE_DECODABET = {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 62, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
+ 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 63, // Underscore at decimal 95
+ -9, // Decimal 96
+ 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
+ 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
+ -9,-9,-9,-9,-9 // Decimal 123 - 127
+ ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ };
+
+/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * I don't get the point of this technique, but someone requested it,
+ * and it is described here:
+ * http://www.faqs.org/qa/rfcc-1940.html .
+ */
+ private final static byte[] _ORDERED_ALPHABET = {
+ (byte)'-',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4',
+ (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'_',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z'
+ };
+
+ /**
+ * Used in decoding the "ordered" dialect of Base64.
+ */
+ private final static byte[] _ORDERED_DECODABET = {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 0, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M'
+ 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 37, // Underscore at decimal 95
+ -9, // Decimal 96
+ 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm'
+ 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z'
+ -9,-9,-9,-9,-9 // Decimal 123 - 127
+ ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ };
+
+
+ /* ******** D E T E R M I N E W H I C H A L P H A B E T ******** */
+
+ /**
+ * Returns one of the _SOMETHING_ALPHABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URLSAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ private final static byte[] getAlphabet(final int options) {
+ if ((options & URL_SAFE) == URL_SAFE) {
+ return _URL_SAFE_ALPHABET;
+ } else if ((options & ORDERED) == ORDERED) {
+ return _ORDERED_ALPHABET;
+ } else {
+ return _STANDARD_ALPHABET;
+ }
+ }
+
+ /**
+ * Returns one of the _SOMETHING_DECODABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URL_SAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ private final static byte[] getDecodabet(final int options) {
+ if ((options & URL_SAFE) == URL_SAFE) {
+ return _URL_SAFE_DECODABET;
+ } else if ((options & ORDERED) == ORDERED) {
+ return _ORDERED_DECODABET;
+ } else {
+ return _STANDARD_DECODABET;
+ }
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to the first three bytes of array threeBytes
+ * and returns a four-byte array in Base64 notation.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes .
+ * The array threeBytes needs only be as big as
+ * numSigBytes .
+ * Code can reuse a byte array by passing a four-byte array as b4 .
+ *
+ * @param b4 A reusable byte array to reduce array instantiation
+ * @param threeBytes the array to convert
+ * @param numSigBytes the number of significant bytes in your array
+ * @return four byte array in Base64 notation.
+ * @since 1.5.1
+ */
+ private static byte[] encode3to4(final byte[] b4, final byte[] threeBytes, final int numSigBytes, final int options) {
+ encode3to4(threeBytes, 0, numSigBytes, b4, 0, options);
+ return b4;
+ }
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination .
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset .
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes .
+ * This is the lowest level of the encoding methods with
+ * all possible parameters.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(final byte[] source, final int srcOffset, final int numSigBytes, final byte[] destination, final int destOffset, final int options) {
+
+ byte[] ALPHABET = getAlphabet(options);
+
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index ALPHABET
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff = (numSigBytes > 0 ? ((source[srcOffset ] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch(numSigBytes) {
+ case 3:
+ destination[destOffset ] = ALPHABET[(inBuff >>> 18) ];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = ALPHABET[(inBuff ) & 0x3f];
+ return destination;
+
+ case 2:
+ destination[destOffset ] = ALPHABET[(inBuff >>> 18) ];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[destOffset ] = ALPHABET[(inBuff >>> 18) ];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Performs Base64 encoding on the raw ByteBuffer,
+ * writing it to the encoded ByteBuffer.
+ * This is an experimental feature. Currently it does not
+ * pass along any options (such as {@link #DO_BREAK_LINES}
+ * or {@link #GZIP}.
+ *
+ * @param raw input buffer
+ * @param encoded output buffer
+ * @since 2.3
+ */
+ public static void encode(final java.nio.ByteBuffer raw, final java.nio.ByteBuffer encoded) {
+ byte[] raw3 = new byte[3];
+ byte[] enc4 = new byte[4];
+
+ while(raw.hasRemaining()) {
+ int rem = Math.min(3,raw.remaining());
+ raw.get(raw3,0,rem);
+ Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS);
+ encoded.put(enc4);
+ } // end input remaining
+ }
+
+ /**
+ * Performs Base64 encoding on the raw ByteBuffer,
+ * writing it to the encoded CharBuffer.
+ * This is an experimental feature. Currently it does not
+ * pass along any options (such as {@link #DO_BREAK_LINES}
+ * or {@link #GZIP}.
+ *
+ * @param raw input buffer
+ * @param encoded output buffer
+ * @since 2.3
+ */
+ public static void encode(final java.nio.ByteBuffer raw, final java.nio.CharBuffer encoded) {
+ byte[] raw3 = new byte[3];
+ byte[] enc4 = new byte[4];
+
+ while(raw.hasRemaining()) {
+ int rem = Math.min(3,raw.remaining());
+ raw.get(raw3,0,rem);
+ Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS);
+ for (int i = 0; i < 4; i++) {
+ encoded.put((char)(enc4[i] & 0xFF));
+ }
+ } // end input remaining
+ }
+
+ /**
+ * Serializes an object and returns the Base64-encoded
+ * version of that serialized object.
+ *
+ * As of v 2.3, if the object
+ * cannot be serialized or there is another error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * The object is not GZip-compressed before being encoded.
+ *
+ * @param serializableObject The object to encode
+ * @return The Base64-encoded object
+ * @throws IOException if there is an error
+ * @throws NullPointerException if serializedObject is null
+ * @since 1.4
+ */
+ public static String encodeObject(final java.io.Serializable serializableObject) throws IOException {
+ return encodeObject(serializableObject, NO_OPTIONS);
+ }
+
+ /**
+ * Serializes an object and returns the Base64-encoded
+ * version of that serialized object.
+ *
+ * As of v 2.3, if the object
+ * cannot be serialized or there is another error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * The object is not GZip-compressed before being encoded.
+ *
+ * Example options:
+ * GZIP: gzip-compresses object before encoding it.
+ * DO_BREAK_LINES: break lines at 76 characters
+ *
+ *
+ * Example: encodeObject(myObj, Base64.GZIP) or
+ *
+ * Example: encodeObject(myObj, Base64.GZIP | Base64.DO_BREAK_LINES)
+ *
+ * @param serializableObject The object to encode
+ * @param options Specified options
+ * @return The Base64-encoded object
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws IOException if there is an error
+ * @since 2.0
+ */
+ public static String encodeObject(final java.io.Serializable serializableObject, final int options) throws IOException {
+ if (serializableObject == null) {
+ throw new NullPointerException("Cannot serialize a null object.");
+ }
+
+ // Streams
+ ByteArrayOutputStream baos = null;
+ java.io.OutputStream b64os = null;
+ GZIPOutputStream gzos = null;
+ ObjectOutputStream oos = null;
+
+ try {
+ // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
+ baos = new ByteArrayOutputStream();
+ b64os = new Base64.OutputStream(baos, ENCODE | options);
+ if ((options & GZIP) != 0) {
+ // Gzip
+ gzos = new GZIPOutputStream(b64os);
+ oos = new ObjectOutputStream(gzos);
+ } else {
+ // Not gzipped
+ oos = new ObjectOutputStream(b64os);
+ }
+ oos.writeObject(serializableObject);
+ } catch (IOException e) {
+ // Catch it and then throw it immediately so that
+ // the finally{} block is called for cleanup.
+ throw e;
+ } finally {
+ IOUtils.closeQuietly(oos, gzos, b64os, baos);
+ }
+
+ // Return value according to relevant encoding.
+ try {
+ return new String(baos.toByteArray(), PREFERRED_ENCODING);
+ } catch (UnsupportedEncodingException uue) {
+ // Fall back to some Java default
+ return new String(baos.toByteArray());
+ } // end catch
+ } // end encode
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * @param source The data to convert
+ * @return The data in Base64-encoded form
+ * @throws NullPointerException if source array is null
+ * @since 1.4
+ */
+ public static String encodeBytes(final byte[] source) {
+ // Since we're not going to have the GZIP encoding turned on,
+ // we're not going to have an IOException thrown, so
+ // we should not force the user to have to catch it.
+ String encoded = null;
+ try {
+ encoded = encodeBytes(source, 0, source.length, NO_OPTIONS);
+ } catch (IOException ex) {
+ assert false : ex.getMessage();
+ } // end catch
+ assert encoded != null;
+ return encoded;
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * Example options:
+ * GZIP: gzip-compresses object before encoding it.
+ * DO_BREAK_LINES: break lines at 76 characters
+ * Note: Technically, this makes your encoding non-compliant.
+ *
+ *
+ * Example: encodeBytes(myData, Base64.GZIP) or
+ *
+ * Example: encodeBytes(myData, Base64.GZIP | Base64.DO_BREAK_LINES)
+ *
+ *
As of v 2.3, if there is an error with the GZIP stream,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param source The data to convert
+ * @param options Specified options
+ * @return The Base64-encoded data as a String
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws IOException if there is an error
+ * @throws NullPointerException if source array is null
+ * @since 2.0
+ */
+ public static String encodeBytes(final byte[] source, final int options) throws IOException {
+ return encodeBytes(source, 0, source.length, options);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * As of v 2.3, if there is an error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @return The Base64-encoded data as a String
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are invalid
+ * @since 1.4
+ */
+ public static String encodeBytes(final byte[] source, final int off, final int len) {
+ // Since we're not going to have the GZIP encoding turned on,
+ // we're not going to have an IOException thrown, so
+ // we should not force the user to have to catch it.
+ String encoded = null;
+ try {
+ encoded = encodeBytes(source, off, len, NO_OPTIONS);
+ } catch (IOException ex) {
+ assert false : ex.getMessage();
+ } // end catch
+ assert encoded != null;
+ return encoded;
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * Example options:
+ * GZIP: gzip-compresses object before encoding it.
+ * DO_BREAK_LINES: break lines at 76 characters
+ * Note: Technically, this makes your encoding non-compliant.
+ *
+ *
+ * Example: encodeBytes(myData, Base64.GZIP) or
+ *
+ * Example: encodeBytes(myData, Base64.GZIP | Base64.DO_BREAK_LINES)
+ *
+ *
As of v 2.3, if there is an error with the GZIP stream,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param options Specified options
+ * @return The Base64-encoded data as a String
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws IOException if there is an error
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are invalid
+ * @since 2.0
+ */
+ public static String encodeBytes(final byte[] source, final int off, final int len, final int options) throws IOException {
+ byte[] encoded = encodeBytesToBytes(source, off, len, options);
+
+ // Return value according to relevant encoding.
+ try {
+ return new String(encoded, PREFERRED_ENCODING);
+ } catch (UnsupportedEncodingException uue) {
+ return new String(encoded);
+ }
+ }
+
+ /**
+ * Similar to {@link #encodeBytes(byte[])} but returns
+ * a byte array instead of instantiating a String. This is more efficient
+ * if you're working with I/O streams and have large data sets to encode.
+ *
+ * @param source The data to convert
+ * @return The Base64-encoded data as a byte[] (of ASCII characters)
+ * @throws NullPointerException if source array is null
+ * @since 2.3.1
+ */
+ public static byte[] encodeBytesToBytes(final byte[] source) {
+ byte[] encoded = null;
+ try {
+ encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS);
+ } catch (IOException ex) {
+ assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
+ }
+ return encoded;
+ }
+
+ /**
+ * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns
+ * a byte array instead of instantiating a String. This is more efficient
+ * if you're working with I/O streams and have large data sets to encode.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param options Specified options
+ * @return The Base64-encoded data as a String
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws IOException if there is an error
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are invalid
+ * @since 2.3.1
+ */
+ public static byte[] encodeBytesToBytes(final byte[] source, final int off, final int len, final int options) throws IOException {
+ if (source == null) {
+ throw new NullPointerException("Cannot serialize a null array.");
+ }
+
+ if (off < 0) {
+ throw new IllegalArgumentException("Cannot have negative offset: " + off);
+ }
+
+ if (len < 0) {
+ throw new IllegalArgumentException("Cannot have length offset: " + len);
+ }
+
+ if (off + len > source.length ) {
+ throw new IllegalArgumentException(String.format("Cannot have offset of %d and length of %d with array of length %d", off,len,source.length));
+ }
+
+ if ((options & GZIP) != 0) {
+ // Compress
+ ByteArrayOutputStream baos = null;
+ GZIPOutputStream gzos = null;
+ Base64.OutputStream b64os = null;
+
+ try {
+ // GZip -> Base64 -> ByteArray
+ baos = new ByteArrayOutputStream();
+ b64os = new Base64.OutputStream(baos, ENCODE | options);
+ gzos = new GZIPOutputStream(b64os);
+
+ gzos.write(source, off, len);
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ IOUtils.closeQuietly(gzos, b64os, baos);
+ }
+
+ return baos.toByteArray();
+ } else {
+ // Don't compress. Better not to use streams at all then.
+ boolean breakLines = (options & DO_BREAK_LINES) != 0;
+
+ //int len43 = len * 4 / 3;
+ //byte[] outBuff = new byte[ (len43) // Main 4:3
+ // + ((len % 3) > 0 ? 4 : 0) // Account for padding
+ // + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New lines
+ // Try to determine more precisely how big the array needs to be.
+ // If we get it right, we don't have to do an array copy, and
+ // we save a bunch of memory.
+ int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding
+ if (breakLines) {
+ encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters
+ }
+ byte[] outBuff = new byte[encLen];
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d+=3, e+=4) {
+ encode3to4(source, d+off, 3, outBuff, e, options);
+
+ lineLength += 4;
+ if (breakLines && lineLength >= MAX_LINE_LENGTH)
+ {
+ outBuff[e+4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // en dfor: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d+off, len - d, outBuff, e, options);
+ e += 4;
+ } // end if: some padding needed
+
+ // Only resize array if we didn't guess it right.
+ if (e <= outBuff.length - 1) {
+ // If breaking lines and the last byte falls right at
+ // the line length (76 bytes per line), there will be
+ // one extra byte, and the array will need to be resized.
+ // Not too bad of an estimate on array size, I'd say.
+ byte[] finalOut = new byte[e];
+ System.arraycopy(outBuff,0, finalOut,0,e);
+ // System.err.println("Having to resize array from " + outBuff.length + " to " + e);
+ return finalOut;
+ } else {
+ // System.err.println("No need to resize array.");
+ return outBuff;
+ }
+ } // end else: don't compress
+ } // end encodeBytesToBytes
+
+/* ******** D E C O D I N G M E T H O D S ******** */
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination .
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset .
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ * This is the lowest level of the decoding methods with
+ * all possible parameters.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param options alphabet type is pulled from this (standard, url-safe, ordered)
+ * @return the number of decoded bytes converted
+ * @throws NullPointerException if source or destination arrays are null
+ * @throws IllegalArgumentException if srcOffset or destOffset are invalid
+ * or there is not enough room in the array.
+ * @since 1.3
+ */
+ private static int decode4to3(final byte[] source, final int srcOffset, final byte[] destination, final int destOffset, final int options) {
+ if (source == null) {
+ throw new NullPointerException("Source array was null.");
+ }
+ if (destination == null) {
+ throw new NullPointerException("Destination array was null.");
+ }
+ if (srcOffset < 0 || srcOffset + 3 >= source.length) {
+ throw new IllegalArgumentException(String.format(
+ "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset));
+ }
+ if (destOffset < 0 || destOffset +2 >= destination.length) {
+ throw new IllegalArgumentException(String.format(
+ "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset));
+ }
+
+ byte[] DECODABET = getDecodabet(options);
+
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ((DECODABET[source[srcOffset ]] << 24) >>> 6)
+ // | ((DECODABET[source[srcOffset + 1]] << 24) >>> 12);
+ int outBuff = ((DECODABET[source[srcOffset ]] & 0xFF) << 18)
+ | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12);
+
+ destination[destOffset] = (byte)(outBuff >>> 16);
+ return 1;
+ }
+
+ // Example: DkL=
+ else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ((DECODABET[source[srcOffset ]] << 24) >>> 6)
+ // | ((DECODABET[source[srcOffset + 1]] << 24) >>> 12)
+ // | ((DECODABET[source[srcOffset + 2]] << 24) >>> 18);
+ int outBuff = ((DECODABET[source[srcOffset ]] & 0xFF) << 18)
+ | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6);
+
+ destination[destOffset ] = (byte)(outBuff >>> 16);
+ destination[destOffset + 1] = (byte)(outBuff >>> 8);
+ return 2;
+ }
+
+ // Example: DkLE
+ else {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ((DECODABET[source[srcOffset ]] << 24) >>> 6)
+ // | ((DECODABET[source[srcOffset + 1]] << 24) >>> 12)
+ // | ((DECODABET[source[srcOffset + 2]] << 24) >>> 18)
+ // | ((DECODABET[source[srcOffset + 3]] << 24) >>> 24);
+ int outBuff = ((DECODABET[source[srcOffset ]] & 0xFF) << 18)
+ | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6)
+ | ((DECODABET[source[srcOffset + 3]] & 0xFF) );
+
+
+ destination[destOffset ] = (byte)(outBuff >> 16);
+ destination[destOffset + 1] = (byte)(outBuff >> 8);
+ destination[destOffset + 2] = (byte)(outBuff );
+
+ return 3;
+ }
+ } // end decodeToBytes
+
+ /**
+ * Low-level access to decoding ASCII characters in
+ * the form of a byte array. Ignores GUNZIP option, if
+ * it's set. This is not generally a recommended method,
+ * although it is used internally as part of the decoding process.
+ * Special case: if len = 0, an empty array is returned. Still,
+ * if you need more speed and reduced memory footprint (and aren't
+ * gzipping), consider this method.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @since 2.3.1
+ */
+ public static byte[] decode(final byte[] source) throws IOException {
+ return decode(source, 0, source.length, Base64.NO_OPTIONS);
+ }
+
+ /**
+ * Low-level access to decoding ASCII characters in
+ * the form of a byte array. Ignores GUNZIP option, if
+ * it's set. This is not generally a recommended method,
+ * although it is used internally as part of the decoding process.
+ * Special case: if len = 0, an empty array is returned. Still,
+ * if you need more speed and reduced memory footprint (and aren't
+ * gzipping), consider this method.
+ *
+ * @param source The Base64 encoded data
+ * @param off The offset of where to begin decoding
+ * @param len The length of characters to decode
+ * @param options Can specify options such as alphabet type to use
+ * @return decoded data
+ * @throws IOException If bogus characters exist in source data
+ * @since 1.3
+ */
+ public static byte[] decode(final byte[] source, final int off, final int len, final int options) throws IOException {
+ // Lots of error checking and exception throwing
+ if (source == null) {
+ throw new NullPointerException("Cannot decode null source array.");
+ }
+ if (off < 0 || off + len > source.length) {
+ throw new IllegalArgumentException(String.format("Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len));
+ }
+
+ if (len == 0) {
+ return new byte[0];
+ } else if (len < 4) {
+ throw new IllegalArgumentException(
+ "Base64-encoded string must have at least four characters, but length specified was " + len);
+ } // end if
+
+ byte[] DECODABET = getDecodabet(options);
+
+ int len34 = len * 3 / 4; // Estimate on array size
+ byte[] outBuff = new byte[len34]; // Upper limit on size of output
+ int outBuffPosn = 0; // Keep track of where we're writing
+
+ byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space
+ int b4Posn = 0; // Keep track of four byte input buffer
+ int i = 0; // Source array counter
+ byte sbiDecode = 0; // Special value from DECODABET
+
+ for (i = off; i < off+len; i++) { // Loop through source
+
+ sbiDecode = DECODABET[source[i]&0xFF];
+
+ // White space, Equals sign, or legit Base64 character
+ // Note the values such as -5 and -9 in the
+ // DECODABETs at the top of the file.
+ if (sbiDecode >= WHITE_SPACE_ENC) {
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ b4[b4Posn++] = source[i]; // Save non-whitespace
+ if (b4Posn > 3) { // Time to decode?
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options);
+ b4Posn = 0;
+
+ // If that was the equals sign, break out of 'for' loop
+ if (source[i] == EQUALS_SIGN) {
+ break;
+ } // end if: equals sign
+ } // end if: quartet built
+ } // end if: equals sign or better
+ } // end if: white space, equals sign or better
+ else {
+ // There's a bad input character in the Base64 stream.
+ throw new IOException(String.format(
+ "Bad Base64 input character decimal %d in array position %d", source[i] & 0xFF, i));
+ } // end else:
+ } // each input character
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ } // end decode
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @return the decoded data
+ * @throws IOException If there is a problem
+ * @since 1.4
+ */
+ public static byte[] decode(final String s) throws IOException {
+ return decode(s, NO_OPTIONS);
+ }
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @param options encode options such as URL_SAFE
+ * @return the decoded data
+ * @throws IOException if there is an error
+ * @throws NullPointerException if s is null
+ * @since 1.4
+ */
+ public static byte[] decode(final String s, final int options) throws IOException {
+ if (s == null) {
+ throw new NullPointerException("Input string was null.");
+ } // end if
+
+ byte[] bytes;
+ try {
+ bytes = s.getBytes(PREFERRED_ENCODING);
+ } // end try
+ catch (UnsupportedEncodingException uee) {
+ bytes = s.getBytes();
+ } // end catch
+ //
+
+ // Decode
+ bytes = decode(bytes, 0, bytes.length, options);
+
+ // Check to see if it's gzip-compressed
+ // GZIP Magic Two-Byte Number: 0x8b1f (35615)
+ boolean dontGunzip = (options & DONT_GUNZIP) != 0;
+ if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) {
+
+ int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
+ if (GZIPInputStream.GZIP_MAGIC == head) {
+ ByteArrayInputStream bais = null;
+ GZIPInputStream gzis = null;
+ ByteArrayOutputStream baos = null;
+ byte[] buffer = new byte[2048];
+ int length = 0;
+
+ try {
+ baos = new ByteArrayOutputStream();
+ bais = new ByteArrayInputStream(bytes);
+ gzis = new GZIPInputStream(bais);
+
+ while((length = gzis.read(buffer)) >= 0) {
+ baos.write(buffer,0,length);
+ } // end while: reading input
+
+ // No error? Get new bytes.
+ bytes = baos.toByteArray();
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ // Just return originally-decoded bytes
+ } finally {
+ IOUtils.closeQuietly(baos, gzis, bais);
+ }
+
+ } // end if: gzipped
+ } // end if: bytes.length >= 2
+
+ return bytes;
+ } // end decode
+
+ /**
+ * Attempts to decode Base64 data and deserialize a Java
+ * Object within. Returns null if there was an error.
+ *
+ * @param encodedObject The Base64 data to decode
+ * @return The decoded and deserialized object
+ * @throws NullPointerException if encodedObject is null
+ * @throws IOException if there is a general error
+ * @throws ClassNotFoundException if the decoded object is of a
+ * class that cannot be found by the JVM
+ * @since 1.5
+ */
+ public static Object decodeToObject(final String encodedObject)
+ throws IOException, java.lang.ClassNotFoundException {
+ return decodeToObject(encodedObject,NO_OPTIONS,null);
+ }
+
+
+ /**
+ * Attempts to decode Base64 data and deserialize a Java
+ * Object within. Returns null if there was an error.
+ * If loader is not null, it will be the class loader
+ * used when deserializing.
+ *
+ * @param encodedObject The Base64 data to decode
+ * @param options Various parameters related to decoding
+ * @param loader Optional class loader to use in deserializing classes.
+ * @return The decoded and deserialized object
+ * @throws NullPointerException if encodedObject is null
+ * @throws IOException if there is a general error
+ * @throws ClassNotFoundException if the decoded object is of a
+ * class that cannot be found by the JVM
+ * @since 2.3.4
+ */
+ public static Object decodeToObject(final String encodedObject, final int options, final ClassLoader loader) throws IOException, ClassNotFoundException {
+ // Decode and gunzip if necessary
+ byte[] objBytes = decode(encodedObject, options);
+
+ ByteArrayInputStream bais = null;
+ ObjectInputStream ois = null;
+ Object obj = null;
+
+ try {
+ bais = new ByteArrayInputStream(objBytes);
+
+ // If no custom class loader is provided, use Java's builtin OIS.
+ if (loader == null) {
+ ois = new ObjectInputStream(bais);
+ } else {
+ // Make a customized object input stream that uses the provided class loader
+ ois = new ObjectInputStream(bais) {
+ @Override
+ public Class> resolveClass(final java.io.ObjectStreamClass streamClass)
+ throws IOException, ClassNotFoundException {
+ Class> c = Class.forName(streamClass.getName(), false, loader);
+ if (c == null) {
+ return super.resolveClass(streamClass);
+ } else {
+ return c; // Class loader knows of this class.
+ } // end else: not null
+ } // end resolveClass
+ }; // end ois
+ } // end else: no custom class loader
+
+ obj = ois.readObject();
+ } // end try
+ catch (IOException e) {
+ throw e; // Catch and throw in order to execute finally{}
+ } // end catch
+ catch (java.lang.ClassNotFoundException e) {
+ throw e; // Catch and throw in order to execute finally{}
+ } // end catch
+ finally {
+ IOUtils.closeQuietly(bais, ois);
+ }
+
+ return obj;
+ } // end decodeObject
+
+
+
+ /**
+ * Convenience method for encoding data to a file.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param dataToEncode byte array of data to encode in base64 form
+ * @param filename Filename for saving encoded data
+ * @throws IOException if there is an error
+ * @throws NullPointerException if dataToEncode is null
+ * @since 2.1
+ */
+ public static void encodeToFile(final byte[] dataToEncode, final String filename)
+ throws IOException {
+
+ if (dataToEncode == null) {
+ throw new NullPointerException("Data to encode was null.");
+ } // end iff
+
+ Base64.OutputStream bos = null;
+ try {
+ bos = new Base64.OutputStream(
+ new FileOutputStream(filename), Base64.ENCODE);
+ bos.write(dataToEncode);
+ } // end try
+ catch (IOException e) {
+ throw e; // Catch and throw to execute finally{} block
+ } // end catch: IOException
+ finally {
+ try { bos.close(); } catch (Exception e) {}
+ } // end finally
+
+ } // end encodeToFile
+
+
+ /**
+ * Convenience method for decoding data to a file.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param dataToDecode Base64-encoded data as a string
+ * @param filename Filename for saving decoded data
+ * @throws IOException if there is an error
+ * @since 2.1
+ */
+ public static void decodeToFile(final String dataToDecode, final String filename)
+ throws IOException {
+
+ Base64.OutputStream bos = null;
+ try {
+ bos = new Base64.OutputStream(
+ new FileOutputStream(filename), Base64.DECODE);
+ bos.write(dataToDecode.getBytes(PREFERRED_ENCODING));
+ } // end try
+ catch (IOException e) {
+ throw e; // Catch and throw to execute finally{} block
+ } // end catch: IOException
+ finally {
+ try { bos.close(); } catch (Exception e) {}
+ } // end finally
+
+ } // end decodeToFile
+
+
+
+
+ /**
+ * Convenience method for reading a base64-encoded
+ * file and decoding it.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param filename Filename for reading encoded data
+ * @return decoded byte array
+ * @throws IOException if there is an error
+ * @since 2.1
+ */
+ public static byte[] decodeFromFile(final String filename)
+ throws IOException {
+
+ Base64.InputStream bis = null;
+ try
+ {
+ // Set up some useful variables
+ File file = new File(filename);
+ byte[] buffer = null;
+ int length = 0;
+ int numBytes = 0;
+
+ // Check for size of file
+ if (file.length() > Integer.MAX_VALUE)
+ {
+ throw new IOException("File is too big for this convenience method (" + file.length() + " bytes).");
+ } // end if: file too big for int index
+ buffer = new byte[(int)file.length()];
+
+ // Open a stream
+ bis = new Base64.InputStream(
+ new BufferedInputStream(
+ new FileInputStream(file)), Base64.DECODE);
+
+ // Read until done
+ while((numBytes = bis.read(buffer, length, 4096)) >= 0) {
+ length += numBytes;
+ } // end while
+
+ // Save in a variable to return
+ final byte[] decodedData = new byte[length];
+ System.arraycopy(buffer, 0, decodedData, 0, length);
+ return decodedData;
+ } catch (IOException e) {
+ throw e; // Catch and release to execute finally{}
+ } finally {
+ IOUtils.closeQuietly(bis);
+ }
+ }
+
+ /**
+ * Convenience method for reading a binary file
+ * and base64-encoding it.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param filename Filename for reading binary data
+ * @return base64-encoded string
+ * @throws IOException if there is an error
+ * @since 2.1
+ */
+ public static String encodeFromFile(final String filename) throws IOException {
+ Base64.InputStream bis = null;
+ try {
+ // Set up some useful variables
+ File file = new File(filename);
+ byte[] buffer = new byte[Math.max((int)(file.length() * 1.4+1),40)]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5)
+ int length = 0;
+ int numBytes = 0;
+
+ // Open a stream
+ bis = new Base64.InputStream(new BufferedInputStream(new FileInputStream(file)), Base64.ENCODE);
+
+ // Read until done
+ while((numBytes = bis.read(buffer, length, 4096)) >= 0) {
+ length += numBytes;
+ } // end while
+
+ return new String(buffer, 0, length, Base64.PREFERRED_ENCODING);
+
+ } catch (IOException e) {
+ throw e; // Catch and release to execute finally{}
+ } finally {
+ IOUtils.closeQuietly(bis);
+ }
+ }
+
+ /**
+ * Reads infile and encodes it to outfile .
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @throws IOException if there is an error
+ * @since 2.2
+ */
+ public static void encodeFileToFile(final String infile, final String outfile) throws IOException {
+ String encoded = Base64.encodeFromFile(infile);
+ java.io.OutputStream out = null;
+ try {
+ out = new BufferedOutputStream(new FileOutputStream(outfile));
+ out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output.
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /**
+ * Reads infile and decodes it to outfile .
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @throws IOException if there is an error
+ * @since 2.2
+ */
+ public static void decodeFileToFile(final String infile, final String outfile) throws IOException {
+ byte[] decoded = Base64.decodeFromFile(infile);
+ java.io.OutputStream out = null;
+ try {
+ out = new BufferedOutputStream(new FileOutputStream(outfile));
+ out.write(decoded);
+ } catch (IOException e) {
+ throw e; // Catch and release to execute finally{}
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /** Defeats instantiation. */
+ private Base64() {}
+
+ /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */
+
+ /**
+ * A {@link Base64.InputStream} will read data from another
+ * java.io.InputStream , given in the constructor,
+ * and encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class InputStream extends java.io.FilterInputStream {
+
+ // Fields
+ private final boolean encode; // Encoding or decoding
+ private int position; // Current position in the buffer
+ private final byte[] buffer; // Small buffer holding converted data
+ private final int bufferLength; // Length of buffer (3 or 4)
+ private int numSigBytes; // Number of meaningful bytes in the buffer
+ private int lineLength;
+ private final boolean breakLines; // Break lines at less than 80 characters
+ private final int options; // Record options used to create the stream.
+ private final byte[] decodabet; // Local copies to avoid extra method calls
+
+ /**
+ * Constructs a {@link Base64.InputStream} in DECODE mode.
+ *
+ * @param in the java.io.InputStream from which to read data.
+ * @since 1.3
+ */
+ public InputStream(final java.io.InputStream in) {
+ this(in, DECODE);
+ }
+
+ /**
+ * Constructs a {@link Base64.InputStream} in
+ * either ENCODE or DECODE mode.
+ *
+ * Valid options:
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DO_BREAK_LINES: break lines at 76 characters
+ * (only meaningful when encoding)
+ *
+ *
+ * Example: new Base64.InputStream(in, Base64.DECODE)
+ *
+ * @param in the java.io.InputStream from which to read data.
+ * @param options Specified options
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DO_BREAK_LINES
+ * @since 2.0
+ */
+ public InputStream(final java.io.InputStream in, final int options) {
+ super(in);
+ this.options = options; // Record for later
+ this.breakLines = (options & DO_BREAK_LINES) > 0;
+ this.encode = (options & ENCODE) > 0;
+ this.bufferLength = encode ? 4 : 3;
+ this.buffer = new byte[bufferLength];
+ this.position = -1;
+ this.lineLength = 0;
+ this.decodabet = getDecodabet(options);
+ }
+
+ /**
+ * Reads enough of the input stream to convert
+ * to/from Base64 and returns the next byte.
+ *
+ * @return next byte
+ * @since 1.3
+ */
+ @Override
+ public int read() throws IOException {
+
+ // Do we need to get data?
+ if (position < 0) {
+ if (encode) {
+ byte[] b3 = new byte[3];
+ int numBinaryBytes = 0;
+ for (int i = 0; i < 3; i++) {
+ int b = in.read();
+
+ // If end of stream, b is -1.
+ if (b >= 0) {
+ b3[i] = (byte)b;
+ numBinaryBytes++;
+ } else {
+ break; // out of for loop
+ } // end else: end of stream
+
+ } // end for: each needed input byte
+
+ if (numBinaryBytes > 0) {
+ encode3to4(b3, 0, numBinaryBytes, buffer, 0, options);
+ position = 0;
+ numSigBytes = 4;
+ } // end if: got data
+ else {
+ return -1; // Must be end of stream
+ } // end else
+ } // end if: encoding
+
+ // Else decoding
+ else {
+ byte[] b4 = new byte[4];
+ int i = 0;
+ for (i = 0; i < 4; i++) {
+ // Read four "meaningful" bytes:
+ int b = 0;
+ do { b = in.read(); }
+ while(b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC);
+
+ if (b < 0) {
+ break; // Reads a -1 if end of stream
+ } // end if: end of stream
+
+ b4[i] = (byte)b;
+ } // end for: each needed input byte
+
+ if (i == 4) {
+ numSigBytes = decode4to3(b4, 0, buffer, 0, options);
+ position = 0;
+ } // end if: got four characters
+ else if (i == 0) {
+ return -1;
+ } // end else if: also padded correctly
+ else {
+ // Must have broken out from above.
+ throw new IOException("Improperly padded Base64 input.");
+ } // end
+
+ } // end else: decode
+ } // end else: get data
+
+ // Got data?
+ if (position >= 0) {
+ // End of relevant data?
+ if (position >= numSigBytes) {
+ return -1;
+ } // end if: got data
+
+ if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) {
+ lineLength = 0;
+ return '\n';
+ } // end if
+ else {
+ lineLength++; // This isn't important when decoding
+ // but throwing an extra "if" seems
+ // just as wasteful.
+
+ int b = buffer[position++];
+
+ if (position >= bufferLength) {
+ position = -1;
+ } // end if: end
+
+ return b & 0xFF; // This is how you "cast" a byte that's
+ // intended to be unsigned.
+ } // end else
+ } // end if: position >= 0
+
+ // Else error
+ else {
+ throw new IOException("Error in Base64 code reading stream.");
+ } // end else
+ } // end read
+
+ /**
+ * Calls {@link #read()} repeatedly until the end of stream
+ * is reached or len bytes are read.
+ * Returns number of bytes read into array or -1 if
+ * end of stream is encountered.
+ *
+ * @param dest array to hold values
+ * @param off offset for array
+ * @param len max number of bytes to read into array
+ * @return bytes read into array or -1 if end of stream is encountered.
+ * @since 1.3
+ */
+ @Override
+ public int read(final byte[] dest, final int off, final int len) throws IOException {
+ int i;
+ int b;
+ for (i = 0; i < len; i++) {
+ b = read();
+
+ if (b >= 0) {
+ dest[off + i] = (byte) b;
+ }
+ else if (i == 0) {
+ return -1;
+ }
+ else {
+ break; // Out of 'for' loop
+ } // Out of 'for' loop
+ } // end for: each byte read
+ return i;
+ } // end read
+
+ } // end inner class InputStream
+
+ /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */
+
+ /**
+ * A {@link Base64.OutputStream} will write data to another
+ * java.io.OutputStream , given in the constructor,
+ * and encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class OutputStream extends java.io.FilterOutputStream {
+
+ private final boolean encode;
+ private int position;
+ private byte[] buffer;
+ private final int bufferLength;
+ private int lineLength;
+ private final boolean breakLines;
+ private final byte[] b4; // Scratch used in a few places
+ private boolean suspendEncoding;
+ private final int options; // Record for later
+ private final byte[] decodabet; // Local copies to avoid extra method calls
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in ENCODE mode.
+ *
+ * @param out the java.io.OutputStream to which data will be written.
+ * @since 1.3
+ */
+ public OutputStream(final java.io.OutputStream out) {
+ this(out, ENCODE);
+ }
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in
+ * either ENCODE or DECODE mode.
+ *
+ * Valid options:
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DO_BREAK_LINES: don't break lines at 76 characters
+ * (only meaningful when encoding)
+ *
+ *
+ * Example: new Base64.OutputStream(out, Base64.ENCODE)
+ *
+ * @param out the java.io.OutputStream to which data will be written.
+ * @param options Specified options.
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DO_BREAK_LINES
+ * @since 1.3
+ */
+ public OutputStream(final java.io.OutputStream out, final int options) {
+ super(out);
+ this.breakLines = (options & DO_BREAK_LINES) != 0;
+ this.encode = (options & ENCODE) != 0;
+ this.bufferLength = encode ? 3 : 4;
+ this.buffer = new byte[bufferLength];
+ this.position = 0;
+ this.lineLength = 0;
+ this.suspendEncoding = false;
+ this.b4 = new byte[4];
+ this.options = options;
+ this.decodabet = getDecodabet(options);
+ }
+
+ /**
+ * Writes the byte to the output stream after converting to/from Base64
+ * notation. When encoding, bytes are buffered three at a time before
+ * the output stream actually gets a write() call. When decoding, bytes
+ * are buffered four at a time.
+ *
+ * @param theByte the byte to write
+ * @since 1.3
+ */
+ @Override
+ public void write(final int theByte) throws IOException {
+ // Encoding suspended?
+ if (suspendEncoding) {
+ this.out.write(theByte);
+ return;
+ }
+
+ // Encode?
+ if (encode) {
+ buffer[position++] = (byte)theByte;
+ if (position >= bufferLength) { // Enough to encode.
+
+ this.out.write(encode3to4(b4, buffer, bufferLength, options));
+
+ lineLength += 4;
+ if (breakLines && lineLength >= MAX_LINE_LENGTH) {
+ this.out.write(NEW_LINE);
+ lineLength = 0;
+ } // end if: end of line
+
+ position = 0;
+ } // end if: enough to output
+ } // end if: encoding
+
+ // Else, Decoding
+ else {
+ // Meaningful Base64 character?
+ if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) {
+ buffer[position++] = (byte)theByte;
+ if (position >= bufferLength) {
+ // Enough to output
+ int len = Base64.decode4to3(buffer, 0, b4, 0, options);
+ out.write(b4, 0, len);
+ position = 0;
+ }
+ } else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) {
+ // Not white space either
+ throw new IOException("Invalid character in Base64 data.");
+ }
+ }
+ }
+
+ /**
+ * Calls {@link #write(int)} repeatedly until len
+ * bytes are written.
+ *
+ * @param theBytes array from which to read bytes
+ * @param off offset for array
+ * @param len max number of bytes to read into array
+ * @since 1.3
+ */
+ @Override
+ public void write(final byte[] theBytes, final int off, final int len) throws IOException {
+ // Encoding suspended?
+ if (suspendEncoding) {
+ this.out.write(theBytes, off, len);
+ return;
+ } // end if: supsended
+
+ for (int i = 0; i < len; i++) {
+ write(theBytes[off + i]);
+ } // end for: each byte written
+ }
+
+ /**
+ * Pads the buffer without closing the stream.
+ *
+ * @throws IOException if there's an error.
+ */
+ public void flushBase64() throws IOException {
+ if (position > 0) {
+ if (encode) {
+ out.write(encode3to4(b4, buffer, position, options));
+ position = 0;
+ } // end if: encoding
+ else {
+ throw new IOException("Base64 input not properly padded.");
+ } // end else: decoding
+ } // end if: buffer partially full
+ }
+
+ /**
+ * Flushes and closes (I think, in the superclass) the stream.
+ *
+ * @since 1.3
+ */
+ @Override
+ public void close() throws IOException {
+ // 1. Ensure that pending characters are written
+ flushBase64();
+
+ // 2. Actually close the stream
+ // Base class both flushes and closes.
+ super.close();
+
+ buffer = null;
+ out = null;
+ }
+
+ /**
+ * Suspends encoding of the stream.
+ * May be helpful if you need to embed a piece of
+ * base64-encoded data in a stream.
+ *
+ * @throws IOException if there's an error flushing
+ * @since 1.5.1
+ */
+ public void suspendEncoding() throws IOException {
+ flushBase64();
+ this.suspendEncoding = true;
+ }
+
+ /**
+ * Resumes encoding of the stream.
+ * May be helpful if you need to embed a piece of
+ * base64-encoded data in a stream.
+ *
+ * @since 1.5.1
+ */
+ public void resumeEncoding() {
+ this.suspendEncoding = false;
+ }
+ } // end inner class OutputStream
+} // end class Base64
diff --git a/src/main/java/org/springframework/roo/support/util/ClassUtils.java b/src/main/java/org/springframework/roo/support/util/ClassUtils.java
new file mode 100644
index 00000000..c4d4a21c
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/ClassUtils.java
@@ -0,0 +1,1027 @@
+package org.springframework.roo.support.util;
+
+/*
+ * Copyright 2002-2010 the original author or authors.
+ *
+ * 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.
+ */
+
+import java.beans.Introspector;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Miscellaneous class utility methods. Mainly for internal use within the
+ * framework; consider
+ * Apache Commons Lang
+ * for a more comprehensive suite of class utilities.
+ *
+ * @author Juergen Hoeller
+ * @author Keith Donald
+ * @author Rob Harrop
+ * @author Sam Brannen
+ * @since 1.1
+ * @see org.springframework.util.TypeUtils
+ * @see ReflectionUtils
+ */
+@SuppressWarnings("all")
+public abstract class ClassUtils {
+
+ /** Suffix for array class names: "[]" */
+ public static final String ARRAY_SUFFIX = "[]";
+
+ /** Prefix for internal array class names: "[" */
+ private static final String INTERNAL_ARRAY_PREFIX = "[";
+
+ /** Prefix for internal non-primitive array class names: "[L" */
+ private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L";
+
+ /** The package separator character '.' */
+ private static final char PACKAGE_SEPARATOR = '.';
+
+ /** The inner class separator character '$' */
+ private static final char INNER_CLASS_SEPARATOR = '$';
+
+ /** The CGLIB class separator character "$$" */
+ public static final String CGLIB_CLASS_SEPARATOR = "$$";
+
+ /** The ".class" file suffix */
+ public static final String CLASS_FILE_SUFFIX = ".class";
+
+ /**
+ * Map with primitive wrapper type as key and corresponding primitive
+ * type as value, for example: Integer.class -> int.class.
+ */
+ private static final Map, Class>> primitiveWrapperTypeMap = new HashMap, Class>>(8);
+
+ /**
+ * Map with primitive type as key and corresponding wrapper
+ * type as value, for example: int.class -> Integer.class.
+ */
+ private static final Map, Class>> primitiveTypeToWrapperMap = new HashMap, Class>>(8);
+
+ /**
+ * Map with primitive type name as key and corresponding primitive
+ * type as value, for example: "int" -> "int.class".
+ */
+ private static final Map> primitiveTypeNameMap = new HashMap>(32);
+
+ /**
+ * Map with common "java.lang" class name as key and corresponding Class as value.
+ * Primarily for efficient deserialization of remote invocations.
+ */
+ private static final Map> commonClassCache = new HashMap>(32);
+
+ static {
+ primitiveWrapperTypeMap.put(Boolean.class, boolean.class);
+ primitiveWrapperTypeMap.put(Byte.class, byte.class);
+ primitiveWrapperTypeMap.put(Character.class, char.class);
+ primitiveWrapperTypeMap.put(Double.class, double.class);
+ primitiveWrapperTypeMap.put(Float.class, float.class);
+ primitiveWrapperTypeMap.put(Integer.class, int.class);
+ primitiveWrapperTypeMap.put(Long.class, long.class);
+ primitiveWrapperTypeMap.put(Short.class, short.class);
+
+ for (Map.Entry, Class>> entry : primitiveWrapperTypeMap.entrySet()) {
+ primitiveTypeToWrapperMap.put(entry.getValue(), entry.getKey());
+ registerCommonClasses(entry.getKey());
+ }
+
+ Set> primitiveTypes = new HashSet>(32);
+ primitiveTypes.addAll(primitiveWrapperTypeMap.values());
+ primitiveTypes.addAll(Arrays.asList(boolean[].class, byte[].class, char[].class, double[].class, float[].class, int[].class, long[].class, short[].class));
+ primitiveTypes.add(void.class);
+ for (Class> primitiveType : primitiveTypes) {
+ primitiveTypeNameMap.put(primitiveType.getName(), primitiveType);
+ }
+
+ registerCommonClasses(Boolean[].class, Byte[].class, Character[].class, Double[].class, Float[].class, Integer[].class, Long[].class, Short[].class);
+ registerCommonClasses(Number.class, Number[].class, String.class, String[].class, Object.class, Object[].class, Class.class, Class[].class);
+ registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, Error.class, StackTraceElement.class, StackTraceElement[].class);
+ }
+
+ /**
+ * Register the given common classes with the ClassUtils cache.
+ */
+ private static void registerCommonClasses(final Class>... commonClasses) {
+ for (Class> clazz : commonClasses) {
+ commonClassCache.put(clazz.getName(), clazz);
+ }
+ }
+
+ /**
+ * Return the default ClassLoader to use: typically the thread context
+ * ClassLoader, if available; the ClassLoader that loaded the ClassUtils
+ * class will be used as fallback.
+ * Call this method if you intend to use the thread context ClassLoader
+ * in a scenario where you absolutely need a non-null ClassLoader reference:
+ * for example, for class path resource loading (but not necessarily for
+ * Class.forName, which accepts a null ClassLoader
+ * reference as well).
+ * @return the default ClassLoader (never null)
+ * @see java.lang.Thread#getContextClassLoader()
+ */
+ public static ClassLoader getDefaultClassLoader() {
+ ClassLoader cl = null;
+ try {
+ cl = Thread.currentThread().getContextClassLoader();
+ } catch (Throwable ex) {
+ // Cannot access thread context ClassLoader - falling back to system class loader...
+ }
+ if (cl == null) {
+ // No thread context class loader -> use class loader of this class.
+ cl = ClassUtils.class.getClassLoader();
+ }
+ return cl;
+ }
+
+ /**
+ * Override the thread context ClassLoader with the environment's bean ClassLoader
+ * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread
+ * context ClassLoader already.
+ * @param classLoaderToUse the actual ClassLoader to use for the thread context
+ * @return the original thread context ClassLoader, or null if not overridden
+ */
+ public static ClassLoader overrideThreadContextClassLoader(final ClassLoader classLoaderToUse) {
+ Thread currentThread = Thread.currentThread();
+ ClassLoader threadContextClassLoader = currentThread.getContextClassLoader();
+ if (classLoaderToUse != null && !classLoaderToUse.equals(threadContextClassLoader)) {
+ currentThread.setContextClassLoader(classLoaderToUse);
+ return threadContextClassLoader;
+ }
+ return null;
+ }
+
+ /**
+ * Replacement for Class.forName() that also returns Class instances
+ * for primitives (like "int") and array class names (like "String[]").
+ *
Always uses the default class loader: that is, preferably the thread context
+ * class loader, or the ClassLoader that loaded the ClassUtils class as fallback.
+ * @param name the name of the Class
+ * @return Class instance for the supplied name
+ * @throws ClassNotFoundException if the class was not found
+ * @throws LinkageError if the class file could not be loaded
+ * @see Class#forName(String, boolean, ClassLoader)
+ * @see #getDefaultClassLoader()
+ * @deprecated as of Spring 3.0, in favor of specifying a ClassLoader explicitly:
+ * see {@link #forName(String, ClassLoader)}
+ */
+ @Deprecated
+ public static Class> forName(final String name) throws ClassNotFoundException, LinkageError {
+ return forName(name, getDefaultClassLoader());
+ }
+
+ /**
+ * Replacement for Class.forName() that also returns Class instances
+ * for primitives (e.g."int") and array class names (e.g. "String[]").
+ * Furthermore, it is also capable of resolving inner class names in Java source
+ * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State").
+ * @param name the name of the Class
+ * @param classLoader the class loader to use
+ * (may be null, which indicates the default class loader)
+ * @return Class instance for the supplied name
+ * @throws ClassNotFoundException if the class was not found
+ * @throws LinkageError if the class file could not be loaded
+ * @see Class#forName(String, boolean, ClassLoader)
+ */
+ public static Class> forName(final String name, final ClassLoader classLoader) throws ClassNotFoundException, LinkageError {
+ Assert.notNull(name, "Name must not be null");
+
+ Class> clazz = resolvePrimitiveClassName(name);
+ if (clazz == null) {
+ clazz = commonClassCache.get(name);
+ }
+ if (clazz != null) {
+ return clazz;
+ }
+
+ // "java.lang.String[]" style arrays
+ if (name.endsWith(ARRAY_SUFFIX)) {
+ String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length());
+ Class> elementClass = forName(elementClassName, classLoader);
+ return Array.newInstance(elementClass, 0).getClass();
+ }
+
+ // "[Ljava.lang.String;" style arrays
+ if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) {
+ String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1);
+ Class> elementClass = forName(elementName, classLoader);
+ return Array.newInstance(elementClass, 0).getClass();
+ }
+
+ // "[[I" or "[[Ljava.lang.String;" style arrays
+ if (name.startsWith(INTERNAL_ARRAY_PREFIX)) {
+ String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length());
+ Class> elementClass = forName(elementName, classLoader);
+ return Array.newInstance(elementClass, 0).getClass();
+ }
+
+ ClassLoader classLoaderToUse = classLoader;
+ if (classLoaderToUse == null) {
+ classLoaderToUse = getDefaultClassLoader();
+ }
+ try {
+ return classLoaderToUse.loadClass(name);
+ } catch (ClassNotFoundException ex) {
+ int lastDotIndex = name.lastIndexOf('.');
+ if (lastDotIndex != -1) {
+ String innerClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1);
+ try {
+ return classLoaderToUse.loadClass(innerClassName);
+ } catch (ClassNotFoundException ex2) {
+ // swallow - let original exception get through
+ }
+ }
+ throw ex;
+ }
+ }
+
+ /**
+ * Resolve the given class name into a Class instance. Supports
+ * primitives (like "int") and array class names (like "String[]").
+ *
This is effectively equivalent to the forName
+ * method with the same arguments, with the only difference being
+ * the exceptions thrown in case of class loading failure.
+ * @param className the name of the Class
+ * @param classLoader the class loader to use
+ * (may be null, which indicates the default class loader)
+ * @return Class instance for the supplied name
+ * @throws IllegalArgumentException if the class name was not resolvable
+ * (that is, the class could not be found or the class file could not be loaded)
+ * @see #forName(String, ClassLoader)
+ */
+ public static Class> resolveClassName(final String className, final ClassLoader classLoader) throws IllegalArgumentException {
+ try {
+ return forName(className, classLoader);
+ } catch (ClassNotFoundException ex) {
+ throw new IllegalArgumentException("Cannot find class [" + className + "]", ex);
+ } catch (LinkageError ex) {
+ throw new IllegalArgumentException("Error loading class [" + className + "]: problem with class file or dependent class.", ex);
+ }
+ }
+
+ /**
+ * Resolve the given class name as primitive class, if appropriate,
+ * according to the JVM's naming rules for primitive classes.
+ *
Also supports the JVM's internal class names for primitive arrays.
+ * Does not support the "[]" suffix notation for primitive arrays;
+ * this is only supported by {@link #forName(String, ClassLoader)}.
+ * @param name the name of the potentially primitive class
+ * @return the primitive class, or null if the name does not denote
+ * a primitive class or primitive array class
+ */
+ public static Class> resolvePrimitiveClassName(final String name) {
+ Class> result = null;
+ // Most class names will be quite long, considering that they
+ // SHOULD sit in a package, so a length check is worthwhile.
+ if (name != null && name.length() <= 8) {
+ // Could be a primitive - likely.
+ result = primitiveTypeNameMap.get(name);
+ }
+ return result;
+ }
+
+ /**
+ * Determine whether the {@link Class} identified by the supplied name is present
+ * and can be loaded. Will return false if either the class or
+ * one of its dependencies is not present or cannot be loaded.
+ * @param className the name of the class to check
+ * @return whether the specified class is present
+ * @deprecated as of Spring 2.5, in favor of {@link #isPresent(String, ClassLoader)}
+ */
+ @Deprecated
+ public static boolean isPresent(final String className) {
+ return isPresent(className, getDefaultClassLoader());
+ }
+
+ /**
+ * Determine whether the {@link Class} identified by the supplied name is present
+ * and can be loaded. Will return false if either the class or
+ * one of its dependencies is not present or cannot be loaded.
+ * @param className the name of the class to check
+ * @param classLoader the class loader to use
+ * (may be null, which indicates the default class loader)
+ * @return whether the specified class is present
+ */
+ public static boolean isPresent(final String className, final ClassLoader classLoader) {
+ try {
+ forName(className, classLoader);
+ return true;
+ } catch (Throwable ex) {
+ // Class or one of its dependencies is not present...
+ return false;
+ }
+ }
+
+ /**
+ * Return the user-defined class for the given instance: usually simply
+ * the class of the given instance, but the original class in case of a
+ * CGLIB-generated subclass.
+ * @param instance the instance to check
+ * @return the user-defined class
+ */
+ public static Class> getUserClass(final Object instance) {
+ Assert.notNull(instance, "Instance must not be null");
+ return getUserClass(instance.getClass());
+ }
+
+ /**
+ * Return the user-defined class for the given class: usually simply the given
+ * class, but the original class in case of a CGLIB-generated subclass.
+ * @param clazz the class to check
+ * @return the user-defined class
+ */
+ public static Class> getUserClass(final Class> clazz) {
+ if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) {
+ Class> superClass = clazz.getSuperclass();
+ if (superClass != null && !Object.class.equals(superClass)) {
+ return superClass;
+ }
+ }
+ return clazz;
+ }
+
+ /**
+ * Check whether the given class is cache-safe in the given context,
+ * i.e. whether it is loaded by the given ClassLoader or a parent of it.
+ * @param clazz the class to analyze
+ * @param classLoader the ClassLoader to potentially cache metadata in
+ */
+ public static boolean isCacheSafe(final Class> clazz, final ClassLoader classLoader) {
+ Assert.notNull(clazz, "Class must not be null");
+ ClassLoader target = clazz.getClassLoader();
+ if (target == null) {
+ return false;
+ }
+ ClassLoader cur = classLoader;
+ if (cur == target) {
+ return true;
+ }
+ while (cur != null) {
+ cur = cur.getParent();
+ if (cur == target) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the class name without the qualified package name.
+ * @param className the className to get the short name for
+ * @return the class name of the class without the package name
+ * @throws IllegalArgumentException if the className is empty
+ */
+ public static String getShortName(final String className) {
+ Assert.hasLength(className, "Class name must not be empty");
+ int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
+ int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
+ if (nameEndIndex == -1) {
+ nameEndIndex = className.length();
+ }
+ String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
+ shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR);
+ return shortName;
+ }
+
+ /**
+ * Get the class name without the qualified package name.
+ * @param clazz the class to get the short name for
+ * @return the class name of the class without the package name
+ */
+ public static String getShortName(final Class> clazz) {
+ return getShortName(getQualifiedName(clazz));
+ }
+
+ /**
+ * Return the short string name of a Java class in uncapitalized JavaBeans
+ * property format. Strips the outer class name in case of an inner class.
+ * @param clazz the class
+ * @return the short name rendered in a standard JavaBeans property format
+ * @see java.beans.Introspector#decapitalize(String)
+ */
+ public static String getShortNameAsProperty(final Class> clazz) {
+ String shortName = ClassUtils.getShortName(clazz);
+ int dotIndex = shortName.lastIndexOf('.');
+ shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName);
+ return Introspector.decapitalize(shortName);
+ }
+
+ /**
+ * Determine the name of the class file, relative to the containing
+ * package: e.g. "String.class"
+ * @param clazz the class
+ * @return the file name of the ".class" file
+ */
+ public static String getClassFileName(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ String className = clazz.getName();
+ int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
+ return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX;
+ }
+
+ /**
+ * Determine the name of the package of the given class:
+ * e.g. "java.lang" for the java.lang.String class.
+ * @param clazz the class
+ * @return the package name, or the empty String if the class
+ * is defined in the default package
+ */
+ public static String getPackageName(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ String className = clazz.getName();
+ int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
+ return (lastDotIndex != -1 ? className.substring(0, lastDotIndex) : "");
+ }
+
+ /**
+ * Return the qualified name of the given class: usually simply
+ * the class name, but component type class name + "[]" for arrays.
+ * @param clazz the class
+ * @return the qualified name of the class
+ */
+ public static String getQualifiedName(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ if (clazz.isArray()) {
+ return getQualifiedNameForArray(clazz);
+ } else {
+ return clazz.getName();
+ }
+ }
+
+ /**
+ * Build a nice qualified name for an array:
+ * component type class name + "[]".
+ * @param clazz the array class
+ * @return a qualified name for the array class
+ */
+ private static String getQualifiedNameForArray(Class> clazz) {
+ StringBuilder result = new StringBuilder();
+ while (clazz.isArray()) {
+ clazz = clazz.getComponentType();
+ result.append(ClassUtils.ARRAY_SUFFIX);
+ }
+ result.insert(0, clazz.getName());
+ return result.toString();
+ }
+
+ /**
+ * Return the qualified name of the given method, consisting of
+ * fully qualified interface/class name + "." + method name.
+ * @param method the method
+ * @return the qualified name of the method
+ */
+ public static String getQualifiedMethodName(final Method method) {
+ Assert.notNull(method, "Method must not be null");
+ return method.getDeclaringClass().getName() + "." + method.getName();
+ }
+
+ /**
+ * Return a descriptive name for the given object's type: usually simply
+ * the class name, but component type class name + "[]" for arrays,
+ * and an appended list of implemented interfaces for JDK proxies.
+ * @param value the value to introspect
+ * @return the qualified name of the class
+ */
+ public static String getDescriptiveType(final Object value) {
+ if (value == null) {
+ return null;
+ }
+ Class> clazz = value.getClass();
+ if (Proxy.isProxyClass(clazz)) {
+ StringBuilder result = new StringBuilder(clazz.getName());
+ result.append(" implementing ");
+ Class>[] ifcs = clazz.getInterfaces();
+ for (int i = 0; i < ifcs.length; i++) {
+ result.append(ifcs[i].getName());
+ if (i < ifcs.length - 1) {
+ result.append(',');
+ }
+ }
+ return result.toString();
+ } else if (clazz.isArray()) {
+ return getQualifiedNameForArray(clazz);
+ } else {
+ return clazz.getName();
+ }
+ }
+
+ /**
+ * Check whether the given class matches the user-specified type name.
+ * @param clazz the class to check
+ * @param typeName the type name to match
+ */
+ public static boolean matchesTypeName(final Class> clazz, final String typeName) {
+ return (typeName != null &&
+ (typeName.equals(clazz.getName()) || typeName.equals(clazz.getSimpleName()) ||
+ (clazz.isArray() && typeName.equals(getQualifiedNameForArray(clazz)))));
+ }
+
+ /**
+ * Determine whether the given class has a public constructor with the given signature.
+ *
Essentially translates NoSuchMethodException to "false".
+ * @param clazz the clazz to analyze
+ * @param parameterTypes the parameter types of the method
+ * @return whether the class has a corresponding constructor
+ * @see java.lang.Class#getMethod
+ */
+ public static boolean hasConstructor(final Class> clazz, final Class>... parameterTypes) {
+ return (getConstructorIfAvailable(clazz, parameterTypes) != null);
+ }
+
+ /**
+ * Determine whether the given class has a public constructor with the given signature,
+ * and return it if available (else return null).
+ *
Essentially translates NoSuchMethodException to null.
+ * @param clazz the clazz to analyze
+ * @param parameterTypes the parameter types of the method
+ * @return the constructor, or null if not found
+ * @see java.lang.Class#getConstructor
+ */
+ public static Constructor getConstructorIfAvailable(final Class clazz, final Class>... parameterTypes) {
+ Assert.notNull(clazz, "Class must not be null");
+ try {
+ return clazz.getConstructor(parameterTypes);
+ } catch (NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Determine whether the given class has a method with the given signature.
+ * Essentially translates NoSuchMethodException to "false".
+ * @param clazz the clazz to analyze
+ * @param methodName the name of the method
+ * @param parameterTypes the parameter types of the method
+ * @return whether the class has a corresponding method
+ * @see java.lang.Class#getMethod
+ */
+ public static boolean hasMethod(final Class> clazz, final String methodName, final Class>... parameterTypes) {
+ return (getMethodIfAvailable(clazz, methodName, parameterTypes) != null);
+ }
+
+ /**
+ * Determine whether the given class has a method with the given signature,
+ * and return it if available (else return null).
+ *
Essentially translates NoSuchMethodException to null.
+ * @param clazz the clazz to analyze
+ * @param methodName the name of the method
+ * @param parameterTypes the parameter types of the method
+ * @return the method, or null if not found
+ * @see java.lang.Class#getMethod
+ */
+ public static Method getMethodIfAvailable(final Class> clazz, final String methodName, final Class>... parameterTypes) {
+ Assert.notNull(clazz, "Class must not be null");
+ Assert.notNull(methodName, "Method name must not be null");
+ try {
+ return clazz.getMethod(methodName, parameterTypes);
+ } catch (NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Return the number of methods with a given name (with any argument types),
+ * for the given class and/or its superclasses. Includes non-public methods.
+ * @param clazz the clazz to check
+ * @param methodName the name of the method
+ * @return the number of methods with the given name
+ */
+ public static int getMethodCountForName(final Class> clazz, final String methodName) {
+ Assert.notNull(clazz, "Class must not be null");
+ Assert.notNull(methodName, "Method name must not be null");
+ int count = 0;
+ Method[] declaredMethods = clazz.getDeclaredMethods();
+ for (Method method : declaredMethods) {
+ if (methodName.equals(method.getName())) {
+ count++;
+ }
+ }
+ Class>[] ifcs = clazz.getInterfaces();
+ for (Class> ifc : ifcs) {
+ count += getMethodCountForName(ifc, methodName);
+ }
+ if (clazz.getSuperclass() != null) {
+ count += getMethodCountForName(clazz.getSuperclass(), methodName);
+ }
+ return count;
+ }
+
+ /**
+ * Does the given class or one of its superclasses at least have one or more
+ * methods with the supplied name (with any argument types)?
+ * Includes non-public methods.
+ * @param clazz the clazz to check
+ * @param methodName the name of the method
+ * @return whether there is at least one method with the given name
+ */
+ public static boolean hasAtLeastOneMethodWithName(final Class> clazz, final String methodName) {
+ Assert.notNull(clazz, "Class must not be null");
+ Assert.notNull(methodName, "Method name must not be null");
+ Method[] declaredMethods = clazz.getDeclaredMethods();
+ for (Method method : declaredMethods) {
+ if (method.getName().equals(methodName)) {
+ return true;
+ }
+ }
+ Class>[] ifcs = clazz.getInterfaces();
+ for (Class> ifc : ifcs) {
+ if (hasAtLeastOneMethodWithName(ifc, methodName)) {
+ return true;
+ }
+ }
+ return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName));
+ }
+
+ /**
+ * Given a method, which may come from an interface, and a target class used
+ * in the current reflective invocation, find the corresponding target method
+ * if there is one. E.g. the method may be IFoo.bar() and the
+ * target class may be DefaultFoo. In this case, the method may be
+ * DefaultFoo.bar(). This enables attributes on that method to be found.
+ *
NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod},
+ * this method does not resolve Java 5 bridge methods automatically.
+ * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod}
+ * if bridge method resolution is desirable (e.g. for obtaining metadata from
+ * the original method definition).
+ * @param method the method to be invoked, which may come from an interface
+ * @param targetClass the target class for the current invocation.
+ * May be null or may not even implement the method.
+ * @return the specific target method, or the original method if the
+ * targetClass doesn't implement it or is null
+ */
+ public static Method getMostSpecificMethod(final Method method, final Class> targetClass) {
+ Method specificMethod = null;
+ if (method != null && isOverridable(method, targetClass) && targetClass != null && !targetClass.equals(method.getDeclaringClass())) {
+ specificMethod = ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes());
+ }
+ return (specificMethod != null ? specificMethod : method);
+ }
+
+ /**
+ * Determine whether the given method is overridable in the given target class.
+ * @param method the method to check
+ * @param targetClass the target class to check against
+ */
+ private static boolean isOverridable(final Method method, final Class> targetClass) {
+ if (Modifier.isPrivate(method.getModifiers())) {
+ return false;
+ }
+ if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) {
+ return true;
+ }
+ return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass));
+ }
+
+ /**
+ * Return a public static method of a class.
+ * @param methodName the static method name
+ * @param clazz the class which defines the method
+ * @param args the parameter types to the method
+ * @return the static method, or null if no static method was found
+ * @throws IllegalArgumentException if the method name is blank or the clazz is null
+ */
+ public static Method getStaticMethod(final Class> clazz, final String methodName, final Class>... args) {
+ Assert.notNull(clazz, "Class must not be null");
+ Assert.notNull(methodName, "Method name must not be null");
+ try {
+ Method method = clazz.getMethod(methodName, args);
+ return Modifier.isStatic(method.getModifiers()) ? method : null;
+ } catch (NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Check if the given class represents a primitive wrapper,
+ * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double.
+ * @param clazz the class to check
+ * @return whether the given class is a primitive wrapper class
+ */
+ public static boolean isPrimitiveWrapper(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ return primitiveWrapperTypeMap.containsKey(clazz);
+ }
+
+ /**
+ * Check if the given class represents a primitive (i.e. boolean, byte,
+ * char, short, int, long, float, or double) or a primitive wrapper
+ * (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double).
+ * @param clazz the class to check
+ * @return whether the given class is a primitive or primitive wrapper class
+ */
+ public static boolean isPrimitiveOrWrapper(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ return (clazz.isPrimitive() || isPrimitiveWrapper(clazz));
+ }
+
+ /**
+ * Check if the given class represents an array of primitives,
+ * i.e. boolean, byte, char, short, int, long, float, or double.
+ * @param clazz the class to check
+ * @return whether the given class is a primitive array class
+ */
+ public static boolean isPrimitiveArray(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ return (clazz.isArray() && clazz.getComponentType().isPrimitive());
+ }
+
+ /**
+ * Check if the given class represents an array of primitive wrappers,
+ * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double.
+ * @param clazz the class to check
+ * @return whether the given class is a primitive wrapper array class
+ */
+ public static boolean isPrimitiveWrapperArray(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType()));
+ }
+
+ /**
+ * Resolve the given class if it is a primitive class,
+ * returning the corresponding primitive wrapper type instead.
+ * @param clazz the class to check
+ * @return the original class, or a primitive wrapper for the original primitive type
+ */
+ public static Class> resolvePrimitiveIfNecessary(final Class> clazz) {
+ Assert.notNull(clazz, "Class must not be null");
+ return (clazz.isPrimitive() && clazz != void.class? primitiveTypeToWrapperMap.get(clazz) : clazz);
+ }
+
+ /**
+ * Check if the right-hand side type may be assigned to the left-hand side
+ * type, assuming setting by reflection. Considers primitive wrapper
+ * classes as assignable to the corresponding primitive types.
+ * @param lhsType the target type
+ * @param rhsType the value type that should be assigned to the target type
+ * @return if the target type is assignable from the value type
+ * @see org.springframework.util.TypeUtils#isAssignable
+ */
+ public static boolean isAssignable(final Class> lhsType, final Class> rhsType) {
+ Assert.notNull(lhsType, "Left-hand side type must not be null");
+ Assert.notNull(rhsType, "Right-hand side type must not be null");
+ return (lhsType.isAssignableFrom(rhsType) || lhsType.equals(primitiveWrapperTypeMap.get(rhsType)));
+ }
+
+ /**
+ * Determine if the given type is assignable from the given value,
+ * assuming setting by reflection. Considers primitive wrapper classes
+ * as assignable to the corresponding primitive types.
+ * @param type the target type
+ * @param value the value that should be assigned to the type
+ * @return if the type is assignable from the value
+ */
+ public static boolean isAssignableValue(final Class> type, final Object value) {
+ Assert.notNull(type, "Type must not be null");
+ return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive());
+ }
+
+ /**
+ * Convert a "/"-based resource path to a "."-based fully qualified class name.
+ * @param resourcePath the resource path pointing to a class
+ * @return the corresponding fully qualified class name
+ */
+ public static String convertResourcePathToClassName(final String resourcePath) {
+ Assert.notNull(resourcePath, "Resource path must not be null");
+ return resourcePath.replace('/', '.');
+ }
+
+ /**
+ * Convert a "."-based fully qualified class name to a "/"-based resource path.
+ * @param className the fully qualified class name
+ * @return the corresponding resource path, pointing to the class
+ */
+ public static String convertClassNameToResourcePath(final String className) {
+ Assert.notNull(className, "Class name must not be null");
+ return className.replace('.', '/');
+ }
+
+ /**
+ * Return a path suitable for use with ClassLoader.getResource
+ * (also suitable for use with Class.getResource by prepending a
+ * slash ('/') to the return value). Built by taking the package of the specified
+ * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash
+ * if necessary, and concatenating the specified resource name to this.
+ * As such, this function may be used to build a path suitable for
+ * loading a resource file that is in the same package as a class file,
+ * although {@link org.springframework.core.io.ClassPathResource} is usually
+ * even more convenient.
+ * @param clazz the Class whose package will be used as the base
+ * @param resourceName the resource name to append. A leading slash is optional.
+ * @return the built-up resource path
+ * @see java.lang.ClassLoader#getResource
+ * @see java.lang.Class#getResource
+ */
+ public static String addResourcePathToPackagePath(final Class> clazz, final String resourceName) {
+ Assert.notNull(resourceName, "Resource name must not be null");
+ if (!resourceName.startsWith("/")) {
+ return classPackageAsResourcePath(clazz) + "/" + resourceName;
+ }
+ return classPackageAsResourcePath(clazz) + resourceName;
+ }
+
+ /**
+ * Given an input class object, return a string which consists of the
+ * class's package name as a pathname, i.e., all dots ('.') are replaced by
+ * slashes ('/'). Neither a leading nor trailing slash is added. The result
+ * could be concatenated with a slash and the name of a resource and fed
+ * directly to ClassLoader.getResource(). For it to be fed to
+ * Class.getResource instead, a leading slash would also have
+ * to be prepended to the returned value.
+ * @param clazz the input class. A null value or the default
+ * (empty) package will result in an empty string ("") being returned.
+ * @return a path which represents the package name
+ * @see ClassLoader#getResource
+ * @see Class#getResource
+ */
+ public static String classPackageAsResourcePath(final Class> clazz) {
+ if (clazz == null) {
+ return "";
+ }
+ String className = clazz.getName();
+ int packageEndIndex = className.lastIndexOf('.');
+ if (packageEndIndex == -1) {
+ return "";
+ }
+ String packageName = className.substring(0, packageEndIndex);
+ return packageName.replace('.', '/');
+ }
+
+ /**
+ * Build a String that consists of the names of the classes/interfaces
+ * in the given array.
+ *
Basically like AbstractCollection.toString(), but stripping
+ * the "class "/"interface " prefix before every class name.
+ * @param classes a Collection of Class objects (may be null)
+ * @return a String of form "[com.foo.Bar, com.foo.Baz]"
+ * @see java.util.AbstractCollection#toString()
+ */
+ public static String classNamesToString(final Class>... classes) {
+ return classNamesToString(Arrays.asList(classes));
+ }
+
+ /**
+ * Build a String that consists of the names of the classes/interfaces
+ * in the given collection.
+ *
Basically like AbstractCollection.toString(), but stripping
+ * the "class "/"interface " prefix before every class name.
+ * @param classes a Collection of Class objects (may be null)
+ * @return a String of form "[com.foo.Bar, com.foo.Baz]"
+ * @see java.util.AbstractCollection#toString()
+ */
+ public static String classNamesToString(final Collection> classes) {
+ if (CollectionUtils.isEmpty(classes)) {
+ return "[]";
+ }
+ StringBuilder sb = new StringBuilder("[");
+ for (Iterator> it = classes.iterator(); it.hasNext();) {
+ Class> clazz = it.next();
+ sb.append(clazz.getName());
+ if (it.hasNext()) {
+ sb.append(", ");
+ }
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ /**
+ * Return all interfaces that the given instance implements as array,
+ * including ones implemented by superclasses.
+ * @param instance the instance to analyze for interfaces
+ * @return all interfaces that the given instance implements as array
+ */
+ public static Class[] getAllInterfaces(final Object instance) {
+ Assert.notNull(instance, "Instance must not be null");
+ return getAllInterfacesForClass(instance.getClass());
+ }
+
+ /**
+ * Return all interfaces that the given class implements as array,
+ * including ones implemented by superclasses.
+ * If the class itself is an interface, it gets returned as sole interface.
+ * @param clazz the class to analyze for interfaces
+ * @return all interfaces that the given object implements as array
+ */
+ public static Class>[] getAllInterfacesForClass(final Class> clazz) {
+ return getAllInterfacesForClass(clazz, null);
+ }
+
+ /**
+ * Return all interfaces that the given class implements as array,
+ * including ones implemented by superclasses.
+ *
If the class itself is an interface, it gets returned as sole interface.
+ * @param clazz the class to analyze for interfaces
+ * @param classLoader the ClassLoader that the interfaces need to be visible in
+ * (may be null when accepting all declared interfaces)
+ * @return all interfaces that the given object implements as array
+ */
+ public static Class>[] getAllInterfacesForClass(final Class> clazz, final ClassLoader classLoader) {
+ Set ifcs = getAllInterfacesForClassAsSet(clazz, classLoader);
+ return ifcs.toArray(new Class[ifcs.size()]);
+ }
+
+ /**
+ * Return all interfaces that the given instance implements as Set,
+ * including ones implemented by superclasses.
+ * @param instance the instance to analyze for interfaces
+ * @return all interfaces that the given instance implements as Set
+ */
+ public static Set getAllInterfacesAsSet(final Object instance) {
+ Assert.notNull(instance, "Instance must not be null");
+ return getAllInterfacesForClassAsSet(instance.getClass());
+ }
+
+ /**
+ * Return all interfaces that the given class implements as Set,
+ * including ones implemented by superclasses.
+ * If the class itself is an interface, it gets returned as sole interface.
+ * @param clazz the class to analyze for interfaces
+ * @return all interfaces that the given object implements as Set
+ */
+ public static Set getAllInterfacesForClassAsSet(final Class clazz) {
+ return getAllInterfacesForClassAsSet(clazz, null);
+ }
+
+ /**
+ * Return all interfaces that the given class implements as Set,
+ * including ones implemented by superclasses.
+ * If the class itself is an interface, it gets returned as sole interface.
+ * @param clazz the class to analyze for interfaces
+ * @param classLoader the ClassLoader that the interfaces need to be visible in
+ * (may be null when accepting all declared interfaces)
+ * @return all interfaces that the given object implements as Set
+ */
+ public static Set getAllInterfacesForClassAsSet(Class clazz, final ClassLoader classLoader) {
+ Assert.notNull(clazz, "Class must not be null");
+ if (clazz.isInterface() && isVisible(clazz, classLoader)) {
+ return Collections.singleton(clazz);
+ }
+ Set interfaces = new LinkedHashSet();
+ while (clazz != null) {
+ Class>[] ifcs = clazz.getInterfaces();
+ for (Class> ifc : ifcs) {
+ interfaces.addAll(getAllInterfacesForClassAsSet(ifc, classLoader));
+ }
+ clazz = clazz.getSuperclass();
+ }
+ return interfaces;
+ }
+
+ /**
+ * Create a composite interface Class for the given interfaces,
+ * implementing the given interfaces in one single Class.
+ * This implementation builds a JDK proxy class for the given interfaces.
+ * @param interfaces the interfaces to merge
+ * @param classLoader the ClassLoader to create the composite Class in
+ * @return the merged interface as Class
+ * @see java.lang.reflect.Proxy#getProxyClass
+ */
+ public static Class> createCompositeInterface(final Class>[] interfaces, final ClassLoader classLoader) {
+ Assert.notEmpty(interfaces, "Interfaces must not be empty");
+ Assert.notNull(classLoader, "ClassLoader must not be null");
+ return Proxy.getProxyClass(classLoader, interfaces);
+ }
+
+ /**
+ * Check whether the given class is visible in the given ClassLoader.
+ * @param clazz the class to check (typically an interface)
+ * @param classLoader the ClassLoader to check against (may be null,
+ * in which case this method will always return true)
+ */
+ public static boolean isVisible(final Class> clazz, final ClassLoader classLoader) {
+ if (classLoader == null) {
+ return true;
+ }
+ try {
+ Class> actualClass = classLoader.loadClass(clazz.getName());
+ return (clazz == actualClass);
+ // Else: different interface class found...
+ } catch (ClassNotFoundException ex) {
+ // No interface class found...
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/support/util/CollectionUtils.java b/src/main/java/org/springframework/roo/support/util/CollectionUtils.java
new file mode 100644
index 00000000..af2a4588
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/CollectionUtils.java
@@ -0,0 +1,367 @@
+package org.springframework.roo.support.util;
+
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Miscellaneous collection utility methods.
+ * Mainly for internal use within the framework.
+ *
+ * @author Juergen Hoeller
+ * @author Rob Harrop
+ * @author Andrew Swan
+ * @since 1.1.3
+ */
+public final class CollectionUtils {
+
+ /**
+ * Return true if the supplied Collection is null
+ * or empty. Otherwise, return false.
+ *
+ * @param collection the Collection to check
+ * @return whether the given Collection is empty
+ */
+ public static boolean isEmpty(final Collection> collection) {
+ return (collection == null || collection.isEmpty());
+ }
+
+ /**
+ * Return true if the supplied Map is null
+ * or empty. Otherwise, return false.
+ *
+ * @param map the Map to check
+ * @return whether the given Map is empty
+ */
+ public static boolean isEmpty(final Map, ?> map) {
+ return (map == null || map.isEmpty());
+ }
+
+ /**
+ * Convert the supplied array into a List. A primitive array gets
+ * converted into a List of the appropriate wrapper type.
+ *
A null source value will be converted to an
+ * empty List.
+ *
+ * @param source the (potentially primitive) array
+ * @return the converted List result
+ * @see ObjectUtils#toObjectArray(Object)
+ */
+ public static List> arrayToList(final Object source) {
+ return Arrays.asList(ObjectUtils.toObjectArray(source));
+ }
+
+ /**
+ * Merge the given array into the given Collection.
+ *
+ * @param array the array to merge (may be null)
+ * @param collection the target Collection to merge the array into
+ */
+ public static void mergeArrayIntoCollection(final Object array, final Collection collection) {
+ if (collection == null) {
+ throw new IllegalArgumentException("Collection must not be null");
+ }
+ final Object[] arr = ObjectUtils.toObjectArray(array);
+ for (final Object elem : arr) {
+ collection.add(elem);
+ }
+ }
+
+ /**
+ * Merge the given Properties instance into the given Map,
+ * copying all properties (key-value pairs) over.
+ * Uses Properties.propertyNames() to even catch
+ * default properties linked into the original Properties instance.
+ *
+ * @param props the Properties instance to merge (may be null)
+ * @param map the target Map to merge the properties into
+ */
+ public static void mergePropertiesIntoMap(final Properties props, final Map map) {
+ if (map == null) {
+ throw new IllegalArgumentException("Map must not be null");
+ }
+ if (props != null) {
+ for (final Enumeration> en = props.propertyNames(); en.hasMoreElements();) {
+ final String key = (String) en.nextElement();
+ map.put(key, props.getProperty(key));
+ }
+ }
+ }
+
+ /**
+ * Check whether the given Iterator contains the given element.
+ *
+ * @param iterator the Iterator to check
+ * @param element the element to look for
+ * @return true if found, false else
+ */
+ public static boolean contains(final Iterator> iterator, final Object element) {
+ if (iterator != null) {
+ while (iterator.hasNext()) {
+ final Object candidate = iterator.next();
+ if (ObjectUtils.nullSafeEquals(candidate, element)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the given Enumeration contains the given element.
+ *
+ * @param enumeration the Enumeration to check
+ * @param element the element to look for
+ * @return true if found, false else
+ */
+ public static boolean contains(final Enumeration> enumeration, final Object element) {
+ if (enumeration != null) {
+ while (enumeration.hasMoreElements()) {
+ final Object candidate = enumeration.nextElement();
+ if (ObjectUtils.nullSafeEquals(candidate, element)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the given Collection contains the given element instance.
+ * Enforces the given instance to be present, rather than returning
+ * true for an equal element as well.
+ *
+ * @param collection the Collection to check
+ * @param element the element to look for
+ * @return true if found, false else
+ */
+ public static boolean containsInstance(final Collection> collection, final Object element) {
+ if (collection != null) {
+ for (final Object candidate : collection) {
+ if (candidate == element) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if any element in 'candidates' is
+ * contained in 'source'; otherwise returns false.
+ *
+ * @param source the source Collection
+ * @param candidates the candidates to search for
+ * @return whether any of the candidates has been found
+ */
+ public static boolean containsAny(final Collection> source, final Collection> candidates) {
+ if (isEmpty(source) || isEmpty(candidates)) {
+ return false;
+ }
+ for (final Object candidate : candidates) {
+ if (source.contains(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the first element in 'candidates' that is contained in
+ * 'source'. If no element in 'candidates' is present in
+ * 'source' returns null. Iteration order is
+ * {@link Collection} implementation specific.
+ *
+ * @param source the source Collection
+ * @param candidates the candidates to search for
+ * @return the first present object, or null if not found
+ */
+ public static Object findFirstMatch(final Collection> source, final Collection> candidates) {
+ if (isEmpty(source) || isEmpty(candidates)) {
+ return null;
+ }
+ for (final Object candidate : candidates) {
+ if (source.contains(candidate)) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find a single value of the given type in the given Collection.
+ *
+ * @param collection the Collection to search
+ * @param type the type to look for
+ * @return a value of the given type found if there is a clear match,
+ * or null if none or more than one such value found
+ */
+ @SuppressWarnings("unchecked")
+ public static T findValueOfType(final Collection> collection, final Class type) {
+ if (isEmpty(collection)) {
+ return null;
+ }
+ T value = null;
+ for (final Object element : collection) {
+ if (type == null || type.isInstance(element)) {
+ if (value != null) {
+ // More than one value found... no clear single value.
+ return null;
+ }
+ value = (T) element;
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Find a single value of one of the given types in the given Collection:
+ * searching the Collection for a value of the first type, then
+ * searching for a value of the second type, etc.
+ *
+ * @param collection the collection to search
+ * @param types the types to look for, in prioritized order
+ * @return a value of one of the given types found if there is a clear match,
+ * or null if none or more than one such value found
+ */
+ public static Object findValueOfType(final Collection> collection, final Class>... types) {
+ if (isEmpty(collection) || ObjectUtils.isEmpty(types)) {
+ return null;
+ }
+ for (final Class> type : types) {
+ final Object value = findValueOfType(collection, type);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Determine whether the given Collection only contains a single unique object.
+ *
+ * @param collection the Collection to check
+ * @return true if the collection contains a single reference or
+ * multiple references to the same instance, false else
+ */
+ public static boolean hasUniqueObject(final Collection> collection) {
+ if (isEmpty(collection)) {
+ return false;
+ }
+ boolean hasCandidate = false;
+ Object candidate = null;
+ for (final Iterator> it = collection.iterator(); it.hasNext();) {
+ final Object elem = it.next();
+ if (!hasCandidate) {
+ hasCandidate = true;
+ candidate = elem;
+ }
+ else if (candidate != elem) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Filters (removes elements from) the given {@link Iterable} using the
+ * given filter.
+ *
+ * @param the type of object being filtered
+ * @param unfiltered the iterable to filter; can be null
+ * @param filter the filter to apply; can be null for none
+ * @return a non-null list
+ */
+ public static List filter(final Iterable extends T> unfiltered, final Filter filter) {
+ final List filtered = new ArrayList();
+ if (unfiltered != null) {
+ for (final T element : unfiltered) {
+ if (filter == null || filter.include(element)) {
+ filtered.add(element);
+ }
+ }
+ }
+ return filtered;
+ }
+
+ /**
+ * Adds the given items to the given collection
+ *
+ * @param the type of item in the collection being updated
+ * @param newItems the items being added; can be null for none
+ * @param existingItems the items being added to; must be modifiable
+ * @return true if the existing collection was modified
+ * @throws UnsupportedOperationException if there are items to add and the
+ * existing collection is not modifiable
+ * @since 1.2.0
+ */
+ public static boolean addAll(final Collection extends T> newItems, final Collection existingItems) {
+ if (existingItems != null && newItems != null) {
+ return existingItems.addAll(newItems);
+ }
+ return false;
+ }
+
+ /**
+ * Populates the given collection by replacing any existing contents with
+ * the given elements, in a null-safe way.
+ *
+ * @param the type of element in the collection
+ * @param collection the collection to populate (can be null)
+ * @param items the items with which to populate the collection (can be
+ * null or empty for none)
+ * @return the given collection (useful if it was anonymous)
+ */
+ public static Collection populate(final Collection collection, final Collection extends T> items) {
+ if (collection != null) {
+ collection.clear();
+ if (items != null) {
+ collection.addAll(items);
+ }
+ }
+ return collection;
+ }
+
+ /**
+ * Returns the first element of the given collection
+ *
+ * @param
+ * @param collection
+ * @return null if the first element is null or
+ * the collection is null or empty
+ */
+ public static T firstElementOf(final Collection extends T> collection) {
+ if (isEmpty(collection)) {
+ return null;
+ }
+ return collection.iterator().next();
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ *
+ * @since 1.2.0
+ */
+ private CollectionUtils() {}
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/support/util/DomUtils.java b/src/main/java/org/springframework/roo/support/util/DomUtils.java
new file mode 100644
index 00000000..af8b4649
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/DomUtils.java
@@ -0,0 +1,295 @@
+package org.springframework.roo.support.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.w3c.dom.CharacterData;
+import org.w3c.dom.Comment;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.EntityReference;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Convenience methods for working with the DOM API,
+ * in particular for working with DOM Nodes and DOM Elements.
+ *
+ * @author Juergen Hoeller
+ * @author Rob Harrop
+ * @author Costin Leau
+ * @author Alan Stewart
+ * @since 1.2.0
+ * @see org.w3c.dom.Node
+ * @see org.w3c.dom.Element
+ */
+public final class DomUtils {
+
+ /**
+ * Retrieve all child elements of the given DOM element that match any of
+ * the given element names. Only look at the direct child level of the
+ * given element; do not go into further depth (in contrast to the
+ * DOM API's getElementsByTagName method).
+ *
+ * @param element the DOM element to analyze
+ * @param childElementNames the child element names to look for
+ * @return a List of child org.w3c.dom.Element instances
+ * @see org.w3c.dom.Element
+ * @see org.w3c.dom.Element#getElementsByTagName
+ */
+ public static List getChildElementsByTagName(final Element element, final String[] childElementNames) {
+ Assert.notNull(element, "Element must not be null");
+ Assert.notNull(childElementNames, "Element names collection must not be null");
+ List childEleNameList = Arrays.asList(childElementNames);
+ NodeList nl = element.getChildNodes();
+ List childEles = new ArrayList();
+ for (int i = 0; i < nl.getLength(); i++) {
+ Node node = nl.item(i);
+ if (node instanceof Element && nodeNameMatch(node, childEleNameList)) {
+ childEles.add((Element) node);
+ }
+ }
+ return childEles;
+ }
+
+ /**
+ * Retrieve all child elements of the given DOM element that match
+ * the given element name. Only look at the direct child level of the
+ * given element; do not go into further depth (in contrast to the
+ * DOM API's getElementsByTagName method).
+ *
+ * @param element the DOM element to analyze
+ * @param childEleName the child element name to look for
+ * @return a List of child org.w3c.dom.Element instances
+ * @see org.w3c.dom.Element
+ * @see org.w3c.dom.Element#getElementsByTagName
+ */
+ public static List getChildElementsByTagName(final Element element, final String childEleName) {
+ return getChildElementsByTagName(element, new String[] {childEleName});
+ }
+
+ /**
+ * Returns the first child element identified by its name.
+ *
+ * @param element the DOM element to analyze
+ * @param childElementName the child element name to look for
+ * @return the org.w3c.dom.Element instance,
+ * or null if none found
+ */
+ public static Element getChildElementByTagName(final Element element, final String childElementName) {
+ Assert.notNull(element, "Element must not be null");
+ Assert.notNull(childElementName, "Element name must not be null");
+ NodeList nl = element.getChildNodes();
+ for (int i = 0; i < nl.getLength(); i++) {
+ Node node = nl.item(i);
+ if (node instanceof Element && nodeNameMatch(node, childElementName)) {
+ return (Element) node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first child element value identified by its name.
+ *
+ * @param element the DOM element to analyze
+ * @param childElementName the child element name to look for
+ * @return the extracted text value,
+ * or null if no child element found
+ */
+ public static String getChildElementValueByTagName(final Element element, final String childElementName) {
+ Element child = getChildElementByTagName(element, childElementName);
+ return (child != null ? getTextValue(child) : null);
+ }
+
+ /**
+ * Extract the text value from the given DOM element, ignoring XML comments.
+ * Appends all CharacterData nodes and EntityReference nodes
+ * into a single String value, excluding Comment nodes.
+ *
+ * @see CharacterData
+ * @see EntityReference
+ * @see Comment
+ */
+ public static String getTextValue(final Element valueElement) {
+ Assert.notNull(valueElement, "Element must not be null");
+ StringBuilder sb = new StringBuilder();
+ NodeList nl = valueElement.getChildNodes();
+ for (int i = 0; i < nl.getLength(); i++) {
+ Node item = nl.item(i);
+ if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) {
+ sb.append(item.getNodeValue());
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Namespace-aware equals comparison. Returns true if either
+ * {@link Node#getLocalName} or {@link Node#getNodeName} equals desiredName,
+ * otherwise returns false.
+ *
+ * @param node (required)
+ * @param desiredName (required)
+ * @return
+ */
+ public static boolean nodeNameEquals(final Node node, final String desiredName) {
+ Assert.notNull(node, "Node must not be null");
+ Assert.notNull(desiredName, "Desired name must not be null");
+ return nodeNameMatch(node, desiredName);
+ }
+
+ /**
+ * Matches the given node's name and local name against the given desired name.
+ *
+ * @param node
+ * @param desiredName
+ * @return
+ */
+ private static boolean nodeNameMatch(final Node node, final String desiredName) {
+ return (desiredName.equals(node.getNodeName()) || desiredName.equals(node.getLocalName()));
+ }
+
+ /**
+ * Matches the given node's name and local name against the given desired names.
+ *
+ * @param node
+ * @param desiredNames
+ * @return
+ */
+ private static boolean nodeNameMatch(final Node node, final Collection> desiredNames) {
+ return (desiredNames.contains(node.getNodeName()) || desiredNames.contains(node.getLocalName()));
+ }
+
+ /**
+ * Removes empty text nodes from the specified node.
+ *
+ * @param node the element where empty text nodes will be removed
+ */
+ public static void removeTextNodes(final Node node) {
+ if (node == null) {
+ return;
+ }
+
+ final NodeList children = node.getChildNodes();
+ for (int i = children.getLength() - 1; i >= 0; i--) {
+ final Node child = children.item(i);
+ switch (child.getNodeType()) {
+ case Node.ELEMENT_NODE:
+ removeTextNodes(child);
+ break;
+ case Node.CDATA_SECTION_NODE:
+ case Node.TEXT_NODE:
+ if (StringUtils.isBlank(child.getNodeValue())) {
+ node.removeChild(child);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns the text content of the given {@link Node}, null safe.
+ *
+ * @param node can be null
+ * @param defaultValue the value to return if the node is null
+ * @return the given default value if the node is null
+ * @see Node#getTextContent()
+ * @since 1.2.0
+ */
+ public static String getTextContent(final Node node, final String defaultValue) {
+ if (node == null) {
+ return defaultValue;
+ }
+ return node.getTextContent();
+ }
+
+ /**
+ * Creates a child element with the given name and parent. Avoids the type
+ * of bug whereby the developer calls {@link Document#createElement(String)}
+ * but forgets to append it to the relevant parent.
+ *
+ * @param tagName the name of the new child (required)
+ * @param parent the parent node (required)
+ * @param document the document to which the parent and child belong (required)
+ * @return the created element
+ * @since 1.2.0
+ */
+ public static Element createChildElement(final String tagName, final Node parent, final Document document) {
+ final Element child = document.createElement(tagName);
+ parent.appendChild(child);
+ return child;
+ }
+
+ /**
+ * Returns the child node with the given tag name, creating it if it does
+ * not exist.
+ *
+ * @param tagName the child tag to look for and possibly create (required)
+ * @param parent the parent in which to look for the child (required)
+ * @param document the document containing the parent (required)
+ * @return the existing or created child (never null)
+ * @since 1.2.0
+ */
+ public static Element createChildIfNotExists(final String tagName, final Node parent, final Document document) {
+ final Element existingChild = XmlUtils.findFirstElement(tagName, parent);
+ if (existingChild != null) {
+ return existingChild;
+ }
+ // No such child; add it
+ return createChildElement(tagName, parent, document);
+ }
+
+ /**
+ * Returns the text content of the first child of the given parent that has
+ * the given tag name, if any.
+ *
+ * @param parent the parent in which to search (required)
+ * @param child the child name for which to search (required)
+ * @return null if there is no such child, otherwise the first
+ * such child's text content
+ */
+ public static String getChildTextContent(final Element parent, final String child) {
+ final List children = XmlUtils.findElements(child, parent);
+ if (children.isEmpty()) {
+ return null;
+ }
+ return getTextContent(children.get(0), null);
+ }
+
+ /**
+ * Checks in under a given root element whether it can find a child element
+ * which matches the name supplied. Returns {@link Element} if exists.
+ *
+ * @param name the Element name (required)
+ * @param root the parent DOM element (required)
+ * @return the Element if discovered
+ */
+ public static Element findFirstElementByName(final String name, final Element root) {
+ Assert.hasText(name, "Element name required");
+ Assert.notNull(root, "Root element required");
+ return (Element) root.getElementsByTagName(name).item(0);
+ }
+
+ /**
+ * Removes any elements matching the given XPath expression, relative to
+ * the given Element
+ *
+ * @param xPath the XPath of the element(s) to remove (can be blank)
+ * @param searchBase the element to which the XPath expression is relative
+ */
+ public static void removeElements(final String xPath, final Element searchBase) {
+ for (final Element elementToDelete : XmlUtils.findElements(xPath, searchBase)) {
+ final Node parentNode = elementToDelete.getParentNode();
+ parentNode.removeChild(elementToDelete);
+ removeTextNodes(parentNode);
+ }
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private DomUtils() {}
+}
diff --git a/src/main/java/org/springframework/roo/support/util/ExceptionUtils.java b/src/main/java/org/springframework/roo/support/util/ExceptionUtils.java
new file mode 100644
index 00000000..49470753
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/ExceptionUtils.java
@@ -0,0 +1,25 @@
+package org.springframework.roo.support.util;
+
+/**
+ * Methods for working with exceptions.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public abstract class ExceptionUtils {
+
+ /**
+ * Obtains the root cause of an exception, if available.
+ *
+ * @param ex to extract the root cause from (required)
+ * @return the root cause, or original exception is unavailable (guaranteed to never be null)
+ */
+ public final static Throwable extractRootCause(final Throwable ex) {
+ Assert.notNull(ex, "An exception is required");
+ Throwable root = ex;
+ if (ex.getCause() != null) {
+ root = ex.getCause();
+ }
+ return root;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/FileCopyUtils.java b/src/main/java/org/springframework/roo/support/util/FileCopyUtils.java
new file mode 100644
index 00000000..d4c1bc0a
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/FileCopyUtils.java
@@ -0,0 +1,231 @@
+package org.springframework.roo.support.util;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+/**
+ * Simple utility methods for file and stream copying. All copy methods use a block size of 4096 bytes, and close all affected streams when done.
+ *
+ *
+ * Mainly for use within the framework, but also useful for application code.
+ *
+ * @author Juergen Hoeller
+ * @since 1.0
+ */
+public final class FileCopyUtils {
+
+ public static final int BUFFER_SIZE = 4096;
+
+ // ---------------------------------------------------------------------
+ // Copy methods for java.io.File
+ // ---------------------------------------------------------------------
+
+ /**
+ * Copy the contents of the given input File to the given output File.
+ *
+ * @param in the file to copy from
+ * @param out the file to copy to
+ * @return the number of bytes copied
+ * @throws IOException in case of I/O errors
+ */
+ public static int copy(final File in, final File out) throws IOException {
+ Assert.notNull(in, "No input File specified");
+ Assert.notNull(out, "No output File specified");
+ return copy(new BufferedInputStream(new FileInputStream(in)), new BufferedOutputStream(new FileOutputStream(out)));
+ }
+
+ /**
+ * Copy the contents of the given byte array to the given output File.
+ *
+ * @param bytes the byte array to copy from
+ * @param file the file to copy to
+ * @throws IOException in case of I/O errors
+ */
+ public static void copy(final byte[] bytes, final File file) throws IOException {
+ Assert.notNull(bytes, "No input byte array specified");
+ Assert.notNull(file, "No output File specified");
+ ByteArrayInputStream in = new ByteArrayInputStream(bytes);
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
+ copy(in, out);
+ }
+
+ /**
+ * Copy the contents of the given input File into a new byte array.
+ *
+ * @param in the file to copy from
+ * @return the new byte array that has been copied to
+ * @throws IOException in case of I/O errors
+ */
+ public static byte[] copyToByteArray(final File in) throws IOException {
+ Assert.notNull(in, "No input File specified");
+ return copyToByteArray(new BufferedInputStream(new FileInputStream(in)));
+ }
+
+ // ---------------------------------------------------------------------
+ // Copy methods for java.io.InputStream / java.io.OutputStream
+ // ---------------------------------------------------------------------
+
+ /**
+ * Copy the contents of the given InputStream to the given OutputStream. Closes both streams when done.
+ *
+ * @param in the stream to copy from
+ * @param out the stream to copy to
+ * @return the number of bytes copied
+ * @throws IOException in case of I/O errors
+ */
+ public static int copy(InputStream in, OutputStream out) throws IOException {
+ Assert.notNull(in, "No InputStream specified");
+ Assert.notNull(out, "No OutputStream specified");
+ if (!(in instanceof BufferedInputStream)) {
+ in = new BufferedInputStream(in);
+ }
+ if (!(out instanceof BufferedOutputStream)) {
+ out = new BufferedOutputStream(out);
+ }
+ try {
+ int byteCount = 0;
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bytesRead = -1;
+ while ((bytesRead = in.read(buffer)) != -1) {
+ out.write(buffer, 0, bytesRead);
+ byteCount += bytesRead;
+ }
+ out.flush();
+ return byteCount;
+ } finally {
+ IOUtils.closeQuietly(in, out);
+ }
+ }
+
+ /**
+ * Copy the contents of the given byte array to the given OutputStream. Closes the stream when done.
+ *
+ * @param bytes the byte array to copy from
+ * @param out the OutputStream to copy to
+ * @throws IOException in case of I/O errors
+ */
+ public static void copy(final byte[] bytes, final OutputStream out) throws IOException {
+ Assert.notNull(bytes, "No input byte array specified");
+ Assert.notNull(out, "No OutputStream specified");
+ try {
+ out.write(bytes);
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /**
+ * Copy the contents of the given InputStream into a new byte array. Closes the stream when done.
+ *
+ * @param in the stream to copy from
+ * @return the new byte array that has been copied to
+ * @throws IOException in case of I/O errors
+ */
+ public static byte[] copyToByteArray(final InputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE);
+ copy(in, out);
+ return out.toByteArray();
+ }
+
+ // ---------------------------------------------------------------------
+ // Copy methods for java.io.Reader / java.io.Writer
+ // ---------------------------------------------------------------------
+
+ /**
+ * Copy the contents of the given Reader to the given Writer. Closes both when done.
+ *
+ * @param in the Reader to copy from
+ * @param out the Writer to copy to
+ * @return the number of characters copied
+ * @throws IOException in case of I/O errors
+ */
+ public static int copy(Reader in, Writer out) throws IOException {
+ Assert.notNull(in, "No Reader specified");
+ Assert.notNull(out, "No Writer specified");
+ if (!(in instanceof BufferedReader)) {
+ in = new BufferedReader(in);
+ }
+ if (!(out instanceof BufferedWriter)) {
+ out = new BufferedWriter(out);
+ }
+ try {
+ int byteCount = 0;
+ char[] buffer = new char[BUFFER_SIZE];
+ int bytesRead = -1;
+ while ((bytesRead = in.read(buffer)) != -1) {
+ out.write(buffer, 0, bytesRead);
+ byteCount += bytesRead;
+ }
+ out.flush();
+ return byteCount;
+ } finally {
+ IOUtils.closeQuietly(in, out);
+ }
+ }
+
+ /**
+ * Copy the contents of the given String to the given output Writer. Closes the writer when done.
+ *
+ * @param in the String to copy from
+ * @param out the Writer to copy to
+ * @throws IOException in case of I/O errors
+ */
+ public static void copy(final String in, Writer out) throws IOException {
+ Assert.notNull(in, "No input String specified");
+ Assert.notNull(out, "No Writer specified");
+ if (!(out instanceof BufferedWriter)) {
+ out = new BufferedWriter(out);
+ }
+ try {
+ out.write(in);
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /**
+ * Copy the contents of the given Reader into a String. Closes the reader when done.
+ *
+ * @param in the reader to copy from
+ * @return the String that has been copied to
+ * @throws IOException in case of I/O errors
+ */
+ public static String copyToString(final Reader in) throws IOException {
+ StringWriter out = new StringWriter();
+ copy(in, out);
+ return out.toString();
+ }
+
+ /**
+ * Returns the contents of the given File as a String.
+ *
+ * Consider using {@link FileUtils#read(File)} instead if any
+ * {@link IOException}s would be unrecoverable.
+ *
+ * @param file the file to read from
+ * @return the contents
+ * @throws IOException in case of I/O errors
+ */
+ public static String copyToString(final File file) throws IOException {
+ return copyToString(new BufferedReader(new FileReader(file)));
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private FileCopyUtils() {}
+}
diff --git a/src/main/java/org/springframework/roo/support/util/FileUtils.java b/src/main/java/org/springframework/roo/support/util/FileUtils.java
new file mode 100644
index 00000000..e7966223
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/FileUtils.java
@@ -0,0 +1,357 @@
+package org.springframework.roo.support.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Pattern;
+
+import org.springframework.roo.support.ant.AntPathMatcher;
+import org.springframework.roo.support.ant.PathMatcher;
+
+/**
+ * Utilities for handling {@link File} instances.
+ *
+ * @author Ben Alex
+ * @since 1.0
+ */
+public final class FileUtils {
+
+ // Constants
+ private static final String BACKSLASH = "\\";
+ private static final String ESCAPED_BACKSLASH = "\\\\";
+
+ /**
+ * The relative file path to the current directory. Should be valid on all
+ * platforms that Roo supports.
+ */
+ public static final String CURRENT_DIRECTORY = ".";
+
+ private static final String WINDOWS_DRIVE_PREFIX = "^[A-Za-z]:";
+
+ // Doesn't check for backslash after the colon, since Java has no issues with paths like c:/Windows
+ private static final Pattern WINDOWS_DRIVE_PATH = Pattern.compile(WINDOWS_DRIVE_PREFIX + ".*");
+
+ private static final PathMatcher PATH_MATCHER;
+
+ static {
+ PATH_MATCHER = new AntPathMatcher();
+ ((AntPathMatcher) PATH_MATCHER).setPathSeparator(File.separator);
+ }
+
+ /**
+ * Deletes the specified {@link File}.
+ *
+ *
+ * If the {@link File} refers to a directory, any contents of that directory (including other directories)
+ * are also deleted.
+ *
+ *
+ * If the {@link File} does not already exist, this method immediately returns true.
+ *
+ * @param file to delete (required; the file may or may not exist)
+ * @return true if the file is fully deleted, or false if there was a failure when deleting
+ */
+ public static boolean deleteRecursively(final File file) {
+ Assert.notNull(file, "File to delete required");
+ if (!file.exists()) {
+ return true;
+ }
+ if (file.isDirectory()) {
+ for (File f : file.listFiles()) {
+ if (!deleteRecursively(f)) {
+ return false;
+ }
+ }
+ }
+ file.delete();
+ return true;
+ }
+
+ /**
+ * Copies the specified source directory to the destination.
+ *
+ *
+ * Both the source must exist. If the destination does not already exist, it will be created. If the destination
+ * does exist, it must be a directory (not a file).
+ *
+ * @param source the already-existing source directory (required)
+ * @param destination the destination directory (required)
+ * @param deleteDestinationOnExit indicates whether to mark any created destinations for deletion on exit
+ * @return true if the copy was successful
+ */
+ public static boolean copyRecursively(final File source, final File destination, final boolean deleteDestinationOnExit) {
+ Assert.notNull(source, "Source directory required");
+ Assert.notNull(destination, "Destination directory required");
+ Assert.isTrue(source.exists(), "Source directory '" + source + "' must exist");
+ Assert.isTrue(source.isDirectory(), "Source directory '" + source + "' must be a directory");
+ if (destination.exists()) {
+ Assert.isTrue(destination.isDirectory(), "Destination directory '" + destination + "' must be a directory");
+ } else {
+ destination.mkdirs();
+ if (deleteDestinationOnExit) {
+ destination.deleteOnExit();
+ }
+ }
+ for (File s : source.listFiles()) {
+ File d = new File(destination, s.getName());
+ if (deleteDestinationOnExit) {
+ d.deleteOnExit();
+ }
+ if (s.isFile()) {
+ try {
+ FileCopyUtils.copy(s, d);
+ } catch (IOException ioe) {
+ return false;
+ }
+ } else {
+ // It's a sub-directory, so copy it
+ d.mkdir();
+ if (!copyRecursively(s, d, deleteDestinationOnExit)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the provided fileName denotes an absolute path on the file system.
+ * On Windows, this includes both paths with and without drive letters, where the latter have to start with '\'.
+ * No check is performed to see if the file actually exists!
+ *
+ * @param fileName name of a file, which could be an absolute path
+ * @return true if the fileName looks like an absolute path for the current OS
+ */
+ public static boolean denotesAbsolutePath(final String fileName) {
+ if (OsUtils.isWindows()) {
+ // first check for drive letter
+ if (WINDOWS_DRIVE_PATH.matcher(fileName).matches()) {
+ return true;
+ }
+ }
+ return fileName.startsWith(File.separator);
+ }
+
+ /**
+ * Returns the part of the given path that represents a directory, in other
+ * words the given path if it's already a directory, or the parent directory
+ * if it's a file.
+ *
+ * @param fileIdentifier the path to parse (required)
+ * @return see above
+ * @since 1.2.0
+ */
+ public static String getFirstDirectory(String fileIdentifier) {
+ fileIdentifier = removeTrailingSeparator(fileIdentifier);
+ if (new File(fileIdentifier).isDirectory()) {
+ return fileIdentifier;
+ }
+ return backOneDirectory(fileIdentifier);
+ }
+
+ /**
+ * Returns the given file system path minus its last element
+ *
+ * @param fileIdentifier
+ * @return
+ * @since 1.2.0
+ */
+ public static String backOneDirectory(String fileIdentifier) {
+ fileIdentifier = removeTrailingSeparator(fileIdentifier);
+ fileIdentifier = fileIdentifier.substring(0, fileIdentifier.lastIndexOf(File.separator));
+ return removeTrailingSeparator(fileIdentifier);
+ }
+
+ /**
+ * Removes any trailing {@link File#separator}s from the given path
+ *
+ * @param path the path to modify (can be null)
+ * @return the modified path
+ * @since 1.2.0
+ */
+ public static String removeTrailingSeparator(String path) {
+ while (path != null && path.endsWith(File.separator)) {
+ path = StringUtils.removeSuffix(path, File.separator);
+ }
+ return path;
+ }
+
+ /**
+ * Indicates whether the given canonical path matches the given Ant-style pattern
+ *
+ * @param antPattern the pattern to check against (can't be blank)
+ * @param canonicalPath the path to check (can't be blank)
+ * @return see above
+ * @since 1.2.0
+ */
+ public static boolean matchesAntPath(final String antPattern, final String canonicalPath) {
+ Assert.hasText(antPattern, "Ant pattern required");
+ Assert.hasText(canonicalPath, "Canonical path required");
+ return PATH_MATCHER.match(antPattern, canonicalPath);
+ }
+
+ /**
+ * Removes any leading or trailing {@link File#separator}s from the given path.
+ *
+ * @param path the path to modify (can be null)
+ * @return the path, modified as above, or null if null was given
+ * @since 1.2.0
+ */
+ public static String removeLeadingAndTrailingSeparators(String path) {
+ if (StringUtils.isBlank(path)) {
+ return path;
+ }
+ while (path.endsWith(File.separator)) {
+ path = StringUtils.removeSuffix(path, File.separator);
+ }
+ while (path.startsWith(File.separator)) {
+ path = StringUtils.removePrefix(path, File.separator);
+ }
+ return path;
+ }
+
+ /**
+ * Ensures that the given path has exactly one trailing {@link File#separator}
+ *
+ * @param path the path to modify (can't be null)
+ * @return the normalised path
+ * @since 1.2.0
+ */
+ public static String ensureTrailingSeparator(final String path) {
+ Assert.notNull(path);
+ return removeTrailingSeparator(path) + File.separatorChar;
+ }
+
+ /**
+ * Returns an operating-system-dependent path consisting of the given
+ * elements, separated by {@link File#separator}.
+ *
+ * @param pathElements the path elements from uppermost downwards (can't be empty)
+ * @return a non-blank string
+ * @since 1.2.0
+ */
+ public static String getSystemDependentPath(final String... pathElements) {
+ return getSystemDependentPath(Arrays.asList(pathElements));
+ }
+
+ /**
+ * Returns an operating-system-dependent path consisting of the given
+ * elements, separated by {@link File#separator}.
+ *
+ * @param pathElements the path elements from uppermost downwards (can't be empty)
+ * @return a non-blank string
+ * @since 1.2.0
+ */
+ public static String getSystemDependentPath(final Collection pathElements) {
+ Assert.notEmpty(pathElements);
+ return StringUtils.collectionToDelimitedString(pathElements, File.separator);
+ }
+
+ /**
+ * Returns the canonical path of the given {@link File}.
+ *
+ * @param file the file for which to find the canonical path (can be null)
+ * @return the canonical path, or null if a null file is given
+ * @since 1.2.0
+ */
+ public static String getCanonicalPath(final File file) {
+ if (file == null) {
+ return null;
+ }
+ try {
+ return file.getCanonicalPath();
+ } catch (final IOException ioe) {
+ throw new IllegalStateException("Cannot determine canonical path for '" + file + "'", ioe);
+ }
+ }
+
+ /**
+ * Returns the platform-specific file separator as a regular expression.
+ *
+ * @return a non-blank regex
+ * @since 1.2.0
+ */
+ public static String getFileSeparatorAsRegex() {
+ final String fileSeparator = File.separator;
+ if (fileSeparator.contains(BACKSLASH)) {
+ // Escape the backslashes
+ return fileSeparator.replace(BACKSLASH, ESCAPED_BACKSLASH);
+ }
+ return fileSeparator;
+ }
+
+ /**
+ * Determines the path to the requested file, relative to the given class.
+ *
+ * @param loadingClass the class to whose package the given file is relative (required)
+ * @param relativeFilename the name of the file relative to that package (required)
+ * @return the full classloader-specific path to the file (never null)
+ * @since 1.2.0
+ */
+ public static String getPath(final Class> loadingClass, final String relativeFilename) {
+ Assert.notNull(loadingClass, "Loading class required");
+ Assert.hasText(relativeFilename, "Filename required");
+ Assert.isTrue(!relativeFilename.startsWith("/"), "Filename shouldn't start with a slash");
+ // Slashes instead of File.separatorChar is correct here, as these are classloader paths (not file system paths)
+ return "/" + loadingClass.getPackage().getName().replace('.', '/') + "/" + relativeFilename;
+ }
+
+ /**
+ * Loads the given file from the classpath.
+ *
+ * @param loadingClass the class from whose package to load the file (required)
+ * @param filename the name of the file to load, relative to that package (required)
+ * @return the file's input stream (never null)
+ * @throws IllegalArgumentException if the given file cannot be found
+ */
+ public static File getFile(final Class> loadingClass, final String filename) {
+ final URL url = loadingClass.getResource(filename);
+ Assert.notNull(url, "Could not locate '" + filename + "' in classpath of " + loadingClass.getName());
+ try {
+ return new File(url.toURI());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Loads the given file from the classpath.
+ *
+ * @param loadingClass the class from whose package to load the file (required)
+ * @param filename the name of the file to load, relative to that package (required)
+ * @return the file's input stream (never null)
+ * @throws IllegalArgumentException if the given file cannot be found
+ */
+ public static InputStream getInputStream(final Class> loadingClass, final String filename) {
+ final InputStream inputStream = loadingClass.getResourceAsStream(filename);
+ Assert.notNull(inputStream, "Could not locate '" + filename + "' in classpath of " + loadingClass.getName());
+ return inputStream;
+ }
+
+ /**
+ * Returns the contents of the given File as a String.
+ *
+ * @param file the file to read from (must be an existing file)
+ * @return the contents
+ * @throws IllegalStateException in case of I/O errors
+ * @since 1.2.0
+ */
+ public static String read(final File file) {
+ try {
+ return FileCopyUtils.copyToString(file);
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ *
+ * @since 1.2.0
+ */
+ private FileUtils() {}
+}
diff --git a/src/main/java/org/springframework/roo/support/util/Filter.java b/src/main/java/org/springframework/roo/support/util/Filter.java
new file mode 100644
index 00000000..05de67c1
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/Filter.java
@@ -0,0 +1,19 @@
+package org.springframework.roo.support.util;
+
+/**
+ * Allows filtering of objects of type T.
+ *
+ * @author Andrew Swan
+ * @since 1.2.0
+ * @param the type of object to be filtered
+ */
+public interface Filter {
+
+ /**
+ * Indicates whether to include the given instance in the filtered results
+ *
+ * @param type the type to evaluate; can be null
+ * @return false to exclude the given type
+ */
+ boolean include(T instance);
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/roo/support/util/HexUtils.java b/src/main/java/org/springframework/roo/support/util/HexUtils.java
new file mode 100644
index 00000000..a8def105
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/HexUtils.java
@@ -0,0 +1,41 @@
+package org.springframework.roo.support.util;
+
+/**
+ * Encodes a given byte array as hex.
+ *
+ *
+ * Most methods in this class were obtained from the Spring Security class,
+ * org.springframework.security.core.codec.Hex. Spring Security is licensed
+ * under the Apache Software License version 2.0 and the following code is used
+ * pursuant to that license.
+ *
+ * @author Luke Taylor
+ * @author Ben Alex
+ * @since 1.1.1
+ */
+public abstract class HexUtils {
+
+ public static String toHex(final byte[] bytes) {
+ return new String(encode(bytes));
+ }
+
+ private static final char[] HEX = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ public static char[] encode(final byte[] bytes) {
+ final int nBytes = bytes.length;
+ char[] result = new char[2 * nBytes];
+ int j = 0;
+
+ for (int i = 0; i < nBytes; i++) {
+ // Char for top 4 bits
+ result[j++] = HEX[(0xF0 & bytes[i]) >>> 4];
+
+ // Bottom 4
+ result[j++] = HEX[(0x0F & bytes[i])];
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/IOUtils.java b/src/main/java/org/springframework/roo/support/util/IOUtils.java
new file mode 100644
index 00000000..1548f2a9
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/IOUtils.java
@@ -0,0 +1,58 @@
+package org.springframework.roo.support.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.zip.ZipFile;
+
+/**
+ * Static helper methods relating to I/O. Inspired by the eponymous class in
+ * Apache Commons I/O.
+ *
+ * @author Andrew Swan
+ * @since 1.2.0
+ */
+public final class IOUtils {
+
+ /**
+ * Quietly closes each of the given {@link Closeable}s, i.e. eats any
+ * {@link IOException}s arising.
+ *
+ * @param closeables the closeables to close (any of which can be
+ * null or already closed)
+ */
+ public static void closeQuietly(final Closeable... closeables) {
+ for (final Closeable closeable : closeables) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Quietly closes each of the given {@link ZipFile}s, i.e. eats any
+ * {@link IOException}s arising.
+ *
+ * @param zipFiles the zipFiles to close (any of which can be
+ * null or already closed)
+ */
+ public static void closeQuietly(final ZipFile... zipFiles) {
+ for (final ZipFile zipFile : zipFiles) {
+ if (zipFile != null) {
+ try {
+ zipFile.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private IOUtils() {}
+}
diff --git a/src/main/java/org/springframework/roo/support/util/MathUtils.java b/src/main/java/org/springframework/roo/support/util/MathUtils.java
new file mode 100644
index 00000000..af2ab44d
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/MathUtils.java
@@ -0,0 +1,16 @@
+package org.springframework.roo.support.util;
+
+/**
+ * A class which contains a number of number manipulation operations
+ *
+ * @author James Tyrrell
+ * @since 1.2.0
+ */
+public class MathUtils {
+
+ public static double round(final double valueToRound, final int numberOfDecimalPlaces) {
+ double multiplicationFactor = Math.pow(10, numberOfDecimalPlaces);
+ double interestedInZeroDPs = valueToRound * multiplicationFactor;
+ return Math.round(interestedInZeroDPs) / multiplicationFactor;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java b/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java
new file mode 100644
index 00000000..0f8d9f37
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java
@@ -0,0 +1,67 @@
+package org.springframework.roo.support.util;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.springframework.roo.support.logging.HandlerUtils;
+
+/**
+ * Retrieves text files from the classloader and displays them on-screen.
+ *
+ *
+ * Respects normal Roo conventions such as all resources should appear under the same
+ * package as the bundle itself etc.
+ *
+ * @author Ben Alex
+ * @since 1.1.1
+ */
+public abstract class MessageDisplayUtils {
+
+ // Constants
+ private static Logger LOGGER = HandlerUtils.getLogger(MessageDisplayUtils.class);
+
+ /**
+ * Displays the requested file via the LOGGER API.
+ *
+ *
+ * Each file must available from the classloader of the "owner". It must also be in the same
+ * package as the class of the "owner". So if the owner is com.foo.Bar, and the file is called
+ * "hello.txt", the file must appear in the same bundle as com.foo.Bar and be available from
+ * the resource path "/com/foo/Hello.txt".
+ *
+ * @param fileName the simple filename (required)
+ * @param owner the class which owns the file (required)
+ * @param important if true, it will display with a higher importance color where possible
+ */
+ public static void displayFile(final String fileName, final Class> owner, final boolean important) {
+ Level level = important ? Level.SEVERE : Level.FINE;
+ String owningPackage = owner.getPackage().getName().replace('.', '/');
+ String fullResourceName = "/" + owningPackage + "/" + fileName;
+ InputStream inputStream = owner.getClassLoader().getResourceAsStream(fullResourceName);
+ if (inputStream == null) {
+ throw new IllegalStateException("Could not locate '" + fileName + "'");
+ }
+ try {
+ String message = FileCopyUtils.copyToString(new InputStreamReader(new BufferedInputStream(inputStream)));
+ LOGGER.log(level, message);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ } finally {
+ IOUtils.closeQuietly(inputStream);
+ }
+ }
+
+ /**
+ * Same as {@link #displayFile(String, Class, boolean)} except it passes false as the
+ * final argument.
+ *
+ * @param fileName the simple filename (required)
+ * @param owner the class which owns the file (required)
+ */
+ public static void displayFile(final String fileName, final Class> owner) {
+ displayFile(fileName, owner, false);
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/NumberUtils.java b/src/main/java/org/springframework/roo/support/util/NumberUtils.java
new file mode 100644
index 00000000..fa7d9363
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/NumberUtils.java
@@ -0,0 +1,81 @@
+package org.springframework.roo.support.util;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * Provides extra functionality for Java Number classes.
+ *
+ * @author Alan Stewart
+ * @since 1.2.0
+ */
+public final class NumberUtils {
+
+ /**
+ * Returns the minimum value in the array.
+ *
+ * @param array an array of Numbers (can be null)
+ * @return the minimum value in the array, or null if all the elements are null
+ */
+ public static BigDecimal min(final Number... array) {
+ return minOrMax(true, array);
+ }
+
+ /**
+ * Returns the maximum value in the array.
+ *
+ * @param array an array of Numbers (can be null)
+ * @return the maximum value in the array, or null if all the elements are null
+ */
+ public static BigDecimal max(final Number... array) {
+ return minOrMax(false, array);
+ }
+
+ /**
+ * Finds the minimum or maxiumum value contained in the given array,
+ * ignoring any null elements
+ *
+ * @param findMinimum false to get the maximum
+ * @param numbers can be null, empty, or contain null
+ * elements
+ * @return null if the array is null, empty, or
+ * all its elements are null
+ */
+ private static BigDecimal minOrMax(final boolean findMinimum, final Number... numbers) {
+ if (numbers == null || numbers.length == 0) {
+ return null;
+ }
+ BigDecimal extreme = null;
+ for (final Number number : numbers) {
+ if (number != null) {
+ final BigDecimal candidate = getBigDecimal(number);
+ if (extreme == null || (findMinimum ? candidate.compareTo(extreme) < 0 : candidate.compareTo(extreme) > 0)) {
+ // The non-null candidate is the new extreme
+ extreme = candidate;
+ }
+ }
+ }
+ return extreme;
+ }
+
+ /**
+ * Converts the given number to a {@link BigDecimal}
+ *
+ * @param number the number to convert (can be null)
+ * @return null if the given number was null
+ */
+ private static BigDecimal getBigDecimal(final Number number) {
+ if (number == null || number instanceof BigDecimal) {
+ return (BigDecimal) number;
+ }
+ if (number instanceof BigInteger) {
+ return new BigDecimal((BigInteger) number);
+ }
+ return new BigDecimal(number.toString());
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private NumberUtils() {}
+}
diff --git a/src/main/java/org/springframework/roo/support/util/ObjectUtils.java b/src/main/java/org/springframework/roo/support/util/ObjectUtils.java
new file mode 100644
index 00000000..ba39e969
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/ObjectUtils.java
@@ -0,0 +1,925 @@
+package org.springframework.roo.support.util;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+
+/**
+ * Miscellaneous object utility methods. Mainly for internal use within the
+ * framework; consider Jakarta's Commons Lang for a more comprehensive suite
+ * of object utilities.
+ *
+ * @author Juergen Hoeller
+ * @author Keith Donald
+ * @author Rod Johnson
+ * @author Rob Harrop
+ * @author Alex Ruiz
+ * @see org.apache.commons.lang.ObjectUtils
+ */
+public final class ObjectUtils {
+
+ // Constants
+ private static final int INITIAL_HASH = 7;
+ private static final int MULTIPLIER = 31;
+
+ private static final String EMPTY_STRING = "";
+ private static final String NULL_STRING = "null";
+ private static final String ARRAY_START = "{";
+ private static final String ARRAY_END = "}";
+ private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END;
+ private static final String ARRAY_ELEMENT_SEPARATOR = ", ";
+
+ /**
+ * Return whether the given throwable is a checked exception:
+ * that is, neither a RuntimeException nor an Error.
+ *
+ * @param ex the throwable to check
+ * @return whether the throwable is a checked exception
+ * @see java.lang.Exception
+ * @see java.lang.RuntimeException
+ * @see java.lang.Error
+ */
+ public static boolean isCheckedException(final Throwable ex) {
+ return !(ex instanceof RuntimeException || ex instanceof Error);
+ }
+
+ /**
+ * Check whether the given exception is compatible with the exceptions
+ * declared in a throws clause.
+ *
+ * @param ex the exception to checked
+ * @param declaredExceptions the exceptions declared in the throws clause
+ * @return whether the given exception is compatible
+ */
+ public static boolean isCompatibleWithThrowsClause(final Throwable ex, final Class>... declaredExceptions) {
+ if (!isCheckedException(ex)) {
+ return true;
+ }
+ if (declaredExceptions != null) {
+ int i = 0;
+ while (i < declaredExceptions.length) {
+ if (declaredExceptions[i].isAssignableFrom(ex.getClass())) {
+ return true;
+ }
+ i++;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return whether the given array is empty: that is, null
+ * or of zero length.
+ *
+ * @param array the array to check
+ * @return whether the given array is empty
+ */
+ public static boolean isEmpty(final Object[] array) {
+ return array == null || array.length == 0;
+ }
+
+ /**
+ * Check whether the given array contains the given element.
+ *
+ * @param array the array to check (may be null,
+ * in which case the return value will always be false)
+ * @param element the element to check for
+ * @return whether the element has been found in the given array
+ */
+ public static boolean containsElement(final Object[] array, final Object element) {
+ if (array == null) {
+ return false;
+ }
+ for (Object arrayEle : array) {
+ if (nullSafeEquals(arrayEle, element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Append the given Object to the given array, returning a new array
+ * consisting of the input array contents plus the given Object.
+ *
+ * @param array the array to append to (can be null)
+ * @param obj the Object to append
+ * @return the new array (of the same component type; never null)
+ */
+ public static Object[] addObjectToArray(final Object[] array, final Object obj) {
+ Class> compType = Object.class;
+ if (array != null) {
+ compType = array.getClass().getComponentType();
+ }
+ else if (obj != null) {
+ compType = obj.getClass();
+ }
+ int newArrLength = (array != null ? array.length + 1 : 1);
+ Object[] newArr = (Object[]) Array.newInstance(compType, newArrLength);
+ if (array != null) {
+ System.arraycopy(array, 0, newArr, 0, array.length);
+ }
+ newArr[newArr.length - 1] = obj;
+ return newArr;
+ }
+
+ /**
+ * Convert the given array (which may be a primitive array) to an
+ * object array (if necessary of primitive wrapper objects).
+ *
A null source value will be converted to an
+ * empty Object array.
+ *
+ * @param source the (potentially primitive) array
+ * @return the corresponding object array (never null)
+ * @throws IllegalArgumentException if the parameter is not an array
+ */
+ public static Object[] toObjectArray(final Object source) {
+ if (source instanceof Object[]) {
+ return (Object[]) source;
+ }
+ if (source == null) {
+ return new Object[0];
+ }
+ if (!source.getClass().isArray()) {
+ throw new IllegalArgumentException("Source is not an array: " + source);
+ }
+ int length = Array.getLength(source);
+ if (length == 0) {
+ return new Object[0];
+ }
+ Class> wrapperType = Array.get(source, 0).getClass();
+ Object[] newArray = (Object[]) Array.newInstance(wrapperType, length);
+ for (int i = 0; i < length; i++) {
+ newArray[i] = Array.get(source, i);
+ }
+ return newArray;
+ }
+
+ //---------------------------------------------------------------------
+ // Convenience methods for content-based equality/hash-code handling
+ //---------------------------------------------------------------------
+
+ /**
+ * Determine if the given objects are equal, returning true
+ * if both are null or false if only one is
+ * null.
+ *
Compares arrays with Arrays.equals, performing an equality
+ * check based on the array elements rather than the array reference.
+ *
+ * @param o1 first Object to compare
+ * @param o2 second Object to compare
+ * @return whether the given objects are equal
+ * @see java.util.Arrays#equals
+ */
+ public static boolean nullSafeEquals(final Object o1, final Object o2) {
+ if (o1 == o2) {
+ return true;
+ }
+ if (o1 == null || o2 == null) {
+ return false;
+ }
+ if (o1.equals(o2)) {
+ return true;
+ }
+ if (o1.getClass().isArray() && o2.getClass().isArray()) {
+ if (o1 instanceof Object[] && o2 instanceof Object[]) {
+ return Arrays.equals((Object[]) o1, (Object[]) o2);
+ }
+ if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
+ return Arrays.equals((boolean[]) o1, (boolean[]) o2);
+ }
+ if (o1 instanceof byte[] && o2 instanceof byte[]) {
+ return Arrays.equals((byte[]) o1, (byte[]) o2);
+ }
+ if (o1 instanceof char[] && o2 instanceof char[]) {
+ return Arrays.equals((char[]) o1, (char[]) o2);
+ }
+ if (o1 instanceof double[] && o2 instanceof double[]) {
+ return Arrays.equals((double[]) o1, (double[]) o2);
+ }
+ if (o1 instanceof float[] && o2 instanceof float[]) {
+ return Arrays.equals((float[]) o1, (float[]) o2);
+ }
+ if (o1 instanceof int[] && o2 instanceof int[]) {
+ return Arrays.equals((int[]) o1, (int[]) o2);
+ }
+ if (o1 instanceof long[] && o2 instanceof long[]) {
+ return Arrays.equals((long[]) o1, (long[]) o2);
+ }
+ if (o1 instanceof short[] && o2 instanceof short[]) {
+ return Arrays.equals((short[]) o1, (short[]) o2);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return as hash code for the given object; typically the value of
+ * {@link Object#hashCode()}. If the object is an array,
+ * this method will delegate to any of the nullSafeHashCode
+ * methods for arrays in this class. If the object is null,
+ * this method returns 0.
+ *
+ * @see #nullSafeHashCode(Object[])
+ * @see #nullSafeHashCode(boolean[])
+ * @see #nullSafeHashCode(byte[])
+ * @see #nullSafeHashCode(char[])
+ * @see #nullSafeHashCode(double[])
+ * @see #nullSafeHashCode(float[])
+ * @see #nullSafeHashCode(int[])
+ * @see #nullSafeHashCode(long[])
+ * @see #nullSafeHashCode(short[])
+ */
+ public static int nullSafeHashCode(final Object obj) {
+ if (obj == null) {
+ return 0;
+ }
+ if (obj.getClass().isArray()) {
+ if (obj instanceof Object[]) {
+ return nullSafeHashCode((Object[]) obj);
+ }
+ if (obj instanceof boolean[]) {
+ return nullSafeHashCode((boolean[]) obj);
+ }
+ if (obj instanceof byte[]) {
+ return nullSafeHashCode((byte[]) obj);
+ }
+ if (obj instanceof char[]) {
+ return nullSafeHashCode((char[]) obj);
+ }
+ if (obj instanceof double[]) {
+ return nullSafeHashCode((double[]) obj);
+ }
+ if (obj instanceof float[]) {
+ return nullSafeHashCode((float[]) obj);
+ }
+ if (obj instanceof int[]) {
+ return nullSafeHashCode((int[]) obj);
+ }
+ if (obj instanceof long[]) {
+ return nullSafeHashCode((long[]) obj);
+ }
+ if (obj instanceof short[]) {
+ return nullSafeHashCode((short[]) obj);
+ }
+ }
+ return obj.hashCode();
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array the array from whose elements to calculate the hash code (can be null)
+ * @return 0 if the array is null
+ */
+ public static int nullSafeHashCode(final Object... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (final Object element : array) {
+ hash = MULTIPLIER * hash + nullSafeHashCode(element);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final boolean... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + hashCode(array[i]);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final byte... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + array[i];
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final char... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + array[i];
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final double... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + hashCode(array[i]);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final float... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + hashCode(array[i]);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final int... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + array[i];
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final long... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + hashCode(array[i]);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array.
+ *
+ * @param array can be null
+ * @return 0 if array is null
+ */
+ public static int nullSafeHashCode(final short... array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ int arraySize = array.length;
+ for (int i = 0; i < arraySize; i++) {
+ hash = MULTIPLIER * hash + array[i];
+ }
+ return hash;
+ }
+
+ /**
+ * Returns the hash code of the given boolean value.
+ *
+ * @param bool the boolean for which to return the hash code
+ * @return see {@link Boolean#hashCode()}
+ */
+ public static int hashCode(final boolean bool) {
+ return Boolean.valueOf(bool).hashCode();
+ }
+
+ /**
+ * Return the same value as {@link Double#hashCode()}.
+ *
+ * @see Double#hashCode()
+ */
+ public static int hashCode(final double dbl) {
+ long bits = Double.doubleToLongBits(dbl);
+ return hashCode(bits);
+ }
+
+ /**
+ * Return the same value as {@link Float#hashCode()}.
+ *
+ * @see Float#hashCode()
+ */
+ public static int hashCode(final float flt) {
+ return Float.floatToIntBits(flt);
+ }
+
+ /**
+ * Return the same value as {@link Long#hashCode()}.
+ *
+ * @see Long#hashCode()
+ */
+ public static int hashCode(final long lng) {
+ return (int) (lng ^ (lng >>> 32));
+ }
+
+ //---------------------------------------------------------------------
+ // Convenience methods for toString output
+ //---------------------------------------------------------------------
+
+ /**
+ * Return a String representation of an object's overall identity.
+ *
+ * @param obj the object (may be null)
+ * @return the object's identity as String representation,
+ * or an empty String if the object was null
+ */
+ public static String identityToString(final Object obj) {
+ if (obj == null) {
+ return EMPTY_STRING;
+ }
+ return obj.getClass().getName() + "@" + getIdentityHexString(obj);
+ }
+
+ /**
+ * Return a hex String form of an object's identity hash code.
+ *
+ * @param obj the object
+ * @return the object's identity code in hex notation
+ */
+ public static String getIdentityHexString(final Object obj) {
+ return Integer.toHexString(System.identityHashCode(obj));
+ }
+
+ /**
+ * Return a content-based String representation if obj is
+ * not null; otherwise returns an empty String.
+ *
Differs from {@link #nullSafeToString(Object)} in that it returns
+ * an empty String rather than "null" for a null value.
+ *
+ * @param obj the object to build a display String for
+ * @return a display String representation of obj
+ * @see #nullSafeToString(Object)
+ */
+ public static String getDisplayString(final Object obj) {
+ if (obj == null) {
+ return EMPTY_STRING;
+ }
+ return nullSafeToString(obj);
+ }
+
+ /**
+ * Determine the class name for the given object.
+ *
Returns "null" if obj is null.
+ *
+ * @param obj the object to introspect (may be null)
+ * @return the corresponding class name
+ */
+ public static String nullSafeClassName(final Object obj) {
+ return (obj != null ? obj.getClass().getName() : NULL_STRING);
+ }
+
+ /**
+ * Return a String representation of the specified Object.
+ *
Builds a String representation of the contents in case of an array.
+ * Returns "null" if obj is null.
+ *
+ * @param obj the object to build a String representation for
+ * @return a String representation of obj
+ */
+ public static String nullSafeToString(final Object obj) {
+ if (obj == null) {
+ return NULL_STRING;
+ }
+ if (obj instanceof String) {
+ return (String) obj;
+ }
+ if (obj instanceof Object[]) {
+ return nullSafeToString((Object[]) obj);
+ }
+ if (obj instanceof boolean[]) {
+ return nullSafeToString((boolean[]) obj);
+ }
+ if (obj instanceof byte[]) {
+ return nullSafeToString((byte[]) obj);
+ }
+ if (obj instanceof char[]) {
+ return nullSafeToString((char[]) obj);
+ }
+ if (obj instanceof double[]) {
+ return nullSafeToString((double[]) obj);
+ }
+ if (obj instanceof float[]) {
+ return nullSafeToString((float[]) obj);
+ }
+ if (obj instanceof int[]) {
+ return nullSafeToString((int[]) obj);
+ }
+ if (obj instanceof long[]) {
+ return nullSafeToString((long[]) obj);
+ }
+ if (obj instanceof short[]) {
+ return nullSafeToString((short[]) obj);
+ }
+ String str = obj.toString();
+ return (str != null ? str : EMPTY_STRING);
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final Object... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+ sb.append(String.valueOf(array[i]));
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final boolean... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final byte... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final char... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+ sb.append("'").append(array[i]).append("'");
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final double... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final float... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final int... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final long... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Return a String representation of the contents of the specified array.
+ *
The String representation consists of a list of the array's elements,
+ * enclosed in curly braces ("{}"). Adjacent elements are separated
+ * by the characters ", " (a comma followed by a space). Returns
+ * "null" if array is null.
+ *
+ * @param array the array to build a String representation for
+ * @return a String representation of array
+ */
+ public static String nullSafeToString(final short... array) {
+ if (array == null) {
+ return NULL_STRING;
+ }
+ int length = array.length;
+ if (length == 0) {
+ return EMPTY_ARRAY;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i == 0) {
+ sb.append(ARRAY_START);
+ }
+ else {
+ sb.append(ARRAY_ELEMENT_SEPARATOR);
+ }
+ sb.append(array[i]);
+ }
+ sb.append(ARRAY_END);
+ return sb.toString();
+ }
+
+ /**
+ * Compares the two given objects, with null equivalent to
+ * null and null "less than" any
+ * non-null instance. Two non-null instances are
+ * compared using the first one's {@link Comparable#compareTo(Object)}
+ * method.
+ *
+ * @param the type of objects being compared
+ * @param one the first object being compared (can be null)
+ * @param other the second object being compared (can be null)
+ * @return see {@link Comparable#compareTo(Object)}
+ * @since 1.2.0
+ */
+ public static int nullSafeComparison(final Comparable one, final T other) {
+ if (one == null) {
+ if (other == null) {
+ return 0;
+ }
+ // First is null, second is not
+ return -1;
+ }
+ // If we get here, the first object is non-null
+ if (other == null) {
+ return 1;
+ }
+ return one.compareTo(other);
+ }
+
+ /**
+ * Returns the String representation of the given object, or if it's
+ * null, the given default string
+ *
+ * @param object the object to represent as a String (can be null)
+ * @param defaultValue the value to return if the given object is null (can itself be blank)
+ * @return see above
+ * @since 1.2.0
+ */
+ public static String toString(final Object object, final String defaultValue) {
+ if (object == null) {
+ return defaultValue;
+ }
+ return object.toString();
+ }
+
+ /**
+ * Returns the given object if not null, otherwise the given
+ * default value
+ *
+ * @param the type of object being defaulted
+ * @param object the object to check (can be null)
+ * @param defaultValue the default value (can be null)
+ * @return null iff both values are null
+ * @since 1.2.0
+ */
+ public static T defaultIfNull(final T object, final T defaultValue) {
+ if (object == null) {
+ return defaultValue;
+ }
+ return object;
+ }
+
+ /**
+ * Constructor is private to prevent instantiation
+ */
+ private ObjectUtils() {}
+}
diff --git a/src/main/java/org/springframework/roo/support/util/OsUtils.java b/src/main/java/org/springframework/roo/support/util/OsUtils.java
new file mode 100644
index 00000000..3da4cd38
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/OsUtils.java
@@ -0,0 +1,15 @@
+package org.springframework.roo.support.util;
+
+/**
+ * Utilities for handling OS-specific behavior.
+ *
+ * @author Joris Kuipers
+ * @since 1.1.1
+ */
+public class OsUtils {
+ private static final boolean WINDOWS_OS = System.getProperty("os.name").toLowerCase().contains("windows");
+
+ public static boolean isWindows() {
+ return WINDOWS_OS;
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/Pair.java b/src/main/java/org/springframework/roo/support/util/Pair.java
new file mode 100644
index 00000000..029cc524
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/Pair.java
@@ -0,0 +1,70 @@
+package org.springframework.roo.support.util;
+
+/**
+ * A pair with a key of type "K" and a value of type "V". Instances are immutable.
+ *
+ * @author Andrew Swan
+ * @since 1.2.0
+ * @param the key type
+ * @param the value type
+ */
+public class Pair {
+
+ // Fields
+ private final K key;
+ private final V value;
+
+ /**
+ * Constructor
+ *
+ * @param key can be null
+ * @param value can be null
+ */
+ public Pair(final K key, final V value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ /**
+ * Returns the key
+ *
+ * @return null if it is
+ */
+ public K getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the value
+ *
+ * @return null if it is
+ */
+ public V getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Pair)) {
+ return false;
+ }
+ final Pair, ?> otherPair = (Pair, ?>) obj;
+ return ObjectUtils.nullSafeEquals(key, otherPair.getKey()) && ObjectUtils.nullSafeEquals(value, otherPair.getValue());
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.nullSafeHashCode(new Object[] {getKey(), getValue()});
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("key: ").append(key);
+ sb.append(", value: ").append(value);
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/PairList.java b/src/main/java/org/springframework/roo/support/util/PairList.java
new file mode 100644
index 00000000..730f88da
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/PairList.java
@@ -0,0 +1,103 @@
+package org.springframework.roo.support.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A {@link List} of {@link Pair}s. Unlike a {@link java.util.Map}, it can have
+ * duplicate and/or null keys.
+ *
+ * @author Andrew Swan
+ * @since 1.2.0
+ * @param the type of key
+ * @param the type of value
+ */
+public class PairList extends ArrayList> {
+
+ // For serialisation
+ private static final long serialVersionUID = 5990417235907246300L;
+
+ /**
+ * Returns the given array of pairs as a modifiable list
+ *
+ * @param the type of key
+ * @param the type of value
+ * @param pairs the pairs to put in a list
+ * @return a non-null list
+ */
+ public PairList(final Pair... pairs) {
+ addAll(Arrays.asList(pairs));
+ }
+
+ /**
+ * Constructor for building a list of the given key-value pairs
+ *
+ * @param keys the keys (can be null)
+ * @param values the values (must be null if the keys are null, otherwise
+ * must be non-null and of the same size as the keys)
+ */
+ public PairList(final List extends K> keys, final List extends V> values) {
+ Assert.isTrue(!(keys == null ^ values == null), "Parameter types and names must either both be null or both be not null");
+ if (keys == null) {
+ Assert.isTrue(values == null, "Parameter names must be null if types are null");
+ }
+ else {
+ Assert.isTrue(values != null, "Parameter names are required if types are provided");
+ Assert.isTrue(keys.size() == values.size(), "Expected " + keys.size() + " values but found " + values.size());
+ for (int i = 0; i < keys.size(); i++) {
+ add(keys.get(i), values.get(i));
+ }
+ }
+ }
+
+ /**
+ * Constructor for an empty list of pairs
+ */
+ public PairList() {
+ // Empty
+ }
+
+ /**
+ * Returns the keys of each {@link Pair} in this list
+ *
+ * @return a non-null list
+ */
+ public List getKeys() {
+ final List keys = new ArrayList();
+ for (final Pair pair : this) {
+ keys.add(pair.getKey());
+ }
+ return keys;
+ }
+
+ /**
+ * Returns the values of each {@link Pair} in this list
+ *
+ * @return a non-null modifiable copy of this list
+ */
+ public List getValues() {
+ final List values = new ArrayList();
+ for (final Pair, V> pair : this) {
+ values.add(pair.getValue());
+ }
+ return values;
+ }
+
+ /**
+ * Adds a new pair to this list with the given key and value
+ *
+ * @param key the key to add; can be null
+ * @param value the value to add; can be null
+ * @return true (as specified by Collection.add(E))
+ */
+ public boolean add(final K key, final V value) {
+ return add(new Pair(key, value));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Pair[] toArray() {
+ return super.toArray(new Pair[size()]);
+ }
+}
diff --git a/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java b/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java
new file mode 100644
index 00000000..89caaa9d
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * 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 org.springframework.roo.support.util;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Simple utility class for working with the reflection API and handling
+ * reflection exceptions.
+ *
+ * Only intended for internal use.
+ *
+ * @author Juergen Hoeller
+ * @author Rob Harrop
+ * @author Rod Johnson
+ * @author Costin Leau
+ * @author Sam Brannen
+ * @since 1.2.2
+ */
+public abstract class ReflectionUtils {
+
+ /**
+ * Attempt to find a {@link Field field} on the supplied {@link Class} with
+ * the supplied name. Searches all superclasses up to {@link Object}.
+ * @param clazz the class to introspect
+ * @param name the name of the field
+ * @return the corresponding Field object, or null if not found
+ */
+ public static Field findField(final Class> clazz, final String name) {
+ return findField(clazz, name, null);
+ }
+
+ /**
+ * Attempt to find a {@link Field field} on the supplied {@link Class} with
+ * the supplied name and/or {@link Class type}. Searches all
+ * superclasses up to {@link Object}.
+ * @param clazz the class to introspect
+ * @param name the name of the field (may be null if type is specified)
+ * @param type the type of the field (may be null if name is specified)
+ * @return the corresponding Field object, or null if not found
+ */
+ public static Field findField(final Class> clazz, final String name, final Class> type) {
+ Assert.notNull(clazz, "Class must not be null");
+ Assert.isTrue(name != null || type != null, "Either name or type of the field must be specified");
+ Class> searchType = clazz;
+ while (!Object.class.equals(searchType) && searchType != null) {
+ Field[] fields = searchType.getDeclaredFields();
+ for (Field field : fields) {
+ if ((name == null || name.equals(field.getName())) &&
+ (type == null || type.equals(field.getType()))) {
+ return field;
+ }
+ }
+ searchType = searchType.getSuperclass();
+ }
+ return null;
+ }
+
+ /**
+ * Set the field represented by the supplied {@link Field field object} on
+ * the specified {@link Object target object} to the specified
+ * value. In accordance with
+ * {@link Field#set(Object, Object)} semantics, the new value is
+ * automatically unwrapped if the underlying field has a primitive type.
+ *
Thrown exceptions are handled via a call to
+ * {@link #handleReflectionException(Exception)}.
+ * @param field the field to set
+ * @param target the target object on which to set the field
+ * @param value the value to set; may be null
+ */
+ public static void setField(final Field field, final Object target, final Object value) {
+ try {
+ field.set(target, value);
+ }
+ catch (IllegalAccessException ex) {
+ handleReflectionException(ex);
+ throw new IllegalStateException(
+ "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Get the field represented by the supplied {@link Field field object} on
+ * the specified {@link Object target object}. In accordance with
+ * {@link Field#get(Object)} semantics, the returned value is
+ * automatically wrapped if the underlying field has a primitive type.
+ *
Thrown exceptions are handled via a call to
+ * {@link #handleReflectionException(Exception)}.
+ * @param field the field to get
+ * @param target the target object from which to get the field
+ * @return the field's current value
+ */
+ public static Object getField(final Field field, final Object target) {
+ try {
+ return field.get(target);
+ }
+ catch (IllegalAccessException ex) {
+ handleReflectionException(ex);
+ throw new IllegalStateException(
+ "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Attempt to find a {@link Method} on the supplied class with the supplied name
+ * and no parameters. Searches all superclasses up to Object.
+ *
Returns null if no {@link Method} can be found.
+ * @param clazz the class to introspect
+ * @param name the name of the method
+ * @return the Method object, or null if none found
+ */
+ public static Method findMethod(final Class> clazz, final String name) {
+ return findMethod(clazz, name, new Class[0]);
+ }
+
+ /**
+ * Attempt to find a {@link Method} on the supplied class with the supplied name
+ * and parameter types. Searches all superclasses up to Object.
+ *
Returns null if no {@link Method} can be found.
+ * @param clazz the class to introspect
+ * @param name the name of the method
+ * @param parameterTypes the parameter types of the method
+ * (may be null to indicate any signature)
+ * @return the Method object, or null if none found
+ */
+ public static Method findMethod(final Class> clazz, final String name, final Class>[] parameterTypes) {
+ Assert.notNull(clazz, "Class must not be null");
+ Assert.notNull(name, "Method name must not be null");
+ Class> searchType = clazz;
+ while (!Object.class.equals(searchType) && searchType != null) {
+ Method[] methods = (searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods());
+ for (Method method : methods) {
+ if (name.equals(method.getName()) &&
+ (parameterTypes == null || Arrays.equals(parameterTypes, method.getParameterTypes()))) {
+ return method;
+ }
+ }
+ searchType = searchType.getSuperclass();
+ }
+ return null;
+ }
+
+ /**
+ * Invoke the specified {@link Method} against the supplied target object
+ * with no arguments. The target object can be null when
+ * invoking a static {@link Method}.
+ *
Thrown exceptions are handled via a call to {@link #handleReflectionException}.
+ * @param method the method to invoke
+ * @param target the target object to invoke the method on
+ * @return the invocation result, if any
+ * @see #invokeMethod(java.lang.reflect.Method, Object, Object[])
+ */
+ public static Object invokeMethod(final Method method, final Object target) {
+ return invokeMethod(method, target, null);
+ }
+
+ /**
+ * Invoke the specified {@link Method} against the supplied target object
+ * with the supplied arguments. The target object can be null
+ * when invoking a static {@link Method}.
+ *
Thrown exceptions are handled via a call to {@link #handleReflectionException}.
+ * @param method the method to invoke
+ * @param target the target object to invoke the method on
+ * @param args the invocation arguments (may be null)
+ * @return the invocation result, if any
+ */
+ public static Object invokeMethod(final Method method, final Object target, final Object[] args) {
+ try {
+ return method.invoke(target, args);
+ }
+ catch (Exception ex) {
+ handleReflectionException(ex);
+ }
+ throw new IllegalStateException("Should never get here");
+ }
+
+ /**
+ * Invoke the specified JDBC API {@link Method} against the supplied
+ * target object with no arguments.
+ * @param method the method to invoke
+ * @param target the target object to invoke the method on
+ * @return the invocation result, if any
+ * @throws SQLException the JDBC API SQLException to rethrow (if any)
+ * @see #invokeJdbcMethod(java.lang.reflect.Method, Object, Object[])
+ */
+ public static Object invokeJdbcMethod(final Method method, final Object target) throws SQLException {
+ return invokeJdbcMethod(method, target, null);
+ }
+
+ /**
+ * Invoke the specified JDBC API {@link Method} against the supplied
+ * target object with the supplied arguments.
+ * @param method the method to invoke
+ * @param target the target object to invoke the method on
+ * @param args the invocation arguments (may be null)
+ * @return the invocation result, if any
+ * @throws SQLException the JDBC API SQLException to rethrow (if any)
+ * @see #invokeMethod(java.lang.reflect.Method, Object, Object[])
+ */
+ public static Object invokeJdbcMethod(final Method method, final Object target, final Object[] args) throws SQLException {
+ try {
+ return method.invoke(target, args);
+ }
+ catch (IllegalAccessException ex) {
+ handleReflectionException(ex);
+ }
+ catch (InvocationTargetException ex) {
+ if (ex.getTargetException() instanceof SQLException) {
+ throw (SQLException) ex.getTargetException();
+ }
+ handleInvocationTargetException(ex);
+ }
+ throw new IllegalStateException("Should never get here");
+ }
+
+ /**
+ * Handle the given reflection exception. Should only be called if
+ * no checked exception is expected to be thrown by the target method.
+ *
Throws the underlying RuntimeException or Error in case of an
+ * InvocationTargetException with such a root cause. Throws an
+ * IllegalStateException with an appropriate message else.
+ * @param ex the reflection exception to handle
+ */
+ public static void handleReflectionException(final Exception ex) {
+ if (ex instanceof NoSuchMethodException) {
+ throw new IllegalStateException("Method not found: " + ex.getMessage());
+ }
+ if (ex instanceof IllegalAccessException) {
+ throw new IllegalStateException("Could not access method: " + ex.getMessage());
+ }
+ if (ex instanceof InvocationTargetException) {
+ handleInvocationTargetException((InvocationTargetException) ex);
+ }
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ handleUnexpectedException(ex);
+ }
+
+ /**
+ * Handle the given invocation target exception. Should only be called if
+ * no checked exception is expected to be thrown by the target method.
+ *
Throws the underlying RuntimeException or Error in case of such
+ * a root cause. Throws an IllegalStateException else.
+ * @param ex the invocation target exception to handle
+ */
+ public static void handleInvocationTargetException(final InvocationTargetException ex) {
+ rethrowRuntimeException(ex.getTargetException());
+ }
+
+ /**
+ * Rethrow the given {@link Throwable exception}, which is presumably the
+ * target exception of an {@link InvocationTargetException}.
+ * Should only be called if no checked exception is expected to be thrown by
+ * the target method.
+ *
Rethrows the underlying exception cast to an {@link RuntimeException}
+ * or {@link Error} if appropriate; otherwise, throws an
+ * {@link IllegalStateException}.
+ * @param ex the exception to rethrow
+ * @throws RuntimeException the rethrown exception
+ */
+ public static void rethrowRuntimeException(final Throwable ex) {
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ if (ex instanceof Error) {
+ throw (Error) ex;
+ }
+ handleUnexpectedException(ex);
+ }
+
+ /**
+ * Rethrow the given {@link Throwable exception}, which is presumably the
+ * target exception of an {@link InvocationTargetException}.
+ * Should only be called if no checked exception is expected to be thrown by
+ * the target method.
+ *
Rethrows the underlying exception cast to an {@link Exception} or
+ * {@link Error} if appropriate; otherwise, throws an
+ * {@link IllegalStateException}.
+ * @param ex the exception to rethrow
+ * @throws Exception the rethrown exception (in case of a checked exception)
+ */
+ public static void rethrowException(final Throwable ex) throws Exception {
+ if (ex instanceof Exception) {
+ throw (Exception) ex;
+ }
+ if (ex instanceof Error) {
+ throw (Error) ex;
+ }
+ handleUnexpectedException(ex);
+ }
+
+ /**
+ * Throws an IllegalStateException with the given exception as root cause.
+ * @param ex the unexpected exception
+ */
+ private static void handleUnexpectedException(final Throwable ex) {
+ throw new IllegalStateException("Unexpected exception thrown", ex);
+ }
+
+ /**
+ * Determine whether the given method explicitly declares the given exception
+ * or one of its superclasses, which means that an exception of that type
+ * can be propagated as-is within a reflective invocation.
+ * @param method the declaring method
+ * @param exceptionType the exception to throw
+ * @return true if the exception can be thrown as-is;
+ * false if it needs to be wrapped
+ */
+ public static boolean declaresException(final Method method, final Class> exceptionType) {
+ Assert.notNull(method, "Method must not be null");
+ Class>[] declaredExceptions = method.getExceptionTypes();
+ for (Class> declaredException : declaredExceptions) {
+ if (declaredException.isAssignableFrom(exceptionType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether the given field is a "public static final" constant.
+ * @param field the field to check
+ */
+ public static boolean isPublicStaticFinal(final Field field) {
+ int modifiers = field.getModifiers();
+ return (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers));
+ }
+
+ /**
+ * Determine whether the given method is an "equals" method.
+ * @see java.lang.Object#equals
+ */
+ public static boolean isEqualsMethod(final Method method) {
+ if (method == null || !method.getName().equals("equals")) {
+ return false;
+ }
+ Class>[] parameterTypes = method.getParameterTypes();
+ return (parameterTypes.length == 1 && parameterTypes[0] == Object.class);
+ }
+
+ /**
+ * Determine whether the given method is a "hashCode" method.
+ * @see java.lang.Object#hashCode
+ */
+ public static boolean isHashCodeMethod(final Method method) {
+ return (method != null && method.getName().equals("hashCode") &&
+ method.getParameterTypes().length == 0);
+ }
+
+ /**
+ * Determine whether the given method is a "toString" method.
+ * @see java.lang.Object#toString()
+ */
+ public static boolean isToStringMethod(final Method method) {
+ return (method != null && method.getName().equals("toString") &&
+ method.getParameterTypes().length == 0);
+ }
+
+ /**
+ * Make the given field accessible, explicitly setting it accessible if necessary.
+ * The setAccessible(true) method is only called when actually necessary,
+ * to avoid unnecessary conflicts with a JVM SecurityManager (if active).
+ * @param field the field to make accessible
+ * @see java.lang.reflect.Field#setAccessible
+ */
+ public static void makeAccessible(final Field field) {
+ if (!Modifier.isPublic(field.getModifiers()) ||
+ !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
+ field.setAccessible(true);
+ }
+ }
+
+ /**
+ * Make the given method accessible, explicitly setting it accessible if necessary.
+ * The setAccessible(true) method is only called when actually necessary,
+ * to avoid unnecessary conflicts with a JVM SecurityManager (if active).
+ * @param method the method to make accessible
+ * @see java.lang.reflect.Method#setAccessible
+ */
+ public static void makeAccessible(final Method method) {
+ if (!Modifier.isPublic(method.getModifiers()) ||
+ !Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
+ method.setAccessible(true);
+ }
+ }
+
+ /**
+ * Make the given constructor accessible, explicitly setting it accessible if necessary.
+ * The setAccessible(true) method is only called when actually necessary,
+ * to avoid unnecessary conflicts with a JVM SecurityManager (if active).
+ * @param ctor the constructor to make accessible
+ * @see java.lang.reflect.Constructor#setAccessible
+ */
+ public static void makeAccessible(final Constructor> ctor) {
+ if (!Modifier.isPublic(ctor.getModifiers()) ||
+ !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) {
+ ctor.setAccessible(true);
+ }
+ }
+
+ /**
+ * Perform the given callback operation on all matching methods of the
+ * given class and superclasses.
+ *
The same named method occurring on subclass and superclass will
+ * appear twice, unless excluded by a {@link MethodFilter}.
+ * @param targetClass class to start looking at
+ * @param mc the callback to invoke for each method
+ * @see #doWithMethods(Class, MethodCallback, MethodFilter)
+ */
+ public static void doWithMethods(final Class> targetClass, final MethodCallback mc) throws IllegalArgumentException {
+ doWithMethods(targetClass, mc, null);
+ }
+
+ /**
+ * Perform the given callback operation on all matching methods of the
+ * given class and superclasses.
+ *
The same named method occurring on subclass and superclass will
+ * appear twice, unless excluded by the specified {@link MethodFilter}.
+ * @param targetClass class to start looking at
+ * @param mc the callback to invoke for each method
+ * @param mf the filter that determines the methods to apply the callback to
+ */
+ public static void doWithMethods(Class> targetClass, final MethodCallback mc, final MethodFilter mf) throws IllegalArgumentException {
+ // Keep backing up the inheritance hierarchy.
+ do {
+ Method[] methods = targetClass.getDeclaredMethods();
+ for (Method method : methods) {
+ if (mf != null && !mf.matches(method)) {
+ continue;
+ }
+ try {
+ mc.doWith(method);
+ }
+ catch (IllegalAccessException ex) {
+ throw new IllegalStateException(
+ "Shouldn't be illegal to access method '" + method.getName() + "': " + ex);
+ }
+ }
+ targetClass = targetClass.getSuperclass();
+ }
+ while (targetClass != null);
+ }
+
+ /**
+ * Get all declared methods on the leaf class and all superclasses.
+ * Leaf class methods are included first.
+ */
+ public static Method[] getAllDeclaredMethods(final Class> leafClass) throws IllegalArgumentException {
+ final List methods = new ArrayList(32);
+ doWithMethods(leafClass, new MethodCallback() {
+ public void doWith(final Method method) {
+ methods.add(method);
+ }
+ });
+ return methods.toArray(new Method[methods.size()]);
+ }
+
+ /**
+ * Invoke the given callback on all fields in the target class,
+ * going up the class hierarchy to get all declared fields.
+ * @param targetClass the target class to analyze
+ * @param fc the callback to invoke for each field
+ */
+ public static void doWithFields(final Class> targetClass, final FieldCallback fc) throws IllegalArgumentException {
+ doWithFields(targetClass, fc, null);
+ }
+
+ /**
+ * Invoke the given callback on all fields in the target class,
+ * going up the class hierarchy to get all declared fields.
+ * @param targetClass the target class to analyze
+ * @param fc the callback to invoke for each field
+ * @param ff the filter that determines the fields to apply the callback to
+ */
+ public static void doWithFields(Class> targetClass, final FieldCallback fc, final FieldFilter ff) throws IllegalArgumentException {
+ // Keep backing up the inheritance hierarchy.
+ do {
+ // Copy each field declared on this class unless it's static or file.
+ Field[] fields = targetClass.getDeclaredFields();
+ for (Field field : fields) {
+ // Skip static and final fields.
+ if (ff != null && !ff.matches(field)) {
+ continue;
+ }
+ try {
+ fc.doWith(field);
+ }
+ catch (IllegalAccessException ex) {
+ throw new IllegalStateException(
+ "Shouldn't be illegal to access field '" + field.getName() + "': " + ex);
+ }
+ }
+ targetClass = targetClass.getSuperclass();
+ }
+ while (targetClass != null && targetClass != Object.class);
+ }
+
+ /**
+ * Given the source object and the destination, which must be the same class
+ * or a subclass, copy all fields, including inherited fields. Designed to
+ * work on objects with public no-arg constructors.
+ * @throws IllegalArgumentException if the arguments are incompatible
+ */
+ public static void shallowCopyFieldState(final Object src, final Object dest) throws IllegalArgumentException {
+ if (src == null) {
+ throw new IllegalArgumentException("Source for field copy cannot be null");
+ }
+ if (dest == null) {
+ throw new IllegalArgumentException("Destination for field copy cannot be null");
+ }
+ if (!src.getClass().isAssignableFrom(dest.getClass())) {
+ throw new IllegalArgumentException("Destination class [" + dest.getClass().getName() +
+ "] must be same or subclass as source class [" + src.getClass().getName() + "]");
+ }
+ doWithFields(src.getClass(), new FieldCallback() {
+ public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException {
+ makeAccessible(field);
+ Object srcValue = field.get(src);
+ field.set(dest, srcValue);
+ }
+ }, COPYABLE_FIELDS);
+ }
+
+ /**
+ * Action to take on each method.
+ */
+ public static interface MethodCallback {
+
+ /**
+ * Perform an operation using the given method.
+ * @param method the method to operate on
+ */
+ void doWith(Method method) throws IllegalArgumentException, IllegalAccessException;
+ }
+
+ /**
+ * Callback optionally used to method fields to be operated on by a method callback.
+ */
+ public static interface MethodFilter {
+
+ /**
+ * Determine whether the given method matches.
+ * @param method the method to check
+ */
+ boolean matches(Method method);
+ }
+
+ /**
+ * Callback interface invoked on each field in the hierarchy.
+ */
+ public static interface FieldCallback {
+
+ /**
+ * Perform an operation using the given field.
+ * @param field the field to operate on
+ */
+ void doWith(Field field) throws IllegalArgumentException, IllegalAccessException;
+ }
+
+ /**
+ * Callback optionally used to filter fields to be operated on by a field callback.
+ */
+ public static interface FieldFilter {
+
+ /**
+ * Determine whether the given field matches.
+ * @param field the field to check
+ */
+ boolean matches(Field field);
+ }
+
+ /**
+ * Pre-built FieldFilter that matches all non-static, non-final fields.
+ */
+ public static FieldFilter COPYABLE_FIELDS = new FieldFilter() {
+ public boolean matches(final Field field) {
+ return !(Modifier.isStatic(field.getModifiers()) ||
+ Modifier.isFinal(field.getModifiers()));
+ }
+ };
+}
diff --git a/src/main/java/org/springframework/roo/support/util/StringUtils.java b/src/main/java/org/springframework/roo/support/util/StringUtils.java
new file mode 100644
index 00000000..ed0891cd
--- /dev/null
+++ b/src/main/java/org/springframework/roo/support/util/StringUtils.java
@@ -0,0 +1,1433 @@
+package org.springframework.roo.support.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+/**
+ * Miscellaneous {@link String} utility methods.
+ *
+ * Mainly for internal use within the framework; consider
+ * Jakarta's Commons Lang
+ * for a more comprehensive suite of String utilities.
+ *
+ *
This class delivers some simple functionality that should really
+ * be provided by the core Java String and {@link StringBuilder}
+ * classes, such as the ability to {@link #replace} all occurrences of a given
+ * substring in a target string. It also provides easy-to-use methods to convert
+ * between delimited strings, such as CSV strings, and collections and arrays.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Keith Donald
+ * @author Rob Harrop
+ * @author Rick Evans
+ * @author Arjen Poutsma
+ * @since 16 April 2001
+ * @see org.apache.commons.lang.StringUtils
+ */
+public final class StringUtils {
+
+ // Constants
+ private static final String FOLDER_SEPARATOR = "/";
+ private static final String WINDOWS_FOLDER_SEPARATOR = "\\";
+ private static final String TOP_PATH = "..";
+ private static final String CURRENT_PATH = ".";
+ private static final char EXTENSION_SEPARATOR = '.';
+
+ /**
+ * The platform-specific line separator.
+ *
+ * @since 1.2.0
+ */
+ public static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+ //---------------------------------------------------------------------
+ // General convenience methods for working with Strings
+ //---------------------------------------------------------------------
+
+ /**
+ * Check that the given CharSequence is neither null nor of length 0.
+ * Note: Will return true for a CharSequence that purely consists of whitespace.
+ *
+ * StringUtils.hasLength(null) = false
+ * StringUtils.hasLength("") = false
+ * StringUtils.hasLength(" ") = true
+ * StringUtils.hasLength("Hello") = true
+ *
+ * @param str the CharSequence to check (may be null)
+ * @return true if the CharSequence is not null and has length
+ * @see #hasText(String)
+ */
+ public static boolean hasLength(final CharSequence str) {
+ return (str != null && str.length() > 0);
+ }
+
+ /**
+ * Check that the given String is neither null nor of length 0.
+ * Note: Will return true for a String that purely consists of whitespace.
+ * @param str the String to check (may be null)
+ * @return true if the String is not null and has length
+ * @see #hasLength(CharSequence)
+ */
+ public static boolean hasLength(final String str) {
+ return hasLength((CharSequence) str);
+ }
+
+ /**
+ * Check whether the given CharSequence has actual text.
+ * More specifically, returns true if the string not null,
+ * its length is greater than 0, and it contains at least one non-whitespace character.
+ *
+ * StringUtils.hasText(null) = false
+ * StringUtils.hasText("") = false
+ * StringUtils.hasText(" ") = false
+ * StringUtils.hasText("12345") = true
+ * StringUtils.hasText(" 12345 ") = true
+ *
+ * @param str the CharSequence to check (may be null)
+ * @return true if the CharSequence is not null,
+ * its length is greater than 0, and it does not contain whitespace only
+ * @see java.lang.Character#isWhitespace
+ */
+ public static boolean hasText(final CharSequence str) {
+ if (!hasLength(str)) {
+ return false;
+ }
+ int strLen = str.length();
+ for (int i = 0; i < strLen; i++) {
+ if (!Character.isWhitespace(str.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the given String has actual text.
+ * More specifically, returns true if the string not null,
+ * its length is greater than 0, and it contains at least one non-whitespace character.
+ * @param str the String to check (may be null)
+ * @return true if the String is not null, its length is
+ * greater than 0, and it does not contain whitespace only
+ * @see #hasText(CharSequence)
+ */
+ public static boolean hasText(final String str) {
+ return hasText((CharSequence) str);
+ }
+
+ /**
+ * Indicates whether the given substring occurs within the given string.
+ * Inspired by the eponymous method in commons-lang.
+ *
+ StringUtils.contains(null, *) = false
+ StringUtils.contains(*, null) = false
+ StringUtils.contains("", "") = true
+ StringUtils.contains("abc", "") = true
+ StringUtils.contains("abc", "a") = true
+ StringUtils.contains("abc", "z") = false
+ *
+ * @param str the string to look within (can be null)
+ * @param substr the string to look for (can be null)
+ * @return see above
+ * @since 1.2.0
+ */
+ public static boolean contains(final String str, final String substr) {
+ if (str == null || substr == null) {
+ return false;
+ }
+ return str.contains(substr);
+ }
+
+ /**
+ * Check whether the given CharSequence contains any whitespace characters.
+ * @param str the CharSequence to check (may be null)
+ * @return true if the CharSequence is not empty and
+ * contains at least 1 whitespace character
+ * @see java.lang.Character#isWhitespace
+ */
+ public static boolean containsWhitespace(final CharSequence str) {
+ if (!hasLength(str)) {
+ return false;
+ }
+ int strLen = str.length();
+ for (int i = 0, n = strLen; i < n; i++) {
+ if (Character.isWhitespace(str.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the given String contains any whitespace characters.
+ * @param str the String to check (may be null)
+ * @return true if the String is not empty and
+ * contains at least 1 whitespace character
+ * @see #containsWhitespace(CharSequence)
+ */
+ public static boolean containsWhitespace(final String str) {
+ return containsWhitespace((CharSequence) str);
+ }
+
+ /**
+ * Trim leading and trailing whitespace from the given String.
+ * @param str the String to check
+ * @return the trimmed String
+ * @see java.lang.Character#isWhitespace
+ */
+ public static String trimWhitespace(final String str) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str);
+ while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) {
+ sb.deleteCharAt(0);
+ }
+ while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Trim all whitespace from the given String:
+ * leading, trailing, and inbetween characters.
+ * @param str the String to check
+ * @return the trimmed String
+ * @see java.lang.Character#isWhitespace
+ */
+ public static String trimAllWhitespace(final String str) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str);
+ int index = 0;
+ while (sb.length() > index) {
+ if (Character.isWhitespace(sb.charAt(index))) {
+ sb.deleteCharAt(index);
+ }
+ else {
+ index++;
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Trim leading whitespace from the given String.
+ * @param str the String to check
+ * @return the trimmed String
+ * @see java.lang.Character#isWhitespace
+ */
+ public static String trimLeadingWhitespace(final String str) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str);
+ while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) {
+ sb.deleteCharAt(0);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Trim trailing whitespace from the given String.
+ * @param str the String to check
+ * @return the trimmed String
+ * @see java.lang.Character#isWhitespace
+ */
+ public static String trimTrailingWhitespace(final String str) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str);
+ while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Trim all occurences of the supplied leading character from the given String.
+ * @param str the String to check
+ * @param leadingCharacter the leading character to be trimmed
+ * @return the trimmed String
+ */
+ public static String trimLeadingCharacter(final String str, final char leadingCharacter) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str);
+ while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) {
+ sb.deleteCharAt(0);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Trim all occurences of the supplied trailing character from the given String.
+ * @param str the String to check
+ * @param trailingCharacter the trailing character to be trimmed
+ * @return the trimmed String
+ */
+ public static String trimTrailingCharacter(final String str, final char trailingCharacter) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str);
+ while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Test if the given String starts with the specified prefix,
+ * ignoring upper/lower case.
+ * @param str the String to check
+ * @param prefix the prefix to look for
+ * @see java.lang.String#startsWith
+ */
+ public static boolean startsWithIgnoreCase(final String str, final String prefix) {
+ if (str == null || prefix == null) {
+ return false;
+ }
+ if (str.startsWith(prefix)) {
+ return true;
+ }
+ if (str.length() < prefix.length()) {
+ return false;
+ }
+ String lcStr = str.substring(0, prefix.length()).toLowerCase();
+ String lcPrefix = prefix.toLowerCase();
+ return lcStr.equals(lcPrefix);
+ }
+
+ /**
+ * Test if the given String ends with the specified suffix,
+ * ignoring upper/lower case.
+ * @param str the String to check
+ * @param suffix the suffix to look for
+ * @see java.lang.String#endsWith
+ */
+ public static boolean endsWithIgnoreCase(final String str, final String suffix) {
+ if (str == null || suffix == null) {
+ return false;
+ }
+ if (str.endsWith(suffix)) {
+ return true;
+ }
+ if (str.length() < suffix.length()) {
+ return false;
+ }
+
+ String lcStr = str.substring(str.length() - suffix.length()).toLowerCase();
+ String lcSuffix = suffix.toLowerCase();
+ return lcStr.equals(lcSuffix);
+ }
+
+ /**
+ * Test whether the given string matches the given substring
+ * at the given index.
+ * @param str the original string (or StringBuilder)
+ * @param index the index in the original string to start matching against
+ * @param substring the substring to match at the given index
+ */
+ public static boolean substringMatch(final CharSequence str, final int index, final CharSequence substring) {
+ for (int j = 0, n = substring.length(); j < n; j++) {
+ int i = index + j;
+ if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Count the occurrences of the substring in string s.
+ * @param str string to search in. Return 0 if this is null.
+ * @param sub string to search for. Return 0 if this is null.
+ */
+ public static int countOccurrencesOf(final String str, final String sub) {
+ if (!hasLength(str) || !hasLength(sub)) {
+ return 0;
+ }
+ int count = 0, pos = 0, idx = 0;
+ while ((idx = str.indexOf(sub, pos)) != -1) {
+ ++count;
+ pos = idx + sub.length();
+ }
+ return count;
+ }
+
+ /**
+ * Returns the given string repeated the given number of times
+ *
+ * @param str the string to repeat (can be null or empty)
+ * @param times the number of times to repeat it
+ * @return null if null is given
+ */
+ public static String repeat(final String str, final int times) {
+ if (!hasLength(str)) {
+ return str;
+ }
+ final StringBuilder sb = new StringBuilder(str.length() * times);
+ for (int i = 0; i < times; i++) {
+ sb.append(str);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Replaces all occurrences of one string within another.
+ *
+ * @param original the string to modify (can be zero length to do nothing)
+ * @param toReplace the string to replace (can be blank to do nothing)
+ * @param replacement the string to replace it with (can be null to do nothing)
+ * @return the original string, modified as necessary
+ */
+ public static String replace(final String original, final String toReplace, final String replacement) {
+ String result = original;
+ String previousResult;
+ do {
+ previousResult = result;
+ result = replaceFirst(previousResult, toReplace, replacement);
+ } while (!equals(previousResult, result));
+ return result;
+ }
+
+ /**
+ * Delete all occurrences of the given substring.
+ * @param inString the original String
+ * @param pattern the pattern to delete all occurrences of
+ * @return the resulting String
+ */
+ public static String delete(final String inString, final String pattern) {
+ return replace(inString, pattern, "");
+ }
+
+ /**
+ * Delete any character in a given String.
+ * @param inString the original String
+ * @param charsToDelete a set of characters to delete.
+ * E.g. "az\n" will delete 'a's, 'z's and new lines.
+ * @return the resulting String
+ */
+ public static String deleteAny(final String inString, final String charsToDelete) {
+ if (!hasLength(inString) || !hasLength(charsToDelete)) {
+ return inString;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = inString.length(); i < n; i++) {
+ char c = inString.charAt(i);
+ if (charsToDelete.indexOf(c) == -1) {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ //---------------------------------------------------------------------
+ // Convenience methods for working with formatted Strings
+ //---------------------------------------------------------------------
+
+ /**
+ * Quote the given String with single quotes.
+ * @param str the input String (e.g. "myString")
+ * @return the quoted String (e.g. "'myString'"),
+ * or null if the input was null
+ */
+ public static String quote(final String str) {
+ return (str != null ? "'" + str + "'" : null);
+ }
+
+ /**
+ * Turn the given Object into a String with single quotes
+ * if it is a String; keeping the Object as-is else.
+ * @param obj the input Object (e.g. "myString")
+ * @return the quoted String (e.g. "'myString'"),
+ * or the input object as-is if not a String
+ */
+ public static Object quoteIfString(final Object obj) {
+ return (obj instanceof String ? quote((String) obj) : obj);
+ }
+
+ /**
+ * Unqualify a string qualified by a '.' dot character. For example,
+ * "this.name.is.qualified", returns "qualified".
+ * @param qualifiedName the qualified name
+ */
+ public static String unqualify(final String qualifiedName) {
+ return unqualify(qualifiedName, '.');
+ }
+
+ /**
+ * Unqualify a string qualified by a separator character. For example,
+ * "this:name:is:qualified" returns "qualified" if using a ':' separator.
+ * @param qualifiedName the qualified name
+ * @param separator the separator
+ */
+ public static String unqualify(final String qualifiedName, final char separator) {
+ return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1);
+ }
+
+ /**
+ * Capitalize a String, changing the first letter to
+ * upper case as per {@link Character#toUpperCase(char)}.
+ * No other letters are changed.
+ * @param str the String to capitalize, may be null
+ * @return the capitalized String, null if null
+ */
+ public static String capitalize(final String str) {
+ return changeFirstCharacterCase(str, true);
+ }
+
+ /**
+ * Uncapitalize a String, changing the first letter to
+ * lower case as per {@link Character#toLowerCase(char)}.
+ * No other letters are changed.
+ * @param str the String to uncapitalize, may be null
+ * @return the uncapitalized String, null if null
+ */
+ public static String uncapitalize(final String str) {
+ return changeFirstCharacterCase(str, false);
+ }
+
+ private static String changeFirstCharacterCase(final String str, final boolean capitalize) {
+ if (!hasText(str)) {
+ return str;
+ }
+ StringBuilder sb = new StringBuilder(str.length());
+ if (capitalize) {
+ sb.append(Character.toUpperCase(str.charAt(0)));
+ } else {
+ sb.append(Character.toLowerCase(str.charAt(0)));
+ }
+ sb.append(str.substring(1));
+ return sb.toString();
+ }
+
+ /**
+ * Extract the filename from the given path,
+ * e.g. "mypath/myfile.txt" -> "myfile.txt".
+ * @param path the file path (may be null)
+ * @return the extracted filename, or null if none
+ */
+ public static String getFilename(final String path) {
+ if (path == null) {
+ return null;
+ }
+ int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);
+ return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path);
+ }
+
+ /**
+ * Extract the filename extension from the given path,
+ * e.g. "mypath/myfile.txt" -> "txt".
+ * @param path the file path (may be null)
+ * @return the extracted filename extension, or null if none
+ */
+ public static String getFilenameExtension(final String path) {
+ if (path == null) {
+ return null;
+ }
+ int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR);
+ return (sepIndex != -1 ? path.substring(sepIndex + 1) : null);
+ }
+
+ /**
+ * Strip the filename extension from the given path,
+ * e.g. "mypath/myfile.txt" -> "mypath/myfile".
+ * @param path the file path (may be null)
+ * @return the path with stripped filename extension,
+ * or null if none
+ */
+ public static String stripFilenameExtension(final String path) {
+ if (path == null) {
+ return null;
+ }
+ int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR);
+ return (sepIndex != -1 ? path.substring(0, sepIndex) : path);
+ }
+
+ /**
+ * Apply the given relative path to the given path,
+ * assuming standard Java folder separation (i.e. "/" separators);
+ * @param path the path to start from (usually a full file path)
+ * @param relativePath the relative path to apply
+ * (relative to the full file path above)
+ * @return the full file path that results from applying the relative path
+ */
+ public static String applyRelativePath(final String path, final String relativePath) {
+ int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);
+ if (separatorIndex != -1) {
+ String newPath = path.substring(0, separatorIndex);
+ if (!relativePath.startsWith(FOLDER_SEPARATOR)) {
+ newPath += FOLDER_SEPARATOR;
+ }
+ return newPath + relativePath;
+ }
+ return relativePath;
+ }
+
+ /**
+ * Normalize the path by suppressing sequences like "path/.." and
+ * inner simple dots.
+ * The result is convenient for path comparison. For other uses,
+ * notice that Windows separators ("\") are replaced by simple slashes.
+ * @param path the original path
+ * @return the normalized path
+ */
+ public static String cleanPath(final String path) {
+ if (path == null) {
+ return null;
+ }
+ String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
+
+ // Strip prefix from path to analyze, to not treat it as part of the
+ // first path element. This is necessary to correctly parse paths like
+ // "file:core/../core/io/Resource.class", where the ".." should just
+ // strip the first "core" directory while keeping the "file:" prefix.
+ int prefixIndex = pathToUse.indexOf(":");
+ String prefix = "";
+ if (prefixIndex != -1) {
+ prefix = pathToUse.substring(0, prefixIndex + 1);
+ pathToUse = pathToUse.substring(prefixIndex + 1);
+ }
+ if (pathToUse.startsWith(FOLDER_SEPARATOR)) {
+ prefix = prefix + FOLDER_SEPARATOR;
+ pathToUse = pathToUse.substring(1);
+ }
+
+ String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
+ List pathElements = new ArrayList();
+ int tops = 0;
+
+ for (int i = pathArray.length - 1; i >= 0; i--) {
+ String element = pathArray[i];
+ if (CURRENT_PATH.equals(element)) {
+ // Points to current directory - drop it.
+ } else if (TOP_PATH.equals(element)) {
+ // Registering top path found.
+ tops++;
+ } else {
+ if (tops > 0) {
+ // Merging path element with element corresponding to top path.
+ tops--;
+ }
+ else {
+ // Normal path element found.
+ pathElements.add(0, element);
+ }
+ }
+ }
+
+ // Remaining top paths need to be retained.
+ for (int i = 0; i < tops; i++) {
+ pathElements.add(0, TOP_PATH);
+ }
+
+ return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
+ }
+
+ /**
+ * Compare two paths after normalization of them.
+ * @param path1 first path for comparison
+ * @param path2 second path for comparison
+ * @return whether the two paths are equivalent after normalization
+ */
+ public static boolean pathEquals(final String path1, final String path2) {
+ return cleanPath(path1).equals(cleanPath(path2));
+ }
+
+ /**
+ * Parse the given localeString into a {@link Locale}.
+ * This is the inverse operation of {@link Locale#toString Locale's toString}.
+ * @param localeString the locale string, following Locale's
+ * toString() format ("en", "en_UK", etc);
+ * also accepts spaces as separators, as an alternative to underscores
+ * @return a corresponding Locale instance
+ */
+ public static Locale parseLocaleString(final String localeString) {
+ String[] parts = tokenizeToStringArray(localeString, "_ ", false, false);
+ String language = (parts.length > 0 ? parts[0] : "");
+ String country = (parts.length > 1 ? parts[1] : "");
+ String variant = "";
+ if (parts.length >= 2) {
+ // There is definitely a variant, and it is everything after the country
+ // code sans the separator between the country code and the variant.
+ int endIndexOfCountryCode = localeString.indexOf(country) + country.length();
+ // Strip off any leading '_' and whitespace, what's left is the variant.
+ variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode));
+ if (variant.startsWith("_")) {
+ variant = trimLeadingCharacter(variant, '_');
+ }
+ }
+ return (language.length() > 0 ? new Locale(language, country, variant) : null);
+ }
+
+ /**
+ * Determine the RFC 3066 compliant language tag,
+ * as used for the HTTP "Accept-Language" header.
+ * @param locale the Locale to transform to a language tag
+ * @return the RFC 3066 compliant language tag as String
+ */
+ public static String toLanguageTag(final Locale locale) {
+ return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : "");
+ }
+
+ //---------------------------------------------------------------------
+ // Convenience methods for working with String arrays
+ //---------------------------------------------------------------------
+
+ /**
+ * Append the given String to the given String array, returning a new array
+ * consisting of the input array contents plus the given String.
+ * @param arr the array to append to (can be null)
+ * @param str the String to append
+ * @return the new array (never null)
+ */
+ public static String[] addStringToArray(final String[] arr, final String str) {
+ if (ObjectUtils.isEmpty(arr)) {
+ return new String[] {str};
+ }
+ String[] newArr = new String[arr.length + 1];
+ System.arraycopy(arr, 0, newArr, 0, arr.length);
+ newArr[arr.length] = str;
+ return newArr;
+ }
+
+ /**
+ * Concatenate the given String arrays into one,
+ * with overlapping array elements included twice.
+ *
The order of elements in the original arrays is preserved.
+ * @param arr1 the first array (can be null)
+ * @param arr2 the second array (can be null)
+ * @return the new array (null if both given arrays were null)
+ */
+ public static String[] concatenateStringArrays(final String[] arr1, final String[] arr2) {
+ if (ObjectUtils.isEmpty(arr1)) {
+ return arr2;
+ }
+ if (ObjectUtils.isEmpty(arr2)) {
+ return arr1;
+ }
+ String[] newArr = new String[arr1.length + arr2.length];
+ System.arraycopy(arr1, 0, newArr, 0, arr1.length);
+ System.arraycopy(arr2, 0, newArr, arr1.length, arr2.length);
+ return newArr;
+ }
+
+ /**
+ * Merge the given String arrays into one, with overlapping
+ * array elements only included once.
+ *
The order of elements in the original arrays is preserved
+ * (with the exception of overlapping elements, which are only
+ * included on their first occurrence).
+ * @param arr1 the first array (can be null)
+ * @param arr2 the second array (can be null)
+ * @return the new array (null if both given arrays were null)
+ */
+ public static String[] mergeStringArrays(final String[] arr1, final String[] arr2) {
+ if (ObjectUtils.isEmpty(arr1)) {
+ return arr2;
+ }
+ if (ObjectUtils.isEmpty(arr2)) {
+ return arr1;
+ }
+ List result = new ArrayList();
+ result.addAll(Arrays.asList(arr1));
+ for (int i = 0, n = arr2.length; i < n; i++) {
+ String str = arr2[i];
+ if (!result.contains(str)) {
+ result.add(str);
+ }
+ }
+ return toStringArray(result);
+ }
+
+ /**
+ * Turn given source String array into sorted array.
+ * @param arr the source array
+ * @return the sorted array (never null)
+ */
+ public static String[] sortStringArray(final String[] arr) {
+ if (ObjectUtils.isEmpty(arr)) {
+ return new String[0];
+ }
+ Arrays.sort(arr);
+ return arr;
+ }
+
+ /**
+ * Copy the given Collection into a String array.
+ * The Collection must contain String elements only.
+ * @param coll the Collection to copy
+ * @return the String array (null if the passed-in
+ * Collection was null)
+ */
+ public static String[] toStringArray(final Collection coll) {
+ if (coll == null) {
+ return null;
+ }
+ return coll.toArray(new String[coll.size()]);
+ }
+
+ /**
+ * Copy the given Enumeration into a String array.
+ * The Enumeration must contain String elements only.
+ * @param enumeration the Enumeration to copy
+ * @return the String array (null if the passed-in
+ * Enumeration was null)
+ */
+ public static String[] toStringArray(final Enumeration enumeration) {
+ if (enumeration == null) {
+ return null;
+ }
+ List list = Collections.list(enumeration);
+ return list.toArray(new String[list.size()]);
+ }
+
+ /**
+ * Trim the elements of the given String array,
+ * calling String.trim() on each of them.
+ * @param arr the original String array
+ * @return the resulting array (of the same size) with trimmed elements
+ */
+ public static String[] trimArrayElements(final String[] arr) {
+ if (ObjectUtils.isEmpty(arr)) {
+ return new String[0];
+ }
+ String[] result = new String[arr.length];
+ for (int i = 0, n = arr.length; i < n; i++) {
+ String element = arr[i];
+ result[i] = (element != null ? element.trim() : null);
+ }
+ return result;
+ }
+
+ /**
+ * Remove duplicate Strings from the given array.
+ * Also sorts the array, as it uses a TreeSet.
+ * @param arr the String array
+ * @return an array without duplicates, in natural sort order
+ */
+ public static String[] removeDuplicateStrings(final String[] arr) {
+ if (ObjectUtils.isEmpty(arr)) {
+ return arr;
+ }
+ Set set = new TreeSet();
+ for (int i = 0, n = arr.length; i < n; i++) {
+ set.add(arr[i]);
+ }
+ return toStringArray(set);
+ }
+
+ /**
+ * Split a String at the first occurrence of the delimiter.
+ * Does not include the delimiter in the result.
+ * @param toSplit the string to split
+ * @param delim to split the string up with
+ * @return a two element array with index 0 being before the delimiter, and
+ * index 1 being after the delimiter (neither element includes the delimiter);
+ * or null if the delimiter wasn't found in the given input String
+ */
+ public static String[] split(final String toSplit, final String delim) {
+ if (!hasLength(toSplit) || !hasLength(delim)) {
+ return null;
+ }
+ int offset = toSplit.indexOf(delim);
+ if (offset < 0) {
+ return null;
+ }
+ String beforeDelimiter = toSplit.substring(0, offset);
+ String afterDelimiter = toSplit.substring(offset + delim.length());
+ return new String[] {beforeDelimiter, afterDelimiter};
+ }
+
+ /**
+ * Take an array Strings and split each element based on the given delimiter.
+ * A Properties instance is then generated, with the left of the
+ * delimiter providing the key, and the right of the delimiter providing the value.
+ * Will trim both the key and value before adding them to the
+ *