View Javadoc
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  
17  package org.eclipse.jgit.storage.file;
18  
19  import static java.nio.charset.StandardCharsets.UTF_8;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.text.MessageFormat;
25  
26  import org.eclipse.jgit.errors.ConfigInvalidException;
27  import org.eclipse.jgit.errors.LockFailedException;
28  import org.eclipse.jgit.internal.JGitText;
29  import org.eclipse.jgit.internal.storage.file.FileSnapshot;
30  import org.eclipse.jgit.internal.storage.file.LockFile;
31  import org.eclipse.jgit.lib.Config;
32  import org.eclipse.jgit.lib.Constants;
33  import org.eclipse.jgit.lib.ObjectId;
34  import org.eclipse.jgit.lib.StoredConfig;
35  import org.eclipse.jgit.util.FS;
36  import org.eclipse.jgit.util.FileUtils;
37  import org.eclipse.jgit.util.IO;
38  import org.eclipse.jgit.util.RawParseUtils;
39  
40  /**
41   * The configuration file that is stored in the file of the file system.
42   */
43  public class FileBasedConfig extends StoredConfig {
44  
45  	private final File configFile;
46  
47  	private final FS fs;
48  
49  	private boolean utf8Bom;
50  
51  	private volatile FileSnapshot snapshot;
52  
53  	private volatile ObjectId hash;
54  
55  	/**
56  	 * Create a configuration with no default fallback.
57  	 *
58  	 * @param cfgLocation
59  	 *            the location of the configuration file on the file system
60  	 * @param fs
61  	 *            the file system abstraction which will be necessary to perform
62  	 *            certain file system operations.
63  	 */
64  	public FileBasedConfig(File cfgLocation, FS fs) {
65  		this(null, cfgLocation, fs);
66  	}
67  
68  	/**
69  	 * The constructor
70  	 *
71  	 * @param base
72  	 *            the base configuration file
73  	 * @param cfgLocation
74  	 *            the location of the configuration file on the file system
75  	 * @param fs
76  	 *            the file system abstraction which will be necessary to perform
77  	 *            certain file system operations.
78  	 */
79  	public FileBasedConfig(Config base, File cfgLocation, FS fs) {
80  		super(base);
81  		configFile = cfgLocation;
82  		this.fs = fs;
83  		this.snapshot = FileSnapshot.DIRTY;
84  		this.hash = ObjectId.zeroId();
85  	}
86  
87  	/** {@inheritDoc} */
88  	@Override
89  	protected boolean notifyUponTransientChanges() {
90  		// we will notify listeners upon save()
91  		return false;
92  	}
93  
94  	/**
95  	 * Get location of the configuration file on disk
96  	 *
97  	 * @return location of the configuration file on disk
98  	 */
99  	public final File getFile() {
100 		return configFile;
101 	}
102 
103 	/**
104 	 * {@inheritDoc}
105 	 * <p>
106 	 * Load the configuration as a Git text style configuration file.
107 	 * <p>
108 	 * If the file does not exist, this configuration is cleared, and thus
109 	 * behaves the same as though the file exists, but is empty.
110 	 */
111 	@Override
112 	public void load() throws IOException, ConfigInvalidException {
113 		try {
114 			FileSnapshot[] lastSnapshot = { null };
115 			Boolean wasRead = FileUtils.readWithRetries(getFile(), f -> {
116 				final FileSnapshot oldSnapshot = snapshot;
117 				final FileSnapshot newSnapshot;
118 				// don't use config in this snapshot to avoid endless recursion
119 				newSnapshot = FileSnapshot.saveNoConfig(f);
120 				lastSnapshot[0] = newSnapshot;
121 				final byte[] in = IO.readFully(f);
122 				final ObjectId newHash = hash(in);
123 				if (hash.equals(newHash)) {
124 					if (oldSnapshot.equals(newSnapshot)) {
125 						oldSnapshot.setClean(newSnapshot);
126 					} else {
127 						snapshot = newSnapshot;
128 					}
129 				} else {
130 					final String decoded;
131 					if (isUtf8(in)) {
132 						decoded = RawParseUtils.decode(UTF_8,
133 								in, 3, in.length);
134 						utf8Bom = true;
135 					} else {
136 						decoded = RawParseUtils.decode(in);
137 					}
138 					fromText(decoded);
139 					snapshot = newSnapshot;
140 					hash = newHash;
141 				}
142 				return Boolean.TRUE;
143 			});
144 			if (wasRead == null) {
145 				clear();
146 				snapshot = lastSnapshot[0];
147 			}
148 		} catch (IOException e) {
149 			throw e;
150 		} catch (Exception e) {
151 			throw new ConfigInvalidException(MessageFormat
152 					.format(JGitText.get().cannotReadFile, getFile()), e);
153 		}
154 	}
155 
156 	/**
157 	 * {@inheritDoc}
158 	 * <p>
159 	 * Save the configuration as a Git text style configuration file.
160 	 * <p>
161 	 * <b>Warning:</b> Although this method uses the traditional Git file
162 	 * locking approach to protect against concurrent writes of the
163 	 * configuration file, it does not ensure that the file has not been
164 	 * modified since the last read, which means updates performed by other
165 	 * objects accessing the same backing file may be lost.
166 	 */
167 	@Override
168 	public void save() throws IOException {
169 		final byte[] out;
170 		final String text = toText();
171 		if (utf8Bom) {
172 			final ByteArrayOutputStream bos = new ByteArrayOutputStream();
173 			bos.write(0xEF);
174 			bos.write(0xBB);
175 			bos.write(0xBF);
176 			bos.write(text.getBytes(UTF_8));
177 			out = bos.toByteArray();
178 		} else {
179 			out = Constants.encode(text);
180 		}
181 
182 		final LockFile lf = new LockFile(getFile());
183 		try {
184 			if (!lf.lock()) {
185 				throw new LockFailedException(getFile());
186 			}
187 			lf.setNeedSnapshotNoConfig(true);
188 			lf.write(out);
189 			if (!lf.commit())
190 				throw new IOException(MessageFormat.format(JGitText.get().cannotCommitWriteTo, getFile()));
191 		} finally {
192 			lf.unlock();
193 		}
194 		snapshot = lf.getCommitSnapshot();
195 		hash = hash(out);
196 		// notify the listeners
197 		fireConfigChangedEvent();
198 	}
199 
200 	/** {@inheritDoc} */
201 	@Override
202 	public void clear() {
203 		hash = hash(new byte[0]);
204 		super.clear();
205 	}
206 
207 	private static ObjectId hash(byte[] rawText) {
208 		return ObjectId.fromRaw(Constants.newMessageDigest().digest(rawText));
209 	}
210 
211 	/** {@inheritDoc} */
212 	@SuppressWarnings("nls")
213 	@Override
214 	public String toString() {
215 		return getClass().getSimpleName() + "[" + getFile().getPath() + "]";
216 	}
217 
218 	/**
219 	 * Whether the currently loaded configuration file is outdated
220 	 *
221 	 * @return returns true if the currently loaded configuration file is older
222 	 *         than the file on disk
223 	 */
224 	public boolean isOutdated() {
225 		return snapshot.isModified(getFile());
226 	}
227 
228 	/**
229 	 * {@inheritDoc}
230 	 *
231 	 * @since 4.10
232 	 */
233 	@Override
234 	protected byte[] readIncludedConfig(String relPath)
235 			throws ConfigInvalidException {
236 		final File file;
237 		if (relPath.startsWith("~/")) { //$NON-NLS-1$
238 			file = fs.resolve(fs.userHome(), relPath.substring(2));
239 		} else {
240 			file = fs.resolve(configFile.getParentFile(), relPath);
241 		}
242 
243 		if (!file.exists()) {
244 			return null;
245 		}
246 
247 		try {
248 			return IO.readFully(file);
249 		} catch (IOException ioe) {
250 			throw new ConfigInvalidException(MessageFormat
251 					.format(JGitText.get().cannotReadFile, relPath), ioe);
252 		}
253 	}
254 }