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