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