diff options
author | David Ostrovsky <david@ostrovsky.org> | 2014-02-17 21:56:36 +0100 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2014-04-10 18:58:07 -0400 |
commit | 7613df52959b6e2ac1094d2263be310fb3e2723b (patch) | |
tree | f0a644a1256dc8665555d94a6d0bd813661c7809 /src/main/java/com/gitblit/utils | |
parent | 41124cddb6edd82c1630efb99b29c839304ed897 (diff) | |
download | gitblit-7613df52959b6e2ac1094d2263be310fb3e2723b.tar.gz gitblit-7613df52959b6e2ac1094d2263be310fb3e2723b.zip |
SSHD: Add support for generic commands
Change-Id: I5a60710323ca674d70e34f7451422ec167105429
Diffstat (limited to 'src/main/java/com/gitblit/utils')
-rw-r--r-- | src/main/java/com/gitblit/utils/IdGenerator.java | 91 | ||||
-rw-r--r-- | src/main/java/com/gitblit/utils/TaskInfoFactory.java | 19 | ||||
-rw-r--r-- | src/main/java/com/gitblit/utils/WorkQueue.java | 340 | ||||
-rw-r--r-- | src/main/java/com/gitblit/utils/cli/CmdLineParser.java | 440 | ||||
-rw-r--r-- | src/main/java/com/gitblit/utils/cli/SubcommandHandler.java | 43 |
5 files changed, 933 insertions, 0 deletions
diff --git a/src/main/java/com/gitblit/utils/IdGenerator.java b/src/main/java/com/gitblit/utils/IdGenerator.java new file mode 100644 index 00000000..d2c1cb23 --- /dev/null +++ b/src/main/java/com/gitblit/utils/IdGenerator.java @@ -0,0 +1,91 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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 com.gitblit.utils; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +/** Simple class to produce 4 billion keys randomly distributed. */ +public class IdGenerator { + /** Format an id created by this class as a hex string. */ + public static String format(int id) { + final char[] r = new char[8]; + for (int p = 7; 0 <= p; p--) { + final int h = id & 0xf; + r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10)); + id >>= 4; + } + return new String(r); + } + + private final AtomicInteger gen; + + @Inject + public IdGenerator() { + gen = new AtomicInteger(new Random().nextInt()); + } + + /** Produce the next identifier. */ + public int next() { + return mix(gen.getAndIncrement()); + } + + private static final int salt = 0x9e3779b9; + + static int mix(int in) { + return mix(salt, in); + } + + /** A very simple bit permutation to mask a simple incrementer. */ + public static int mix(final int salt, final int in) { + short v0 = hi16(in); + short v1 = lo16(in); + v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; + v1 += ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; + return result(v0, v1); + } + + /* For testing only. */ + static int unmix(final int in) { + short v0 = hi16(in); + short v1 = lo16(in); + v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; + v0 -= ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; + return result(v0, v1); + } + + private static short hi16(final int in) { + return (short) ( // + ((in >>> 24 & 0xff)) | // + ((in >>> 16 & 0xff) << 8) // + ); + } + + private static short lo16(final int in) { + return (short) ( // + ((in >>> 8 & 0xff)) | // + ((in & 0xff) << 8) // + ); + } + + private static int result(final short v0, final short v1) { + return ((v0 & 0xff) << 24) | // + (((v0 >>> 8) & 0xff) << 16) | // + ((v1 & 0xff) << 8) | // + ((v1 >>> 8) & 0xff); + } +} diff --git a/src/main/java/com/gitblit/utils/TaskInfoFactory.java b/src/main/java/com/gitblit/utils/TaskInfoFactory.java new file mode 100644 index 00000000..111af27b --- /dev/null +++ b/src/main/java/com/gitblit/utils/TaskInfoFactory.java @@ -0,0 +1,19 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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 com.gitblit.utils; + +public interface TaskInfoFactory<T> { + T getTaskInfo(WorkQueue.Task<?> task); +} diff --git a/src/main/java/com/gitblit/utils/WorkQueue.java b/src/main/java/com/gitblit/utils/WorkQueue.java new file mode 100644 index 00000000..778e754c --- /dev/null +++ b/src/main/java/com/gitblit/utils/WorkQueue.java @@ -0,0 +1,340 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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 com.gitblit.utils; + +import com.google.common.collect.Lists; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +/** Delayed execution of tasks using a background thread pool. */ +public class WorkQueue { + private static final Logger log = LoggerFactory.getLogger(WorkQueue.class); + private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION = + new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + log.error("WorkQueue thread " + t.getName() + " threw exception", e); + } + }; + + private Executor defaultQueue; + private final IdGenerator idGenerator; + private final CopyOnWriteArrayList<Executor> queues; + + @Inject + public WorkQueue(final IdGenerator idGenerator) { + this.idGenerator = idGenerator; + this.queues = new CopyOnWriteArrayList<Executor>(); + } + + /** Get the default work queue, for miscellaneous tasks. */ + public synchronized Executor getDefaultQueue() { + if (defaultQueue == null) { + defaultQueue = createQueue(1, "WorkQueue"); + } + return defaultQueue; + } + + /** Create a new executor queue with one thread. */ + public Executor createQueue(final int poolsize, final String prefix) { + final Executor r = new Executor(poolsize, prefix); + r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + queues.add(r); + return r; + } + + /** Get all of the tasks currently scheduled in any work queue. */ + public List<Task<?>> getTasks() { + final List<Task<?>> r = new ArrayList<Task<?>>(); + for (final Executor e : queues) { + e.addAllTo(r); + } + return r; + } + + public <T> List<T> getTaskInfos(TaskInfoFactory<T> factory) { + List<T> taskInfos = Lists.newArrayList(); + for (Executor exe : queues) { + for (Task<?> task : exe.getTasks()) { + taskInfos.add(factory.getTaskInfo(task)); + } + } + return taskInfos; + } + + /** Locate a task by its unique id, null if no task matches. */ + public Task<?> getTask(final int id) { + Task<?> result = null; + for (final Executor e : queues) { + final Task<?> t = e.getTask(id); + if (t != null) { + if (result != null) { + // Don't return the task if we have a duplicate. Lie instead. + return null; + } else { + result = t; + } + } + } + return result; + } + + public void stop() { + for (final Executor p : queues) { + p.shutdown(); + boolean isTerminated; + do { + try { + isTerminated = p.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + isTerminated = false; + } + } while (!isTerminated); + } + queues.clear(); + } + + /** An isolated queue. */ + public class Executor extends ScheduledThreadPoolExecutor { + private final ConcurrentHashMap<Integer, Task<?>> all; + + Executor(final int corePoolSize, final String prefix) { + super(corePoolSize, new ThreadFactory() { + private final ThreadFactory parent = Executors.defaultThreadFactory(); + private final AtomicInteger tid = new AtomicInteger(1); + + @Override + public Thread newThread(final Runnable task) { + final Thread t = parent.newThread(task); + t.setName(prefix + "-" + tid.getAndIncrement()); + t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION); + return t; + } + }); + + all = new ConcurrentHashMap<Integer, Task<?>>( // + corePoolSize << 1, // table size + 0.75f, // load factor + corePoolSize + 4 // concurrency level + ); + } + + public void unregisterWorkQueue() { + queues.remove(this); + } + + @Override + protected <V> RunnableScheduledFuture<V> decorateTask( + final Runnable runnable, RunnableScheduledFuture<V> r) { + r = super.decorateTask(runnable, r); + for (;;) { + final int id = idGenerator.next(); + + Task<V> task; + task = new Task<V>(runnable, r, this, id); + + if (all.putIfAbsent(task.getTaskId(), task) == null) { + return task; + } + } + } + + @Override + protected <V> RunnableScheduledFuture<V> decorateTask( + final Callable<V> callable, final RunnableScheduledFuture<V> task) { + throw new UnsupportedOperationException("Callable not implemented"); + } + + void remove(final Task<?> task) { + all.remove(task.getTaskId(), task); + } + + Task<?> getTask(final int id) { + return all.get(id); + } + + void addAllTo(final List<Task<?>> list) { + list.addAll(all.values()); // iterator is thread safe + } + + Collection<Task<?>> getTasks() { + return all.values(); + } + } + + /** Runnable needing to know it was canceled. */ + public interface CancelableRunnable extends Runnable { + /** Notifies the runnable it was canceled. */ + public void cancel(); + } + + /** A wrapper around a scheduled Runnable, as maintained in the queue. */ + public static class Task<V> implements RunnableScheduledFuture<V> { + /** + * Summarized status of a single task. + * <p> + * Tasks have the following state flow: + * <ol> + * <li>{@link #SLEEPING}: if scheduled with a non-zero delay.</li> + * <li>{@link #READY}: waiting for an available worker thread.</li> + * <li>{@link #RUNNING}: actively executing on a worker thread.</li> + * <li>{@link #DONE}: finished executing, if not periodic.</li> + * </ol> + */ + public static enum State { + // Ordered like this so ordinal matches the order we would + // prefer to see tasks sorted in: done before running, + // running before ready, ready before sleeping. + // + DONE, CANCELLED, RUNNING, READY, SLEEPING, OTHER + } + + private final Runnable runnable; + private final RunnableScheduledFuture<V> task; + private final Executor executor; + private final int taskId; + private final AtomicBoolean running; + private final Date startTime; + + Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor, + int taskId) { + this.runnable = runnable; + this.task = task; + this.executor = executor; + this.taskId = taskId; + this.running = new AtomicBoolean(); + this.startTime = new Date(); + } + + public int getTaskId() { + return taskId; + } + + public State getState() { + if (isCancelled()) { + return State.CANCELLED; + } else if (isDone() && !isPeriodic()) { + return State.DONE; + } else if (running.get()) { + return State.RUNNING; + } + + final long delay = getDelay(TimeUnit.MILLISECONDS); + if (delay <= 0) { + return State.READY; + } else if (0 < delay) { + return State.SLEEPING; + } + + return State.OTHER; + } + + public Date getStartTime() { + return startTime; + } + + public boolean cancel(boolean mayInterruptIfRunning) { + if (task.cancel(mayInterruptIfRunning)) { + // Tiny abuse of running: if the task needs to know it was + // canceled (to clean up resources) and it hasn't started + // yet the task's run method won't execute. So we tag it + // as running and allow it to clean up. This ensures we do + // not invoke cancel twice. + // + if (runnable instanceof CancelableRunnable + && running.compareAndSet(false, true)) { + ((CancelableRunnable) runnable).cancel(); + } + executor.remove(this); + executor.purge(); + return true; + + } else { + return false; + } + } + + public int compareTo(Delayed o) { + return task.compareTo(o); + } + + public V get() throws InterruptedException, ExecutionException { + return task.get(); + } + + public V get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return task.get(timeout, unit); + } + + public long getDelay(TimeUnit unit) { + return task.getDelay(unit); + } + + public boolean isCancelled() { + return task.isCancelled(); + } + + public boolean isDone() { + return task.isDone(); + } + + public boolean isPeriodic() { + return task.isPeriodic(); + } + + public void run() { + if (running.compareAndSet(false, true)) { + try { + task.run(); + } finally { + if (isPeriodic()) { + running.set(false); + } else { + executor.remove(this); + } + } + } + } + + @Override + public String toString() { + return runnable.toString(); + } + } +} diff --git a/src/main/java/com/gitblit/utils/cli/CmdLineParser.java b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java new file mode 100644 index 00000000..def76df4 --- /dev/null +++ b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * + * (Taken from JGit org.eclipse.jgit.pgm.opt.CmdLineParser.) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Git Development Community nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.gitblit.utils.cli; + +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.IllegalAnnotationError; +import org.kohsuke.args4j.NamedOptionDef; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.BooleanOptionHandler; +import org.kohsuke.args4j.spi.EnumOptionHandler; +import org.kohsuke.args4j.spi.FieldSetter; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Setter; + +import com.google.common.base.Strings; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +/** + * Extended command line parser which handles --foo=value arguments. + * <p> + * The args4j package does not natively handle --foo=value and instead prefers + * to see --foo value on the command line. Many users are used to the GNU style + * --foo=value long option, so we convert from the GNU style format to the + * args4j style format prior to invoking args4j for parsing. + */ +public class CmdLineParser { + public interface Factory { + CmdLineParser create(Object bean); + } + + private final MyParser parser; + + @SuppressWarnings("rawtypes") + private Map<String, OptionHandler> options; + + /** + * Creates a new command line owner that parses arguments/options and set them + * into the given object. + * + * @param bean instance of a class annotated by + * {@link org.kohsuke.args4j.Option} and + * {@link org.kohsuke.args4j.Argument}. this object will receive + * values. + * + * @throws IllegalAnnotationError if the option bean class is using args4j + * annotations incorrectly. + */ + public CmdLineParser(Object bean) + throws IllegalAnnotationError { + this.parser = new MyParser(bean); + } + + public void addArgument(Setter<?> setter, Argument a) { + parser.addArgument(setter, a); + } + + public void addOption(Setter<?> setter, Option o) { + parser.addOption(setter, o); + } + + public void printSingleLineUsage(Writer w, ResourceBundle rb) { + parser.printSingleLineUsage(w, rb); + } + + public void printUsage(Writer out, ResourceBundle rb) { + parser.printUsage(out, rb); + } + + public void printDetailedUsage(String name, StringWriter out) { + out.write(name); + printSingleLineUsage(out, null); + out.write('\n'); + out.write('\n'); + printUsage(out, null); + out.write('\n'); + } + + public void printQueryStringUsage(String name, StringWriter out) { + out.write(name); + + char next = '?'; + List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>(); + for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) { + if (handler.option instanceof NamedOptionDef) { + NamedOptionDef n = (NamedOptionDef) handler.option; + + if (handler instanceof BooleanOptionHandler) { + booleans.add(n); + continue; + } + + if (!n.required()) { + out.write('['); + } + out.write(next); + next = '&'; + if (n.name().startsWith("--")) { + out.write(n.name().substring(2)); + } else if (n.name().startsWith("-")) { + out.write(n.name().substring(1)); + } else { + out.write(n.name()); + } + out.write('='); + + out.write(metaVar(handler, n)); + if (!n.required()) { + out.write(']'); + } + if (n.isMultiValued()) { + out.write('*'); + } + } + } + for (NamedOptionDef n : booleans) { + if (!n.required()) { + out.write('['); + } + out.write(next); + next = '&'; + if (n.name().startsWith("--")) { + out.write(n.name().substring(2)); + } else if (n.name().startsWith("-")) { + out.write(n.name().substring(1)); + } else { + out.write(n.name()); + } + if (!n.required()) { + out.write(']'); + } + } + } + + private static String metaVar(OptionHandler<?> handler, NamedOptionDef n) { + String var = n.metaVar(); + if (Strings.isNullOrEmpty(var)) { + var = handler.getDefaultMetaVariable(); + if (handler instanceof EnumOptionHandler) { + var = var.substring(1, var.length() - 1).replace(" ", ""); + } + } + return var; + } + + public boolean wasHelpRequestedByOption() { + return parser.help.value; + } + + public void parseArgument(final String... args) throws CmdLineException { + List<String> tmp = Lists.newArrayListWithCapacity(args.length); + for (int argi = 0; argi < args.length; argi++) { + final String str = args[argi]; + if (str.equals("--")) { + while (argi < args.length) + tmp.add(args[argi++]); + break; + } + + if (str.startsWith("--")) { + final int eq = str.indexOf('='); + if (eq > 0) { + tmp.add(str.substring(0, eq)); + tmp.add(str.substring(eq + 1)); + continue; + } + } + + tmp.add(str); + } + parser.parseArgument(tmp.toArray(new String[tmp.size()])); + } + + public void parseOptionMap(Map<String, String[]> parameters) + throws CmdLineException { + Multimap<String, String> map = LinkedHashMultimap.create(); + for (Map.Entry<String, String[]> ent : parameters.entrySet()) { + for (String val : ent.getValue()) { + map.put(ent.getKey(), val); + } + } + parseOptionMap(map); + } + + public void parseOptionMap(Multimap<String, String> params) + throws CmdLineException { + List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size()); + for (final String key : params.keySet()) { + String name = makeOption(key); + + if (isBoolean(name)) { + boolean on = false; + for (String value : params.get(key)) { + on = toBoolean(key, value); + } + if (on) { + tmp.add(name); + } + } else { + for (String value : params.get(key)) { + tmp.add(name); + tmp.add(value); + } + } + } + parser.parseArgument(tmp.toArray(new String[tmp.size()])); + } + + public boolean isBoolean(String name) { + return findHandler(makeOption(name)) instanceof BooleanOptionHandler; + } + + private String makeOption(String name) { + if (!name.startsWith("-")) { + if (name.length() == 1) { + name = "-" + name; + } else { + name = "--" + name; + } + } + return name; + } + + @SuppressWarnings("rawtypes") + private OptionHandler findHandler(String name) { + if (options == null) { + options = index(parser.options); + } + return options.get(name); + } + + @SuppressWarnings("rawtypes") + private static Map<String, OptionHandler> index(List<OptionHandler> in) { + Map<String, OptionHandler> m = Maps.newHashMap(); + for (OptionHandler handler : in) { + if (handler.option instanceof NamedOptionDef) { + NamedOptionDef def = (NamedOptionDef) handler.option; + if (!def.isArgument()) { + m.put(def.name(), handler); + for (String alias : def.aliases()) { + m.put(alias, handler); + } + } + } + } + return m; + } + + private boolean toBoolean(String name, String value) throws CmdLineException { + if ("true".equals(value) || "t".equals(value) + || "yes".equals(value) || "y".equals(value) + || "on".equals(value) + || "1".equals(value) + || value == null || "".equals(value)) { + return true; + } + + if ("false".equals(value) || "f".equals(value) + || "no".equals(value) || "n".equals(value) + || "off".equals(value) + || "0".equals(value)) { + return false; + } + + throw new CmdLineException(parser, String.format( + "invalid boolean \"%s=%s\"", name, value)); + } + + private class MyParser extends org.kohsuke.args4j.CmdLineParser { + @SuppressWarnings("rawtypes") + private List<OptionHandler> options; + private HelpOption help; + + MyParser(final Object bean) { + super(bean); + ensureOptionsInitialized(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + protected OptionHandler createOptionHandler(final OptionDef option, + final Setter setter) { + if (isHandlerSpecified(option) || isEnum(setter) || isPrimitive(setter)) { + return add(super.createOptionHandler(option, setter)); + } + +// OptionHandlerFactory<?> factory = handlers.get(setter.getType()); +// if (factory != null) { +// return factory.create(this, option, setter); +// } + return add(super.createOptionHandler(option, setter)); + } + + @SuppressWarnings("rawtypes") + private OptionHandler add(OptionHandler handler) { + ensureOptionsInitialized(); + options.add(handler); + return handler; + } + + private void ensureOptionsInitialized() { + if (options == null) { + help = new HelpOption(); + options = Lists.newArrayList(); + addOption(help, help); + } + } + + private boolean isHandlerSpecified(final OptionDef option) { + return option.handler() != OptionHandler.class; + } + + private <T> boolean isEnum(Setter<T> setter) { + return Enum.class.isAssignableFrom(setter.getType()); + } + + private <T> boolean isPrimitive(Setter<T> setter) { + return setter.getType().isPrimitive(); + } + } + + private static class HelpOption implements Option, Setter<Boolean> { + private boolean value; + + @Override + public String name() { + return "--help"; + } + + @Override + public String[] aliases() { + return new String[] {"-h"}; + } + + @Override + public String[] depends() { + return new String[] {}; + } + + @Override + public boolean hidden() { + return false; + } + + @Override + public String usage() { + return "display this help text"; + } + + @Override + public void addValue(Boolean val) { + value = val; + } + + @Override + public Class<? extends OptionHandler<Boolean>> handler() { + return BooleanOptionHandler.class; + } + + @Override + public String metaVar() { + return ""; + } + + @Override + public boolean required() { + return false; + } + + @Override + public Class<? extends Annotation> annotationType() { + return Option.class; + } + + @Override + public FieldSetter asFieldSetter() { + throw new UnsupportedOperationException(); + } + + @Override + public AnnotatedElement asAnnotatedElement() { + throw new UnsupportedOperationException(); + } + + @Override + public Class<Boolean> getType() { + return Boolean.class; + } + + @Override + public boolean isMultiValued() { + return false; + } + } +} diff --git a/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java b/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java new file mode 100644 index 00000000..b1ace324 --- /dev/null +++ b/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java @@ -0,0 +1,43 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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 com.gitblit.utils.cli; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +public class SubcommandHandler extends OptionHandler<String> { + + public SubcommandHandler(CmdLineParser parser, + OptionDef option, Setter<String> setter) { + super(parser, option, setter); + } + + @Override + public final int parseArguments(final Parameters params) + throws CmdLineException { + setter.addValue(params.getParameter(0)); + owner.stopOptionParsing(); + return 1; + } + + @Override + public final String getDefaultMetaVariable() { + return "COMMAND"; + } +} |