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.IOException;
  21. import java.text.MessageFormat;

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

  35. /**
  36.  * The configuration file that is stored in the file of the file system.
  37.  */
  38. public class FileBasedConfig extends StoredConfig {

  39.     private final File configFile;

  40.     private final FS fs;

  41.     private boolean utf8Bom;

  42.     private volatile FileSnapshot snapshot;

  43.     private volatile ObjectId hash;

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

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

  74.     /** {@inheritDoc} */
  75.     @Override
  76.     protected boolean notifyUponTransientChanges() {
  77.         // we will notify listeners upon save()
  78.         return false;
  79.     }

  80.     /**
  81.      * Get location of the configuration file on disk
  82.      *
  83.      * @return location of the configuration file on disk
  84.      */
  85.     public final File getFile() {
  86.         return configFile;
  87.     }

  88.     /**
  89.      * {@inheritDoc}
  90.      * <p>
  91.      * Load the configuration as a Git text style configuration file.
  92.      * <p>
  93.      * If the file does not exist, this configuration is cleared, and thus
  94.      * behaves the same as though the file exists, but is empty.
  95.      */
  96.     @Override
  97.     public void load() throws IOException, ConfigInvalidException {
  98.         try {
  99.             FileSnapshot[] lastSnapshot = { null };
  100.             Boolean wasRead = FileUtils.readWithRetries(getFile(), f -> {
  101.                 final FileSnapshot oldSnapshot = snapshot;
  102.                 final FileSnapshot newSnapshot;
  103.                 // don't use config in this snapshot to avoid endless recursion
  104.                 newSnapshot = FileSnapshot.saveNoConfig(f);
  105.                 lastSnapshot[0] = newSnapshot;
  106.                 final byte[] in = IO.readFully(f);
  107.                 final ObjectId newHash = hash(in);
  108.                 if (hash.equals(newHash)) {
  109.                     if (oldSnapshot.equals(newSnapshot)) {
  110.                         oldSnapshot.setClean(newSnapshot);
  111.                     } else {
  112.                         snapshot = newSnapshot;
  113.                     }
  114.                 } else {
  115.                     final String decoded;
  116.                     if (isUtf8(in)) {
  117.                         decoded = RawParseUtils.decode(UTF_8,
  118.                                 in, 3, in.length);
  119.                         utf8Bom = true;
  120.                     } else {
  121.                         decoded = RawParseUtils.decode(in);
  122.                     }
  123.                     fromText(decoded);
  124.                     snapshot = newSnapshot;
  125.                     hash = newHash;
  126.                 }
  127.                 return Boolean.TRUE;
  128.             });
  129.             if (wasRead == null) {
  130.                 clear();
  131.                 snapshot = lastSnapshot[0];
  132.             }
  133.         } catch (IOException e) {
  134.             throw e;
  135.         } catch (Exception e) {
  136.             throw new ConfigInvalidException(MessageFormat
  137.                     .format(JGitText.get().cannotReadFile, getFile()), e);
  138.         }
  139.     }

  140.     /**
  141.      * {@inheritDoc}
  142.      * <p>
  143.      * Save the configuration as a Git text style configuration file.
  144.      * <p>
  145.      * <b>Warning:</b> Although this method uses the traditional Git file
  146.      * locking approach to protect against concurrent writes of the
  147.      * configuration file, it does not ensure that the file has not been
  148.      * modified since the last read, which means updates performed by other
  149.      * objects accessing the same backing file may be lost.
  150.      */
  151.     @Override
  152.     public void save() throws IOException {
  153.         final byte[] out;
  154.         final String text = toText();
  155.         if (utf8Bom) {
  156.             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
  157.             bos.write(0xEF);
  158.             bos.write(0xBB);
  159.             bos.write(0xBF);
  160.             bos.write(text.getBytes(UTF_8));
  161.             out = bos.toByteArray();
  162.         } else {
  163.             out = Constants.encode(text);
  164.         }

  165.         final LockFile lf = new LockFile(getFile());
  166.         try {
  167.             if (!lf.lock()) {
  168.                 throw new LockFailedException(getFile());
  169.             }
  170.             lf.setNeedSnapshotNoConfig(true);
  171.             lf.write(out);
  172.             if (!lf.commit())
  173.                 throw new IOException(MessageFormat.format(JGitText.get().cannotCommitWriteTo, getFile()));
  174.         } finally {
  175.             lf.unlock();
  176.         }
  177.         snapshot = lf.getCommitSnapshot();
  178.         hash = hash(out);
  179.         // notify the listeners
  180.         fireConfigChangedEvent();
  181.     }

  182.     /** {@inheritDoc} */
  183.     @Override
  184.     public void clear() {
  185.         hash = hash(new byte[0]);
  186.         super.clear();
  187.     }

  188.     private static ObjectId hash(byte[] rawText) {
  189.         return ObjectId.fromRaw(Constants.newMessageDigest().digest(rawText));
  190.     }

  191.     /** {@inheritDoc} */
  192.     @SuppressWarnings("nls")
  193.     @Override
  194.     public String toString() {
  195.         return getClass().getSimpleName() + "[" + getFile().getPath() + "]";
  196.     }

  197.     /**
  198.      * Whether the currently loaded configuration file is outdated
  199.      *
  200.      * @return returns true if the currently loaded configuration file is older
  201.      *         than the file on disk
  202.      */
  203.     public boolean isOutdated() {
  204.         return snapshot.isModified(getFile());
  205.     }

  206.     /**
  207.      * {@inheritDoc}
  208.      *
  209.      * @since 4.10
  210.      */
  211.     @Override
  212.     protected byte[] readIncludedConfig(String relPath)
  213.             throws ConfigInvalidException {
  214.         final File file;
  215.         if (relPath.startsWith("~/")) { //$NON-NLS-1$
  216.             file = fs.resolve(fs.userHome(), relPath.substring(2));
  217.         } else {
  218.             file = fs.resolve(configFile.getParentFile(), relPath);
  219.         }

  220.         if (!file.exists()) {
  221.             return null;
  222.         }

  223.         try {
  224.             return IO.readFully(file);
  225.         } catch (IOException ioe) {
  226.             throw new ConfigInvalidException(MessageFormat
  227.                     .format(JGitText.get().cannotReadFile, relPath), ioe);
  228.         }
  229.     }
  230. }