View Javadoc
1   /*
2    * Copyright (C) 2009, Google Inc.
3    * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com>
4    * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com>
5    * Copyright (C) 2012, Daniel Megert <daniel_megert@ch.ibm.com> and others
6    *
7    * This program and the accompanying materials are made available under the
8    * terms of the Eclipse Distribution License v. 1.0 which is available at
9    * https://www.eclipse.org/org/documents/edl-v10.php.
10   *
11   * SPDX-License-Identifier: BSD-3-Clause
12   */
13  
14  package org.eclipse.jgit.util;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.net.InetAddress;
19  import java.net.UnknownHostException;
20  import java.nio.file.InvalidPathException;
21  import java.nio.file.Path;
22  import java.nio.file.Paths;
23  import java.security.AccessController;
24  import java.security.PrivilegedAction;
25  import java.text.DateFormat;
26  import java.text.SimpleDateFormat;
27  import java.util.Locale;
28  import java.util.TimeZone;
29  import java.util.concurrent.atomic.AtomicReference;
30  
31  import org.eclipse.jgit.errors.ConfigInvalidException;
32  import org.eclipse.jgit.errors.CorruptObjectException;
33  import org.eclipse.jgit.internal.JGitText;
34  import org.eclipse.jgit.lib.Config;
35  import org.eclipse.jgit.lib.Constants;
36  import org.eclipse.jgit.lib.ObjectChecker;
37  import org.eclipse.jgit.lib.StoredConfig;
38  import org.eclipse.jgit.storage.file.FileBasedConfig;
39  import org.eclipse.jgit.util.time.MonotonicClock;
40  import org.eclipse.jgit.util.time.MonotonicSystemClock;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  /**
45   * Interface to read values from the system.
46   * <p>
47   * When writing unit tests, extending this interface with a custom class
48   * permits to simulate an access to a system variable or property and
49   * permits to control the user's global configuration.
50   * </p>
51   */
52  public abstract class SystemReader {
53  
54  	private static final Logger LOG = LoggerFactory
55  			.getLogger(SystemReader.class);
56  
57  	private static final SystemReader DEFAULT;
58  
59  	private static Boolean isMacOS;
60  
61  	private static Boolean isWindows;
62  
63  	static {
64  		SystemReader r = new Default();
65  		r.init();
66  		DEFAULT = r;
67  	}
68  
69  	private static class Default extends SystemReader {
70  		private volatile String hostname;
71  
72  		@Override
73  		public String getenv(String variable) {
74  			return System.getenv(variable);
75  		}
76  
77  		@Override
78  		public String getProperty(String key) {
79  			return System.getProperty(key);
80  		}
81  
82  		@Override
83  		public FileBasedConfig openSystemConfig(Config parent, FS fs) {
84  			if (StringUtils
85  					.isEmptyOrNull(getenv(Constants.GIT_CONFIG_NOSYSTEM_KEY))) {
86  				File configFile = fs.getGitSystemConfig();
87  				if (configFile != null) {
88  					return new FileBasedConfig(parent, configFile, fs);
89  				}
90  			}
91  			return new FileBasedConfig(parent, null, fs) {
92  				@Override
93  				public void load() {
94  					// empty, do not load
95  				}
96  
97  				@Override
98  				public boolean isOutdated() {
99  					// regular class would bomb here
100 					return false;
101 				}
102 			};
103 		}
104 
105 		@Override
106 		public FileBasedConfig openUserConfig(Config parent, FS fs) {
107 			return new FileBasedConfig(parent, new File(fs.userHome(), ".gitconfig"), //$NON-NLS-1$
108 					fs);
109 		}
110 
111 		private Path getXDGConfigHome(FS fs) {
112 			String configHomePath = getenv(Constants.XDG_CONFIG_HOME);
113 			if (StringUtils.isEmptyOrNull(configHomePath)) {
114 				configHomePath = new File(fs.userHome(), ".config") //$NON-NLS-1$
115 						.getAbsolutePath();
116 			}
117 			try {
118 				return Paths.get(configHomePath);
119 			} catch (InvalidPathException e) {
120 				LOG.error(JGitText.get().logXDGConfigHomeInvalid,
121 						configHomePath, e);
122 			}
123 			return null;
124 		}
125 
126 		@Override
127 		public FileBasedConfig openJGitConfig(Config parent, FS fs) {
128 			Path xdgPath = getXDGConfigHome(fs);
129 			if (xdgPath != null) {
130 				Path configPath = xdgPath.resolve("jgit") //$NON-NLS-1$
131 						.resolve(Constants.CONFIG);
132 				return new FileBasedConfig(parent, configPath.toFile(), fs);
133 			}
134 			return new FileBasedConfig(parent,
135 					new File(fs.userHome(), ".jgitconfig"), fs); //$NON-NLS-1$
136 		}
137 
138 		@Override
139 		public String getHostname() {
140 			if (hostname == null) {
141 				try {
142 					InetAddress localMachine = InetAddress.getLocalHost();
143 					hostname = localMachine.getCanonicalHostName();
144 				} catch (UnknownHostException e) {
145 					// we do nothing
146 					hostname = "localhost"; //$NON-NLS-1$
147 				}
148 				assert hostname != null;
149 			}
150 			return hostname;
151 		}
152 
153 		@Override
154 		public long getCurrentTime() {
155 			return System.currentTimeMillis();
156 		}
157 
158 		@Override
159 		public int getTimezone(long when) {
160 			return getTimeZone().getOffset(when) / (60 * 1000);
161 		}
162 	}
163 
164 	private static volatile SystemReader INSTANCE = DEFAULT;
165 
166 	/**
167 	 * Get the current SystemReader instance
168 	 *
169 	 * @return the current SystemReader instance.
170 	 */
171 	public static SystemReader getInstance() {
172 		return INSTANCE;
173 	}
174 
175 	/**
176 	 * Set a new SystemReader instance to use when accessing properties.
177 	 *
178 	 * @param newReader
179 	 *            the new instance to use when accessing properties, or null for
180 	 *            the default instance.
181 	 */
182 	public static void setInstance(SystemReader newReader) {
183 		isMacOS = null;
184 		isWindows = null;
185 		if (newReader == null)
186 			INSTANCE = DEFAULT;
187 		else {
188 			newReader.init();
189 			INSTANCE = newReader;
190 		}
191 	}
192 
193 	private ObjectChecker platformChecker;
194 
195 	private AtomicReference<FileBasedConfig> systemConfig = new AtomicReference<>();
196 
197 	private AtomicReference<FileBasedConfig> userConfig = new AtomicReference<>();
198 
199 	private AtomicReference<FileBasedConfig> jgitConfig = new AtomicReference<>();
200 
201 	private void init() {
202 		// Creating ObjectChecker must be deferred. Unit tests change
203 		// behavior of is{Windows,MacOS} in constructor of subclass.
204 		if (platformChecker == null)
205 			setPlatformChecker();
206 	}
207 
208 	/**
209 	 * Should be used in tests when the platform is explicitly changed.
210 	 *
211 	 * @since 3.6
212 	 */
213 	protected final void setPlatformChecker() {
214 		platformChecker = new ObjectChecker()
215 			.setSafeForWindows(isWindows())
216 			.setSafeForMacOS(isMacOS());
217 	}
218 
219 	/**
220 	 * Gets the hostname of the local host. If no hostname can be found, the
221 	 * hostname is set to the default value "localhost".
222 	 *
223 	 * @return the canonical hostname
224 	 */
225 	public abstract String getHostname();
226 
227 	/**
228 	 * Get value of the system variable
229 	 *
230 	 * @param variable
231 	 *            system variable to read
232 	 * @return value of the system variable
233 	 */
234 	public abstract String getenv(String variable);
235 
236 	/**
237 	 * Get value of the system property
238 	 *
239 	 * @param key
240 	 *            of the system property to read
241 	 * @return value of the system property
242 	 */
243 	public abstract String getProperty(String key);
244 
245 	/**
246 	 * Open the git configuration found in the user home. Use
247 	 * {@link #getUserConfig()} to get the current git configuration in the user
248 	 * home since it manages automatic reloading when the gitconfig file was
249 	 * modified and avoids unnecessary reloads.
250 	 *
251 	 * @param parent
252 	 *            a config with values not found directly in the returned config
253 	 * @param fs
254 	 *            the file system abstraction which will be necessary to perform
255 	 *            certain file system operations.
256 	 * @return the git configuration found in the user home
257 	 */
258 	public abstract FileBasedConfig openUserConfig(Config parent, FS fs);
259 
260 	/**
261 	 * Open the gitconfig configuration found in the system-wide "etc"
262 	 * directory. Use {@link #getSystemConfig()} to get the current system-wide
263 	 * git configuration since it manages automatic reloading when the gitconfig
264 	 * file was modified and avoids unnecessary reloads.
265 	 *
266 	 * @param parent
267 	 *            a config with values not found directly in the returned
268 	 *            config. Null is a reasonable value here.
269 	 * @param fs
270 	 *            the file system abstraction which will be necessary to perform
271 	 *            certain file system operations.
272 	 * @return the gitconfig configuration found in the system-wide "etc"
273 	 *         directory
274 	 */
275 	public abstract FileBasedConfig openSystemConfig(Config parent, FS fs);
276 
277 	/**
278 	 * Open the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. Use
279 	 * {@link #getJGitConfig()} to get the current jgit configuration in the
280 	 * user home since it manages automatic reloading when the jgit config file
281 	 * was modified and avoids unnecessary reloads.
282 	 *
283 	 * @param parent
284 	 *            a config with values not found directly in the returned config
285 	 * @param fs
286 	 *            the file system abstraction which will be necessary to perform
287 	 *            certain file system operations.
288 	 * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
289 	 * @since 5.5.2
290 	 */
291 	public abstract FileBasedConfig openJGitConfig(Config parent, FS fs);
292 
293 	/**
294 	 * Get the git configuration found in the user home. The configuration will
295 	 * be reloaded automatically if the configuration file was modified. Also
296 	 * reloads the system config if the system config file was modified. If the
297 	 * configuration file wasn't modified returns the cached configuration.
298 	 *
299 	 * @return the git configuration found in the user home
300 	 * @throws ConfigInvalidException
301 	 *             if configuration is invalid
302 	 * @throws IOException
303 	 *             if something went wrong when reading files
304 	 * @since 5.1.9
305 	 */
306 	public StoredConfig getUserConfig()
307 			throws ConfigInvalidException, IOException {
308 		FileBasedConfig c = userConfig.get();
309 		if (c == null) {
310 			userConfig.compareAndSet(null,
311 					openUserConfig(getSystemConfig(), FS.DETECTED));
312 			c = userConfig.get();
313 		}
314 		// on the very first call this will check a second time if the system
315 		// config is outdated
316 		updateAll(c);
317 		return c;
318 	}
319 
320 	/**
321 	 * Get the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. The
322 	 * configuration will be reloaded automatically if the configuration file
323 	 * was modified. If the configuration file wasn't modified returns the
324 	 * cached configuration.
325 	 *
326 	 * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
327 	 * @throws ConfigInvalidException
328 	 *             if configuration is invalid
329 	 * @throws IOException
330 	 *             if something went wrong when reading files
331 	 * @since 5.5.2
332 	 */
333 	public StoredConfig getJGitConfig()
334 			throws ConfigInvalidException, IOException {
335 		FileBasedConfig c = jgitConfig.get();
336 		if (c == null) {
337 			jgitConfig.compareAndSet(null,
338 					openJGitConfig(null, FS.DETECTED));
339 			c = jgitConfig.get();
340 		}
341 		updateAll(c);
342 		return c;
343 	}
344 
345 	/**
346 	 * Get the gitconfig configuration found in the system-wide "etc" directory.
347 	 * The configuration will be reloaded automatically if the configuration
348 	 * file was modified otherwise returns the cached system level config.
349 	 *
350 	 * @return the gitconfig configuration found in the system-wide "etc"
351 	 *         directory
352 	 * @throws ConfigInvalidException
353 	 *             if configuration is invalid
354 	 * @throws IOException
355 	 *             if something went wrong when reading files
356 	 * @since 5.1.9
357 	 */
358 	public StoredConfig getSystemConfig()
359 			throws ConfigInvalidException, IOException {
360 		FileBasedConfig c = systemConfig.get();
361 		if (c == null) {
362 			systemConfig.compareAndSet(null,
363 					openSystemConfig(getJGitConfig(), FS.DETECTED));
364 			c = systemConfig.get();
365 		}
366 		updateAll(c);
367 		return c;
368 	}
369 
370 	/**
371 	 * Update config and its parents if they seem modified
372 	 *
373 	 * @param config
374 	 *            configuration to reload if outdated
375 	 * @throws ConfigInvalidException
376 	 *             if configuration is invalid
377 	 * @throws IOException
378 	 *             if something went wrong when reading files
379 	 */
380 	private void updateAll(Config config)
381 			throws ConfigInvalidException, IOException {
382 		if (config == null) {
383 			return;
384 		}
385 		updateAll(config.getBaseConfig());
386 		if (config instanceof FileBasedConfig) {
387 			FileBasedConfig cfg = (FileBasedConfig) config;
388 			if (cfg.isOutdated()) {
389 				LOG.debug("loading config {}", cfg); //$NON-NLS-1$
390 				cfg.load();
391 			}
392 		}
393 	}
394 
395 	/**
396 	 * Get the current system time
397 	 *
398 	 * @return the current system time
399 	 */
400 	public abstract long getCurrentTime();
401 
402 	/**
403 	 * Get clock instance preferred by this system.
404 	 *
405 	 * @return clock instance preferred by this system.
406 	 * @since 4.6
407 	 */
408 	public MonotonicClock getClock() {
409 		return new MonotonicSystemClock();
410 	}
411 
412 	/**
413 	 * Get the local time zone
414 	 *
415 	 * @param when
416 	 *            a system timestamp
417 	 * @return the local time zone
418 	 */
419 	public abstract int getTimezone(long when);
420 
421 	/**
422 	 * Get system time zone, possibly mocked for testing
423 	 *
424 	 * @return system time zone, possibly mocked for testing
425 	 * @since 1.2
426 	 */
427 	public TimeZone getTimeZone() {
428 		return TimeZone.getDefault();
429 	}
430 
431 	/**
432 	 * Get the locale to use
433 	 *
434 	 * @return the locale to use
435 	 * @since 1.2
436 	 */
437 	public Locale getLocale() {
438 		return Locale.getDefault();
439 	}
440 
441 	/**
442 	 * Returns a simple date format instance as specified by the given pattern.
443 	 *
444 	 * @param pattern
445 	 *            the pattern as defined in
446 	 *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
447 	 * @return the simple date format
448 	 * @since 2.0
449 	 */
450 	public SimpleDateFormat getSimpleDateFormat(String pattern) {
451 		return new SimpleDateFormat(pattern);
452 	}
453 
454 	/**
455 	 * Returns a simple date format instance as specified by the given pattern.
456 	 *
457 	 * @param pattern
458 	 *            the pattern as defined in
459 	 *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
460 	 * @param locale
461 	 *            locale to be used for the {@code SimpleDateFormat}
462 	 * @return the simple date format
463 	 * @since 3.2
464 	 */
465 	public SimpleDateFormat getSimpleDateFormat(String pattern, Locale locale) {
466 		return new SimpleDateFormat(pattern, locale);
467 	}
468 
469 	/**
470 	 * Returns a date/time format instance for the given styles.
471 	 *
472 	 * @param dateStyle
473 	 *            the date style as specified in
474 	 *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
475 	 * @param timeStyle
476 	 *            the time style as specified in
477 	 *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
478 	 * @return the date format
479 	 * @since 2.0
480 	 */
481 	public DateFormat getDateTimeInstance(int dateStyle, int timeStyle) {
482 		return DateFormat.getDateTimeInstance(dateStyle, timeStyle);
483 	}
484 
485 	/**
486 	 * Whether we are running on Windows.
487 	 *
488 	 * @return true if we are running on Windows.
489 	 */
490 	public boolean isWindows() {
491 		if (isWindows == null) {
492 			String osDotName = getOsName();
493 			isWindows = Boolean.valueOf(osDotName.startsWith("Windows")); //$NON-NLS-1$
494 		}
495 		return isWindows.booleanValue();
496 	}
497 
498 	/**
499 	 * Whether we are running on Mac OS X
500 	 *
501 	 * @return true if we are running on Mac OS X
502 	 */
503 	public boolean isMacOS() {
504 		if (isMacOS == null) {
505 			String osDotName = getOsName();
506 			isMacOS = Boolean.valueOf(
507 					"Mac OS X".equals(osDotName) || "Darwin".equals(osDotName)); //$NON-NLS-1$ //$NON-NLS-2$
508 		}
509 		return isMacOS.booleanValue();
510 	}
511 
512 	private String getOsName() {
513 		return AccessController.doPrivileged(
514 				(PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$
515 		);
516 	}
517 
518 	/**
519 	 * Check tree path entry for validity.
520 	 * <p>
521 	 * Scans a multi-directory path string such as {@code "src/main.c"}.
522 	 *
523 	 * @param path path string to scan.
524 	 * @throws org.eclipse.jgit.errors.CorruptObjectException path is invalid.
525 	 * @since 3.6
526 	 */
527 	public void checkPath(String path) throws CorruptObjectException {
528 		platformChecker.checkPath(path);
529 	}
530 
531 	/**
532 	 * Check tree path entry for validity.
533 	 * <p>
534 	 * Scans a multi-directory path string such as {@code "src/main.c"}.
535 	 *
536 	 * @param path
537 	 *            path string to scan.
538 	 * @throws org.eclipse.jgit.errors.CorruptObjectException
539 	 *             path is invalid.
540 	 * @since 4.2
541 	 */
542 	public void checkPath(byte[] path) throws CorruptObjectException {
543 		platformChecker.checkPath(path, 0, path.length);
544 	}
545 }