View Javadoc
1   /*
2    * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
3    * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
4    * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
5    * Copyright (C) 2008-2010, Google Inc.
6    * Copyright (C) 2009, Google, Inc.
7    * Copyright (C) 2009, JetBrains s.r.o.
8    * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
9    * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org>
10   * Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com>
11   * and other copyright owners as documented in the project's IP log.
12   *
13   * This program and the accompanying materials are made available
14   * under the terms of the Eclipse Distribution License v1.0 which
15   * accompanies this distribution, is reproduced below, and is
16   * available at http://www.eclipse.org/org/documents/edl-v10.php
17   *
18   * All rights reserved.
19   *
20   * Redistribution and use in source and binary forms, with or
21   * without modification, are permitted provided that the following
22   * conditions are met:
23   *
24   * - Redistributions of source code must retain the above copyright
25   *   notice, this list of conditions and the following disclaimer.
26   *
27   * - Redistributions in binary form must reproduce the above
28   *   copyright notice, this list of conditions and the following
29   *   disclaimer in the documentation and/or other materials provided
30   *   with the distribution.
31   *
32   * - Neither the name of the Eclipse Foundation, Inc. nor the
33   *   names of its contributors may be used to endorse or promote
34   *   products derived from this software without specific prior
35   *   written permission.
36   *
37   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
38   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
39   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
40   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
41   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
42   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
43   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
44   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
45   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
46   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
47   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
48   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
49   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
50   */
51  
52  package org.eclipse.jgit.lib;
53  
54  import static org.eclipse.jgit.lib.Constants.CHARSET;
55  
56  import java.text.MessageFormat;
57  import java.util.ArrayList;
58  import java.util.Collections;
59  import java.util.List;
60  import java.util.Locale;
61  import java.util.Set;
62  import java.util.concurrent.TimeUnit;
63  import java.util.concurrent.atomic.AtomicReference;
64  
65  import org.eclipse.jgit.annotations.Nullable;
66  import org.eclipse.jgit.errors.ConfigInvalidException;
67  import org.eclipse.jgit.events.ConfigChangedEvent;
68  import org.eclipse.jgit.events.ConfigChangedListener;
69  import org.eclipse.jgit.events.ListenerHandle;
70  import org.eclipse.jgit.events.ListenerList;
71  import org.eclipse.jgit.internal.JGitText;
72  import org.eclipse.jgit.transport.RefSpec;
73  import org.eclipse.jgit.util.RawParseUtils;
74  
75  /**
76   * Git style {@code .config}, {@code .gitconfig}, {@code .gitmodules} file.
77   */
78  public class Config {
79  
80  	private static final String[] EMPTY_STRING_ARRAY = {};
81  
82  	static final long KiB = 1024;
83  	static final long MiB = 1024 * KiB;
84  	static final long GiB = 1024 * MiB;
85  	private static final int MAX_DEPTH = 10;
86  
87  	private static final TypedConfigGetter DEFAULT_GETTER = new DefaultTypedConfigGetter();
88  
89  	private static TypedConfigGetter typedGetter = DEFAULT_GETTER;
90  
91  	/** the change listeners */
92  	private final ListenerList listeners = new ListenerList();
93  
94  	/**
95  	 * Immutable current state of the configuration data.
96  	 * <p>
97  	 * This state is copy-on-write. It should always contain an immutable list
98  	 * of the configuration keys/values.
99  	 */
100 	private final AtomicReference<ConfigSnapshot> state;
101 
102 	private final Config baseConfig;
103 
104 	/**
105 	 * Magic value indicating a missing entry.
106 	 * <p>
107 	 * This value is tested for reference equality in some contexts, so we
108 	 * must ensure it is a special copy of the empty string.  It also must
109 	 * be treated like the empty string.
110 	 */
111 	static final String MAGIC_EMPTY_VALUE = new String();
112 
113 	/**
114 	 * Create a configuration with no default fallback.
115 	 */
116 	public Config() {
117 		this(null);
118 	}
119 
120 	/**
121 	 * Create an empty configuration with a fallback for missing keys.
122 	 *
123 	 * @param defaultConfig
124 	 *            the base configuration to be consulted when a key is missing
125 	 *            from this configuration instance.
126 	 */
127 	public Config(Config defaultConfig) {
128 		baseConfig = defaultConfig;
129 		state = new AtomicReference<>(newState());
130 	}
131 
132 	/**
133 	 * Globally sets a {@link org.eclipse.jgit.lib.TypedConfigGetter} that is
134 	 * subsequently used to read typed values from all git configs.
135 	 *
136 	 * @param getter
137 	 *            to use; if {@code null} use the default getter.
138 	 * @since 4.9
139 	 */
140 	public static void setTypedConfigGetter(TypedConfigGetter getter) {
141 		typedGetter = getter == null ? DEFAULT_GETTER : getter;
142 	}
143 
144 	/**
145 	 * Escape the value before saving
146 	 *
147 	 * @param x
148 	 *            the value to escape
149 	 * @return the escaped value
150 	 */
151 	static String escapeValue(String x) {
152 		if (x.isEmpty()) {
153 			return ""; //$NON-NLS-1$
154 		}
155 
156 		boolean needQuote = x.charAt(0) == ' ' || x.charAt(x.length() - 1) == ' ';
157 		StringBuilder r = new StringBuilder(x.length());
158 		for (int k = 0; k < x.length(); k++) {
159 			char c = x.charAt(k);
160 			// git-config(1) lists the limited set of supported escape sequences, but
161 			// the documentation is otherwise not especially normative. In particular,
162 			// which ones of these produce and/or require escaping and/or quoting
163 			// around them is not documented and was discovered by trial and error.
164 			// In summary:
165 			//
166 			// * Quotes are only required if there is leading/trailing whitespace or a
167 			//   comment character.
168 			// * Bytes that have a supported escape sequence are escaped, except for
169 			//   \b for some reason which isn't.
170 			// * Needing an escape sequence is not sufficient reason to quote the
171 			//   value.
172 			switch (c) {
173 			case '\0':
174 				// Unix command line calling convention cannot pass a '\0' as an
175 				// argument, so there is no equivalent way in C git to store a null byte
176 				// in a config value.
177 				throw new IllegalArgumentException(
178 						JGitText.get().configValueContainsNullByte);
179 
180 			case '\n':
181 				r.append('\\').append('n');
182 				break;
183 
184 			case '\t':
185 				r.append('\\').append('t');
186 				break;
187 
188 			case '\b':
189 				// Doesn't match `git config foo.bar $'x\by'`, which doesn't escape the
190 				// \x08, but since both escaped and unescaped forms are readable, we'll
191 				// prefer internal consistency here.
192 				r.append('\\').append('b');
193 				break;
194 
195 			case '\\':
196 				r.append('\\').append('\\');
197 				break;
198 
199 			case '"':
200 				r.append('\\').append('"');
201 				break;
202 
203 			case '#':
204 			case ';':
205 				needQuote = true;
206 				r.append(c);
207 				break;
208 
209 			default:
210 				r.append(c);
211 				break;
212 			}
213 		}
214 
215 		return needQuote ? '"' + r.toString() + '"' : r.toString();
216 	}
217 
218 	static String escapeSubsection(String x) {
219 		if (x.isEmpty()) {
220 			return "\"\""; //$NON-NLS-1$
221 		}
222 
223 		StringBuilder r = new StringBuilder(x.length() + 2).append('"');
224 		for (int k = 0; k < x.length(); k++) {
225 			char c = x.charAt(k);
226 
227 			// git-config(1) lists the limited set of supported escape sequences
228 			// (which is even more limited for subsection names than for values).
229 			switch (c) {
230 			case '\0':
231 				throw new IllegalArgumentException(
232 						JGitText.get().configSubsectionContainsNullByte);
233 
234 			case '\n':
235 				throw new IllegalArgumentException(
236 						JGitText.get().configSubsectionContainsNewline);
237 
238 			case '\\':
239 			case '"':
240 				r.append('\\').append(c);
241 				break;
242 
243 			default:
244 				r.append(c);
245 				break;
246 			}
247 		}
248 
249 		return r.append('"').toString();
250 	}
251 
252 	/**
253 	 * Obtain an integer value from the configuration.
254 	 *
255 	 * @param section
256 	 *            section the key is grouped within.
257 	 * @param name
258 	 *            name of the key to get.
259 	 * @param defaultValue
260 	 *            default value to return if no value was present.
261 	 * @return an integer value from the configuration, or defaultValue.
262 	 */
263 	public int getInt(final String section, final String name,
264 			final int defaultValue) {
265 		return typedGetter.getInt(this, section, null, name, defaultValue);
266 	}
267 
268 	/**
269 	 * Obtain an integer value from the configuration.
270 	 *
271 	 * @param section
272 	 *            section the key is grouped within.
273 	 * @param subsection
274 	 *            subsection name, such a remote or branch name.
275 	 * @param name
276 	 *            name of the key to get.
277 	 * @param defaultValue
278 	 *            default value to return if no value was present.
279 	 * @return an integer value from the configuration, or defaultValue.
280 	 */
281 	public int getInt(final String section, String subsection,
282 			final String name, final int defaultValue) {
283 		return typedGetter.getInt(this, section, subsection, name,
284 				defaultValue);
285 	}
286 
287 	/**
288 	 * Obtain an integer value from the configuration.
289 	 *
290 	 * @param section
291 	 *            section the key is grouped within.
292 	 * @param name
293 	 *            name of the key to get.
294 	 * @param defaultValue
295 	 *            default value to return if no value was present.
296 	 * @return an integer value from the configuration, or defaultValue.
297 	 */
298 	public long getLong(String section, String name, long defaultValue) {
299 		return typedGetter.getLong(this, section, null, name, defaultValue);
300 	}
301 
302 	/**
303 	 * Obtain an integer value from the configuration.
304 	 *
305 	 * @param section
306 	 *            section the key is grouped within.
307 	 * @param subsection
308 	 *            subsection name, such a remote or branch name.
309 	 * @param name
310 	 *            name of the key to get.
311 	 * @param defaultValue
312 	 *            default value to return if no value was present.
313 	 * @return an integer value from the configuration, or defaultValue.
314 	 */
315 	public long getLong(final String section, String subsection,
316 			final String name, final long defaultValue) {
317 		return typedGetter.getLong(this, section, subsection, name,
318 				defaultValue);
319 	}
320 
321 	/**
322 	 * Get a boolean value from the git config
323 	 *
324 	 * @param section
325 	 *            section the key is grouped within.
326 	 * @param name
327 	 *            name of the key to get.
328 	 * @param defaultValue
329 	 *            default value to return if no value was present.
330 	 * @return true if any value or defaultValue is true, false for missing or
331 	 *         explicit false
332 	 */
333 	public boolean getBoolean(final String section, final String name,
334 			final boolean defaultValue) {
335 		return typedGetter.getBoolean(this, section, null, name, defaultValue);
336 	}
337 
338 	/**
339 	 * Get a boolean value from the git config
340 	 *
341 	 * @param section
342 	 *            section the key is grouped within.
343 	 * @param subsection
344 	 *            subsection name, such a remote or branch name.
345 	 * @param name
346 	 *            name of the key to get.
347 	 * @param defaultValue
348 	 *            default value to return if no value was present.
349 	 * @return true if any value or defaultValue is true, false for missing or
350 	 *         explicit false
351 	 */
352 	public boolean getBoolean(final String section, String subsection,
353 			final String name, final boolean defaultValue) {
354 		return typedGetter.getBoolean(this, section, subsection, name,
355 				defaultValue);
356 	}
357 
358 	/**
359 	 * Parse an enumeration from the configuration.
360 	 *
361 	 * @param section
362 	 *            section the key is grouped within.
363 	 * @param subsection
364 	 *            subsection name, such a remote or branch name.
365 	 * @param name
366 	 *            name of the key to get.
367 	 * @param defaultValue
368 	 *            default value to return if no value was present.
369 	 * @return the selected enumeration value, or {@code defaultValue}.
370 	 */
371 	public <T extends Enum<?>> T getEnum(final String section,
372 			final String subsection, final String name, final T defaultValue) {
373 		final T[] all = allValuesOf(defaultValue);
374 		return typedGetter.getEnum(this, all, section, subsection, name,
375 				defaultValue);
376 	}
377 
378 	@SuppressWarnings("unchecked")
379 	private static <T> T[] allValuesOf(T value) {
380 		try {
381 			return (T[]) value.getClass().getMethod("values").invoke(null); //$NON-NLS-1$
382 		} catch (Exception err) {
383 			String typeName = value.getClass().getName();
384 			String msg = MessageFormat.format(
385 					JGitText.get().enumValuesNotAvailable, typeName);
386 			throw new IllegalArgumentException(msg, err);
387 		}
388 	}
389 
390 	/**
391 	 * Parse an enumeration from the configuration.
392 	 *
393 	 * @param all
394 	 *            all possible values in the enumeration which should be
395 	 *            recognized. Typically {@code EnumType.values()}.
396 	 * @param section
397 	 *            section the key is grouped within.
398 	 * @param subsection
399 	 *            subsection name, such a remote or branch name.
400 	 * @param name
401 	 *            name of the key to get.
402 	 * @param defaultValue
403 	 *            default value to return if no value was present.
404 	 * @return the selected enumeration value, or {@code defaultValue}.
405 	 */
406 	public <T extends Enum<?>> T getEnum(final T[] all, final String section,
407 			final String subsection, final String name, final T defaultValue) {
408 		return typedGetter.getEnum(this, all, section, subsection, name,
409 				defaultValue);
410 	}
411 
412 	/**
413 	 * Get string value or null if not found.
414 	 *
415 	 * @param section
416 	 *            the section
417 	 * @param subsection
418 	 *            the subsection for the value
419 	 * @param name
420 	 *            the key name
421 	 * @return a String value from the config, <code>null</code> if not found
422 	 */
423 	public String getString(final String section, String subsection,
424 			final String name) {
425 		return getRawString(section, subsection, name);
426 	}
427 
428 	/**
429 	 * Get a list of string values
430 	 * <p>
431 	 * If this instance was created with a base, the base's values are returned
432 	 * first (if any).
433 	 *
434 	 * @param section
435 	 *            the section
436 	 * @param subsection
437 	 *            the subsection for the value
438 	 * @param name
439 	 *            the key name
440 	 * @return array of zero or more values from the configuration.
441 	 */
442 	public String[] getStringList(final String section, String subsection,
443 			final String name) {
444 		String[] base;
445 		if (baseConfig != null)
446 			base = baseConfig.getStringList(section, subsection, name);
447 		else
448 			base = EMPTY_STRING_ARRAY;
449 
450 		String[] self = getRawStringList(section, subsection, name);
451 		if (self == null)
452 			return base;
453 		if (base.length == 0)
454 			return self;
455 		String[] res = new String[base.length + self.length];
456 		int n = base.length;
457 		System.arraycopy(base, 0, res, 0, n);
458 		System.arraycopy(self, 0, res, n, self.length);
459 		return res;
460 	}
461 
462 	/**
463 	 * Parse a numerical time unit, such as "1 minute", from the configuration.
464 	 *
465 	 * @param section
466 	 *            section the key is in.
467 	 * @param subsection
468 	 *            subsection the key is in, or null if not in a subsection.
469 	 * @param name
470 	 *            the key name.
471 	 * @param defaultValue
472 	 *            default value to return if no value was present.
473 	 * @param wantUnit
474 	 *            the units of {@code defaultValue} and the return value, as
475 	 *            well as the units to assume if the value does not contain an
476 	 *            indication of the units.
477 	 * @return the value, or {@code defaultValue} if not set, expressed in
478 	 *         {@code units}.
479 	 * @since 4.5
480 	 */
481 	public long getTimeUnit(String section, String subsection, String name,
482 			long defaultValue, TimeUnit wantUnit) {
483 		return typedGetter.getTimeUnit(this, section, subsection, name,
484 				defaultValue, wantUnit);
485 	}
486 
487 	/**
488 	 * Parse a list of {@link org.eclipse.jgit.transport.RefSpec}s from the
489 	 * configuration.
490 	 *
491 	 * @param section
492 	 *            section the key is in.
493 	 * @param subsection
494 	 *            subsection the key is in, or null if not in a subsection.
495 	 * @param name
496 	 *            the key name.
497 	 * @return a possibly empty list of
498 	 *         {@link org.eclipse.jgit.transport.RefSpec}s
499 	 * @since 4.9
500 	 */
501 	public List<RefSpec> getRefSpecs(String section, String subsection,
502 			String name) {
503 		return typedGetter.getRefSpecs(this, section, subsection, name);
504 	}
505 
506 	/**
507 	 * Get set of all subsections of specified section within this configuration
508 	 * and its base configuration
509 	 *
510 	 * @param section
511 	 *            section to search for.
512 	 * @return set of all subsections of specified section within this
513 	 *         configuration and its base configuration; may be empty if no
514 	 *         subsection exists. The set's iterator returns sections in the
515 	 *         order they are declared by the configuration starting from this
516 	 *         instance and progressing through the base.
517 	 */
518 	public Set<String> getSubsections(String section) {
519 		return getState().getSubsections(section);
520 	}
521 
522 	/**
523 	 * Get the sections defined in this {@link org.eclipse.jgit.lib.Config}.
524 	 *
525 	 * @return the sections defined in this {@link org.eclipse.jgit.lib.Config}.
526 	 *         The set's iterator returns sections in the order they are
527 	 *         declared by the configuration starting from this instance and
528 	 *         progressing through the base.
529 	 */
530 	public Set<String> getSections() {
531 		return getState().getSections();
532 	}
533 
534 	/**
535 	 * Get the list of names defined for this section
536 	 *
537 	 * @param section
538 	 *            the section
539 	 * @return the list of names defined for this section
540 	 */
541 	public Set<String> getNames(String section) {
542 		return getNames(section, null);
543 	}
544 
545 	/**
546 	 * Get the list of names defined for this subsection
547 	 *
548 	 * @param section
549 	 *            the section
550 	 * @param subsection
551 	 *            the subsection
552 	 * @return the list of names defined for this subsection
553 	 */
554 	public Set<String> getNames(String section, String subsection) {
555 		return getState().getNames(section, subsection);
556 	}
557 
558 	/**
559 	 * Get the list of names defined for this section
560 	 *
561 	 * @param section
562 	 *            the section
563 	 * @param recursive
564 	 *            if {@code true} recursively adds the names defined in all base
565 	 *            configurations
566 	 * @return the list of names defined for this section
567 	 * @since 3.2
568 	 */
569 	public Set<String> getNames(String section, boolean recursive) {
570 		return getState().getNames(section, null, recursive);
571 	}
572 
573 	/**
574 	 * Get the list of names defined for this section
575 	 *
576 	 * @param section
577 	 *            the section
578 	 * @param subsection
579 	 *            the subsection
580 	 * @param recursive
581 	 *            if {@code true} recursively adds the names defined in all base
582 	 *            configurations
583 	 * @return the list of names defined for this subsection
584 	 * @since 3.2
585 	 */
586 	public Set<String> getNames(String section, String subsection,
587 			boolean recursive) {
588 		return getState().getNames(section, subsection, recursive);
589 	}
590 
591 	/**
592 	 * Obtain a handle to a parsed set of configuration values.
593 	 *
594 	 * @param <T>
595 	 *            type of configuration model to return.
596 	 * @param parser
597 	 *            parser which can create the model if it is not already
598 	 *            available in this configuration file. The parser is also used
599 	 *            as the key into a cache and must obey the hashCode and equals
600 	 *            contract in order to reuse a parsed model.
601 	 * @return the parsed object instance, which is cached inside this config.
602 	 */
603 	@SuppressWarnings("unchecked")
604 	public <T> T get(SectionParser<T> parser) {
605 		final ConfigSnapshot myState = getState();
606 		T obj = (T) myState.cache.get(parser);
607 		if (obj == null) {
608 			obj = parser.parse(this);
609 			myState.cache.put(parser, obj);
610 		}
611 		return obj;
612 	}
613 
614 	/**
615 	 * Remove a cached configuration object.
616 	 * <p>
617 	 * If the associated configuration object has not yet been cached, this
618 	 * method has no effect.
619 	 *
620 	 * @param parser
621 	 *            parser used to obtain the configuration object.
622 	 * @see #get(SectionParser)
623 	 */
624 	public void uncache(SectionParser<?> parser) {
625 		state.get().cache.remove(parser);
626 	}
627 
628 	/**
629 	 * Adds a listener to be notified about changes.
630 	 * <p>
631 	 * Clients are supposed to remove the listeners after they are done with
632 	 * them using the {@link org.eclipse.jgit.events.ListenerHandle#remove()}
633 	 * method
634 	 *
635 	 * @param listener
636 	 *            the listener
637 	 * @return the handle to the registered listener
638 	 */
639 	public ListenerHandle addChangeListener(ConfigChangedListener listener) {
640 		return listeners.addConfigChangedListener(listener);
641 	}
642 
643 	/**
644 	 * Determine whether to issue change events for transient changes.
645 	 * <p>
646 	 * If <code>true</code> is returned (which is the default behavior),
647 	 * {@link #fireConfigChangedEvent()} will be called upon each change.
648 	 * <p>
649 	 * Subclasses that override this to return <code>false</code> are
650 	 * responsible for issuing {@link #fireConfigChangedEvent()} calls
651 	 * themselves.
652 	 *
653 	 * @return <code></code>
654 	 */
655 	protected boolean notifyUponTransientChanges() {
656 		return true;
657 	}
658 
659 	/**
660 	 * Notifies the listeners
661 	 */
662 	protected void fireConfigChangedEvent() {
663 		listeners.dispatch(new ConfigChangedEvent());
664 	}
665 
666 	String getRawString(final String section, final String subsection,
667 			final String name) {
668 		String[] lst = getRawStringList(section, subsection, name);
669 		if (lst != null) {
670 			return lst[lst.length - 1];
671 		} else if (baseConfig != null) {
672 			return baseConfig.getRawString(section, subsection, name);
673 		} else {
674 			return null;
675 		}
676 	}
677 
678 	private String[] getRawStringList(String section, String subsection,
679 			String name) {
680 		return state.get().get(section, subsection, name);
681 	}
682 
683 	private ConfigSnapshot getState() {
684 		ConfigSnapshot cur, upd;
685 		do {
686 			cur = state.get();
687 			final ConfigSnapshot base = getBaseState();
688 			if (cur.baseState == base)
689 				return cur;
690 			upd = new ConfigSnapshot(cur.entryList, base);
691 		} while (!state.compareAndSet(cur, upd));
692 		return upd;
693 	}
694 
695 	private ConfigSnapshot getBaseState() {
696 		return baseConfig != null ? baseConfig.getState() : null;
697 	}
698 
699 	/**
700 	 * Add or modify a configuration value. The parameters will result in a
701 	 * configuration entry like this.
702 	 *
703 	 * <pre>
704 	 * [section &quot;subsection&quot;]
705 	 *         name = value
706 	 * </pre>
707 	 *
708 	 * @param section
709 	 *            section name, e.g "branch"
710 	 * @param subsection
711 	 *            optional subsection value, e.g. a branch name
712 	 * @param name
713 	 *            parameter name, e.g. "filemode"
714 	 * @param value
715 	 *            parameter value
716 	 */
717 	public void setInt(final String section, final String subsection,
718 			final String name, final int value) {
719 		setLong(section, subsection, name, value);
720 	}
721 
722 	/**
723 	 * Add or modify a configuration value. The parameters will result in a
724 	 * configuration entry like this.
725 	 *
726 	 * <pre>
727 	 * [section &quot;subsection&quot;]
728 	 *         name = value
729 	 * </pre>
730 	 *
731 	 * @param section
732 	 *            section name, e.g "branch"
733 	 * @param subsection
734 	 *            optional subsection value, e.g. a branch name
735 	 * @param name
736 	 *            parameter name, e.g. "filemode"
737 	 * @param value
738 	 *            parameter value
739 	 */
740 	public void setLong(final String section, final String subsection,
741 			final String name, final long value) {
742 		final String s;
743 
744 		if (value >= GiB && (value % GiB) == 0)
745 			s = String.valueOf(value / GiB) + "g"; //$NON-NLS-1$
746 		else if (value >= MiB && (value % MiB) == 0)
747 			s = String.valueOf(value / MiB) + "m"; //$NON-NLS-1$
748 		else if (value >= KiB && (value % KiB) == 0)
749 			s = String.valueOf(value / KiB) + "k"; //$NON-NLS-1$
750 		else
751 			s = String.valueOf(value);
752 
753 		setString(section, subsection, name, s);
754 	}
755 
756 	/**
757 	 * Add or modify a configuration value. The parameters will result in a
758 	 * configuration entry like this.
759 	 *
760 	 * <pre>
761 	 * [section &quot;subsection&quot;]
762 	 *         name = value
763 	 * </pre>
764 	 *
765 	 * @param section
766 	 *            section name, e.g "branch"
767 	 * @param subsection
768 	 *            optional subsection value, e.g. a branch name
769 	 * @param name
770 	 *            parameter name, e.g. "filemode"
771 	 * @param value
772 	 *            parameter value
773 	 */
774 	public void setBoolean(final String section, final String subsection,
775 			final String name, final boolean value) {
776 		setString(section, subsection, name, value ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
777 	}
778 
779 	/**
780 	 * Add or modify a configuration value. The parameters will result in a
781 	 * configuration entry like this.
782 	 *
783 	 * <pre>
784 	 * [section &quot;subsection&quot;]
785 	 *         name = value
786 	 * </pre>
787 	 *
788 	 * @param section
789 	 *            section name, e.g "branch"
790 	 * @param subsection
791 	 *            optional subsection value, e.g. a branch name
792 	 * @param name
793 	 *            parameter name, e.g. "filemode"
794 	 * @param value
795 	 *            parameter value
796 	 */
797 	public <T extends Enum<?>> void setEnum(final String section,
798 			final String subsection, final String name, final T value) {
799 		String n;
800 		if (value instanceof ConfigEnum)
801 			n = ((ConfigEnum) value).toConfigValue();
802 		else
803 			n = value.name().toLowerCase(Locale.ROOT).replace('_', ' ');
804 		setString(section, subsection, name, n);
805 	}
806 
807 	/**
808 	 * Add or modify a configuration value. The parameters will result in a
809 	 * configuration entry like this.
810 	 *
811 	 * <pre>
812 	 * [section &quot;subsection&quot;]
813 	 *         name = value
814 	 * </pre>
815 	 *
816 	 * @param section
817 	 *            section name, e.g "branch"
818 	 * @param subsection
819 	 *            optional subsection value, e.g. a branch name
820 	 * @param name
821 	 *            parameter name, e.g. "filemode"
822 	 * @param value
823 	 *            parameter value, e.g. "true"
824 	 */
825 	public void setString(final String section, final String subsection,
826 			final String name, final String value) {
827 		setStringList(section, subsection, name, Collections
828 				.singletonList(value));
829 	}
830 
831 	/**
832 	 * Remove a configuration value.
833 	 *
834 	 * @param section
835 	 *            section name, e.g "branch"
836 	 * @param subsection
837 	 *            optional subsection value, e.g. a branch name
838 	 * @param name
839 	 *            parameter name, e.g. "filemode"
840 	 */
841 	public void unset(final String section, final String subsection,
842 			final String name) {
843 		setStringList(section, subsection, name, Collections
844 				.<String> emptyList());
845 	}
846 
847 	/**
848 	 * Remove all configuration values under a single section.
849 	 *
850 	 * @param section
851 	 *            section name, e.g "branch"
852 	 * @param subsection
853 	 *            optional subsection value, e.g. a branch name
854 	 */
855 	public void unsetSection(String section, String subsection) {
856 		ConfigSnapshot src, res;
857 		do {
858 			src = state.get();
859 			res = unsetSection(src, section, subsection);
860 		} while (!state.compareAndSet(src, res));
861 	}
862 
863 	private ConfigSnapshot unsetSection(final ConfigSnapshot srcState,
864 			final String section,
865 			final String subsection) {
866 		final int max = srcState.entryList.size();
867 		final ArrayList<ConfigLine> r = new ArrayList<>(max);
868 
869 		boolean lastWasMatch = false;
870 		for (ConfigLine e : srcState.entryList) {
871 			if (e.match(section, subsection)) {
872 				// Skip this record, it's for the section we are removing.
873 				lastWasMatch = true;
874 				continue;
875 			}
876 
877 			if (lastWasMatch && e.section == null && e.subsection == null)
878 				continue; // skip this padding line in the section.
879 			r.add(e);
880 		}
881 
882 		return newState(r);
883 	}
884 
885 	/**
886 	 * Set a configuration value.
887 	 *
888 	 * <pre>
889 	 * [section &quot;subsection&quot;]
890 	 *         name = value1
891 	 *         name = value2
892 	 * </pre>
893 	 *
894 	 * @param section
895 	 *            section name, e.g "branch"
896 	 * @param subsection
897 	 *            optional subsection value, e.g. a branch name
898 	 * @param name
899 	 *            parameter name, e.g. "filemode"
900 	 * @param values
901 	 *            list of zero or more values for this key.
902 	 */
903 	public void setStringList(final String section, final String subsection,
904 			final String name, final List<String> values) {
905 		ConfigSnapshot src, res;
906 		do {
907 			src = state.get();
908 			res = replaceStringList(src, section, subsection, name, values);
909 		} while (!state.compareAndSet(src, res));
910 		if (notifyUponTransientChanges())
911 			fireConfigChangedEvent();
912 	}
913 
914 	private ConfigSnapshot replaceStringList(final ConfigSnapshot srcState,
915 			final String section, final String subsection, final String name,
916 			final List<String> values) {
917 		final List<ConfigLine> entries = copy(srcState, values);
918 		int entryIndex = 0;
919 		int valueIndex = 0;
920 		int insertPosition = -1;
921 
922 		// Reset the first n Entry objects that match this input name.
923 		//
924 		while (entryIndex < entries.size() && valueIndex < values.size()) {
925 			final ConfigLine e = entries.get(entryIndex);
926 			if (e.match(section, subsection, name)) {
927 				entries.set(entryIndex, e.forValue(values.get(valueIndex++)));
928 				insertPosition = entryIndex + 1;
929 			}
930 			entryIndex++;
931 		}
932 
933 		// Remove any extra Entry objects that we no longer need.
934 		//
935 		if (valueIndex == values.size() && entryIndex < entries.size()) {
936 			while (entryIndex < entries.size()) {
937 				final ConfigLine e = entries.get(entryIndex++);
938 				if (e.match(section, subsection, name))
939 					entries.remove(--entryIndex);
940 			}
941 		}
942 
943 		// Insert new Entry objects for additional/new values.
944 		//
945 		if (valueIndex < values.size() && entryIndex == entries.size()) {
946 			if (insertPosition < 0) {
947 				// We didn't find a matching key above, but maybe there
948 				// is already a section available that matches. Insert
949 				// after the last key of that section.
950 				//
951 				insertPosition = findSectionEnd(entries, section, subsection);
952 			}
953 			if (insertPosition < 0) {
954 				// We didn't find any matching section header for this key,
955 				// so we must create a new section header at the end.
956 				//
957 				final ConfigLine e = new ConfigLine();
958 				e.section = section;
959 				e.subsection = subsection;
960 				entries.add(e);
961 				insertPosition = entries.size();
962 			}
963 			while (valueIndex < values.size()) {
964 				final ConfigLine e = new ConfigLine();
965 				e.section = section;
966 				e.subsection = subsection;
967 				e.name = name;
968 				e.value = values.get(valueIndex++);
969 				entries.add(insertPosition++, e);
970 			}
971 		}
972 
973 		return newState(entries);
974 	}
975 
976 	private static List<ConfigLine> copy(final ConfigSnapshot src,
977 			final List<String> values) {
978 		// At worst we need to insert 1 line for each value, plus 1 line
979 		// for a new section header. Assume that and allocate the space.
980 		//
981 		final int max = src.entryList.size() + values.size() + 1;
982 		final ArrayList<ConfigLine> r = new ArrayList<>(max);
983 		r.addAll(src.entryList);
984 		return r;
985 	}
986 
987 	private static int findSectionEnd(final List<ConfigLine> entries,
988 			final String section, final String subsection) {
989 		for (int i = 0; i < entries.size(); i++) {
990 			ConfigLine e = entries.get(i);
991 			if (e.match(section, subsection, null)) {
992 				i++;
993 				while (i < entries.size()) {
994 					e = entries.get(i);
995 					if (e.match(section, subsection, e.name))
996 						i++;
997 					else
998 						break;
999 				}
1000 				return i;
1001 			}
1002 		}
1003 		return -1;
1004 	}
1005 
1006 	/**
1007 	 * Get this configuration, formatted as a Git style text file.
1008 	 *
1009 	 * @return this configuration, formatted as a Git style text file.
1010 	 */
1011 	public String toText() {
1012 		final StringBuilder out = new StringBuilder();
1013 		for (ConfigLine e : state.get().entryList) {
1014 			if (e.prefix != null)
1015 				out.append(e.prefix);
1016 			if (e.section != null && e.name == null) {
1017 				out.append('[');
1018 				out.append(e.section);
1019 				if (e.subsection != null) {
1020 					out.append(' ');
1021 					String escaped = escapeValue(e.subsection);
1022 					// make sure to avoid double quotes here
1023 					boolean quoted = escaped.startsWith("\"") //$NON-NLS-1$
1024 							&& escaped.endsWith("\""); //$NON-NLS-1$
1025 					if (!quoted)
1026 						out.append('"');
1027 					out.append(escaped);
1028 					if (!quoted)
1029 						out.append('"');
1030 				}
1031 				out.append(']');
1032 			} else if (e.section != null && e.name != null) {
1033 				if (e.prefix == null || "".equals(e.prefix)) //$NON-NLS-1$
1034 					out.append('\t');
1035 				out.append(e.name);
1036 				if (MAGIC_EMPTY_VALUE != e.value) {
1037 					out.append(" ="); //$NON-NLS-1$
1038 					if (e.value != null) {
1039 						out.append(' ');
1040 						out.append(escapeValue(e.value));
1041 					}
1042 				}
1043 				if (e.suffix != null)
1044 					out.append(' ');
1045 			}
1046 			if (e.suffix != null)
1047 				out.append(e.suffix);
1048 			out.append('\n');
1049 		}
1050 		return out.toString();
1051 	}
1052 
1053 	/**
1054 	 * Clear this configuration and reset to the contents of the parsed string.
1055 	 *
1056 	 * @param text
1057 	 *            Git style text file listing configuration properties.
1058 	 * @throws org.eclipse.jgit.errors.ConfigInvalidException
1059 	 *             the text supplied is not formatted correctly. No changes were
1060 	 *             made to {@code this}.
1061 	 */
1062 	public void fromText(String text) throws ConfigInvalidException {
1063 		state.set(newState(fromTextRecurse(text, 1)));
1064 	}
1065 
1066 	private List<ConfigLine> fromTextRecurse(String text, int depth)
1067 			throws ConfigInvalidException {
1068 		if (depth > MAX_DEPTH) {
1069 			throw new ConfigInvalidException(
1070 					JGitText.get().tooManyIncludeRecursions);
1071 		}
1072 		final List<ConfigLine> newEntries = new ArrayList<>();
1073 		final StringReader in = new StringReader(text);
1074 		ConfigLine last = null;
1075 		ConfigLine e = new ConfigLine();
1076 		for (;;) {
1077 			int input = in.read();
1078 			if (-1 == input) {
1079 				if (e.section != null)
1080 					newEntries.add(e);
1081 				break;
1082 			}
1083 
1084 			final char c = (char) input;
1085 			if ('\n' == c) {
1086 				// End of this entry.
1087 				newEntries.add(e);
1088 				if (e.section != null)
1089 					last = e;
1090 				e = new ConfigLine();
1091 
1092 			} else if (e.suffix != null) {
1093 				// Everything up until the end-of-line is in the suffix.
1094 				e.suffix += c;
1095 
1096 			} else if (';' == c || '#' == c) {
1097 				// The rest of this line is a comment; put into suffix.
1098 				e.suffix = String.valueOf(c);
1099 
1100 			} else if (e.section == null && Character.isWhitespace(c)) {
1101 				// Save the leading whitespace (if any).
1102 				if (e.prefix == null)
1103 					e.prefix = ""; //$NON-NLS-1$
1104 				e.prefix += c;
1105 
1106 			} else if ('[' == c) {
1107 				// This is a section header.
1108 				e.section = readSectionName(in);
1109 				input = in.read();
1110 				if ('"' == input) {
1111 					e.subsection = readSubsectionName(in);
1112 					input = in.read();
1113 				}
1114 				if (']' != input)
1115 					throw new ConfigInvalidException(JGitText.get().badGroupHeader);
1116 				e.suffix = ""; //$NON-NLS-1$
1117 
1118 			} else if (last != null) {
1119 				// Read a value.
1120 				e.section = last.section;
1121 				e.subsection = last.subsection;
1122 				in.reset();
1123 				e.name = readKeyName(in);
1124 				if (e.name.endsWith("\n")) { //$NON-NLS-1$
1125 					e.name = e.name.substring(0, e.name.length() - 1);
1126 					e.value = MAGIC_EMPTY_VALUE;
1127 				} else
1128 					e.value = readValue(in);
1129 
1130 				if (e.section.equalsIgnoreCase("include")) { //$NON-NLS-1$
1131 					addIncludedConfig(newEntries, e, depth);
1132 				}
1133 			} else
1134 				throw new ConfigInvalidException(JGitText.get().invalidLineInConfigFile);
1135 		}
1136 
1137 		return newEntries;
1138 	}
1139 
1140 	/**
1141 	 * Read the included config from the specified (possibly) relative path
1142 	 *
1143 	 * @param relPath
1144 	 *            possibly relative path to the included config, as specified in
1145 	 *            this config
1146 	 * @return the read bytes, or null if the included config should be ignored
1147 	 * @throws org.eclipse.jgit.errors.ConfigInvalidException
1148 	 *             if something went wrong while reading the config
1149 	 * @since 4.10
1150 	 */
1151 	@Nullable
1152 	protected byte[] readIncludedConfig(String relPath)
1153 			throws ConfigInvalidException {
1154 		return null;
1155 	}
1156 
1157 	private void addIncludedConfig(final List<ConfigLine> newEntries,
1158 			ConfigLine line, int depth) throws ConfigInvalidException {
1159 		if (!line.name.equalsIgnoreCase("path") || //$NON-NLS-1$
1160 				line.value == null || line.value.equals(MAGIC_EMPTY_VALUE)) {
1161 			throw new ConfigInvalidException(MessageFormat.format(
1162 					JGitText.get().invalidLineInConfigFileWithParam, line));
1163 		}
1164 		byte[] bytes = readIncludedConfig(line.value);
1165 		if (bytes == null) {
1166 			return;
1167 		}
1168 
1169 		String decoded;
1170 		if (isUtf8(bytes)) {
1171 			decoded = RawParseUtils.decode(CHARSET, bytes, 3, bytes.length);
1172 		} else {
1173 			decoded = RawParseUtils.decode(bytes);
1174 		}
1175 		try {
1176 			newEntries.addAll(fromTextRecurse(decoded, depth + 1));
1177 		} catch (ConfigInvalidException e) {
1178 			throw new ConfigInvalidException(MessageFormat
1179 					.format(JGitText.get().cannotReadFile, line.value), e);
1180 		}
1181 	}
1182 
1183 	private ConfigSnapshot newState() {
1184 		return new ConfigSnapshot(Collections.<ConfigLine> emptyList(),
1185 				getBaseState());
1186 	}
1187 
1188 	private ConfigSnapshot newState(List<ConfigLine> entries) {
1189 		return new ConfigSnapshot(Collections.unmodifiableList(entries),
1190 				getBaseState());
1191 	}
1192 
1193 	/**
1194 	 * Clear the configuration file
1195 	 */
1196 	protected void clear() {
1197 		state.set(newState());
1198 	}
1199 
1200 	/**
1201 	 * Check if bytes should be treated as UTF-8 or not.
1202 	 *
1203 	 * @param bytes
1204 	 *            the bytes to check encoding for.
1205 	 * @return true if bytes should be treated as UTF-8, false otherwise.
1206 	 * @since 4.4
1207 	 */
1208 	protected boolean isUtf8(final byte[] bytes) {
1209 		return bytes.length >= 3 && bytes[0] == (byte) 0xEF
1210 				&& bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF;
1211 	}
1212 
1213 	private static String readSectionName(StringReader in)
1214 			throws ConfigInvalidException {
1215 		final StringBuilder name = new StringBuilder();
1216 		for (;;) {
1217 			int c = in.read();
1218 			if (c < 0)
1219 				throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile);
1220 
1221 			if (']' == c) {
1222 				in.reset();
1223 				break;
1224 			}
1225 
1226 			if (' ' == c || '\t' == c) {
1227 				for (;;) {
1228 					c = in.read();
1229 					if (c < 0)
1230 						throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile);
1231 
1232 					if ('"' == c) {
1233 						in.reset();
1234 						break;
1235 					}
1236 
1237 					if (' ' == c || '\t' == c)
1238 						continue; // Skipped...
1239 					throw new ConfigInvalidException(MessageFormat.format(JGitText.get().badSectionEntry, name));
1240 				}
1241 				break;
1242 			}
1243 
1244 			if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c)
1245 				name.append((char) c);
1246 			else
1247 				throw new ConfigInvalidException(MessageFormat.format(JGitText.get().badSectionEntry, name));
1248 		}
1249 		return name.toString();
1250 	}
1251 
1252 	private static String readKeyName(StringReader in)
1253 			throws ConfigInvalidException {
1254 		final StringBuilder name = new StringBuilder();
1255 		for (;;) {
1256 			int c = in.read();
1257 			if (c < 0)
1258 				throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile);
1259 
1260 			if ('=' == c)
1261 				break;
1262 
1263 			if (' ' == c || '\t' == c) {
1264 				for (;;) {
1265 					c = in.read();
1266 					if (c < 0)
1267 						throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile);
1268 
1269 					if ('=' == c)
1270 						break;
1271 
1272 					if (';' == c || '#' == c || '\n' == c) {
1273 						in.reset();
1274 						break;
1275 					}
1276 
1277 					if (' ' == c || '\t' == c)
1278 						continue; // Skipped...
1279 					throw new ConfigInvalidException(JGitText.get().badEntryDelimiter);
1280 				}
1281 				break;
1282 			}
1283 
1284 			if (Character.isLetterOrDigit((char) c) || c == '-') {
1285 				// From the git-config man page:
1286 				// The variable names are case-insensitive and only
1287 				// alphanumeric characters and - are allowed.
1288 				name.append((char) c);
1289 			} else if ('\n' == c) {
1290 				in.reset();
1291 				name.append((char) c);
1292 				break;
1293 			} else
1294 				throw new ConfigInvalidException(MessageFormat.format(JGitText.get().badEntryName, name));
1295 		}
1296 		return name.toString();
1297 	}
1298 
1299 	private static String readSubsectionName(StringReader in)
1300 			throws ConfigInvalidException {
1301 		StringBuilder r = new StringBuilder();
1302 		for (;;) {
1303 			int c = in.read();
1304 			if (c < 0) {
1305 				break;
1306 			}
1307 
1308 			if ('\n' == c) {
1309 				throw new ConfigInvalidException(
1310 						JGitText.get().newlineInQuotesNotAllowed);
1311 			}
1312 			if ('\\' == c) {
1313 				c = in.read();
1314 				switch (c) {
1315 				case -1:
1316 					throw new ConfigInvalidException(JGitText.get().endOfFileInEscape);
1317 
1318 				case '\\':
1319 				case '"':
1320 					r.append((char) c);
1321 					continue;
1322 
1323 				default:
1324 					// C git simply drops backslashes if the escape sequence is not
1325 					// recognized.
1326 					r.append((char) c);
1327 					continue;
1328 				}
1329 			}
1330 			if ('"' == c) {
1331 				break;
1332 			}
1333 
1334 			r.append((char) c);
1335 		}
1336 		return r.toString();
1337 	}
1338 
1339 	private static String readValue(StringReader in)
1340 			throws ConfigInvalidException {
1341 		StringBuilder value = new StringBuilder();
1342 		StringBuilder trailingSpaces = null;
1343 		boolean quote = false;
1344 		boolean inLeadingSpace = true;
1345 
1346 		for (;;) {
1347 			int c = in.read();
1348 			if (c < 0) {
1349 				break;
1350 			}
1351 			if ('\n' == c) {
1352 				if (quote) {
1353 					throw new ConfigInvalidException(
1354 							JGitText.get().newlineInQuotesNotAllowed);
1355 				}
1356 				in.reset();
1357 				break;
1358 			}
1359 
1360 			if (!quote && (';' == c || '#' == c)) {
1361 				if (trailingSpaces != null) {
1362 					trailingSpaces.setLength(0);
1363 				}
1364 				in.reset();
1365 				break;
1366 			}
1367 
1368 			char cc = (char) c;
1369 			if (Character.isWhitespace(cc)) {
1370 				if (inLeadingSpace) {
1371 					continue;
1372 				}
1373 				if (trailingSpaces == null) {
1374 					trailingSpaces = new StringBuilder();
1375 				}
1376 				trailingSpaces.append(cc);
1377 				continue;
1378 			} else {
1379 				inLeadingSpace = false;
1380 				if (trailingSpaces != null) {
1381 					value.append(trailingSpaces);
1382 					trailingSpaces.setLength(0);
1383 				}
1384 			}
1385 
1386 			if ('\\' == c) {
1387 				c = in.read();
1388 				switch (c) {
1389 				case -1:
1390 					throw new ConfigInvalidException(JGitText.get().endOfFileInEscape);
1391 				case '\n':
1392 					continue;
1393 				case 't':
1394 					value.append('\t');
1395 					continue;
1396 				case 'b':
1397 					value.append('\b');
1398 					continue;
1399 				case 'n':
1400 					value.append('\n');
1401 					continue;
1402 				case '\\':
1403 					value.append('\\');
1404 					continue;
1405 				case '"':
1406 					value.append('"');
1407 					continue;
1408 				default:
1409 					throw new ConfigInvalidException(MessageFormat.format(
1410 							JGitText.get().badEscape,
1411 							Character.valueOf(((char) c))));
1412 				}
1413 			}
1414 
1415 			if ('"' == c) {
1416 				quote = !quote;
1417 				continue;
1418 			}
1419 
1420 			value.append(cc);
1421 		}
1422 		return value.length() > 0 ? value.toString() : null;
1423 	}
1424 
1425 	/**
1426 	 * Parses a section of the configuration into an application model object.
1427 	 * <p>
1428 	 * Instances must implement hashCode and equals such that model objects can
1429 	 * be cached by using the {@code SectionParser} as a key of a HashMap.
1430 	 * <p>
1431 	 * As the {@code SectionParser} itself is used as the key of the internal
1432 	 * HashMap applications should be careful to ensure the SectionParser key
1433 	 * does not retain unnecessary application state which may cause memory to
1434 	 * be held longer than expected.
1435 	 *
1436 	 * @param <T>
1437 	 *            type of the application model created by the parser.
1438 	 */
1439 	public static interface SectionParser<T> {
1440 		/**
1441 		 * Create a model object from a configuration.
1442 		 *
1443 		 * @param cfg
1444 		 *            the configuration to read values from.
1445 		 * @return the application model instance.
1446 		 */
1447 		T parse(Config cfg);
1448 	}
1449 
1450 	private static class StringReader {
1451 		private final char[] buf;
1452 
1453 		private int pos;
1454 
1455 		StringReader(String in) {
1456 			buf = in.toCharArray();
1457 		}
1458 
1459 		int read() {
1460 			try {
1461 				return buf[pos++];
1462 			} catch (ArrayIndexOutOfBoundsException e) {
1463 				pos = buf.length;
1464 				return -1;
1465 			}
1466 		}
1467 
1468 		void reset() {
1469 			pos--;
1470 		}
1471 	}
1472 
1473 	/**
1474 	 * Converts enumeration values into configuration options and vice-versa,
1475 	 * allowing to match a config option with an enum value.
1476 	 *
1477 	 */
1478 	public static interface ConfigEnum {
1479 		/**
1480 		 * Converts enumeration value into a string to be save in config.
1481 		 *
1482 		 * @return the enum value as config string
1483 		 */
1484 		String toConfigValue();
1485 
1486 		/**
1487 		 * Checks if the given string matches with enum value.
1488 		 *
1489 		 * @param in
1490 		 *            the string to match
1491 		 * @return true if the given string matches enum value, false otherwise
1492 		 */
1493 		boolean matchConfigValue(String in);
1494 	}
1495 }