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.ConfigInvalidException;
59 import org.eclipse.jgit.errors.LockFailedException;
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 boolean utf8Bom;
78
79 private volatile FileSnapshot snapshot;
80
81 private volatile ObjectId hash;
82
83 /**
84 * Create a configuration with no default fallback.
85 *
86 * @param cfgLocation
87 * the location of the configuration file on the file system
88 * @param fs
89 * the file system abstraction which will be necessary to perform
90 * certain file system operations.
91 */
92 public FileBasedConfig(File cfgLocation, FS fs) {
93 this(null, cfgLocation, fs);
94 }
95
96 /**
97 * The constructor
98 *
99 * @param base
100 * the base configuration file
101 * @param cfgLocation
102 * the location of the configuration file on the file system
103 * @param fs
104 * the file system abstraction which will be necessary to perform
105 * certain file system operations.
106 */
107 public FileBasedConfig(Config base, File cfgLocation, FS fs) {
108 super(base);
109 configFile = cfgLocation;
110 this.snapshot = FileSnapshot.DIRTY;
111 this.hash = ObjectId.zeroId();
112 }
113
114 @Override
115 protected boolean notifyUponTransientChanges() {
116 // we will notify listeners upon save()
117 return false;
118 }
119
120 /** @return location of the configuration file on disk */
121 public final File getFile() {
122 return configFile;
123 }
124
125 /**
126 * Load the configuration as a Git text style configuration file.
127 * <p>
128 * If the file does not exist, this configuration is cleared, and thus
129 * behaves the same as though the file exists, but is empty.
130 *
131 * @throws IOException
132 * the file could not be read (but does exist).
133 * @throws ConfigInvalidException
134 * the file is not a properly formatted configuration file.
135 */
136 @Override
137 public void load() throws IOException, ConfigInvalidException {
138 final FileSnapshot oldSnapshot = snapshot;
139 final FileSnapshot newSnapshot = FileSnapshot.save(getFile());
140 try {
141 final byte[] in = IO.readFully(getFile());
142 final ObjectId newHash = hash(in);
143 if (hash.equals(newHash)) {
144 if (oldSnapshot.equals(newSnapshot))
145 oldSnapshot.setClean(newSnapshot);
146 else
147 snapshot = newSnapshot;
148 } else {
149 final String decoded;
150 if (isUtf8(in)) {
151 decoded = RawParseUtils.decode(RawParseUtils.UTF8_CHARSET,
152 in, 3, in.length);
153 utf8Bom = true;
154 } else {
155 decoded = RawParseUtils.decode(in);
156 }
157 fromText(decoded);
158 snapshot = newSnapshot;
159 hash = newHash;
160 }
161 } catch (FileNotFoundException noFile) {
162 if (configFile.exists()) {
163 throw noFile;
164 }
165 clear();
166 snapshot = newSnapshot;
167 } catch (IOException e) {
168 final IOException e2 = new IOException(MessageFormat.format(JGitText.get().cannotReadFile, getFile()));
169 e2.initCause(e);
170 throw e2;
171 } catch (ConfigInvalidException e) {
172 throw new ConfigInvalidException(MessageFormat.format(JGitText.get().cannotReadFile, getFile()), e);
173 }
174 }
175
176 /**
177 * Save the configuration as a Git text style configuration file.
178 * <p>
179 * <b>Warning:</b> Although this method uses the traditional Git file
180 * locking approach to protect against concurrent writes of the
181 * configuration file, it does not ensure that the file has not been
182 * modified since the last read, which means updates performed by other
183 * objects accessing the same backing file may be lost.
184 *
185 * @throws IOException
186 * the file could not be written.
187 */
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(RawParseUtils.UTF8_CHARSET.name()));
197 out = bos.toByteArray();
198 } else {
199 out = Constants.encode(text);
200 }
201
202 final LockFile lf = new LockFile(getFile());
203 if (!lf.lock())
204 throw new LockFailedException(getFile());
205 try {
206 lf.setNeedSnapshot(true);
207 lf.write(out);
208 if (!lf.commit())
209 throw new IOException(MessageFormat.format(JGitText.get().cannotCommitWriteTo, getFile()));
210 } finally {
211 lf.unlock();
212 }
213 snapshot = lf.getCommitSnapshot();
214 hash = hash(out);
215 // notify the listeners
216 fireConfigChangedEvent();
217 }
218
219 @Override
220 public void clear() {
221 hash = hash(new byte[0]);
222 super.clear();
223 }
224
225 private static ObjectId hash(final byte[] rawText) {
226 return ObjectId.fromRaw(Constants.newMessageDigest().digest(rawText));
227 }
228
229 @SuppressWarnings("nls")
230 @Override
231 public String toString() {
232 return getClass().getSimpleName() + "[" + getFile().getPath() + "]";
233 }
234
235 /**
236 * @return returns true if the currently loaded configuration file is older
237 * than the file on disk
238 */
239 public boolean isOutdated() {
240 return snapshot.isModified(getFile());
241 }
242 }