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