comparison src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java @ 498:0205a5c4566b

Issue 38: preserve user formatting and comments when updating configuration files
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 26 Oct 2012 18:17:15 +0200
parents ba36f66c32b4
children
comparison
equal deleted inserted replaced
497:02140be396d5 498:0205a5c4566b
15 * contact TMate Software at support@hg4j.com 15 * contact TMate Software at support@hg4j.com
16 */ 16 */
17 package org.tmatesoft.hg.core; 17 package org.tmatesoft.hg.core;
18 18
19 import java.io.File; 19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileOutputStream;
20 import java.io.IOException; 22 import java.io.IOException;
21 import java.util.ArrayList; 23 import java.util.LinkedList;
22 import java.util.LinkedHashMap;
23 import java.util.List; 24 import java.util.List;
24 import java.util.Map; 25 import java.util.Map;
25 26
26 import org.tmatesoft.hg.internal.ConfigFile; 27 import org.tmatesoft.hg.internal.ConfigFile;
27 import org.tmatesoft.hg.internal.Experimental; 28 import org.tmatesoft.hg.internal.ConfigFileParser;
28 import org.tmatesoft.hg.internal.Internals; 29 import org.tmatesoft.hg.internal.Internals;
29 import org.tmatesoft.hg.repo.HgInternals; 30 import org.tmatesoft.hg.repo.HgInternals;
31 import org.tmatesoft.hg.repo.HgInvalidStateException;
30 import org.tmatesoft.hg.repo.HgRepository; 32 import org.tmatesoft.hg.repo.HgRepository;
31 33
32 /** 34 /**
33 * WORK IN PROGRESS, DO NOT USE 35 * Command to alter Mercurial configuration settings at various levels (system-wide, user-wide, repository-wide).
34 * 36 *
35 * @author Artem Tikhomirov 37 * @author Artem Tikhomirov
36 * @author TMate Software Ltd. 38 * @author TMate Software Ltd.
37 */ 39 */
38 @Experimental(reason="Investigating approaches to alter Hg configuration files")
39 public final class HgUpdateConfigCommand extends HgAbstractCommand<HgUpdateConfigCommand> { 40 public final class HgUpdateConfigCommand extends HgAbstractCommand<HgUpdateConfigCommand> {
40 41
42 private final SessionContext sessionCtx;
41 private final File configFile; 43 private final File configFile;
42 44
43 private Map<String,List<String>> toRemove; 45 private final List<Operation> changes = new LinkedList<Operation>();
44 private Map<String,Map<String,String>> toSet; 46 private boolean ignoreMissingKeys = false;
45
46 private final SessionContext sessionCtx;
47 47
48 private HgUpdateConfigCommand(SessionContext sessionContext, File configurationFile) { 48 private HgUpdateConfigCommand(SessionContext sessionContext, File configurationFile) {
49 sessionCtx = sessionContext; 49 sessionCtx = sessionContext;
50 configFile = configurationFile; 50 configFile = configurationFile;
51 } 51 }
52 52
53 public static HgUpdateConfigCommand forRepository(HgRepository hgRepo) { 53 public static HgUpdateConfigCommand forRepository(HgRepository hgRepo) {
54 // XXX HgRepository to implement SessionContext.Provider (with getContext())?
55 return new HgUpdateConfigCommand(hgRepo.getSessionContext(), HgInternals.getImplementationRepo(hgRepo).getFileFromRepoDir("hgrc")); 54 return new HgUpdateConfigCommand(hgRepo.getSessionContext(), HgInternals.getImplementationRepo(hgRepo).getFileFromRepoDir("hgrc"));
56 } 55 }
57 56
58 public static HgUpdateConfigCommand forUser(SessionContext ctx) { 57 public static HgUpdateConfigCommand forUser(SessionContext ctx) {
59 return new HgUpdateConfigCommand(ctx, Internals.getUserConfigurationFileToWrite(ctx)); 58 return new HgUpdateConfigCommand(ctx, Internals.getUserConfigurationFileToWrite(ctx));
60 } 59 }
61 60
62 public static HgUpdateConfigCommand forInstallation(SessionContext ctx) { 61 public static HgUpdateConfigCommand forInstallation(SessionContext ctx) {
63 return new HgUpdateConfigCommand(ctx, Internals.getInstallationConfigurationFileToWrite(ctx)); 62 return new HgUpdateConfigCommand(ctx, Internals.getInstallationConfigurationFileToWrite(ctx));
64 } 63 }
65 64
66 /** 65 /**
67 * Remove a property altogether 66 * Remove an entry altogether. If no entry with the key found, {@link #execute()} fails.
68 * @return <code>this</code> for convenience 67 *
69 */ 68 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
70 public HgUpdateConfigCommand remove(String section, String key) { 69 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name
71 if (toRemove == null) { 70 * @return <code>this</code> for convenience
72 toRemove = new LinkedHashMap<String, List<String>>(); 71 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
73 } 72 */
74 List<String> s = toRemove.get(section); 73 public HgUpdateConfigCommand remove(String section, String key) throws IllegalArgumentException {
75 if (s == null) { 74 checkSection(section);
76 toRemove.put(section, s = new ArrayList<String>(5)); 75 checkKey(key);
77 } 76 changes.add(Operation.deleteEntry(section, key));
78 s.add(key); 77 return this;
79 if (toSet != null && toSet.containsKey(section)) { 78 }
80 toSet.get(section).remove(key); 79
81 } 80 /**
82 return this; 81 * Delete single attribute in a multi-valued property. If specified value not found among values of
83 } 82 * the identified entry, {@link #execute()} fails.
84 83 *
85 /** 84 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
86 * Delete single attribute in a multi-valued property 85 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name
87 * @return <code>this</code> for convenience 86 * @param value one of the values to remove, not <code>null</code> or an empty value
88 */ 87 * @return <code>this</code> for convenience
89 public HgUpdateConfigCommand remove(String section, String key, String value) { 88 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
90 throw new UnsupportedOperationException(); 89 */
91 } 90 public HgUpdateConfigCommand remove(String section, String key, String value) throws IllegalArgumentException {
92 91 checkSection(section);
93 /** 92 checkKey(key);
94 * Set single-valued properties or update multi-valued with a single value 93 changes.add(Operation.deleteValue(section, key, value));
95 * @return <code>this</code> for convenience 94 return this;
96 */ 95 }
97 public HgUpdateConfigCommand put(String section, String key, String value) { 96
98 if (toSet == null) { 97 /**
99 toSet = new LinkedHashMap<String, Map<String,String>>(); 98 * Set single-valued property or update multi-valued with a single value
100 } 99 *
101 Map<String,String> s = toSet.get(section); 100 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
102 if (s == null) { 101 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name
103 toSet.put(section, s = new LinkedHashMap<String, String>()); 102 * @param value new value, may be <code>null</code>
104 } 103 * @return <code>this</code> for convenience
105 s.put(key, value); 104 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
106 return this; 105 */
107 } 106 public HgUpdateConfigCommand put(String section, String key, String value) throws IllegalArgumentException {
108 107 checkSection(section);
109 /** 108 checkKey(key);
110 * Multi-valued properties 109 changes.add(Operation.setValue(section, key, value));
111 * @return <code>this</code> for convenience 110 return this;
112 */ 111 }
113 public HgUpdateConfigCommand add(String section, String key, String value) { 112
114 throw new UnsupportedOperationException(); 113 /**
115 } 114 * Add value to a multi-valued entry. If specified entry not found, {@link #execute()} fails.
116 115 *
117 /** 116 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
118 * Perform config file update 117 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name
119 * 118 * @param value new value to add, not <code>null</code> or an empty value
119 * @return <code>this</code> for convenience
120 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
121 */
122 public HgUpdateConfigCommand add(String section, String key, String value) throws IllegalArgumentException {
123 checkSection(section);
124 checkKey(key);
125 changes.add(Operation.addValue(section, key, value));
126 return this;
127 }
128
129 /**
130 * Tells whether {@link #execute()} shall fail with exception if keys selected for modification were not found.
131 * If <code>true</code>, missing keys would be silently ignored.
132 * When <code>false</code>(<em>default</em>), exception would be raised.
133 *
134 * @param ignoreMissing pass <code>true</code> to ignore any incorrect keys
135 * @return <code>this</code> for convenience
136 */
137 public HgUpdateConfigCommand ignoreMissing(boolean ignoreMissing) {
138 ignoreMissingKeys = ignoreMissing;
139 return this;
140 }
141
142 /**
143 * Perform configuration file update.
144 *
145 * @throws HgMissingConfigElementException if attempt to alter an entry failed to find one, and missing keys are not ignored
146 * @throws HgIOException when configuration file read/write attemt has failed
120 * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state 147 * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
121 */ 148 */
122 public void execute() throws HgException { 149 public void execute() throws HgMissingConfigElementException, HgIOException, HgException {
123 try { 150 try {
124 ConfigFile cfg = new ConfigFile(sessionCtx); 151 ConfigFile cfgRead = new ConfigFile(sessionCtx);
125 cfg.addLocation(configFile); 152 cfgRead.addLocation(configFile);
126 if (toRemove != null) { 153 ConfigFileParser cfgWrite = new ConfigFileParser();
127 for (Map.Entry<String,List<String>> s : toRemove.entrySet()) { 154 FileInputStream fis = new FileInputStream(configFile);
128 for (String e : s.getValue()) { 155 cfgWrite.parse(fis);
129 cfg.putString(s.getKey(), e, null); 156 fis.close();
130 } 157 for (Operation op : changes) {
158 if (!ignoreMissingKeys && !cfgRead.hasSection(op.section)) {
159 throw new HgMissingConfigElementException("Bad section name", op.section, op.key);
160 }
161 Map<String, String> sect = cfgRead.getSection(op.section);
162 if (!ignoreMissingKeys && !sect.containsKey(op.key)) {
163 throw new HgMissingConfigElementException("Bad key name", op.section, op.key);
164 }
165 String oldValue = sect.get(op.key);
166 if (oldValue == null) {
167 oldValue = "";
168 }
169 switch (op.kind) {
170 case AddValue: {
171 String separator = ", "; // XXX shall parse and find out separator kind in use
172 String newValue = oldValue + separator + op.value;
173 if (sect.containsKey(op.key)) {
174 cfgWrite.change(op.section, op.key, newValue);
175 } else {
176 cfgWrite.add(op.section, op.key, newValue);
177 }
178 break;
179 }
180 case DelValue: {
181 if (!ignoreMissingKeys && (oldValue.length() == 0 || !oldValue.contains(op.value))) {
182 throw new HgMissingConfigElementException(String.format("Bad value '%s' to delete from '%s'", op.value, oldValue), op.section, op.key);
183 }
184 int start = oldValue.indexOf(op.value);
185 if (start == -1) {
186 // nothing to change
187 break;
188 }
189 int commaPos = -1;
190 for (int i = start-1; i >=0; i--) {
191 if (oldValue.charAt(i) == ',') {
192 commaPos = i;
193 break;
194 }
195 }
196 for (int i = start + op.value.length(); commaPos == -1 && i < oldValue.length(); i++) {
197 if (oldValue.charAt(i) == ',') {
198 commaPos = i;
199 break;
200 }
201 }
202 String newValue;
203 if (commaPos >= 0) {
204 if (commaPos < start) {
205 // from preceding comma up to end of value
206 newValue = oldValue.substring(0, commaPos) + oldValue.substring(start + op.value.length());
207 } else {
208 // from value start up to and including subsequent comma
209 newValue = oldValue.substring(0, start) + oldValue.substring(commaPos+1);
210 }
211 } else {
212 // found no separator, just remove the value
213 // extra whitespaces (if space, not a comma is a separator) won't hurt
214 newValue = oldValue.substring(0, start) + oldValue.substring(start + op.value.length());
215 }
216 cfgWrite.change(op.section, op.key, newValue);
217 break;
218 }
219 case SetValue: {
220 if (sect.containsKey(op.key)) {
221 cfgWrite.change(op.section, op.key, op.value);
222 } else {
223 cfgWrite.add(op.section, op.key, op.value);
224 }
225 break;
226 }
227 case DelEntry: {
228 cfgWrite.delete(op.section, op.key);
229 break;
230 }
231 default: throw new HgInvalidStateException(String.format("Unknown change %s", op.kind));
131 } 232 }
132 } 233 }
133 if (toSet != null) { 234 FileOutputStream fos = new FileOutputStream(configFile);
134 for (Map.Entry<String,Map<String,String>> s : toSet.entrySet()) { 235 cfgWrite.update(fos);
135 for (Map.Entry<String, String> e : s.getValue().entrySet()) { 236 fos.close();
136 cfg.putString(s.getKey(), e.getKey(), e.getValue());
137 }
138 }
139 }
140 cfg.writeTo(configFile);
141 } catch (IOException ex) { 237 } catch (IOException ex) {
142 String m = String.format("Failed to update configuration file %s", configFile); 238 String m = String.format("Failed to update configuration file %s", configFile);
143 throw new HgBadArgumentException(m, ex); // TODO [post-1.0] better exception, it's not bad argument case 239 throw new HgBadArgumentException(m, ex); // TODO [post-1.0] better exception, it's not bad argument case
144 } 240 }
145 } 241 }
146 242
147 243 private static void checkSection(String section) throws IllegalArgumentException {
148 public static void main(String[] args) throws Exception { 244 if (section == null || section.trim().length() == 0) {
149 HgUpdateConfigCommand cmd = HgUpdateConfigCommand.forUser(null); 245 throw new IllegalArgumentException(String.format("Section name can't be empty: %s", section));
150 cmd.remove("test1", "sample1"); 246 }
151 cmd.put("test2", "sample2", "value2"); 247 }
152 cmd.put("ui", "user-name", "Another User <email@domain.com>"); 248
153 cmd.execute(); 249 private static void checkKey(String key) throws IllegalArgumentException {
250 if (key == null || key.trim().length() == 0) {
251 throw new IllegalArgumentException(String.format("Entry key can't be empty: %s", key));
252 }
253 }
254
255
256 private static class Operation {
257 private enum OpKind { AddValue, SetValue, DelValue, DelEntry };
258
259 public final OpKind kind;
260 public final String section;
261 public final String key;
262 public final String value;
263
264 private Operation(OpKind t, String s, String k, String v) {
265 kind = t;
266 section = s;
267 key = k;
268 value = v;
269 }
270
271 public static Operation deleteEntry(String section, String key) throws IllegalArgumentException {
272 return new Operation(OpKind.DelEntry, section, key, null);
273 }
274 public static Operation deleteValue(String section, String key, String value) throws IllegalArgumentException {
275 if (value == null || value.trim().length() == 0) {
276 throw new IllegalArgumentException(String.format("Can't remove empty value '%s'", value));
277 }
278 return new Operation(OpKind.DelValue, section, key, value);
279 }
280 public static Operation addValue(String section, String key, String value) throws IllegalArgumentException {
281 if (value == null || value.trim().length() == 0) {
282 throw new IllegalArgumentException(String.format("Can't add empty value '%s'", value));
283 }
284 return new Operation(OpKind.AddValue, section, key, value);
285 }
286 public static Operation setValue(String section, String key, String value) throws IllegalArgumentException {
287 return new Operation(OpKind.SetValue, section, key, value);
288 }
154 } 289 }
155 } 290 }