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>
9 * and other copyright owners as documented in the project's IP log.
10 *
11 * This program and the accompanying materials are made available
12 * under the terms of the Eclipse Distribution License v1.0 which
13 * accompanies this distribution, is reproduced below, and is
14 * available at http://www.eclipse.org/org/documents/edl-v10.php
15 *
16 * All rights reserved.
17 *
18 * Redistribution and use in source and binary forms, with or
19 * without modification, are permitted provided that the following
20 * conditions are met:
21 *
22 * - Redistributions of source code must retain the above copyright
23 * notice, this list of conditions and the following disclaimer.
24 *
25 * - Redistributions in binary form must reproduce the above
26 * copyright notice, this list of conditions and the following
27 * disclaimer in the documentation and/or other materials provided
28 * with the distribution.
29 *
30 * - Neither the name of the Eclipse Foundation, Inc. nor the
31 * names of its contributors may be used to endorse or promote
32 * products derived from this software without specific prior
33 * written permission.
34 *
35 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
36 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
37 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
38 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
40 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
41 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
42 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
43 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
44 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
45 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
46 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
47 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48 */
49
50 package org.eclipse.jgit.storage.file;
51
52 import java.io.ByteArrayOutputStream;
53 import java.io.File;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.text.MessageFormat;
57
58 import org.eclipse.jgit.errors.LockFailedException;
59 import org.eclipse.jgit.errors.ConfigInvalidException;
60 import org.eclipse.jgit.internal.JGitText;
61 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
62 import org.eclipse.jgit.internal.storage.file.LockFile;
63 import org.eclipse.jgit.lib.Config;
64 import org.eclipse.jgit.lib.Constants;
65 import org.eclipse.jgit.lib.ObjectId;
66 import org.eclipse.jgit.lib.StoredConfig;
67 import org.eclipse.jgit.util.FS;
68 import org.eclipse.jgit.util.IO;
69 import org.eclipse.jgit.util.RawParseUtils;
70
71 /**
72 * The configuration file that is stored in the file of the file system.
73 */
74 public class FileBasedConfig extends StoredConfig {
75 private final File configFile;
76
77 private final FS fs;
78
79 private boolean utf8Bom;
80
81 private volatile FileSnapshot snapshot;
82
83 private volatile ObjectId hash;
84
85 /**
86 * Create a configuration with no default fallback.
87 *
88 * @param cfgLocation
89 * the location of the configuration file on the file system
90 * @param fs
91 * the file system abstraction which will be necessary to perform
92 * certain file system operations.
93 */
94 public FileBasedConfig(File cfgLocation, FS fs) {
95 this(null, cfgLocation, fs);
96 }
97
98 /**
99 * The constructor
100 *
101 * @param base
102 * the base configuration file
103 * @param cfgLocation
104 * the location of the configuration file on the file system
105 * @param fs
106 * the file system abstraction which will be necessary to perform
107 * certain file system operations.
108 */
109 public FileBasedConfig(Config base, File cfgLocation, FS fs) {
110 super(base);
111 configFile = cfgLocation;
112 this.fs = fs;
113 this.snapshot = FileSnapshot.DIRTY;
114 this.hash = ObjectId.zeroId();
115 }
116
117 @Override
118 protected boolean notifyUponTransientChanges() {
119 // we will notify listeners upon save()
120 return false;
121 }
122
123 /** @return location of the configuration file on disk */
124 public final File getFile() {
125 return configFile;
126 }
127
128 /**
129 * Load the configuration as a Git text style configuration file.
130 * <p>
131 * If the file does not exist, this configuration is cleared, and thus
132 * behaves the same as though the file exists, but is empty.
133 *
134 * @throws IOException
135 * the file could not be read (but does exist).
136 * @throws ConfigInvalidException
137 * the file is not a properly formatted configuration file.
138 */
139 @Override
140 public void load() throws IOException, ConfigInvalidException {
141 final FileSnapshot oldSnapshot = snapshot;
142 final FileSnapshot newSnapshot = FileSnapshot.save(getFile());
143 try {
144 final byte[] in = IO.readFully(getFile());
145 final ObjectId newHash = hash(in);
146 if (hash.equals(newHash)) {
147 if (oldSnapshot.equals(newSnapshot))
148 oldSnapshot.setClean(newSnapshot);
149 else
150 snapshot = newSnapshot;
151 } else {
152 final String decoded;
153 if (in.length >= 3 && in[0] == (byte) 0xEF
154 && in[1] == (byte) 0xBB && in[2] == (byte) 0xBF) {
155 decoded = RawParseUtils.decode(RawParseUtils.UTF8_CHARSET,
156 in, 3, in.length);
157 utf8Bom = true;
158 } else {
159 decoded = RawParseUtils.decode(in);
160 }
161 fromText(decoded);
162 snapshot = newSnapshot;
163 hash = newHash;
164 }
165 } catch (FileNotFoundException noFile) {
166 clear();
167 snapshot = newSnapshot;
168 } catch (IOException e) {
169 final IOException e2 = new IOException(MessageFormat.format(JGitText.get().cannotReadFile, getFile()));
170 e2.initCause(e);
171 throw e2;
172 } catch (ConfigInvalidException e) {
173 throw new ConfigInvalidException(MessageFormat.format(JGitText.get().cannotReadFile, getFile()), e);
174 }
175 }
176
177 /**
178 * Save the configuration as a Git text style configuration file.
179 * <p>
180 * <b>Warning:</b> Although this method uses the traditional Git file
181 * locking approach to protect against concurrent writes of the
182 * configuration file, it does not ensure that the file has not been
183 * modified since the last read, which means updates performed by other
184 * objects accessing the same backing file may be lost.
185 *
186 * @throws IOException
187 * the file could not be written.
188 */
189 public void save() throws IOException {
190 final byte[] out;
191 final String text = toText();
192 if (utf8Bom) {
193 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
194 bos.write(0xEF);
195 bos.write(0xBB);
196 bos.write(0xBF);
197 bos.write(text.getBytes(RawParseUtils.UTF8_CHARSET.name()));
198 out = bos.toByteArray();
199 } else {
200 out = Constants.encode(text);
201 }
202
203 final LockFile lf = new LockFile(getFile(), fs);
204 if (!lf.lock())
205 throw new LockFailedException(getFile());
206 try {
207 lf.setNeedSnapshot(true);
208 lf.write(out);
209 if (!lf.commit())
210 throw new IOException(MessageFormat.format(JGitText.get().cannotCommitWriteTo, getFile()));
211 } finally {
212 lf.unlock();
213 }
214 snapshot = lf.getCommitSnapshot();
215 hash = hash(out);
216 // notify the listeners
217 fireConfigChangedEvent();
218 }
219
220 @Override
221 public void clear() {
222 hash = hash(new byte[0]);
223 super.clear();
224 }
225
226 private static ObjectId hash(final byte[] rawText) {
227 return ObjectId.fromRaw(Constants.newMessageDigest().digest(rawText));
228 }
229
230 @SuppressWarnings("nls")
231 @Override
232 public String toString() {
233 return getClass().getSimpleName() + "[" + getFile().getPath() + "]";
234 }
235
236 /**
237 * @return returns true if the currently loaded configuration file is older
238 * than the file on disk
239 */
240 public boolean isOutdated() {
241 return snapshot.isModified(getFile());
242 }
243 }