SystemReader.java

  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. package org.eclipse.jgit.util;

  14. import java.io.File;
  15. import java.io.IOException;
  16. import java.net.InetAddress;
  17. import java.net.UnknownHostException;
  18. import java.nio.file.InvalidPathException;
  19. import java.nio.file.Path;
  20. import java.nio.file.Paths;
  21. import java.security.AccessController;
  22. import java.security.PrivilegedAction;
  23. import java.text.DateFormat;
  24. import java.text.SimpleDateFormat;
  25. import java.util.Locale;
  26. import java.util.TimeZone;
  27. import java.util.concurrent.atomic.AtomicReference;

  28. import org.eclipse.jgit.errors.ConfigInvalidException;
  29. import org.eclipse.jgit.errors.CorruptObjectException;
  30. import org.eclipse.jgit.internal.JGitText;
  31. import org.eclipse.jgit.lib.Config;
  32. import org.eclipse.jgit.lib.Constants;
  33. import org.eclipse.jgit.lib.ObjectChecker;
  34. import org.eclipse.jgit.lib.StoredConfig;
  35. import org.eclipse.jgit.storage.file.FileBasedConfig;
  36. import org.eclipse.jgit.util.time.MonotonicClock;
  37. import org.eclipse.jgit.util.time.MonotonicSystemClock;
  38. import org.slf4j.Logger;
  39. import org.slf4j.LoggerFactory;

  40. /**
  41.  * Interface to read values from the system.
  42.  * <p>
  43.  * When writing unit tests, extending this interface with a custom class
  44.  * permits to simulate an access to a system variable or property and
  45.  * permits to control the user's global configuration.
  46.  * </p>
  47.  */
  48. public abstract class SystemReader {

  49.     private static final Logger LOG = LoggerFactory
  50.             .getLogger(SystemReader.class);

  51.     private static final SystemReader DEFAULT;

  52.     private static volatile Boolean isMacOS;

  53.     private static volatile Boolean isWindows;

  54.     static {
  55.         SystemReader r = new Default();
  56.         r.init();
  57.         DEFAULT = r;
  58.     }

  59.     private static class Default extends SystemReader {
  60.         private volatile String hostname;

  61.         @Override
  62.         public String getenv(String variable) {
  63.             return System.getenv(variable);
  64.         }

  65.         @Override
  66.         public String getProperty(String key) {
  67.             return System.getProperty(key);
  68.         }

  69.         @Override
  70.         public FileBasedConfig openSystemConfig(Config parent, FS fs) {
  71.             if (StringUtils
  72.                     .isEmptyOrNull(getenv(Constants.GIT_CONFIG_NOSYSTEM_KEY))) {
  73.                 File configFile = fs.getGitSystemConfig();
  74.                 if (configFile != null) {
  75.                     return new FileBasedConfig(parent, configFile, fs);
  76.                 }
  77.             }
  78.             return new FileBasedConfig(parent, null, fs) {
  79.                 @Override
  80.                 public void load() {
  81.                     // empty, do not load
  82.                 }

  83.                 @Override
  84.                 public boolean isOutdated() {
  85.                     // regular class would bomb here
  86.                     return false;
  87.                 }
  88.             };
  89.         }

  90.         @Override
  91.         public FileBasedConfig openUserConfig(Config parent, FS fs) {
  92.             return new FileBasedConfig(parent, new File(fs.userHome(), ".gitconfig"), //$NON-NLS-1$
  93.                     fs);
  94.         }

  95.         private Path getXDGConfigHome(FS fs) {
  96.             String configHomePath = getenv(Constants.XDG_CONFIG_HOME);
  97.             if (StringUtils.isEmptyOrNull(configHomePath)) {
  98.                 configHomePath = new File(fs.userHome(), ".config") //$NON-NLS-1$
  99.                         .getAbsolutePath();
  100.             }
  101.             try {
  102.                 return Paths.get(configHomePath);
  103.             } catch (InvalidPathException e) {
  104.                 LOG.error(JGitText.get().logXDGConfigHomeInvalid,
  105.                         configHomePath, e);
  106.             }
  107.             return null;
  108.         }

  109.         @Override
  110.         public FileBasedConfig openJGitConfig(Config parent, FS fs) {
  111.             Path xdgPath = getXDGConfigHome(fs);
  112.             if (xdgPath != null) {
  113.                 Path configPath = xdgPath.resolve("jgit") //$NON-NLS-1$
  114.                         .resolve(Constants.CONFIG);
  115.                 return new FileBasedConfig(parent, configPath.toFile(), fs);
  116.             }
  117.             return new FileBasedConfig(parent,
  118.                     new File(fs.userHome(), ".jgitconfig"), fs); //$NON-NLS-1$
  119.         }

  120.         @Override
  121.         public String getHostname() {
  122.             if (hostname == null) {
  123.                 try {
  124.                     InetAddress localMachine = InetAddress.getLocalHost();
  125.                     hostname = localMachine.getCanonicalHostName();
  126.                 } catch (UnknownHostException e) {
  127.                     // we do nothing
  128.                     hostname = "localhost"; //$NON-NLS-1$
  129.                 }
  130.                 assert hostname != null;
  131.             }
  132.             return hostname;
  133.         }

  134.         @Override
  135.         public long getCurrentTime() {
  136.             return System.currentTimeMillis();
  137.         }

  138.         @Override
  139.         public int getTimezone(long when) {
  140.             return getTimeZone().getOffset(when) / (60 * 1000);
  141.         }
  142.     }

  143.     private static volatile SystemReader INSTANCE = DEFAULT;

  144.     /**
  145.      * Get the current SystemReader instance
  146.      *
  147.      * @return the current SystemReader instance.
  148.      */
  149.     public static SystemReader getInstance() {
  150.         return INSTANCE;
  151.     }

  152.     /**
  153.      * Set a new SystemReader instance to use when accessing properties.
  154.      *
  155.      * @param newReader
  156.      *            the new instance to use when accessing properties, or null for
  157.      *            the default instance.
  158.      */
  159.     public static void setInstance(SystemReader newReader) {
  160.         isMacOS = null;
  161.         isWindows = null;
  162.         if (newReader == null)
  163.             INSTANCE = DEFAULT;
  164.         else {
  165.             newReader.init();
  166.             INSTANCE = newReader;
  167.         }
  168.     }

  169.     private ObjectChecker platformChecker;

  170.     private AtomicReference<FileBasedConfig> systemConfig = new AtomicReference<>();

  171.     private AtomicReference<FileBasedConfig> userConfig = new AtomicReference<>();

  172.     private AtomicReference<FileBasedConfig> jgitConfig = new AtomicReference<>();

  173.     private void init() {
  174.         // Creating ObjectChecker must be deferred. Unit tests change
  175.         // behavior of is{Windows,MacOS} in constructor of subclass.
  176.         if (platformChecker == null)
  177.             setPlatformChecker();
  178.     }

  179.     /**
  180.      * Should be used in tests when the platform is explicitly changed.
  181.      *
  182.      * @since 3.6
  183.      */
  184.     protected final void setPlatformChecker() {
  185.         platformChecker = new ObjectChecker()
  186.             .setSafeForWindows(isWindows())
  187.             .setSafeForMacOS(isMacOS());
  188.     }

  189.     /**
  190.      * Gets the hostname of the local host. If no hostname can be found, the
  191.      * hostname is set to the default value "localhost".
  192.      *
  193.      * @return the canonical hostname
  194.      */
  195.     public abstract String getHostname();

  196.     /**
  197.      * Get value of the system variable
  198.      *
  199.      * @param variable
  200.      *            system variable to read
  201.      * @return value of the system variable
  202.      */
  203.     public abstract String getenv(String variable);

  204.     /**
  205.      * Get value of the system property
  206.      *
  207.      * @param key
  208.      *            of the system property to read
  209.      * @return value of the system property
  210.      */
  211.     public abstract String getProperty(String key);

  212.     /**
  213.      * Open the git configuration found in the user home. Use
  214.      * {@link #getUserConfig()} to get the current git configuration in the user
  215.      * home since it manages automatic reloading when the gitconfig file was
  216.      * modified and avoids unnecessary reloads.
  217.      *
  218.      * @param parent
  219.      *            a config with values not found directly in the returned config
  220.      * @param fs
  221.      *            the file system abstraction which will be necessary to perform
  222.      *            certain file system operations.
  223.      * @return the git configuration found in the user home
  224.      */
  225.     public abstract FileBasedConfig openUserConfig(Config parent, FS fs);

  226.     /**
  227.      * Open the gitconfig configuration found in the system-wide "etc"
  228.      * directory. Use {@link #getSystemConfig()} to get the current system-wide
  229.      * git configuration since it manages automatic reloading when the gitconfig
  230.      * file was modified and avoids unnecessary reloads.
  231.      *
  232.      * @param parent
  233.      *            a config with values not found directly in the returned
  234.      *            config. Null is a reasonable value here.
  235.      * @param fs
  236.      *            the file system abstraction which will be necessary to perform
  237.      *            certain file system operations.
  238.      * @return the gitconfig configuration found in the system-wide "etc"
  239.      *         directory
  240.      */
  241.     public abstract FileBasedConfig openSystemConfig(Config parent, FS fs);

  242.     /**
  243.      * Open the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. Use
  244.      * {@link #getJGitConfig()} to get the current jgit configuration in the
  245.      * user home since it manages automatic reloading when the jgit config file
  246.      * was modified and avoids unnecessary reloads.
  247.      *
  248.      * @param parent
  249.      *            a config with values not found directly in the returned config
  250.      * @param fs
  251.      *            the file system abstraction which will be necessary to perform
  252.      *            certain file system operations.
  253.      * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
  254.      * @since 5.5.2
  255.      */
  256.     public abstract FileBasedConfig openJGitConfig(Config parent, FS fs);

  257.     /**
  258.      * Get the git configuration found in the user home. The configuration will
  259.      * be reloaded automatically if the configuration file was modified. Also
  260.      * reloads the system config if the system config file was modified. If the
  261.      * configuration file wasn't modified returns the cached configuration.
  262.      *
  263.      * @return the git configuration found in the user home
  264.      * @throws ConfigInvalidException
  265.      *             if configuration is invalid
  266.      * @throws IOException
  267.      *             if something went wrong when reading files
  268.      * @since 5.1.9
  269.      */
  270.     public StoredConfig getUserConfig()
  271.             throws ConfigInvalidException, IOException {
  272.         FileBasedConfig c = userConfig.get();
  273.         if (c == null) {
  274.             userConfig.compareAndSet(null,
  275.                     openUserConfig(getSystemConfig(), FS.DETECTED));
  276.             c = userConfig.get();
  277.         }
  278.         // on the very first call this will check a second time if the system
  279.         // config is outdated
  280.         updateAll(c);
  281.         return c;
  282.     }

  283.     /**
  284.      * Get the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. The
  285.      * configuration will be reloaded automatically if the configuration file
  286.      * was modified. If the configuration file wasn't modified returns the
  287.      * cached configuration.
  288.      *
  289.      * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
  290.      * @throws ConfigInvalidException
  291.      *             if configuration is invalid
  292.      * @throws IOException
  293.      *             if something went wrong when reading files
  294.      * @since 5.5.2
  295.      */
  296.     public StoredConfig getJGitConfig()
  297.             throws ConfigInvalidException, IOException {
  298.         FileBasedConfig c = jgitConfig.get();
  299.         if (c == null) {
  300.             jgitConfig.compareAndSet(null,
  301.                     openJGitConfig(null, FS.DETECTED));
  302.             c = jgitConfig.get();
  303.         }
  304.         updateAll(c);
  305.         return c;
  306.     }

  307.     /**
  308.      * Get the gitconfig configuration found in the system-wide "etc" directory.
  309.      * The configuration will be reloaded automatically if the configuration
  310.      * file was modified otherwise returns the cached system level config.
  311.      *
  312.      * @return the gitconfig configuration found in the system-wide "etc"
  313.      *         directory
  314.      * @throws ConfigInvalidException
  315.      *             if configuration is invalid
  316.      * @throws IOException
  317.      *             if something went wrong when reading files
  318.      * @since 5.1.9
  319.      */
  320.     public StoredConfig getSystemConfig()
  321.             throws ConfigInvalidException, IOException {
  322.         FileBasedConfig c = systemConfig.get();
  323.         if (c == null) {
  324.             systemConfig.compareAndSet(null,
  325.                     openSystemConfig(getJGitConfig(), FS.DETECTED));
  326.             c = systemConfig.get();
  327.         }
  328.         updateAll(c);
  329.         return c;
  330.     }

  331.     /**
  332.      * Update config and its parents if they seem modified
  333.      *
  334.      * @param config
  335.      *            configuration to reload if outdated
  336.      * @throws ConfigInvalidException
  337.      *             if configuration is invalid
  338.      * @throws IOException
  339.      *             if something went wrong when reading files
  340.      */
  341.     private void updateAll(Config config)
  342.             throws ConfigInvalidException, IOException {
  343.         if (config == null) {
  344.             return;
  345.         }
  346.         updateAll(config.getBaseConfig());
  347.         if (config instanceof FileBasedConfig) {
  348.             FileBasedConfig cfg = (FileBasedConfig) config;
  349.             if (cfg.isOutdated()) {
  350.                 LOG.debug("loading config {}", cfg); //$NON-NLS-1$
  351.                 cfg.load();
  352.             }
  353.         }
  354.     }

  355.     /**
  356.      * Get the current system time
  357.      *
  358.      * @return the current system time
  359.      */
  360.     public abstract long getCurrentTime();

  361.     /**
  362.      * Get clock instance preferred by this system.
  363.      *
  364.      * @return clock instance preferred by this system.
  365.      * @since 4.6
  366.      */
  367.     public MonotonicClock getClock() {
  368.         return new MonotonicSystemClock();
  369.     }

  370.     /**
  371.      * Get the local time zone
  372.      *
  373.      * @param when
  374.      *            a system timestamp
  375.      * @return the local time zone
  376.      */
  377.     public abstract int getTimezone(long when);

  378.     /**
  379.      * Get system time zone, possibly mocked for testing
  380.      *
  381.      * @return system time zone, possibly mocked for testing
  382.      * @since 1.2
  383.      */
  384.     public TimeZone getTimeZone() {
  385.         return TimeZone.getDefault();
  386.     }

  387.     /**
  388.      * Get the locale to use
  389.      *
  390.      * @return the locale to use
  391.      * @since 1.2
  392.      */
  393.     public Locale getLocale() {
  394.         return Locale.getDefault();
  395.     }

  396.     /**
  397.      * Returns a simple date format instance as specified by the given pattern.
  398.      *
  399.      * @param pattern
  400.      *            the pattern as defined in
  401.      *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
  402.      * @return the simple date format
  403.      * @since 2.0
  404.      */
  405.     public SimpleDateFormat getSimpleDateFormat(String pattern) {
  406.         return new SimpleDateFormat(pattern);
  407.     }

  408.     /**
  409.      * Returns a simple date format instance as specified by the given pattern.
  410.      *
  411.      * @param pattern
  412.      *            the pattern as defined in
  413.      *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
  414.      * @param locale
  415.      *            locale to be used for the {@code SimpleDateFormat}
  416.      * @return the simple date format
  417.      * @since 3.2
  418.      */
  419.     public SimpleDateFormat getSimpleDateFormat(String pattern, Locale locale) {
  420.         return new SimpleDateFormat(pattern, locale);
  421.     }

  422.     /**
  423.      * Returns a date/time format instance for the given styles.
  424.      *
  425.      * @param dateStyle
  426.      *            the date style as specified in
  427.      *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
  428.      * @param timeStyle
  429.      *            the time style as specified in
  430.      *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
  431.      * @return the date format
  432.      * @since 2.0
  433.      */
  434.     public DateFormat getDateTimeInstance(int dateStyle, int timeStyle) {
  435.         return DateFormat.getDateTimeInstance(dateStyle, timeStyle);
  436.     }

  437.     /**
  438.      * Whether we are running on Windows.
  439.      *
  440.      * @return true if we are running on Windows.
  441.      */
  442.     public boolean isWindows() {
  443.         if (isWindows == null) {
  444.             String osDotName = getOsName();
  445.             isWindows = Boolean.valueOf(osDotName.startsWith("Windows")); //$NON-NLS-1$
  446.         }
  447.         return isWindows.booleanValue();
  448.     }

  449.     /**
  450.      * Whether we are running on Mac OS X
  451.      *
  452.      * @return true if we are running on Mac OS X
  453.      */
  454.     public boolean isMacOS() {
  455.         if (isMacOS == null) {
  456.             String osDotName = getOsName();
  457.             isMacOS = Boolean.valueOf(
  458.                     "Mac OS X".equals(osDotName) || "Darwin".equals(osDotName)); //$NON-NLS-1$ //$NON-NLS-2$
  459.         }
  460.         return isMacOS.booleanValue();
  461.     }

  462.     private String getOsName() {
  463.         return AccessController.doPrivileged(
  464.                 (PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$
  465.         );
  466.     }

  467.     /**
  468.      * Check tree path entry for validity.
  469.      * <p>
  470.      * Scans a multi-directory path string such as {@code "src/main.c"}.
  471.      *
  472.      * @param path path string to scan.
  473.      * @throws org.eclipse.jgit.errors.CorruptObjectException path is invalid.
  474.      * @since 3.6
  475.      */
  476.     public void checkPath(String path) throws CorruptObjectException {
  477.         platformChecker.checkPath(path);
  478.     }

  479.     /**
  480.      * Check tree path entry for validity.
  481.      * <p>
  482.      * Scans a multi-directory path string such as {@code "src/main.c"}.
  483.      *
  484.      * @param path
  485.      *            path string to scan.
  486.      * @throws org.eclipse.jgit.errors.CorruptObjectException
  487.      *             path is invalid.
  488.      * @since 4.2
  489.      */
  490.     public void checkPath(byte[] path) throws CorruptObjectException {
  491.         platformChecker.checkPath(path, 0, path.length);
  492.     }
  493. }