| /* |
| * 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 java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| 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 java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| 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.util.IO; |
| import org.eclipse.jgit.util.RawParseUtils; |
| import org.eclipse.jgit.util.StringUtils; |
| |
| |
| /** |
| * Git style {@code .config}, {@code .gitconfig}, {@code .gitmodules} file. |
| */ |
| public class Config { |
| private static final String[] EMPTY_STRING_ARRAY = {}; |
| private static final long KiB = 1024; |
| private static final long MiB = 1024 * KiB; |
| private static final long GiB = 1024 * MiB; |
| private static final int MAX_DEPTH = 10; |
| |
| /** 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 MAGIC_EMPTY_VALUE = 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()); |
| } |
| |
| /** |
| * Escape the value before saving |
| * |
| * @param x |
| * the value to escape |
| * @return the escaped value |
| */ |
| private static String escapeValue(final String x) { |
| boolean inquote = false; |
| int lineStart = 0; |
| final StringBuilder r = new StringBuilder(x.length()); |
| for (int k = 0; k < x.length(); k++) { |
| final char c = x.charAt(k); |
| switch (c) { |
| case '\n': |
| if (inquote) { |
| r.append('"'); |
| inquote = false; |
| } |
| r.append("\\n\\\n"); //$NON-NLS-1$ |
| lineStart = r.length(); |
| break; |
| |
| case '\t': |
| r.append("\\t"); //$NON-NLS-1$ |
| break; |
| |
| case '\b': |
| r.append("\\b"); //$NON-NLS-1$ |
| break; |
| |
| case '\\': |
| r.append("\\\\"); //$NON-NLS-1$ |
| break; |
| |
| case '"': |
| r.append("\\\""); //$NON-NLS-1$ |
| break; |
| |
| case ';': |
| case '#': |
| if (!inquote) { |
| r.insert(lineStart, '"'); |
| inquote = true; |
| } |
| r.append(c); |
| break; |
| |
| case ' ': |
| if (!inquote && r.length() > 0 |
| && r.charAt(r.length() - 1) == ' ') { |
| r.insert(lineStart, '"'); |
| inquote = true; |
| } |
| r.append(' '); |
| break; |
| |
| default: |
| r.append(c); |
| break; |
| } |
| } |
| if (inquote) { |
| r.append('"'); |
| } |
| return r.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 getInt(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) { |
| final long val = getLong(section, subsection, name, defaultValue); |
| if (Integer.MIN_VALUE <= val && val <= Integer.MAX_VALUE) |
| return (int) val; |
| throw new IllegalArgumentException(MessageFormat.format(JGitText.get().integerValueOutOfRange |
| , section, name)); |
| } |
| |
| /** |
| * 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 getLong(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) { |
| final String str = getString(section, subsection, name); |
| if (str == null) |
| return defaultValue; |
| |
| String n = str.trim(); |
| if (n.length() == 0) |
| return defaultValue; |
| |
| long mul = 1; |
| switch (StringUtils.toLowerCase(n.charAt(n.length() - 1))) { |
| case 'g': |
| mul = GiB; |
| break; |
| case 'm': |
| mul = MiB; |
| break; |
| case 'k': |
| mul = KiB; |
| break; |
| } |
| if (mul > 1) |
| n = n.substring(0, n.length() - 1).trim(); |
| if (n.length() == 0) |
| return defaultValue; |
| |
| try { |
| return mul * Long.parseLong(n); |
| } catch (NumberFormatException nfe) { |
| throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidIntegerValue |
| , section, name, str)); |
| } |
| } |
| |
| /** |
| * 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 getBoolean(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) { |
| String n = getRawString(section, subsection, name); |
| if (n == null) |
| return defaultValue; |
| if (MAGIC_EMPTY_VALUE == n) |
| return true; |
| try { |
| return StringUtils.toBoolean(n); |
| } catch (IllegalArgumentException err) { |
| throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidBooleanValue |
| , section, name, n)); |
| } |
| } |
| |
| /** |
| * Parse an enumeration from the configuration. |
| * |
| * @param <T> |
| * type of the enumeration object. |
| * @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 getEnum(all, section, subsection, name, defaultValue); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static <T> T[] allValuesOf(final 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 <T> |
| * type of the enumeration object. |
| * @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) { |
| String value = getString(section, subsection, name); |
| if (value == null) |
| return defaultValue; |
| |
| if (all[0] instanceof ConfigEnum) { |
| for (T t : all) { |
| if (((ConfigEnum) t).matchConfigValue(value)) |
| return t; |
| } |
| } |
| |
| String n = value.replace(' ', '_'); |
| |
| // Because of c98abc9c0586c73ef7df4172644b7dd21c979e9d being used in |
| // the real world before its breakage was fully understood, we must |
| // also accept '-' as though it were ' '. |
| n = n.replace('-', '_'); |
| |
| T trueState = null; |
| T falseState = null; |
| for (T e : all) { |
| if (StringUtils.equalsIgnoreCase(e.name(), n)) |
| return e; |
| else if (StringUtils.equalsIgnoreCase(e.name(), "TRUE")) //$NON-NLS-1$ |
| trueState = e; |
| else if (StringUtils.equalsIgnoreCase(e.name(), "FALSE")) //$NON-NLS-1$ |
| falseState = e; |
| } |
| |
| // This is an odd little fallback. C Git sometimes allows boolean |
| // values in a tri-state with other things. If we have both a true |
| // and a false value in our enumeration, assume its one of those. |
| // |
| if (trueState != null && falseState != null) { |
| try { |
| return StringUtils.toBoolean(n) ? trueState : falseState; |
| } catch (IllegalArgumentException err) { |
| // Fall through and use our custom error below. |
| } |
| } |
| |
| if (subsection != null) |
| throw new IllegalArgumentException(MessageFormat.format( |
| JGitText.get().enumValueNotSupported3, section, subsection, |
| name, value)); |
| else |
| throw new IllegalArgumentException( |
| MessageFormat.format(JGitText.get().enumValueNotSupported2, |
| section, name, value)); |
| } |
| |
| /** |
| * 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) { |
| String valueString = getString(section, subsection, name); |
| |
| if (valueString == null) { |
| return defaultValue; |
| } |
| |
| String s = valueString.trim(); |
| if (s.length() == 0) { |
| return defaultValue; |
| } |
| |
| if (s.startsWith("-")/* negative */) { //$NON-NLS-1$ |
| throw notTimeUnit(section, subsection, name, valueString); |
| } |
| |
| Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$") //$NON-NLS-1$ |
| .matcher(valueString); |
| if (!m.matches()) { |
| return defaultValue; |
| } |
| |
| String digits = m.group(1); |
| String unitName = m.group(2).trim(); |
| |
| TimeUnit inputUnit; |
| int inputMul; |
| |
| if (unitName.isEmpty()) { |
| inputUnit = wantUnit; |
| inputMul = 1; |
| |
| } else if (match(unitName, "ms", "milliseconds")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| inputUnit = TimeUnit.MILLISECONDS; |
| inputMul = 1; |
| |
| } else if (match(unitName, "s", "sec", "second", "seconds")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ |
| inputUnit = TimeUnit.SECONDS; |
| inputMul = 1; |
| |
| } else if (match(unitName, "m", "min", "minute", "minutes")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ |
| inputUnit = TimeUnit.MINUTES; |
| inputMul = 1; |
| |
| } else if (match(unitName, "h", "hr", "hour", "hours")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ |
| inputUnit = TimeUnit.HOURS; |
| inputMul = 1; |
| |
| } else if (match(unitName, "d", "day", "days")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| inputUnit = TimeUnit.DAYS; |
| inputMul = 1; |
| |
| } else if (match(unitName, "w", "week", "weeks")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| inputUnit = TimeUnit.DAYS; |
| inputMul = 7; |
| |
| } else if (match(unitName, "mon", "month", "months")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| inputUnit = TimeUnit.DAYS; |
| inputMul = 30; |
| |
| } else if (match(unitName, "y", "year", "years")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| inputUnit = TimeUnit.DAYS; |
| inputMul = 365; |
| |
| } else { |
| throw notTimeUnit(section, subsection, name, valueString); |
| } |
| |
| try { |
| return wantUnit.convert(Long.parseLong(digits) * inputMul, |
| inputUnit); |
| } catch (NumberFormatException nfe) { |
| throw notTimeUnit(section, subsection, unitName, valueString); |
| } |
| } |
| |
| private static boolean match(final String a, final String... cases) { |
| for (final String b : cases) { |
| if (b != null && b.equalsIgnoreCase(a)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private IllegalArgumentException notTimeUnit(String section, |
| String subsection, String name, String valueString) { |
| if (subsection != null) { |
| return new IllegalArgumentException( |
| MessageFormat.format(JGitText.get().invalidTimeUnitValue3, |
| section, subsection, name, valueString)); |
| } |
| return new IllegalArgumentException( |
| MessageFormat.format(JGitText.get().invalidTimeUnitValue2, |
| section, name, valueString)); |
| } |
| |
| /** |
| * @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(final String section) { |
| return getState().getSubsections(section); |
| } |
| |
| /** |
| * @return the sections defined in this {@link 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(); |
| } |
| |
| /** |
| * @param section |
| * the section |
| * @return the list of names defined for this section |
| */ |
| public Set<String> getNames(String section) { |
| return getNames(section, null); |
| } |
| |
| /** |
| * @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); |
| } |
| |
| /** |
| * @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); |
| } |
| |
| /** |
| * @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(final 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(final 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 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()); |
| } |
| |
| private 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 <T> |
| * type of the enumeration object. |
| * @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.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.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.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); |
| } |
| 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) { |
| for (int i = 0; i < entries.size(); i++) { |
| ConfigLine e = entries.get(i); |
| 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; |
| } |
| |
| /** |
| * @return this configuration, formatted as a Git style text file. |
| */ |
| public String toText() { |
| final StringBuilder out = new StringBuilder(); |
| for (final ConfigLine e : state.get().entryList) { |
| 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 (MAGIC_EMPTY_VALUE != 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 ConfigInvalidException |
| * the text supplied is not formatted correctly. No changes were |
| * made to {@code this}. |
| */ |
| public void fromText(final String text) throws ConfigInvalidException { |
| state.set(newState(fromTextRecurse(text, 1))); |
| } |
| |
| private List<ConfigLine> fromTextRecurse(final String text, int depth) |
| 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(); |
| 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(); |
| |
| } 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 = readValue(in, true, '"'); |
| 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 = MAGIC_EMPTY_VALUE; |
| } else |
| e.value = readValue(in, false, -1); |
| |
| if (e.section.equals("include")) { //$NON-NLS-1$ |
| addIncludedConfig(newEntries, e, depth); |
| } |
| } else |
| throw new ConfigInvalidException(JGitText.get().invalidLineInConfigFile); |
| } |
| |
| return newEntries; |
| } |
| |
| private void addIncludedConfig(final List<ConfigLine> newEntries, |
| ConfigLine line, int depth) throws ConfigInvalidException { |
| if (!line.name.equals("path") || //$NON-NLS-1$ |
| line.value == null || line.value.equals(MAGIC_EMPTY_VALUE)) { |
| throw new ConfigInvalidException( |
| JGitText.get().invalidLineInConfigFile); |
| } |
| File path = new File(line.value); |
| try { |
| byte[] bytes = IO.readFully(path); |
| String decoded; |
| if (isUtf8(bytes)) { |
| decoded = RawParseUtils.decode(RawParseUtils.UTF8_CHARSET, |
| bytes, 3, bytes.length); |
| } else { |
| decoded = RawParseUtils.decode(bytes); |
| } |
| newEntries.addAll(fromTextRecurse(decoded, depth + 1)); |
| } catch (FileNotFoundException fnfe) { |
| if (path.exists()) { |
| throw new ConfigInvalidException(MessageFormat |
| .format(JGitText.get().cannotReadFile, path), fnfe); |
| } |
| } catch (IOException ioe) { |
| throw new ConfigInvalidException( |
| MessageFormat.format(JGitText.get().cannotReadFile, path), |
| ioe); |
| } |
| } |
| |
| private ConfigSnapshot newState() { |
| return new ConfigSnapshot(Collections.<ConfigLine> emptyList(), |
| getBaseState()); |
| } |
| |
| private ConfigSnapshot newState(final 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(final 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(final 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 readValue(final StringReader in, boolean quote, |
| final int eol) throws ConfigInvalidException { |
| final StringBuilder value = new StringBuilder(); |
| boolean space = false; |
| for (;;) { |
| int c = in.read(); |
| if (c < 0) { |
| break; |
| } |
| |
| if ('\n' == c) { |
| if (quote) |
| throw new ConfigInvalidException(JGitText.get().newlineInQuotesNotAllowed); |
| in.reset(); |
| break; |
| } |
| |
| if (eol == c) |
| break; |
| |
| if (!quote) { |
| if (Character.isWhitespace((char) c)) { |
| space = true; |
| continue; |
| } |
| if (';' == c || '#' == c) { |
| in.reset(); |
| break; |
| } |
| } |
| |
| if (space) { |
| if (value.length() > 0) |
| value.append(' '); |
| space = false; |
| } |
| |
| 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; |
| default: |
| throw new ConfigInvalidException(MessageFormat.format( |
| JGitText.get().badEscape, |
| Character.valueOf(((char) c)))); |
| } |
| } |
| |
| if ('"' == c) { |
| quote = !quote; |
| continue; |
| } |
| |
| value.append((char) c); |
| } |
| return value.length() > 0 ? value.toString() : null; |
| } |
| |
| /** |
| * 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(final String in) { |
| buf = in.toCharArray(); |
| } |
| |
| int read() { |
| try { |
| return buf[pos++]; |
| } catch (ArrayIndexOutOfBoundsException e) { |
| pos = buf.length; |
| return -1; |
| } |
| } |
| |
| 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); |
| } |
| } |