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