view src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java @ 709:497e697636fc

Report merged lines as changed block if possible, not as a sequence of added/deleted blocks. To facilitate access to merge parent lines AddBlock got mergeLineAt() method that reports index of the line in the second parent (if any), while insertedAt() has been changed to report index in the first parent always
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Aug 2013 16:23:27 +0200
parents 0205a5c4566b
children
line wrap: on
line source
/*
 * Copyright (c) 2012 TMate Software Ltd
 *  
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@hg4j.com
 */
package org.tmatesoft.hg.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.tmatesoft.hg.internal.ConfigFile;
import org.tmatesoft.hg.internal.ConfigFileParser;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.repo.HgInternals;
import org.tmatesoft.hg.repo.HgInvalidStateException;
import org.tmatesoft.hg.repo.HgRepository;

/**
 * Command to alter Mercurial configuration settings at various levels (system-wide, user-wide, repository-wide).
 *
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public final class HgUpdateConfigCommand extends HgAbstractCommand<HgUpdateConfigCommand> {
	
	private final SessionContext sessionCtx;
	private final File configFile;
	
	private final List<Operation> changes = new LinkedList<Operation>();
	private boolean ignoreMissingKeys = false;

	private HgUpdateConfigCommand(SessionContext sessionContext, File configurationFile) {
		sessionCtx = sessionContext;
		configFile = configurationFile;
	}
	
	public static HgUpdateConfigCommand forRepository(HgRepository hgRepo) {
		return new HgUpdateConfigCommand(hgRepo.getSessionContext(), HgInternals.getImplementationRepo(hgRepo).getFileFromRepoDir("hgrc"));
	}
	
	public static HgUpdateConfigCommand forUser(SessionContext ctx) {
		return new HgUpdateConfigCommand(ctx, Internals.getUserConfigurationFileToWrite(ctx));
	}
	
	public static HgUpdateConfigCommand forInstallation(SessionContext ctx) {
		return new HgUpdateConfigCommand(ctx, Internals.getInstallationConfigurationFileToWrite(ctx));
	}

	/**
	 * Remove an entry altogether. If no entry with the key found, {@link #execute()} fails.
	 * 
	 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
	 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name 
	 * @return <code>this</code> for convenience
	 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
	 */
	public HgUpdateConfigCommand remove(String section, String key) throws IllegalArgumentException {
		checkSection(section);
		checkKey(key);
		changes.add(Operation.deleteEntry(section, key));
		return this;
	}

	/**
	 * Delete single attribute in a multi-valued property. If specified value not found among values of
	 * the identified entry, {@link #execute()} fails. 
	 * 
	 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
	 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name 
	 * @param value one of the values to remove, not <code>null</code> or an empty value
	 * @return <code>this</code> for convenience
	 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
	 */
	public HgUpdateConfigCommand remove(String section, String key, String value) throws IllegalArgumentException {
		checkSection(section);
		checkKey(key);
		changes.add(Operation.deleteValue(section, key, value));
		return this;
	}
	
	/**
	 * Set single-valued property or update multi-valued with a single value
	 * 
	 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
	 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name 
	 * @param value new value, may be <code>null</code>
	 * @return <code>this</code> for convenience
	 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
	 */
	public HgUpdateConfigCommand put(String section, String key, String value) throws IllegalArgumentException {
		checkSection(section);
		checkKey(key);
		changes.add(Operation.setValue(section, key, value));
		return this;
	}
	
	/**
	 * Add value to a multi-valued entry. If specified entry not found, {@link #execute()} fails.
	 * 
	 * @param section identifies section to alter, not <code>null</code> or otherwise ill-formed name
	 * @param key identifies entry within section, not <code>null</code> or otherwise ill-formed name 
	 * @param value new value to add, not <code>null</code> or an empty value
	 * @return <code>this</code> for convenience
	 * @throws IllegalArgumentException if arguments are <code>null</code> or empty
	 */
	public HgUpdateConfigCommand add(String section, String key, String value) throws IllegalArgumentException {
		checkSection(section);
		checkKey(key);
		changes.add(Operation.addValue(section, key, value));
		return this;
	}
	
	/**
	 * Tells whether {@link #execute()} shall fail with exception if keys selected for modification were not found. 
	 * If <code>true</code>, missing keys would be silently ignored. 
	 * When <code>false</code>(<em>default</em>), exception would be raised.
	 *  
	 * @param ignoreMissing pass <code>true</code> to ignore any incorrect keys
	 * @return <code>this</code> for convenience
	 */
	public HgUpdateConfigCommand ignoreMissing(boolean ignoreMissing) {
		ignoreMissingKeys = ignoreMissing;
		return this;
	}
	
	/**
	 * Perform configuration file update.
	 * 
	 * @throws HgMissingConfigElementException if attempt to alter an entry failed to find one, and missing keys are not ignored
	 * @throws HgIOException when configuration file read/write attemt has failed
	 * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
	 */
	public void execute() throws HgMissingConfigElementException, HgIOException, HgException {
		try {
			ConfigFile cfgRead = new ConfigFile(sessionCtx);
			cfgRead.addLocation(configFile);
			ConfigFileParser cfgWrite = new ConfigFileParser();
			FileInputStream fis = new FileInputStream(configFile);
			cfgWrite.parse(fis);
			fis.close();
			for (Operation op : changes) {
				if (!ignoreMissingKeys && !cfgRead.hasSection(op.section)) {
					throw new HgMissingConfigElementException("Bad section name", op.section, op.key);
				}
				Map<String, String> sect = cfgRead.getSection(op.section);
				if (!ignoreMissingKeys && !sect.containsKey(op.key)) {
					throw new HgMissingConfigElementException("Bad key name", op.section, op.key);
				}
				String oldValue = sect.get(op.key);
				if (oldValue == null) {
					oldValue = "";
				}
				switch (op.kind) {
				case AddValue: {
					String separator = ", "; // XXX shall parse and find out separator kind in use
					String newValue = oldValue + separator + op.value;
					if (sect.containsKey(op.key)) {
						cfgWrite.change(op.section, op.key, newValue);
					} else {
						cfgWrite.add(op.section, op.key, newValue);
					}
					break;
				}
				case DelValue: {
					if (!ignoreMissingKeys && (oldValue.length() == 0 || !oldValue.contains(op.value))) {
						throw new HgMissingConfigElementException(String.format("Bad value '%s' to delete from '%s'", op.value, oldValue), op.section, op.key);
					}
					int start = oldValue.indexOf(op.value);
					if (start == -1) {
						// nothing to change
						break;
					}
					int commaPos = -1;
					for (int i = start-1; i >=0; i--) {
						if (oldValue.charAt(i) == ',') {
							commaPos = i;
							break;
						}
					}
					for (int i = start + op.value.length(); commaPos == -1 && i < oldValue.length(); i++) {
						if (oldValue.charAt(i) == ',') {
							commaPos = i;
							break;
						}
					}
					String newValue;
					if (commaPos >= 0) {
						if (commaPos < start) {
							// from preceding comma up to end of value
							newValue = oldValue.substring(0, commaPos) + oldValue.substring(start + op.value.length());
						} else {
							// from value start up to and including subsequent comma
							newValue = oldValue.substring(0, start) + oldValue.substring(commaPos+1);
						}
					} else {
						// found no separator, just remove the value
						// extra whitespaces (if space, not a comma is a separator) won't hurt
						newValue = oldValue.substring(0, start) + oldValue.substring(start + op.value.length());
					}
					cfgWrite.change(op.section, op.key, newValue);
					break;
				}
				case SetValue: {
					if (sect.containsKey(op.key)) {
						cfgWrite.change(op.section, op.key, op.value);
					} else {
						cfgWrite.add(op.section, op.key, op.value);
					}
					break;
				}
				case DelEntry: {
					cfgWrite.delete(op.section, op.key);
					break;
				}
				default: throw new HgInvalidStateException(String.format("Unknown change %s", op.kind));
				}
			}
			FileOutputStream fos = new FileOutputStream(configFile);
			cfgWrite.update(fos);
			fos.close();
		} catch (IOException ex) {
			String m = String.format("Failed to update configuration file %s", configFile);
			throw new HgBadArgumentException(m, ex); // TODO [post-1.0] better exception, it's not bad argument case
		}
	}
	
	private static void checkSection(String section) throws IllegalArgumentException {
		if (section == null || section.trim().length() == 0) {
			throw new IllegalArgumentException(String.format("Section name can't be empty: %s", section));
		}
	}
	
	private static void checkKey(String key) throws IllegalArgumentException {
		if (key == null || key.trim().length() == 0) {
			throw new IllegalArgumentException(String.format("Entry key can't be empty: %s", key));
		}
	}


	private static class Operation {
		private enum OpKind { AddValue, SetValue, DelValue, DelEntry };

		public final OpKind kind;
		public final String section;
		public final String key;
		public final String value;
		
		private Operation(OpKind t, String s, String k, String v) {
			kind = t;
			section = s;
			key = k;
			value = v;
		}
		
		public static Operation deleteEntry(String section, String key) throws IllegalArgumentException {
			return new Operation(OpKind.DelEntry, section, key, null);
		}
		public static Operation deleteValue(String section, String key, String value) throws IllegalArgumentException {
			if (value == null || value.trim().length() == 0) {
				throw new IllegalArgumentException(String.format("Can't remove empty value '%s'", value));
			}
			return new Operation(OpKind.DelValue, section, key, value);
		}
		public static Operation addValue(String section, String key, String value) throws IllegalArgumentException {
			if (value == null || value.trim().length() == 0) {
				throw new IllegalArgumentException(String.format("Can't add empty value '%s'", value));
			}
			return new Operation(OpKind.AddValue, section, key, value);
		}
		public static Operation setValue(String section, String key, String value) throws IllegalArgumentException {
			return new Operation(OpKind.SetValue, section, key, value);
		}
	}
}