Mercurial > jhg
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 } |