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