FileBasedConfig.java

  1. /*
  2.  * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
  3.  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
  4.  * Copyright (C) 2009, Google Inc.
  5.  * Copyright (C) 2009, JetBrains s.r.o.
  6.  * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com>
  7.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  8.  * Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com> and others
  9.  *
  10.  * This program and the accompanying materials are made available under the
  11.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  12.  * https://www.eclipse.org/org/documents/edl-v10.php.
  13.  *
  14.  * SPDX-License-Identifier: BSD-3-Clause
  15.  */

  16. package org.eclipse.jgit.storage.file;

  17. import static java.nio.charset.StandardCharsets.UTF_8;

  18. import java.io.ByteArrayOutputStream;
  19. import java.io.File;
  20. import java.io.FileNotFoundException;
  21. import java.io.IOException;
  22. import java.text.MessageFormat;

  23. import org.eclipse.jgit.errors.ConfigInvalidException;
  24. import org.eclipse.jgit.errors.LockFailedException;
  25. import org.eclipse.jgit.internal.JGitText;
  26. import org.eclipse.jgit.internal.storage.file.FileSnapshot;
  27. import org.eclipse.jgit.internal.storage.file.LockFile;
  28. import org.eclipse.jgit.lib.Config;
  29. import org.eclipse.jgit.lib.Constants;
  30. import org.eclipse.jgit.lib.ObjectId;
  31. import org.eclipse.jgit.lib.StoredConfig;
  32. import org.eclipse.jgit.util.FS;
  33. import org.eclipse.jgit.util.FileUtils;
  34. import org.eclipse.jgit.util.IO;
  35. import org.eclipse.jgit.util.RawParseUtils;
  36. import org.slf4j.Logger;
  37. import org.slf4j.LoggerFactory;

  38. /**
  39.  * The configuration file that is stored in the file of the file system.
  40.  */
  41. public class FileBasedConfig extends StoredConfig {
  42.     private static final Logger LOG = LoggerFactory
  43.             .getLogger(FileBasedConfig.class);

  44.     private final File configFile;

  45.     private final FS fs;

  46.     private boolean utf8Bom;

  47.     private volatile FileSnapshot snapshot;

  48.     private volatile ObjectId hash;

  49.     /**
  50.      * Create a configuration with no default fallback.
  51.      *
  52.      * @param cfgLocation
  53.      *            the location of the configuration file on the file system
  54.      * @param fs
  55.      *            the file system abstraction which will be necessary to perform
  56.      *            certain file system operations.
  57.      */
  58.     public FileBasedConfig(File cfgLocation, FS fs) {
  59.         this(null, cfgLocation, fs);
  60.     }

  61.     /**
  62.      * The constructor
  63.      *
  64.      * @param base
  65.      *            the base configuration file
  66.      * @param cfgLocation
  67.      *            the location of the configuration file on the file system
  68.      * @param fs
  69.      *            the file system abstraction which will be necessary to perform
  70.      *            certain file system operations.
  71.      */
  72.     public FileBasedConfig(Config base, File cfgLocation, FS fs) {
  73.         super(base);
  74.         configFile = cfgLocation;
  75.         this.fs = fs;
  76.         this.snapshot = FileSnapshot.DIRTY;
  77.         this.hash = ObjectId.zeroId();
  78.     }

  79.     /** {@inheritDoc} */
  80.     @Override
  81.     protected boolean notifyUponTransientChanges() {
  82.         // we will notify listeners upon save()
  83.         return false;
  84.     }

  85.     /**
  86.      * Get location of the configuration file on disk
  87.      *
  88.      * @return location of the configuration file on disk
  89.      */
  90.     public final File getFile() {
  91.         return configFile;
  92.     }

  93.     /**
  94.      * {@inheritDoc}
  95.      * <p>
  96.      * Load the configuration as a Git text style configuration file.
  97.      * <p>
  98.      * If the file does not exist, this configuration is cleared, and thus
  99.      * behaves the same as though the file exists, but is empty.
  100.      */
  101.     @Override
  102.     public void load() throws IOException, ConfigInvalidException {
  103.         final int maxRetries = 5;
  104.         int retryDelayMillis = 20;
  105.         int retries = 0;
  106.         while (true) {
  107.             final FileSnapshot oldSnapshot = snapshot;
  108.             final FileSnapshot newSnapshot;
  109.             // don't use config in this snapshot to avoid endless recursion
  110.             newSnapshot = FileSnapshot.saveNoConfig(getFile());
  111.             try {
  112.                 final byte[] in = IO.readFully(getFile());
  113.                 final ObjectId newHash = hash(in);
  114.                 if (hash.equals(newHash)) {
  115.                     if (oldSnapshot.equals(newSnapshot)) {
  116.                         oldSnapshot.setClean(newSnapshot);
  117.                     } else {
  118.                         snapshot = newSnapshot;
  119.                     }
  120.                 } else {
  121.                     final String decoded;
  122.                     if (isUtf8(in)) {
  123.                         decoded = RawParseUtils.decode(UTF_8,
  124.                                 in, 3, in.length);
  125.                         utf8Bom = true;
  126.                     } else {
  127.                         decoded = RawParseUtils.decode(in);
  128.                     }
  129.                     fromText(decoded);
  130.                     snapshot = newSnapshot;
  131.                     hash = newHash;
  132.                 }
  133.                 return;
  134.             } catch (FileNotFoundException noFile) {
  135.                 // might be locked by another process (see exception Javadoc)
  136.                 if (retries < maxRetries && configFile.exists()) {
  137.                     if (LOG.isDebugEnabled()) {
  138.                         LOG.debug(MessageFormat.format(
  139.                                 JGitText.get().configHandleMayBeLocked,
  140.                                 Integer.valueOf(retries)), noFile);
  141.                     }
  142.                     try {
  143.                         Thread.sleep(retryDelayMillis);
  144.                     } catch (InterruptedException e) {
  145.                         Thread.currentThread().interrupt();
  146.                     }
  147.                     retries++;
  148.                     retryDelayMillis *= 2; // max wait 1260 ms
  149.                     continue;
  150.                 }
  151.                 if (configFile.exists()) {
  152.                     throw noFile;
  153.                 }
  154.                 clear();
  155.                 snapshot = newSnapshot;
  156.                 return;
  157.             } catch (IOException e) {
  158.                 if (FileUtils.isStaleFileHandle(e)
  159.                         && retries < maxRetries) {
  160.                     if (LOG.isDebugEnabled()) {
  161.                         LOG.debug(MessageFormat.format(
  162.                                 JGitText.get().configHandleIsStale,
  163.                                 Integer.valueOf(retries)), e);
  164.                     }
  165.                     retries++;
  166.                     continue;
  167.                 }
  168.                 throw new IOException(MessageFormat
  169.                         .format(JGitText.get().cannotReadFile, getFile()), e);
  170.             } catch (ConfigInvalidException e) {
  171.                 throw new ConfigInvalidException(MessageFormat
  172.                         .format(JGitText.get().cannotReadFile, getFile()), e);
  173.             }
  174.         }
  175.     }

  176.     /**
  177.      * {@inheritDoc}
  178.      * <p>
  179.      * Save the configuration as a Git text style configuration file.
  180.      * <p>
  181.      * <b>Warning:</b> Although this method uses the traditional Git file
  182.      * locking approach to protect against concurrent writes of the
  183.      * configuration file, it does not ensure that the file has not been
  184.      * modified since the last read, which means updates performed by other
  185.      * objects accessing the same backing file may be lost.
  186.      */
  187.     @Override
  188.     public void save() throws IOException {
  189.         final byte[] out;
  190.         final String text = toText();
  191.         if (utf8Bom) {
  192.             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
  193.             bos.write(0xEF);
  194.             bos.write(0xBB);
  195.             bos.write(0xBF);
  196.             bos.write(text.getBytes(UTF_8));
  197.             out = bos.toByteArray();
  198.         } else {
  199.             out = Constants.encode(text);
  200.         }

  201.         final LockFile lf = new LockFile(getFile());
  202.         if (!lf.lock())
  203.             throw new LockFailedException(getFile());
  204.         try {
  205.             lf.setNeedSnapshot(true);
  206.             lf.write(out);
  207.             if (!lf.commit())
  208.                 throw new IOException(MessageFormat.format(JGitText.get().cannotCommitWriteTo, getFile()));
  209.         } finally {
  210.             lf.unlock();
  211.         }
  212.         snapshot = lf.getCommitSnapshot();
  213.         hash = hash(out);
  214.         // notify the listeners
  215.         fireConfigChangedEvent();
  216.     }

  217.     /** {@inheritDoc} */
  218.     @Override
  219.     public void clear() {
  220.         hash = hash(new byte[0]);
  221.         super.clear();
  222.     }

  223.     private static ObjectId hash(byte[] rawText) {
  224.         return ObjectId.fromRaw(Constants.newMessageDigest().digest(rawText));
  225.     }

  226.     /** {@inheritDoc} */
  227.     @SuppressWarnings("nls")
  228.     @Override
  229.     public String toString() {
  230.         return getClass().getSimpleName() + "[" + getFile().getPath() + "]";
  231.     }

  232.     /**
  233.      * Whether the currently loaded configuration file is outdated
  234.      *
  235.      * @return returns true if the currently loaded configuration file is older
  236.      *         than the file on disk
  237.      */
  238.     public boolean isOutdated() {
  239.         return snapshot.isModified(getFile());
  240.     }

  241.     /**
  242.      * {@inheritDoc}
  243.      *
  244.      * @since 4.10
  245.      */
  246.     @Override
  247.     protected byte[] readIncludedConfig(String relPath)
  248.             throws ConfigInvalidException {
  249.         final File file;
  250.         if (relPath.startsWith("~/")) { //$NON-NLS-1$
  251.             file = fs.resolve(fs.userHome(), relPath.substring(2));
  252.         } else {
  253.             file = fs.resolve(configFile.getParentFile(), relPath);
  254.         }

  255.         if (!file.exists()) {
  256.             return null;
  257.         }

  258.         try {
  259.             return IO.readFully(file);
  260.         } catch (IOException ioe) {
  261.             throw new ConfigInvalidException(MessageFormat
  262.                     .format(JGitText.get().cannotReadFile, relPath), ioe);
  263.         }
  264.     }
  265. }