You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1529 lines
43 KiB
1529 lines
43 KiB
/* |
|
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> |
|
* Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> |
|
* Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> |
|
* Copyright (C) 2008-2010, Google Inc. |
|
* Copyright (C) 2009, Google, Inc. |
|
* Copyright (C) 2009, JetBrains s.r.o. |
|
* Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> |
|
* Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> |
|
* Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com> |
|
* and other copyright owners as documented in the project's IP log. |
|
* |
|
* This program and the accompanying materials are made available |
|
* under the terms of the Eclipse Distribution License v1.0 which |
|
* accompanies this distribution, is reproduced below, and is |
|
* available at http://www.eclipse.org/org/documents/edl-v10.php |
|
* |
|
* 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 Eclipse Foundation, Inc. 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 org.eclipse.jgit.lib; |
|
|
|
import static java.nio.charset.StandardCharsets.UTF_8; |
|
|
|
import java.text.MessageFormat; |
|
import java.util.ArrayList; |
|
import java.util.Collections; |
|
import java.util.List; |
|
import java.util.Locale; |
|
import java.util.Set; |
|
import java.util.concurrent.TimeUnit; |
|
import java.util.concurrent.atomic.AtomicReference; |
|
|
|
import org.eclipse.jgit.errors.ConfigInvalidException; |
|
import org.eclipse.jgit.events.ConfigChangedEvent; |
|
import org.eclipse.jgit.events.ConfigChangedListener; |
|
import org.eclipse.jgit.events.ListenerHandle; |
|
import org.eclipse.jgit.events.ListenerList; |
|
import org.eclipse.jgit.internal.JGitText; |
|
import org.eclipse.jgit.transport.RefSpec; |
|
import org.eclipse.jgit.util.RawParseUtils; |
|
|
|
/** |
|
* Git style {@code .config}, {@code .gitconfig}, {@code .gitmodules} file. |
|
*/ |
|
public class Config { |
|
|
|
private static final String[] EMPTY_STRING_ARRAY = {}; |
|
|
|
static final long KiB = 1024; |
|
static final long MiB = 1024 * KiB; |
|
static final long GiB = 1024 * MiB; |
|
private static final int MAX_DEPTH = 10; |
|
|
|
private static final TypedConfigGetter DEFAULT_GETTER = new DefaultTypedConfigGetter(); |
|
|
|
private static TypedConfigGetter typedGetter = DEFAULT_GETTER; |
|
|
|
/** the change listeners */ |
|
private final ListenerList listeners = new ListenerList(); |
|
|
|
/** |
|
* Immutable current state of the configuration data. |
|
* <p> |
|
* This state is copy-on-write. It should always contain an immutable list |
|
* of the configuration keys/values. |
|
*/ |
|
private final AtomicReference<ConfigSnapshot> state; |
|
|
|
private final Config baseConfig; |
|
|
|
/** |
|
* Magic value indicating a missing entry. |
|
* <p> |
|
* This value is tested for reference equality in some contexts, so we |
|
* must ensure it is a special copy of the empty string. It also must |
|
* be treated like the empty string. |
|
*/ |
|
private static final String MISSING_ENTRY = new String(); |
|
|
|
/** |
|
* Create a configuration with no default fallback. |
|
*/ |
|
public Config() { |
|
this(null); |
|
} |
|
|
|
/** |
|
* Create an empty configuration with a fallback for missing keys. |
|
* |
|
* @param defaultConfig |
|
* the base configuration to be consulted when a key is missing |
|
* from this configuration instance. |
|
*/ |
|
public Config(Config defaultConfig) { |
|
baseConfig = defaultConfig; |
|
state = new AtomicReference<>(newState()); |
|
} |
|
|
|
/** |
|
* Check if a given string is the "missing" value. |
|
* |
|
* @param value string to be checked. |
|
* @return true if the given string is the "missing" value. |
|
* @since 5.4 |
|
*/ |
|
@SuppressWarnings({ "ReferenceEquality", "StringEquality" }) |
|
public static boolean isMissing(String value) { |
|
return value == MISSING_ENTRY; |
|
} |
|
|
|
/** |
|
* Globally sets a {@link org.eclipse.jgit.lib.TypedConfigGetter} that is |
|
* subsequently used to read typed values from all git configs. |
|
* |
|
* @param getter |
|
* to use; if {@code null} use the default getter. |
|
* @since 4.9 |
|
*/ |
|
public static void setTypedConfigGetter(TypedConfigGetter getter) { |
|
typedGetter = getter == null ? DEFAULT_GETTER : getter; |
|
} |
|
|
|
/** |
|
* Escape the value before saving |
|
* |
|
* @param x |
|
* the value to escape |
|
* @return the escaped value |
|
*/ |
|
static String escapeValue(String x) { |
|
if (x.isEmpty()) { |
|
return ""; //$NON-NLS-1$ |
|
} |
|
|
|
boolean needQuote = x.charAt(0) == ' ' || x.charAt(x.length() - 1) == ' '; |
|
StringBuilder r = new StringBuilder(x.length()); |
|
for (int k = 0; k < x.length(); k++) { |
|
char c = x.charAt(k); |
|
// git-config(1) lists the limited set of supported escape sequences, but |
|
// the documentation is otherwise not especially normative. In particular, |
|
// which ones of these produce and/or require escaping and/or quoting |
|
// around them is not documented and was discovered by trial and error. |
|
// In summary: |
|
// |
|
// * Quotes are only required if there is leading/trailing whitespace or a |
|
// comment character. |
|
// * Bytes that have a supported escape sequence are escaped, except for |
|
// \b for some reason which isn't. |
|
// * Needing an escape sequence is not sufficient reason to quote the |
|
// value. |
|
switch (c) { |
|
case '\0': |
|
// Unix command line calling convention cannot pass a '\0' as an |
|
// argument, so there is no equivalent way in C git to store a null byte |
|
// in a config value. |
|
throw new IllegalArgumentException( |
|
JGitText.get().configValueContainsNullByte); |
|
|
|
case '\n': |
|
r.append('\\').append('n'); |
|
break; |
|
|
|
case '\t': |
|
r.append('\\').append('t'); |
|
break; |
|
|
|
case '\b': |
|
// Doesn't match `git config foo.bar $'x\by'`, which doesn't escape the |
|
// \x08, but since both escaped and unescaped forms are readable, we'll |
|
// prefer internal consistency here. |
|
r.append('\\').append('b'); |
|
break; |
|
|
|
case '\\': |
|
r.append('\\').append('\\'); |
|
break; |
|
|
|
case '"': |
|
r.append('\\').append('"'); |
|
break; |
|
|
|
case '#': |
|
case ';': |
|
needQuote = true; |
|
r.append(c); |
|
break; |
|
|
|
default: |
|
r.append(c); |
|
break; |
|
} |
|
} |
|
|
|
return needQuote ? '"' + r.toString() + '"' : r.toString(); |
|
} |
|
|
|
static String escapeSubsection(String x) { |
|
if (x.isEmpty()) { |
|
return "\"\""; //$NON-NLS-1$ |
|
} |
|
|
|
StringBuilder r = new StringBuilder(x.length() + 2).append('"'); |
|
for (int k = 0; k < x.length(); k++) { |
|
char c = x.charAt(k); |
|
|
|
// git-config(1) lists the limited set of supported escape sequences |
|
// (which is even more limited for subsection names than for values). |
|
switch (c) { |
|
case '\0': |
|
throw new IllegalArgumentException( |
|
JGitText.get().configSubsectionContainsNullByte); |
|
|
|
case '\n': |
|
throw new IllegalArgumentException( |
|
JGitText.get().configSubsectionContainsNewline); |
|
|
|
case '\\': |
|
case '"': |
|
r.append('\\').append(c); |
|
break; |
|
|
|
default: |
|
r.append(c); |
|
break; |
|
} |
|
} |
|
|
|
return r.append('"').toString(); |
|
} |
|
|
|
/** |
|
* Obtain an integer value from the configuration. |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return an integer value from the configuration, or defaultValue. |
|
*/ |
|
public int getInt(final String section, final String name, |
|
final int defaultValue) { |
|
return typedGetter.getInt(this, section, null, name, defaultValue); |
|
} |
|
|
|
/** |
|
* Obtain an integer value from the configuration. |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param subsection |
|
* subsection name, such a remote or branch name. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return an integer value from the configuration, or defaultValue. |
|
*/ |
|
public int getInt(final String section, String subsection, |
|
final String name, final int defaultValue) { |
|
return typedGetter.getInt(this, section, subsection, name, |
|
defaultValue); |
|
} |
|
|
|
/** |
|
* Obtain an integer value from the configuration. |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return an integer value from the configuration, or defaultValue. |
|
*/ |
|
public long getLong(String section, String name, long defaultValue) { |
|
return typedGetter.getLong(this, section, null, name, defaultValue); |
|
} |
|
|
|
/** |
|
* Obtain an integer value from the configuration. |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param subsection |
|
* subsection name, such a remote or branch name. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return an integer value from the configuration, or defaultValue. |
|
*/ |
|
public long getLong(final String section, String subsection, |
|
final String name, final long defaultValue) { |
|
return typedGetter.getLong(this, section, subsection, name, |
|
defaultValue); |
|
} |
|
|
|
/** |
|
* Get a boolean value from the git config |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return true if any value or defaultValue is true, false for missing or |
|
* explicit false |
|
*/ |
|
public boolean getBoolean(final String section, final String name, |
|
final boolean defaultValue) { |
|
return typedGetter.getBoolean(this, section, null, name, defaultValue); |
|
} |
|
|
|
/** |
|
* Get a boolean value from the git config |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param subsection |
|
* subsection name, such a remote or branch name. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return true if any value or defaultValue is true, false for missing or |
|
* explicit false |
|
*/ |
|
public boolean getBoolean(final String section, String subsection, |
|
final String name, final boolean defaultValue) { |
|
return typedGetter.getBoolean(this, section, subsection, name, |
|
defaultValue); |
|
} |
|
|
|
/** |
|
* Parse an enumeration from the configuration. |
|
* |
|
* @param section |
|
* section the key is grouped within. |
|
* @param subsection |
|
* subsection name, such a remote or branch name. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return the selected enumeration value, or {@code defaultValue}. |
|
*/ |
|
public <T extends Enum<?>> T getEnum(final String section, |
|
final String subsection, final String name, final T defaultValue) { |
|
final T[] all = allValuesOf(defaultValue); |
|
return typedGetter.getEnum(this, all, section, subsection, name, |
|
defaultValue); |
|
} |
|
|
|
@SuppressWarnings("unchecked") |
|
private static <T> T[] allValuesOf(T value) { |
|
try { |
|
return (T[]) value.getClass().getMethod("values").invoke(null); //$NON-NLS-1$ |
|
} catch (Exception err) { |
|
String typeName = value.getClass().getName(); |
|
String msg = MessageFormat.format( |
|
JGitText.get().enumValuesNotAvailable, typeName); |
|
throw new IllegalArgumentException(msg, err); |
|
} |
|
} |
|
|
|
/** |
|
* Parse an enumeration from the configuration. |
|
* |
|
* @param all |
|
* all possible values in the enumeration which should be |
|
* recognized. Typically {@code EnumType.values()}. |
|
* @param section |
|
* section the key is grouped within. |
|
* @param subsection |
|
* subsection name, such a remote or branch name. |
|
* @param name |
|
* name of the key to get. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @return the selected enumeration value, or {@code defaultValue}. |
|
*/ |
|
public <T extends Enum<?>> T getEnum(final T[] all, final String section, |
|
final String subsection, final String name, final T defaultValue) { |
|
return typedGetter.getEnum(this, all, section, subsection, name, |
|
defaultValue); |
|
} |
|
|
|
/** |
|
* Get string value or null if not found. |
|
* |
|
* @param section |
|
* the section |
|
* @param subsection |
|
* the subsection for the value |
|
* @param name |
|
* the key name |
|
* @return a String value from the config, <code>null</code> if not found |
|
*/ |
|
public String getString(final String section, String subsection, |
|
final String name) { |
|
return getRawString(section, subsection, name); |
|
} |
|
|
|
/** |
|
* Get a list of string values |
|
* <p> |
|
* If this instance was created with a base, the base's values are returned |
|
* first (if any). |
|
* |
|
* @param section |
|
* the section |
|
* @param subsection |
|
* the subsection for the value |
|
* @param name |
|
* the key name |
|
* @return array of zero or more values from the configuration. |
|
*/ |
|
public String[] getStringList(final String section, String subsection, |
|
final String name) { |
|
String[] base; |
|
if (baseConfig != null) |
|
base = baseConfig.getStringList(section, subsection, name); |
|
else |
|
base = EMPTY_STRING_ARRAY; |
|
|
|
String[] self = getRawStringList(section, subsection, name); |
|
if (self == null) |
|
return base; |
|
if (base.length == 0) |
|
return self; |
|
String[] res = new String[base.length + self.length]; |
|
int n = base.length; |
|
System.arraycopy(base, 0, res, 0, n); |
|
System.arraycopy(self, 0, res, n, self.length); |
|
return res; |
|
} |
|
|
|
/** |
|
* Parse a numerical time unit, such as "1 minute", from the configuration. |
|
* |
|
* @param section |
|
* section the key is in. |
|
* @param subsection |
|
* subsection the key is in, or null if not in a subsection. |
|
* @param name |
|
* the key name. |
|
* @param defaultValue |
|
* default value to return if no value was present. |
|
* @param wantUnit |
|
* the units of {@code defaultValue} and the return value, as |
|
* well as the units to assume if the value does not contain an |
|
* indication of the units. |
|
* @return the value, or {@code defaultValue} if not set, expressed in |
|
* {@code units}. |
|
* @since 4.5 |
|
*/ |
|
public long getTimeUnit(String section, String subsection, String name, |
|
long defaultValue, TimeUnit wantUnit) { |
|
return typedGetter.getTimeUnit(this, section, subsection, name, |
|
defaultValue, wantUnit); |
|
} |
|
|
|
/** |
|
* Parse a list of {@link org.eclipse.jgit.transport.RefSpec}s from the |
|
* configuration. |
|
* |
|
* @param section |
|
* section the key is in. |
|
* @param subsection |
|
* subsection the key is in, or null if not in a subsection. |
|
* @param name |
|
* the key name. |
|
* @return a possibly empty list of |
|
* {@link org.eclipse.jgit.transport.RefSpec}s |
|
* @since 4.9 |
|
*/ |
|
public List<RefSpec> getRefSpecs(String section, String subsection, |
|
String name) { |
|
return typedGetter.getRefSpecs(this, section, subsection, name); |
|
} |
|
|
|
/** |
|
* Get set of all subsections of specified section within this configuration |
|
* and its base configuration |
|
* |
|
* @param section |
|
* section to search for. |
|
* @return set of all subsections of specified section within this |
|
* configuration and its base configuration; may be empty if no |
|
* subsection exists. The set's iterator returns sections in the |
|
* order they are declared by the configuration starting from this |
|
* instance and progressing through the base. |
|
*/ |
|
public Set<String> getSubsections(String section) { |
|
return getState().getSubsections(section); |
|
} |
|
|
|
/** |
|
* Get the sections defined in this {@link org.eclipse.jgit.lib.Config}. |
|
* |
|
* @return the sections defined in this {@link org.eclipse.jgit.lib.Config}. |
|
* The set's iterator returns sections in the order they are |
|
* declared by the configuration starting from this instance and |
|
* progressing through the base. |
|
*/ |
|
public Set<String> getSections() { |
|
return getState().getSections(); |
|
} |
|
|
|
/** |
|
* Get the list of names defined for this section |
|
* |
|
* @param section |
|
* the section |
|
* @return the list of names defined for this section |
|
*/ |
|
public Set<String> getNames(String section) { |
|
return getNames(section, null); |
|
} |
|
|
|
/** |
|
* Get the list of names defined for this subsection |
|
* |
|
* @param section |
|
* the section |
|
* @param subsection |
|
* the subsection |
|
* @return the list of names defined for this subsection |
|
*/ |
|
public Set<String> getNames(String section, String subsection) { |
|
return getState().getNames(section, subsection); |
|
} |
|
|
|
/** |
|
* Get the list of names defined for this section |
|
* |
|
* @param section |
|
* the section |
|
* @param recursive |
|
* if {@code true} recursively adds the names defined in all base |
|
* configurations |
|
* @return the list of names defined for this section |
|
* @since 3.2 |
|
*/ |
|
public Set<String> getNames(String section, boolean recursive) { |
|
return getState().getNames(section, null, recursive); |
|
} |
|
|
|
/** |
|
* Get the list of names defined for this section |
|
* |
|
* @param section |
|
* the section |
|
* @param subsection |
|
* the subsection |
|
* @param recursive |
|
* if {@code true} recursively adds the names defined in all base |
|
* configurations |
|
* @return the list of names defined for this subsection |
|
* @since 3.2 |
|
*/ |
|
public Set<String> getNames(String section, String subsection, |
|
boolean recursive) { |
|
return getState().getNames(section, subsection, recursive); |
|
} |
|
|
|
/** |
|
* Obtain a handle to a parsed set of configuration values. |
|
* |
|
* @param <T> |
|
* type of configuration model to return. |
|
* @param parser |
|
* parser which can create the model if it is not already |
|
* available in this configuration file. The parser is also used |
|
* as the key into a cache and must obey the hashCode and equals |
|
* contract in order to reuse a parsed model. |
|
* @return the parsed object instance, which is cached inside this config. |
|
*/ |
|
@SuppressWarnings("unchecked") |
|
public <T> T get(SectionParser<T> parser) { |
|
final ConfigSnapshot myState = getState(); |
|
T obj = (T) myState.cache.get(parser); |
|
if (obj == null) { |
|
obj = parser.parse(this); |
|
myState.cache.put(parser, obj); |
|
} |
|
return obj; |
|
} |
|
|
|
/** |
|
* Remove a cached configuration object. |
|
* <p> |
|
* If the associated configuration object has not yet been cached, this |
|
* method has no effect. |
|
* |
|
* @param parser |
|
* parser used to obtain the configuration object. |
|
* @see #get(SectionParser) |
|
*/ |
|
public void uncache(SectionParser<?> parser) { |
|
state.get().cache.remove(parser); |
|
} |
|
|
|
/** |
|
* Adds a listener to be notified about changes. |
|
* <p> |
|
* Clients are supposed to remove the listeners after they are done with |
|
* them using the {@link org.eclipse.jgit.events.ListenerHandle#remove()} |
|
* method |
|
* |
|
* @param listener |
|
* the listener |
|
* @return the handle to the registered listener |
|
*/ |
|
public ListenerHandle addChangeListener(ConfigChangedListener listener) { |
|
return listeners.addConfigChangedListener(listener); |
|
} |
|
|
|
/** |
|
* Determine whether to issue change events for transient changes. |
|
* <p> |
|
* If <code>true</code> is returned (which is the default behavior), |
|
* {@link #fireConfigChangedEvent()} will be called upon each change. |
|
* <p> |
|
* Subclasses that override this to return <code>false</code> are |
|
* responsible for issuing {@link #fireConfigChangedEvent()} calls |
|
* themselves. |
|
* |
|
* @return <code></code> |
|
*/ |
|
protected boolean notifyUponTransientChanges() { |
|
return true; |
|
} |
|
|
|
/** |
|
* Notifies the listeners |
|
*/ |
|
protected void fireConfigChangedEvent() { |
|
listeners.dispatch(new ConfigChangedEvent()); |
|
} |
|
|
|
String getRawString(final String section, final String subsection, |
|
final String name) { |
|
String[] lst = getRawStringList(section, subsection, name); |
|
if (lst != null) { |
|
return lst[lst.length - 1]; |
|
} else if (baseConfig != null) { |
|
return baseConfig.getRawString(section, subsection, name); |
|
} else { |
|
return null; |
|
} |
|
} |
|
|
|
private String[] getRawStringList(String section, String subsection, |
|
String name) { |
|
return state.get().get(section, subsection, name); |
|
} |
|
|
|
private ConfigSnapshot getState() { |
|
ConfigSnapshot cur, upd; |
|
do { |
|
cur = state.get(); |
|
final ConfigSnapshot base = getBaseState(); |
|
if (cur.baseState == base) |
|
return cur; |
|
upd = new ConfigSnapshot(cur.entryList, base); |
|
} while (!state.compareAndSet(cur, upd)); |
|
return upd; |
|
} |
|
|
|
private ConfigSnapshot getBaseState() { |
|
return baseConfig != null ? baseConfig.getState() : null; |
|
} |
|
|
|
/** |
|
* Add or modify a configuration value. The parameters will result in a |
|
* configuration entry like this. |
|
* |
|
* <pre> |
|
* [section "subsection"] |
|
* name = value |
|
* </pre> |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
* @param value |
|
* parameter value |
|
*/ |
|
public void setInt(final String section, final String subsection, |
|
final String name, final int value) { |
|
setLong(section, subsection, name, value); |
|
} |
|
|
|
/** |
|
* Add or modify a configuration value. The parameters will result in a |
|
* configuration entry like this. |
|
* |
|
* <pre> |
|
* [section "subsection"] |
|
* name = value |
|
* </pre> |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
* @param value |
|
* parameter value |
|
*/ |
|
public void setLong(final String section, final String subsection, |
|
final String name, final long value) { |
|
final String s; |
|
|
|
if (value >= GiB && (value % GiB) == 0) |
|
s = String.valueOf(value / GiB) + "g"; //$NON-NLS-1$ |
|
else if (value >= MiB && (value % MiB) == 0) |
|
s = String.valueOf(value / MiB) + "m"; //$NON-NLS-1$ |
|
else if (value >= KiB && (value % KiB) == 0) |
|
s = String.valueOf(value / KiB) + "k"; //$NON-NLS-1$ |
|
else |
|
s = String.valueOf(value); |
|
|
|
setString(section, subsection, name, s); |
|
} |
|
|
|
/** |
|
* Add or modify a configuration value. The parameters will result in a |
|
* configuration entry like this. |
|
* |
|
* <pre> |
|
* [section "subsection"] |
|
* name = value |
|
* </pre> |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
* @param value |
|
* parameter value |
|
*/ |
|
public void setBoolean(final String section, final String subsection, |
|
final String name, final boolean value) { |
|
setString(section, subsection, name, value ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$ |
|
} |
|
|
|
/** |
|
* Add or modify a configuration value. The parameters will result in a |
|
* configuration entry like this. |
|
* |
|
* <pre> |
|
* [section "subsection"] |
|
* name = value |
|
* </pre> |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
* @param value |
|
* parameter value |
|
*/ |
|
public <T extends Enum<?>> void setEnum(final String section, |
|
final String subsection, final String name, final T value) { |
|
String n; |
|
if (value instanceof ConfigEnum) |
|
n = ((ConfigEnum) value).toConfigValue(); |
|
else |
|
n = value.name().toLowerCase(Locale.ROOT).replace('_', ' '); |
|
setString(section, subsection, name, n); |
|
} |
|
|
|
/** |
|
* Add or modify a configuration value. The parameters will result in a |
|
* configuration entry like this. |
|
* |
|
* <pre> |
|
* [section "subsection"] |
|
* name = value |
|
* </pre> |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
* @param value |
|
* parameter value, e.g. "true" |
|
*/ |
|
public void setString(final String section, final String subsection, |
|
final String name, final String value) { |
|
setStringList(section, subsection, name, Collections |
|
.singletonList(value)); |
|
} |
|
|
|
/** |
|
* Remove a configuration value. |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
*/ |
|
public void unset(final String section, final String subsection, |
|
final String name) { |
|
setStringList(section, subsection, name, Collections |
|
.<String> emptyList()); |
|
} |
|
|
|
/** |
|
* Remove all configuration values under a single section. |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
*/ |
|
public void unsetSection(String section, String subsection) { |
|
ConfigSnapshot src, res; |
|
do { |
|
src = state.get(); |
|
res = unsetSection(src, section, subsection); |
|
} while (!state.compareAndSet(src, res)); |
|
} |
|
|
|
private ConfigSnapshot unsetSection(final ConfigSnapshot srcState, |
|
final String section, |
|
final String subsection) { |
|
final int max = srcState.entryList.size(); |
|
final ArrayList<ConfigLine> r = new ArrayList<>(max); |
|
|
|
boolean lastWasMatch = false; |
|
for (ConfigLine e : srcState.entryList) { |
|
if (e.includedFrom == null && e.match(section, subsection)) { |
|
// Skip this record, it's for the section we are removing. |
|
lastWasMatch = true; |
|
continue; |
|
} |
|
|
|
if (lastWasMatch && e.section == null && e.subsection == null) |
|
continue; // skip this padding line in the section. |
|
r.add(e); |
|
} |
|
|
|
return newState(r); |
|
} |
|
|
|
/** |
|
* Set a configuration value. |
|
* |
|
* <pre> |
|
* [section "subsection"] |
|
* name = value1 |
|
* name = value2 |
|
* </pre> |
|
* |
|
* @param section |
|
* section name, e.g "branch" |
|
* @param subsection |
|
* optional subsection value, e.g. a branch name |
|
* @param name |
|
* parameter name, e.g. "filemode" |
|
* @param values |
|
* list of zero or more values for this key. |
|
*/ |
|
public void setStringList(final String section, final String subsection, |
|
final String name, final List<String> values) { |
|
ConfigSnapshot src, res; |
|
do { |
|
src = state.get(); |
|
res = replaceStringList(src, section, subsection, name, values); |
|
} while (!state.compareAndSet(src, res)); |
|
if (notifyUponTransientChanges()) |
|
fireConfigChangedEvent(); |
|
} |
|
|
|
private ConfigSnapshot replaceStringList(final ConfigSnapshot srcState, |
|
final String section, final String subsection, final String name, |
|
final List<String> values) { |
|
final List<ConfigLine> entries = copy(srcState, values); |
|
int entryIndex = 0; |
|
int valueIndex = 0; |
|
int insertPosition = -1; |
|
|
|
// Reset the first n Entry objects that match this input name. |
|
// |
|
while (entryIndex < entries.size() && valueIndex < values.size()) { |
|
final ConfigLine e = entries.get(entryIndex); |
|
if (e.includedFrom == null && e.match(section, subsection, name)) { |
|
entries.set(entryIndex, e.forValue(values.get(valueIndex++))); |
|
insertPosition = entryIndex + 1; |
|
} |
|
entryIndex++; |
|
} |
|
|
|
// Remove any extra Entry objects that we no longer need. |
|
// |
|
if (valueIndex == values.size() && entryIndex < entries.size()) { |
|
while (entryIndex < entries.size()) { |
|
final ConfigLine e = entries.get(entryIndex++); |
|
if (e.includedFrom == null |
|
&& e.match(section, subsection, name)) |
|
entries.remove(--entryIndex); |
|
} |
|
} |
|
|
|
// Insert new Entry objects for additional/new values. |
|
// |
|
if (valueIndex < values.size() && entryIndex == entries.size()) { |
|
if (insertPosition < 0) { |
|
// We didn't find a matching key above, but maybe there |
|
// is already a section available that matches. Insert |
|
// after the last key of that section. |
|
// |
|
insertPosition = findSectionEnd(entries, section, subsection, |
|
true); |
|
} |
|
if (insertPosition < 0) { |
|
// We didn't find any matching section header for this key, |
|
// so we must create a new section header at the end. |
|
// |
|
final ConfigLine e = new ConfigLine(); |
|
e.section = section; |
|
e.subsection = subsection; |
|
entries.add(e); |
|
insertPosition = entries.size(); |
|
} |
|
while (valueIndex < values.size()) { |
|
final ConfigLine e = new ConfigLine(); |
|
e.section = section; |
|
e.subsection = subsection; |
|
e.name = name; |
|
e.value = values.get(valueIndex++); |
|
entries.add(insertPosition++, e); |
|
} |
|
} |
|
|
|
return newState(entries); |
|
} |
|
|
|
private static List<ConfigLine> copy(final ConfigSnapshot src, |
|
final List<String> values) { |
|
// At worst we need to insert 1 line for each value, plus 1 line |
|
// for a new section header. Assume that and allocate the space. |
|
// |
|
final int max = src.entryList.size() + values.size() + 1; |
|
final ArrayList<ConfigLine> r = new ArrayList<>(max); |
|
r.addAll(src.entryList); |
|
return r; |
|
} |
|
|
|
private static int findSectionEnd(final List<ConfigLine> entries, |
|
final String section, final String subsection, |
|
boolean skipIncludedLines) { |
|
for (int i = 0; i < entries.size(); i++) { |
|
ConfigLine e = entries.get(i); |
|
if (e.includedFrom != null && skipIncludedLines) { |
|
continue; |
|
} |
|
|
|
if (e.match(section, subsection, null)) { |
|
i++; |
|
while (i < entries.size()) { |
|
e = entries.get(i); |
|
if (e.match(section, subsection, e.name)) |
|
i++; |
|
else |
|
break; |
|
} |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
/** |
|
* Get this configuration, formatted as a Git style text file. |
|
* |
|
* @return this configuration, formatted as a Git style text file. |
|
*/ |
|
public String toText() { |
|
final StringBuilder out = new StringBuilder(); |
|
for (ConfigLine e : state.get().entryList) { |
|
if (e.includedFrom != null) |
|
continue; |
|
if (e.prefix != null) |
|
out.append(e.prefix); |
|
if (e.section != null && e.name == null) { |
|
out.append('['); |
|
out.append(e.section); |
|
if (e.subsection != null) { |
|
out.append(' '); |
|
String escaped = escapeValue(e.subsection); |
|
// make sure to avoid double quotes here |
|
boolean quoted = escaped.startsWith("\"") //$NON-NLS-1$ |
|
&& escaped.endsWith("\""); //$NON-NLS-1$ |
|
if (!quoted) |
|
out.append('"'); |
|
out.append(escaped); |
|
if (!quoted) |
|
out.append('"'); |
|
} |
|
out.append(']'); |
|
} else if (e.section != null && e.name != null) { |
|
if (e.prefix == null || "".equals(e.prefix)) //$NON-NLS-1$ |
|
out.append('\t'); |
|
out.append(e.name); |
|
if (!isMissing(e.value)) { |
|
out.append(" ="); //$NON-NLS-1$ |
|
if (e.value != null) { |
|
out.append(' '); |
|
out.append(escapeValue(e.value)); |
|
} |
|
} |
|
if (e.suffix != null) |
|
out.append(' '); |
|
} |
|
if (e.suffix != null) |
|
out.append(e.suffix); |
|
out.append('\n'); |
|
} |
|
return out.toString(); |
|
} |
|
|
|
/** |
|
* Clear this configuration and reset to the contents of the parsed string. |
|
* |
|
* @param text |
|
* Git style text file listing configuration properties. |
|
* @throws org.eclipse.jgit.errors.ConfigInvalidException |
|
* the text supplied is not formatted correctly. No changes were |
|
* made to {@code this}. |
|
*/ |
|
public void fromText(String text) throws ConfigInvalidException { |
|
state.set(newState(fromTextRecurse(text, 1, null))); |
|
} |
|
|
|
private List<ConfigLine> fromTextRecurse(String text, int depth, |
|
String includedFrom) throws ConfigInvalidException { |
|
if (depth > MAX_DEPTH) { |
|
throw new ConfigInvalidException( |
|
JGitText.get().tooManyIncludeRecursions); |
|
} |
|
final List<ConfigLine> newEntries = new ArrayList<>(); |
|
final StringReader in = new StringReader(text); |
|
ConfigLine last = null; |
|
ConfigLine e = new ConfigLine(); |
|
e.includedFrom = includedFrom; |
|
for (;;) { |
|
int input = in.read(); |
|
if (-1 == input) { |
|
if (e.section != null) |
|
newEntries.add(e); |
|
break; |
|
} |
|
|
|
final char c = (char) input; |
|
if ('\n' == c) { |
|
// End of this entry. |
|
newEntries.add(e); |
|
if (e.section != null) |
|
last = e; |
|
e = new ConfigLine(); |
|
e.includedFrom = includedFrom; |
|
} else if (e.suffix != null) { |
|
// Everything up until the end-of-line is in the suffix. |
|
e.suffix += c; |
|
|
|
} else if (';' == c || '#' == c) { |
|
// The rest of this line is a comment; put into suffix. |
|
e.suffix = String.valueOf(c); |
|
|
|
} else if (e.section == null && Character.isWhitespace(c)) { |
|
// Save the leading whitespace (if any). |
|
if (e.prefix == null) |
|
e.prefix = ""; //$NON-NLS-1$ |
|
e.prefix += c; |
|
|
|
} else if ('[' == c) { |
|
// This is a section header. |
|
e.section = readSectionName(in); |
|
input = in.read(); |
|
if ('"' == input) { |
|
e.subsection = readSubsectionName(in); |
|
input = in.read(); |
|
} |
|
if (']' != input) |
|
throw new ConfigInvalidException(JGitText.get().badGroupHeader); |
|
e.suffix = ""; //$NON-NLS-1$ |
|
|
|
} else if (last != null) { |
|
// Read a value. |
|
e.section = last.section; |
|
e.subsection = last.subsection; |
|
in.reset(); |
|
e.name = readKeyName(in); |
|
if (e.name.endsWith("\n")) { //$NON-NLS-1$ |
|
e.name = e.name.substring(0, e.name.length() - 1); |
|
e.value = MISSING_ENTRY; |
|
} else |
|
e.value = readValue(in); |
|
|
|
if (e.section.equalsIgnoreCase("include")) { //$NON-NLS-1$ |
|
addIncludedConfig(newEntries, e, depth); |
|
} |
|
} else |
|
throw new ConfigInvalidException(JGitText.get().invalidLineInConfigFile); |
|
} |
|
|
|
return newEntries; |
|
} |
|
|
|
/** |
|
* Read the included config from the specified (possibly) relative path |
|
* |
|
* @param relPath |
|
* possibly relative path to the included config, as specified in |
|
* this config |
|
* @return the read bytes, or null if the included config should be ignored |
|
* @throws org.eclipse.jgit.errors.ConfigInvalidException |
|
* if something went wrong while reading the config |
|
* @since 4.10 |
|
*/ |
|
protected byte[] readIncludedConfig(String relPath) |
|
throws ConfigInvalidException { |
|
return null; |
|
} |
|
|
|
private void addIncludedConfig(final List<ConfigLine> newEntries, |
|
ConfigLine line, int depth) throws ConfigInvalidException { |
|
if (!line.name.equalsIgnoreCase("path") || //$NON-NLS-1$ |
|
line.value == null || line.value.equals(MISSING_ENTRY)) { |
|
throw new ConfigInvalidException(MessageFormat.format( |
|
JGitText.get().invalidLineInConfigFileWithParam, line)); |
|
} |
|
byte[] bytes = readIncludedConfig(line.value); |
|
if (bytes == null) { |
|
return; |
|
} |
|
|
|
String decoded; |
|
if (isUtf8(bytes)) { |
|
decoded = RawParseUtils.decode(UTF_8, bytes, 3, bytes.length); |
|
} else { |
|
decoded = RawParseUtils.decode(bytes); |
|
} |
|
try { |
|
newEntries.addAll(fromTextRecurse(decoded, depth + 1, line.value)); |
|
} catch (ConfigInvalidException e) { |
|
throw new ConfigInvalidException(MessageFormat |
|
.format(JGitText.get().cannotReadFile, line.value), e); |
|
} |
|
} |
|
|
|
private ConfigSnapshot newState() { |
|
return new ConfigSnapshot(Collections.<ConfigLine> emptyList(), |
|
getBaseState()); |
|
} |
|
|
|
private ConfigSnapshot newState(List<ConfigLine> entries) { |
|
return new ConfigSnapshot(Collections.unmodifiableList(entries), |
|
getBaseState()); |
|
} |
|
|
|
/** |
|
* Clear the configuration file |
|
*/ |
|
protected void clear() { |
|
state.set(newState()); |
|
} |
|
|
|
/** |
|
* Check if bytes should be treated as UTF-8 or not. |
|
* |
|
* @param bytes |
|
* the bytes to check encoding for. |
|
* @return true if bytes should be treated as UTF-8, false otherwise. |
|
* @since 4.4 |
|
*/ |
|
protected boolean isUtf8(final byte[] bytes) { |
|
return bytes.length >= 3 && bytes[0] == (byte) 0xEF |
|
&& bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF; |
|
} |
|
|
|
private static String readSectionName(StringReader in) |
|
throws ConfigInvalidException { |
|
final StringBuilder name = new StringBuilder(); |
|
for (;;) { |
|
int c = in.read(); |
|
if (c < 0) |
|
throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile); |
|
|
|
if (']' == c) { |
|
in.reset(); |
|
break; |
|
} |
|
|
|
if (' ' == c || '\t' == c) { |
|
for (;;) { |
|
c = in.read(); |
|
if (c < 0) |
|
throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile); |
|
|
|
if ('"' == c) { |
|
in.reset(); |
|
break; |
|
} |
|
|
|
if (' ' == c || '\t' == c) |
|
continue; // Skipped... |
|
throw new ConfigInvalidException(MessageFormat.format(JGitText.get().badSectionEntry, name)); |
|
} |
|
break; |
|
} |
|
|
|
if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c) |
|
name.append((char) c); |
|
else |
|
throw new ConfigInvalidException(MessageFormat.format(JGitText.get().badSectionEntry, name)); |
|
} |
|
return name.toString(); |
|
} |
|
|
|
private static String readKeyName(StringReader in) |
|
throws ConfigInvalidException { |
|
final StringBuilder name = new StringBuilder(); |
|
for (;;) { |
|
int c = in.read(); |
|
if (c < 0) |
|
throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile); |
|
|
|
if ('=' == c) |
|
break; |
|
|
|
if (' ' == c || '\t' == c) { |
|
for (;;) { |
|
c = in.read(); |
|
if (c < 0) |
|
throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile); |
|
|
|
if ('=' == c) |
|
break; |
|
|
|
if (';' == c || '#' == c || '\n' == c) { |
|
in.reset(); |
|
break; |
|
} |
|
|
|
if (' ' == c || '\t' == c) |
|
continue; // Skipped... |
|
throw new ConfigInvalidException(JGitText.get().badEntryDelimiter); |
|
} |
|
break; |
|
} |
|
|
|
if (Character.isLetterOrDigit((char) c) || c == '-') { |
|
// From the git-config man page: |
|
// The variable names are case-insensitive and only |
|
// alphanumeric characters and - are allowed. |
|
name.append((char) c); |
|
} else if ('\n' == c) { |
|
in.reset(); |
|
name.append((char) c); |
|
break; |
|
} else |
|
throw new ConfigInvalidException(MessageFormat.format(JGitText.get().badEntryName, name)); |
|
} |
|
return name.toString(); |
|
} |
|
|
|
private static String readSubsectionName(StringReader in) |
|
throws ConfigInvalidException { |
|
StringBuilder r = new StringBuilder(); |
|
for (;;) { |
|
int c = in.read(); |
|
if (c < 0) { |
|
break; |
|
} |
|
|
|
if ('\n' == c) { |
|
throw new ConfigInvalidException( |
|
JGitText.get().newlineInQuotesNotAllowed); |
|
} |
|
if ('\\' == c) { |
|
c = in.read(); |
|
switch (c) { |
|
case -1: |
|
throw new ConfigInvalidException(JGitText.get().endOfFileInEscape); |
|
|
|
case '\\': |
|
case '"': |
|
r.append((char) c); |
|
continue; |
|
|
|
default: |
|
// C git simply drops backslashes if the escape sequence is not |
|
// recognized. |
|
r.append((char) c); |
|
continue; |
|
} |
|
} |
|
if ('"' == c) { |
|
break; |
|
} |
|
|
|
r.append((char) c); |
|
} |
|
return r.toString(); |
|
} |
|
|
|
private static String readValue(StringReader in) |
|
throws ConfigInvalidException { |
|
StringBuilder value = new StringBuilder(); |
|
StringBuilder trailingSpaces = null; |
|
boolean quote = false; |
|
boolean inLeadingSpace = true; |
|
|
|
for (;;) { |
|
int c = in.read(); |
|
if (c < 0) { |
|
break; |
|
} |
|
if ('\n' == c) { |
|
if (quote) { |
|
throw new ConfigInvalidException( |
|
JGitText.get().newlineInQuotesNotAllowed); |
|
} |
|
in.reset(); |
|
break; |
|
} |
|
|
|
if (!quote && (';' == c || '#' == c)) { |
|
if (trailingSpaces != null) { |
|
trailingSpaces.setLength(0); |
|
} |
|
in.reset(); |
|
break; |
|
} |
|
|
|
char cc = (char) c; |
|
if (Character.isWhitespace(cc)) { |
|
if (inLeadingSpace) { |
|
continue; |
|
} |
|
if (trailingSpaces == null) { |
|
trailingSpaces = new StringBuilder(); |
|
} |
|
trailingSpaces.append(cc); |
|
continue; |
|
} |
|
inLeadingSpace = false; |
|
if (trailingSpaces != null) { |
|
value.append(trailingSpaces); |
|
trailingSpaces.setLength(0); |
|
} |
|
|
|
if ('\\' == c) { |
|
c = in.read(); |
|
switch (c) { |
|
case -1: |
|
throw new ConfigInvalidException(JGitText.get().endOfFileInEscape); |
|
case '\n': |
|
continue; |
|
case 't': |
|
value.append('\t'); |
|
continue; |
|
case 'b': |
|
value.append('\b'); |
|
continue; |
|
case 'n': |
|
value.append('\n'); |
|
continue; |
|
case '\\': |
|
value.append('\\'); |
|
continue; |
|
case '"': |
|
value.append('"'); |
|
continue; |
|
case '\r': { |
|
int next = in.read(); |
|
if (next == '\n') { |
|
continue; // CR-LF |
|
} else if (next >= 0) { |
|
in.reset(); |
|
} |
|
break; |
|
} |
|
default: |
|
break; |
|
} |
|
throw new ConfigInvalidException( |
|
MessageFormat.format(JGitText.get().badEscape, |
|
Character.isAlphabetic(c) |
|
? Character.valueOf(((char) c)) |
|
: toUnicodeLiteral(c))); |
|
} |
|
|
|
if ('"' == c) { |
|
quote = !quote; |
|
continue; |
|
} |
|
|
|
value.append(cc); |
|
} |
|
return value.length() > 0 ? value.toString() : null; |
|
} |
|
|
|
private static String toUnicodeLiteral(int c) { |
|
return String.format("\\u%04x", //$NON-NLS-1$ |
|
Integer.valueOf(c)); |
|
} |
|
|
|
/** |
|
* Parses a section of the configuration into an application model object. |
|
* <p> |
|
* Instances must implement hashCode and equals such that model objects can |
|
* be cached by using the {@code SectionParser} as a key of a HashMap. |
|
* <p> |
|
* As the {@code SectionParser} itself is used as the key of the internal |
|
* HashMap applications should be careful to ensure the SectionParser key |
|
* does not retain unnecessary application state which may cause memory to |
|
* be held longer than expected. |
|
* |
|
* @param <T> |
|
* type of the application model created by the parser. |
|
*/ |
|
public static interface SectionParser<T> { |
|
/** |
|
* Create a model object from a configuration. |
|
* |
|
* @param cfg |
|
* the configuration to read values from. |
|
* @return the application model instance. |
|
*/ |
|
T parse(Config cfg); |
|
} |
|
|
|
private static class StringReader { |
|
private final char[] buf; |
|
|
|
private int pos; |
|
|
|
StringReader(String in) { |
|
buf = in.toCharArray(); |
|
} |
|
|
|
int read() { |
|
if (pos >= buf.length) { |
|
return -1; |
|
} |
|
return buf[pos++]; |
|
} |
|
|
|
void reset() { |
|
pos--; |
|
} |
|
} |
|
|
|
/** |
|
* Converts enumeration values into configuration options and vice-versa, |
|
* allowing to match a config option with an enum value. |
|
* |
|
*/ |
|
public static interface ConfigEnum { |
|
/** |
|
* Converts enumeration value into a string to be save in config. |
|
* |
|
* @return the enum value as config string |
|
*/ |
|
String toConfigValue(); |
|
|
|
/** |
|
* Checks if the given string matches with enum value. |
|
* |
|
* @param in |
|
* the string to match |
|
* @return true if the given string matches enum value, false otherwise |
|
*/ |
|
boolean matchConfigValue(String in); |
|
} |
|
}
|
|
|