package charactermanaj.model.io;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.image.BufferedImage;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import charactermanaj.graphics.filters.ColorConv;
import charactermanaj.graphics.filters.ColorConvertParameter;
import charactermanaj.graphics.io.FileImageResource;
import charactermanaj.graphics.io.ImageLoader;
import charactermanaj.graphics.io.ImageSaveHelper;
import charactermanaj.model.AppConfig;
import charactermanaj.model.CharacterData;
import charactermanaj.model.ColorGroup;
import charactermanaj.model.ColorInfo;
import charactermanaj.model.Layer;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsColorInfo;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.model.PartsSet;
import charactermanaj.ui.MainFrame;
import charactermanaj.util.ApplicationLogger;
import charactermanaj.util.UserData;
import charactermanaj.util.UserDataFactory;

public class CharacterDataPersistent {

	/**
	 * キャラクター定義ファイル名
	 */
	public static final String CONFIG_FILE = "character.xml";
	
	/**
	 * ロガー
	 */
	private static final Logger logger = ApplicationLogger.getLogger();
	
	/**
	 * サンプルイメージファイル名
	 */
	public static final String SAMPLE_IMAGE_FILENAME =  "preview.png";

	/**
	 * キャラクター定義バージョン
	 */
	public static final String VERSION_SIG_1_0 = "1.0";
	
	/**
	 * キャラクター定義XMLファイルの名前空間
	 */
	public static final String NS = "http://com.exmaple/charactermanaj";

	
	/**
	 * キャラクター定義XML用のスキーマ定義リソース名
	 */
	private static final String CHARACTER_XML_SCHEMA = "/schema/character.xsd";
	
	/**
	 * パーツセット定義XMLのスキーマ定義リソース名
	 */
	private static final String PARTSSET_XML_SCHEMA = "/schema/partsset.xsd";
	
	/**
	 * リソースに格納されているデフォルトのキャラクター定義.<br>
	 * スキーマではないが、スキーマと同じディレクトリ上にあるので類似の扱いとする.<br>
	 */
	private static final String DEFAULT_CHARACTER_XML = "/schema/character.xml";

	
	/**
	 * プロファイルの列挙時のエラーハンドラ.<br> 
	 * @author seraphy
	 */
	public interface ProfileListErrorHandler {
		
		/**
		 * エラーが発生したことを通知される
		 * @param baseDir 読み込み対象のXMLのURL
		 * @param ex 例外
		 */
		void occureException(File baseDir, Throwable ex);
	}
	
	/**
	 * プロファイル列挙時のデフォルトのエラーハンドラ.<br>
	 * 標準エラー出力にメッセージをプリントするだけで何もしない.<br>
	 */
	public static final ProfileListErrorHandler DEFAULT_ERROR_HANDLER = new ProfileListErrorHandler() {
		public void occureException(File baseDir, Throwable ex) {
			logger.log(Level.WARNING, "invalid profile. :" + baseDir, ex);
		}
	};

	/**
	 * スキーマのキャッシュ.
	 */
	private HashMap<String, Schema> schemaMap = new HashMap<String, Schema>();

	/**
	 * JAXPで使用するデフォルトのエラーハンドラ
	 */
	private static final ErrorHandler errorHandler = new ErrorHandler() {
		public void error(SAXParseException exception) throws SAXException {
			throw exception;
		}
		public void fatalError(SAXParseException exception) throws SAXException {
			throw exception;
		}
		public void warning(SAXParseException exception) throws SAXException {
			throw exception;
		}
	};

	/**
	 * プライベートコンストラクタ.<br>
	 * シングルトン実装であるため、一度だけ呼び出される.
	 */
	private CharacterDataPersistent() {
		super();
	}
	
	/**
	 * シングルトン
	 */
	private static final CharacterDataPersistent singleton = new CharacterDataPersistent();
	
	
	/**
	 * インスタンスを取得する
	 * @return インスタンス
	 */
	public static CharacterDataPersistent getInstance() {
		return singleton;
	}
	
	/**
	 * キャラクターデータを新規に保存する.<br>
	 * REVがnullである場合は保存に先立ってランダムにREVが設定される.<br>
	 * 保存先ディレクトリはユーザー固有のキャラクターデータ保存先のディレクトリにキャラクター定義のIDを基本とする
	 * ディレクトリを作成して保存される.<br>
	 * ただし、そのディレクトリがすでに存在する場合はランダムな名前で決定される.<br>
	 * 実際のxmlの保存先にあわせてDocBaseが設定されて返される.<br>
	 * @param characterData キャラクターデータ (IDは設定済みであること.それ以外はvalid, editableであること。)
	 * @throws IOException 失敗
	 */
	public void createProfile(CharacterData characterData) throws IOException {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}

		String id = characterData.getId();
		if (id == null || id.trim().length() == 0) {
			throw new IOException("missing character-id:" + characterData);
		}

		// ユーザー個別のキャラクターデータ保存先ディレクトリを取得
		AppConfig appConfig = AppConfig.getInstance();
		File characterDir = appConfig.getUserCharactersDir();
		characterDir.mkdirs();

		// 新規に保存先ディレクトリを作成.
		// 同じ名前のディレクトリがある場合はランダムなサフィックスを付与して回避する
		File baseDir = null;
		String suffix = "";
		for (int retry = 0;;retry++) {
			baseDir = new File(characterDir, characterData.getId() + suffix);
			if (!baseDir.exists()) {
				break;
			}
			if (retry > 20) {
				throw new IOException("character directory conflict.:" + baseDir);
			}
			suffix = "." + UUID.randomUUID().toString();
		}
		baseDir.mkdirs();

		// 保存先を確認
		File characterPropXML = new File(baseDir, "character.xml");
		if (characterPropXML.exists() && !characterPropXML.isFile()) {
			throw new IOException("character.xml is not a regular file.:" + characterPropXML);
		}
		if (characterPropXML.exists() && !characterPropXML.canWrite()) {
			throw new IOException("character.xml is not writable.:" + characterPropXML);
		}
		
		// DocBaseを実際の保存先に更新
		URL docBase = characterPropXML.toURL();
		characterData.setDocBase(docBase);

		// リビジョンが指定されてなければ新規にリビジョンを割り当てる。
		if (characterData.getRev() == null) {
			characterData.setRev("rev" + UUID.randomUUID());
		}

		// 保存する.
		saveCharacterDataToXML(characterData);

		// ディレクトリを準備する
		preparePartsDir(characterData);
	}
	
	/**
	 * キャラクターデータを更新する.
	 * 構造的に異なる更新である場合、revが更新される.<br>
	 * @param characterData キャラクターデータ(有効かつ編集可能であること)
	 * @param structureNotCompatible 構造の変更である場合(revを更新する場合)
	 * @throws IOException 失敗
	 */
	public void updateProfile(CharacterData characterData, boolean structureNotCompatible) throws IOException {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		if (!characterData.canWrite()) {
			throw new IOException("protected profile: " + characterData);
		}
		if (!characterData.isValid()) {
			throw new IOException("invalid profile: " + characterData);
		}
		
		// 構造の変更の場合はリビジョンを更新する.
		if (structureNotCompatible) {
			characterData.setRev("rev" + UUID.randomUUID());
		}

		// 保存する
		saveCharacterDataToXML(characterData);

		// ディレクトリを準備する
		preparePartsDir(characterData);
	}
	
	/**
	 * キャラクターデータのパーツイメージを保存するディレクトリを準備する
	 * @param characterData キャラクターデータ
	 * @param baseDir ベースディレクトリ
	 * @throws IOException 失敗
	 */
	protected void preparePartsDir(CharacterData characterData) throws IOException {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		if (!characterData.canWrite()) {
			throw new IOException("protected profile: " + characterData);
		}
		if (!characterData.isValid()) {
			throw new IOException("invalid profile: " + characterData);
		}
		
		URL docBase = characterData.getDocBase();
		if (!docBase.getProtocol().equals("file")) {
			throw new IOException("not file protocol: " + docBase);
		}
		File docBaseFile = new File(docBase.getFile());
		File baseDir = docBaseFile.getParentFile();
		
		baseDir.mkdirs();
		for (PartsCategory partsCategory : characterData.getPartsCategories()) {
			for (Layer layer : partsCategory.getLayers()) {
				String dir = layer.getDir();
				if (dir != null) {
					File layerDir = new File(baseDir, layer.getDir());
					layerDir.mkdirs();
				}
			}
		}
	}
	
	
	/**
	 * プロファイルを列挙する.<br>
	 * 読み取りに失敗した場合はエラーハンドラに通知されるが例外は返されない.<br>
	 * 一つも正常なプロファイルがない場合は空のリストが返される.<br>
	 * @param errorHandler エラーハンドラ、不要ならばnull
	 * @return プロファイルのリスト(表示名順)、もしくは空
	 */
	public List<CharacterData> listProfiles(ProfileListErrorHandler errorHandler) {
		AppConfig appConfig = AppConfig.getInstance();
		File[] baseDirs = {
				appConfig.getSystemCharactersDir(),
				appConfig.getUserCharactersDir(),
				};
		
		ArrayList<CharacterData> profiles = new ArrayList<CharacterData>();
		for (File baseDir : baseDirs) {
			if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory()) {
				continue;
			}
			for (File dir : baseDir.listFiles(new FileFilter() {
				public boolean accept(File pathname) {
					boolean accept = pathname.isDirectory() && !pathname.getName().startsWith(".");
					if (accept) {
						File configFile = new File(pathname, CONFIG_FILE);
						accept = configFile.exists() && configFile.canRead();
					}
					return accept;
				}
			})) {
				File characterDataXml = new File(dir, "character.xml");
				if (characterDataXml.exists()) {
					try {
						File docBaseFile = new File(dir, CONFIG_FILE);
						CharacterData characterData = loadProfile(docBaseFile.toURL());
						profiles.add(characterData);

					} catch (Exception ex) {
						if (errorHandler != null) {
							errorHandler.occureException(dir, ex);
						}
					}
				}
			}
		}

		Collections.sort(profiles, CharacterData.SORT_DISPLAYNAME);

		return Collections.unmodifiableList(profiles);
	}
	
	/**
	 * character.xmlのキャッシュファイルの位置を取得する.
	 * @param docBase
	 * @return character.xmlのシリアライズデータを格納するUserData
	 */
	protected UserData getCharacterDataCacheUserFile(URL docBase) {
		if (docBase == null) {
			throw new IllegalArgumentException();
		}
		
		String[] splitedName = docBase.getFile().split("/");
		String lastName = splitedName[splitedName.length - 1];
		
		UserDataFactory userDataFactory = UserDataFactory.getInstance();
		return userDataFactory.getMangledNamedUserData(docBase, lastName + "-cache.ser");
	}
	
	public CharacterData loadProfile(URL docBase) throws IOException {
		if (docBase == null) {
			throw new IllegalArgumentException();
		}

		// docBaseの最終更新日を取得する.
		long lastModified;
		URLConnection conn = docBase.openConnection();
		try {
			lastModified = conn.getLastModified();
		} finally {
			// コネクションを閉じる.
			conn.getInputStream().close();
			conn = null;
		}

		// XML解析済みの中間ファイルがあれば、それをデシリアライズする.
		UserData serializedFile = getCharacterDataCacheUserFile(docBase);
		if (serializedFile.exists()) {
			// 更新日がxmlと等しいか、より新しい場合のみ有効とする.
			if (lastModified > 0 && serializedFile.lastModified() >= lastModified) {
				try {
					CharacterData characterData = (CharacterData) serializedFile.load();

					// DocBaseがデシリアライズ結果と一致しないかぎり、有効としない.
					URL serializedDocBase = characterData.getDocBase();
					if (serializedDocBase == null || !docBase.sameFile(serializedDocBase)) {
						throw new IOException("docBase mismatch. actual=" + serializedDocBase + "/expected=" + docBase);
					}

					// デシリアライズ結果を有効なキャラクターデータとして返す.
					return characterData;

				} catch (Exception ex) {
					logger.log(Level.WARNING, "cached character.xml loading failed.: " + docBase, ex);
					// デシリアライズに失敗した場合は無視して継続
				}
			}
		}

		// XMLから読み取る
		CharacterData characterData = loadCharacterDataFromXML(docBase);

		// XMLの読み取り結果をシリアライズする.
		try {
			serializedFile.save(characterData);
			
		} catch (Exception ex) {
			logger.log(Level.WARNING, "cached character.xml creation failed.: " + docBase, ex);
			// シリアライズに失敗しても処理は継続する.
		}
		
		return characterData;
	}

	/**
	 * キャラクター定義(プロファイル)をロードする.
	 * @param docBase 対象xml
	 * @return キャラクターデータ
	 * @throws IOException 
	 */
	public CharacterData loadCharacterDataFromXML(URL docBase) throws IOException {
		if (docBase == null) {
			throw new IllegalArgumentException();
		}
		CharacterData cd;
		InputStream is = docBase.openStream();
		try {
			cd = loadCharacterDataFromXML(is, docBase, true);
		} finally {
			is.close();
		}
		return cd;
	}
	
	/**
	 * デフォルトのキャラクター定義を生成して返す.<br>
	 * 生成された伽セクター定義のdocBaseはnullであるため、使用する場合はdocBaseをセットすること.<br>
	 * @return キャラクター定義
	 */
	public CharacterData createDefaultCharacterData() {
		try {
			URL defaultCharacter = getSchemaURL(DEFAULT_CHARACTER_XML); // スキーマではなくドキュメントxmlであるが同じ扱いでロードできる

			CharacterData cd;
			InputStream is = defaultCharacter.openStream();
			try {
				cd = loadCharacterDataFromXML(is, null, true);
			} finally {
				is.close();
			}
			
			return cd;
		
		} catch (IOException ex) {
			throw new RuntimeException("can not create the default profile from application's resource", ex);
		}
	}

	/**
	 * XMLコンテンツに対する入力ストリームからキャラクターデータを取り出す.<br>
	 * docbaseはXMLファイルの位置を示すものであり、XMLデータ中には含まれず、キャラクターデータのロード時にセットされる.<br>
	 * そのため引数としてdocbaseを引き渡す.<br>
	 * XMLの形式チェックを行う場合は、checkSchemaをtrueとする.<br>
	 * XMLの厳密な形式にあわずとも読み取れるものだけ読み取るのであれば、これをfalseとする.<br>
	 * この場合、バージョンチェックもスキップされ、名前、ID、カテゴリが一つもないものも受け入れるので、呼び出しもとでチェックをかけるべきである。<br>
	 * @param is 入力ストリーム
	 * @param docBase XMLファイルの位置を示すURL、nullの場合はnullが設定される。
	 * @param checkSchema 厳密にスキーマと一致するか検証する場合、true。読み取れるものだけ読み取る場合はfalse
	 * @return 読み取られたプロファイル
	 * @throws IOException 読み取りに失敗
	 */
	public CharacterData loadCharacterDataFromXML(InputStream is, URL docBase, boolean checkSchema) throws IOException {
		if (is == null) {
			throw new IllegalArgumentException();
		}
		Schema schema = loadSchema(CHARACTER_XML_SCHEMA);
		
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		if (checkSchema) {
			factory.setSchema(schema);
		}
		
		Document doc;
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			final ArrayList<SAXParseException> errors = new ArrayList<SAXParseException>();
			builder.setErrorHandler(new ErrorHandler() {
				public void error(SAXParseException exception) throws SAXException {
					errors.add(exception);
				}
				public void fatalError(SAXParseException exception) throws SAXException {
					errors.add(exception);
				}
				public void warning(SAXParseException exception) throws SAXException {
					errors.add(exception);
				}
			});
			
			doc = builder.parse(is);
			if (errors.size() > 0) {
				throw errors.get(0);
			}
		
		} catch (ParserConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration Exception.", ex);

		} catch (SAXException ex) {
			IOException ex2 = new IOException("CharacterData read failed.");
			ex2.initCause(ex);
			throw ex2;
		}

		XPath xpath = createXPath();
		
		CharacterData characterData = new CharacterData();
		characterData.setDocBase(docBase);
		
		try {
			// check version
			if (checkSchema) {
				Node nodeVersion = (Node) xpath.evaluate("/pre:character/@version", doc, XPathConstants.NODE);
				if (!nodeVersion.getTextContent().equals(VERSION_SIG_1_0)) {
					throw new IOException("unsupported version: " + nodeVersion.toString());
				}
			}
			
			// character id
			Node nodeCharacterId = (Node) xpath.evaluate("/pre:character/@id", doc, XPathConstants.NODE);
			String characterId = nodeCharacterId.getTextContent().trim();
			characterData.setId(characterId);
	
			// character rev
			Node nodeCharacterRev = (Node) xpath.evaluate("/pre:character/@rev", doc, XPathConstants.NODE);
			String characterRev = nodeCharacterRev.getTextContent().trim();
			characterData.setRev(characterRev);

			// language
			Locale locale = Locale.getDefault();
			String lang = locale.getLanguage();
			
			// character name
			Node nodeCharacterName = (Node) xpath.evaluate(
					"/pre:character/pre:name[lang('" + lang + "')]", doc, XPathConstants.NODE);
			if (nodeCharacterName == null) {
				nodeCharacterName = (Node) xpath.evaluate(
						"/pre:character/pre:name[position() = 1]", doc, XPathConstants.NODE);
			}
			String characterName = nodeCharacterName.getTextContent();
			characterData.setName(characterName);
			
			// author/description
			Node nodeAuthor = (Node) xpath.evaluate(
					"/pre:character/pre:information/pre:author[lang('" + lang + "')]", doc, XPathConstants.NODE);
			if (nodeAuthor == null) {
				nodeAuthor = (Node) xpath.evaluate(
						"/pre:character/pre:information/pre:author[position() = 1]", doc, XPathConstants.NODE);
			}
			characterData.setAuthor(nodeAuthor == null ? "" : nodeAuthor.getTextContent().trim());
			Node nodeDescription = (Node) xpath.evaluate(
					"/pre:character/pre:information/pre:description[lang('" + lang + "')]", doc, XPathConstants.NODE);
			if (nodeDescription == null) {
				nodeDescription = (Node) xpath.evaluate(
						"/pre:character/pre:information/pre:description[position() = 1]", doc, XPathConstants.NODE);
			}
			
			// 説明の改行コードはXML上ではLF、キャラクターデータとしてインスタンス化された場合はプラットフォーム固有に変換される.
			String note = (nodeDescription == null) ? "" : nodeDescription.getTextContent().trim();
			characterData.setDescription(note);
			
			// image size
			int width = Integer.parseInt(((Node) xpath.evaluate(
					"/pre:character/pre:image-size/pre:width", doc,
					XPathConstants.NODE)).getTextContent());
			int height = Integer.parseInt(((Node) xpath.evaluate(
					"/pre:character/pre:image-size/pre:height", doc,
					XPathConstants.NODE)).getTextContent());
			characterData.setImageSize(new Dimension(width, height));
			
			// settings
			XPathExpression expSettingsEntry = xpath.compile("pre:character/pre:settings/pre:entry");
			for (Node nodeSettingsEntry : iterable((NodeList) expSettingsEntry.evaluate(doc, XPathConstants.NODESET))) {
				Element elmSettingsEntry = (Element) nodeSettingsEntry;
				String key = elmSettingsEntry.getAttribute("key");
				String value = elmSettingsEntry.getTextContent();
				characterData.setProperty(key, value);
			}
			
			// color-group
			ArrayList<ColorGroup> colorGroups = new ArrayList<ColorGroup>();
			XPathExpression expDisplayNameByLocale = xpath.compile("pre:display-name[lang('" + lang + "')]");
			XPathExpression expDisplayNameByDefault = xpath.compile("pre:display-name[position() = 1]");
			XPathExpression expColorGroupId = xpath.compile("@id");
			for (Node nodeColorGroup : iterable((NodeList) xpath.evaluate(
					"/pre:character/pre:colorGroups/pre:colorGroup", doc, XPathConstants.NODESET))) {
				String id = ((Node) expColorGroupId.evaluate(nodeColorGroup, XPathConstants.NODE)).getTextContent().trim();
				Node nodeDisplayName = (Node) expDisplayNameByLocale.evaluate(nodeColorGroup, XPathConstants.NODE);
				if (nodeDisplayName == null) {
					nodeDisplayName = (Node) expDisplayNameByDefault.evaluate(nodeColorGroup, XPathConstants.NODE);
				}
				String colorGroupDisplayName = nodeDisplayName.getTextContent();
				ColorGroup colorGroup = new ColorGroup(id, colorGroupDisplayName);
				colorGroups.add(colorGroup);
			}
			characterData.setColorGroups(colorGroups);
	
			// category
			ArrayList<PartsCategory> categories = new ArrayList<PartsCategory>();
			XPathExpression expCategoryId = xpath.compile("@id");
			XPathExpression expMultipleSelectable = xpath.compile("@multipleSelectable");
			XPathExpression expLayers = xpath.compile("pre:layers/pre:layer");
			XPathExpression expVisibleRows = xpath.compile("pre:visible-rows");
			XPathExpression expLayerOrder = xpath.compile("pre:order");
			XPathExpression expLayerColorGroup = xpath.compile("pre:colorGroup");
			XPathExpression expLayerDir = xpath.compile("pre:dir");
			for (Node nodeCategory : iterable((NodeList) xpath.evaluate(
					"/pre:character/pre:categories/pre:category", doc, XPathConstants.NODESET))) {
				String categoryId = ((Node) expCategoryId.evaluate(nodeCategory, XPathConstants.NODE)).getTextContent().trim();
				boolean multipleSelectable = Boolean.parseBoolean(((Node) expMultipleSelectable.evaluate(
					nodeCategory, XPathConstants.NODE)).getTextContent());
				
				int visibleRows = Integer.parseInt(((Node) expVisibleRows.evaluate(nodeCategory, XPathConstants.NODE)).getTextContent());
				
				Node nodeDisplayName = (Node) expDisplayNameByLocale.evaluate(nodeCategory, XPathConstants.NODE);
				if (nodeDisplayName == null) {
					nodeDisplayName = (Node) expDisplayNameByDefault.evaluate(nodeCategory, XPathConstants.NODE);
				}
				String categoryDisplayName = nodeDisplayName.getTextContent();
				
				ArrayList<Layer> layers = new ArrayList<Layer>();
				for (Node nodeLayer : iterable((NodeList) expLayers.evaluate(nodeCategory, XPathConstants.NODESET))) {
					NamedNodeMap attr = nodeLayer.getAttributes();
					String layerId = attr.getNamedItem("id").getTextContent().trim();
					String layerDir = ((Node) expLayerDir.evaluate(nodeLayer, XPathConstants.NODE)).getTextContent();
					int order = Integer.valueOf(((Node) expLayerOrder.evaluate(nodeLayer, XPathConstants.NODE)).getTextContent());
	
					Node nodeLayerDisplayName = (Node) expDisplayNameByLocale.evaluate(nodeLayer, XPathConstants.NODE);
					if (nodeLayerDisplayName == null) {
						nodeLayerDisplayName = (Node) expDisplayNameByDefault.evaluate(nodeLayer, XPathConstants.NODE);
					}
					String layerDisplayName = nodeLayerDisplayName.getTextContent();
					
					Node nodeLayerColorGroup = (Node) expLayerColorGroup.evaluate(nodeLayer, XPathConstants.NODE);
					ColorGroup colorGroup = null;
					boolean initSync = false;
					if (nodeLayerColorGroup != null) {
						NamedNodeMap attrColorGroup = nodeLayerColorGroup.getAttributes();
						String colorGroupRefId = attrColorGroup.getNamedItem("refid").getTextContent().trim();
						colorGroup = characterData.getColorGroup(colorGroupRefId);
						initSync = Boolean.valueOf(attrColorGroup.getNamedItem("init-sync").getTextContent());
					}
	
					Layer layer = new Layer(layerId, layerDisplayName, order, colorGroup, initSync, layerDir);
					layers.add(layer);
				}
	
				PartsCategory category = new PartsCategory(categories.size(), categoryId,
						categoryDisplayName, multipleSelectable, visibleRows, layers.toArray(new Layer[layers.size()]));
				categories.add(category);
			}
	
			characterData.setPartsCategories(categories.toArray(new PartsCategory[categories.size()]));
	
			// presets
			Node nodePresets = (Node) xpath.evaluate("/pre:character/pre:presets", doc, XPathConstants.NODE);
			if (nodePresets != null) {
				loadPartsSet(characterData, nodePresets, true);
			}

		} catch (XPathExpressionException ex) {
			IOException ex2 = new IOException("CharacterData invalid format.");
			ex2.initCause(ex);
			throw ex2;
		}
		
		return characterData;
	}
	
	protected void saveCharacterDataToXML(CharacterData characterData) throws IOException {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		if (!characterData.canWrite()) {
			throw new IOException("protected profile: " + characterData);
		}
		if (!characterData.isValid()) {
			throw new IOException("invalid profile: " + characterData);
		}

		URL docBase = characterData.getDocBase();
		if (!docBase.getProtocol().equals("file")) {
			throw new IOException("not file protocol: " + docBase);
		}
		
		// XML形式で保存(メモリへ)
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		try {
			writeXMLCharacterData(characterData, bos);
		} finally {
			bos.close();
		}

		// 成功したら実際にファイルに出力
		File characterPropXML = new File(docBase.getFile());
		File baseDir = characterPropXML.getParentFile();
		baseDir.mkdirs();

		FileOutputStream fos = new FileOutputStream(characterPropXML);
		try {
			fos.write(bos.toByteArray());
		} finally {
			fos.close();
		}
	}
		
	public void writeXMLCharacterData(CharacterData characterData, OutputStream outstm) throws IOException {
		if (outstm == null || characterData == null) {
			throw new IllegalArgumentException();
		}
		
		Locale locale = Locale.getDefault();
		String lang = locale.getLanguage();

		Document doc;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	        factory.setNamespaceAware(true);
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
		} catch (ParserConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration failed.", ex);
		}

		Element root = doc.createElementNS(NS, "character");
		root.setAttribute("version", VERSION_SIG_1_0);
		
		root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI);
		root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
		root.setAttribute("xsi:schemaLocation", NS + " character.xsd");
		root.setAttribute("id", characterData.getId());
		root.setAttribute("rev", characterData.getRev());
		doc.appendChild(root);
		
		// name
		Element nodeName = doc.createElementNS(NS, "name");
		Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
		attrLang.setValue(lang);
		nodeName.setAttributeNodeNS(attrLang);
		nodeName.setTextContent(characterData.getName());
		root.appendChild(nodeName);
		
		// information
		String author = characterData.getAuthor();
		String description = characterData.getDescription();
		if ((author != null && author.length() > 0) || (description != null && description.length() > 0)) {
			Element nodeInfomation = doc.createElementNS(NS, "information");
			if (author != null && author.length() > 0) {
				Element nodeAuthor = doc.createElementNS(NS, "author");
				Attr attrNodeAuthorLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
				attrNodeAuthorLang.setValue(lang);
				nodeAuthor.setAttributeNodeNS(attrNodeAuthorLang);
				nodeAuthor.setTextContent(author);
				nodeInfomation.appendChild(nodeAuthor);
			}
			if (description != null && description.length() > 0) {
				
				// 説明の改行コードはXML上ではLFとする.
				description = description.replace("\r\n", "\n");
				description = description.replace("\r", "\n");
				
				Element nodeDescription = doc.createElementNS(NS, "description");
				Attr attrNodeDescriptionLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
				attrNodeDescriptionLang.setValue(lang);
				nodeDescription.setAttributeNodeNS(attrNodeDescriptionLang);
				nodeDescription.setTextContent(description);
				nodeInfomation.appendChild(nodeDescription);
			}
			root.appendChild(nodeInfomation);
		}
		
		// size
		Element nodeSize = doc.createElementNS(NS, "image-size");
		Element nodeWidth = doc.createElementNS(NS, "width");
		nodeWidth.setTextContent(Integer.toString((int) characterData.getImageSize().getWidth()));
		Element nodeHeight = doc.createElementNS(NS, "height");
		nodeHeight.setTextContent(Integer.toString((int) characterData.getImageSize().getHeight()));
		nodeSize.appendChild(nodeWidth);
		nodeSize.appendChild(nodeHeight);
		root.appendChild(nodeSize);
		
		// settings
		Element nodeSettings = doc.createElementNS(NS, "settings");
		root.appendChild(nodeSettings);
		for (String settingsEntryName : characterData.getPropertyNames()) {
			String value = characterData.getProperty(settingsEntryName);
			if (value != null) {
				Element nodeEntry = doc.createElementNS(NS, "entry");
				nodeEntry.setAttribute("key", settingsEntryName);
				nodeEntry.setTextContent(value);
				nodeSettings.appendChild(nodeEntry);
			}
		}
		
		// categories
		Element nodeCategories = doc.createElementNS(NS, "categories");
		for (PartsCategory category : characterData.getPartsCategories()) {
			// category
			Element nodeCategory = doc.createElementNS(NS, "category");
			nodeCategory.setAttribute("id", category.getCategoryId());
			nodeCategory.setAttribute("multipleSelectable", category.isMultipleSelectable() ? "true" : "false");
			
			// visible-rows
			Element nodeVisibleRows = doc.createElementNS(NS, "visible-rows");
			nodeVisibleRows.setTextContent(Integer.toString(category.getVisibleRows()));
			nodeCategory.appendChild(nodeVisibleRows);
			
			// category name
			Element nodeCategoryName = doc.createElementNS(NS, "display-name");
			Attr attrCategoryNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
			attrCategoryNameLang.setValue(lang);
			nodeCategoryName.setAttributeNodeNS(attrCategoryNameLang);
			nodeCategoryName.setTextContent(category.getLocalizedCategoryName());
			nodeCategory.appendChild(nodeCategoryName);
			
			// layers
			Element nodeLayers = doc.createElementNS(NS, "layers");
			for (Layer layer : category.getLayers()) {
				// layer
				Element nodeLayer = doc.createElementNS(NS, "layer");
				nodeLayer.setAttribute("id", layer.getId());
				
				Element nodeLayerName = doc.createElementNS(NS, "display-name");
				Attr attrLayerNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
				attrLayerNameLang.setValue(lang);
				nodeLayerName.setAttributeNodeNS(attrLayerNameLang);
				nodeLayerName.setTextContent(layer.getLocalizedName());
				nodeLayer.appendChild(nodeLayerName);
				
				Element nodeOrder = doc.createElementNS(NS, "order");
				nodeOrder.setTextContent(Integer.toString(layer.getOrder()));
				nodeLayer.appendChild(nodeOrder);
				
				ColorGroup colorGroup = layer.getColorGroup();
				if (colorGroup != null && colorGroup.isEnabled()) {
					Element nodeColorGroup = doc.createElementNS(NS, "colorGroup");
					nodeColorGroup.setAttribute("refid", colorGroup.getId());
					nodeColorGroup.setAttribute("init-sync", layer.isInitSync() ? "true" : "false");
					nodeLayer.appendChild(nodeColorGroup);
				}
				
				Element nodeDir = doc.createElementNS(NS, "dir");
				nodeDir.setTextContent(layer.getDir());
				nodeLayer.appendChild(nodeDir);
				nodeLayers.appendChild(nodeLayer);
			}
			nodeCategory.appendChild(nodeLayers);
			
			nodeCategories.appendChild(nodeCategory);
		}
		root.appendChild(nodeCategories);
		
		// ColorGroupを構築する
		Collection<ColorGroup> colorGroups = characterData.getColorGroups();
		if (colorGroups.size() > 0) {
			Element nodeColorGroups = doc.createElementNS(NS, "colorGroups");
			int colorGroupCount = 0;
			for (ColorGroup colorGroup : colorGroups) {
				if (!colorGroup.isEnabled()) {
					continue;
				}
				Element nodeColorGroup = doc.createElementNS(NS, "colorGroup");
				nodeColorGroup.setAttribute("id", colorGroup.getId());
				Element nodeColorGroupName = doc.createElementNS(NS, "display-name");
				Attr attrColorGroupNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
				attrColorGroupNameLang.setValue(lang);
				nodeColorGroupName.setAttributeNodeNS(attrColorGroupNameLang);
				nodeColorGroupName.setTextContent(colorGroup.getLocalizedName());
				nodeColorGroup.appendChild(nodeColorGroupName);
				nodeColorGroups.appendChild(nodeColorGroup);
				colorGroupCount++;
			}
			if (colorGroupCount > 0) {
				root.appendChild(nodeColorGroups);
			}
		}

		// presetsのelementを構築する.
		Element nodePresets = doc.createElementNS(NS, "presets");
		if (writePartsSetElements(doc, nodePresets, characterData, true, false) > 0) {
			root.appendChild(nodePresets);
		}

		// output xml
		TransformerFactory txFactory = TransformerFactory.newInstance();
		txFactory.setAttribute("indent-number", Integer.valueOf(4));
		Transformer tfmr;
		try {
			tfmr = txFactory.newTransformer();
		} catch (TransformerConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration Failed.", ex);
		}
		tfmr.setOutputProperty(OutputKeys.INDENT, "yes"); 

		// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745
		final String encoding = "UTF-8";
		tfmr.setOutputProperty("encoding", encoding);
		try {
			tfmr.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(outstm, Charset.forName(encoding))));

		} catch (TransformerException ex) {
			IOException ex2 = new IOException("XML Convert failed.");
			ex2.initCause(ex);
			throw ex2;
		}
	}
	
	public void saveFavorites(CharacterData characterData) throws IOException {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}

		// xml形式
		UserData favoritesData = getFavoritesUserData(characterData, "xml");
		OutputStream os = favoritesData.getOutputStream();
		try {
			saveFavorites(characterData, os);

		} finally {
			os.close();
		}
		
		// serialize形式
		UserData favoritesSer = getFavoritesUserData(characterData, "ser");
		Collection<PartsSet> partsSetsOrg = characterData.getPartsSets().values(); 
		ArrayList<PartsSet> partsSets = new ArrayList<PartsSet>(partsSetsOrg);
		favoritesSer.save(partsSets);
	}
	
	protected UserData getFavoritesUserData(CharacterData characterData, String serializeType) {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		UserDataFactory userDataFactory = UserDataFactory.getInstance();
		String dataname = characterData.getId() + "-favorites." + serializeType;
		URL docBase = characterData.getDocBase();
		UserData favoritesData = userDataFactory.getMangledNamedUserData(docBase, dataname); 
		return favoritesData;
	}
	
	protected void saveFavorites(CharacterData characterData, OutputStream outstm) throws IOException {
		if (characterData == null || outstm == null) {
			throw new IllegalArgumentException();
		}

		Document doc;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory	.newInstance();
			factory.setNamespaceAware(true);
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();

		} catch (ParserConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration Exception.", ex);
		}

		Element root = doc.createElementNS(NS, "partssets");
		
		root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI);
		root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
		root.setAttribute("xsi:schemaLocation", NS + " partsset.xsd");
		doc.appendChild(root);

		// presetsのelementを構築する.(Presetは除く)
		writePartsSetElements(doc, root, characterData, false, true);
		
		// output xml
		TransformerFactory txFactory = TransformerFactory.newInstance();
		txFactory.setAttribute("indent-number", Integer.valueOf(4));
		Transformer tfmr;
		try {
			tfmr = txFactory.newTransformer();
		} catch (TransformerConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration Failed.", ex);
		}
		tfmr.setOutputProperty(OutputKeys.INDENT, "yes"); 

		// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745
		final String encoding = "UTF-8";
		tfmr.setOutputProperty("encoding", encoding);
		try {
			tfmr.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(outstm, Charset.forName(encoding))));

		} catch (TransformerException ex) {
			IOException ex2 = new IOException("XML Convert failed.");
			ex2.initCause(ex);
			throw ex2;
		}
	}
	
	/**
	 * キャラクターデータ内のPresetおよびFavotiesのPartssetの双方共通のパーツセット要素のリストを構築する.
	 * @param doc ドキュメントオブジェクト(createElementNS用)
	 * @param baseElement 親要素、キャラクターデータの場合はPreset、Favoritesの場合はPartssetを示す要素
	 * @param characterData キャラクターデータ
	 * @param writePresets Preset属性のパーツセットを登録する場合はtrue、Preset属性時はデフォルトプリセット属性も(あれば)登録される
	 * @param writeFavorites Preset属性のないパーツセットを登録する場合はtrue
	 * @return 登録したパーツセットの個数
	 */
	protected int writePartsSetElements(Document doc, Element baseElement, CharacterData characterData, boolean writePresets, boolean writeFavorites) {
		Map<String, PartsSet> partsSetMap = characterData.getPartsSets();
		
		Locale locale = Locale.getDefault();
		String lang = locale.getLanguage();
		
		HashMap<String, PartsSet> registeredPartsSetMap = new HashMap<String, PartsSet>();
		
		for (Map.Entry<String, PartsSet> partsSetsEntry : partsSetMap.entrySet()) {
			PartsSet partsSet = partsSetsEntry.getValue();
			if (partsSet.isPresetParts() && !writePresets) {
				continue;
			}
			if (!partsSet.isPresetParts() && !writeFavorites) {
				continue;
			}
			
			if (partsSet.isEmpty()) {
				// 空のパーツセットは登録しない.
				continue;
			}

			String partsSetId = partsSet.getPartsSetId();
			String localizedName = partsSet.getLocalizedName();
			
			Element nodePreset = doc.createElementNS(NS, "preset");
			nodePreset.setAttribute("id", partsSetId);
			baseElement.appendChild(nodePreset);
			registeredPartsSetMap.put(partsSet.getPartsSetId(), partsSet);
	
			// display-name
			Element nodeName = doc.createElementNS(NS, "display-name");
			Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
			attrLang.setValue(lang);
			nodeName.setAttributeNode(attrLang);
			nodeName.setTextContent(localizedName);
			nodePreset.appendChild(nodeName);
			
			// bgColor
			Color bgColor = partsSet.getBgColor();
			if (bgColor != null) {
				Element nodeBgColor = doc.createElementNS(NS, "background-color");
				nodeBgColor.setAttribute("color", "#" + Integer.toHexString(bgColor.getRGB() & 0xffffff));
				nodePreset.appendChild(nodeBgColor);
			}
			
			// affine transform parameter
			double[] affineTransformParameter = partsSet.getAffineTransformParameter();
			if (affineTransformParameter != null) {
				Element nodeAffineTransform = doc.createElementNS(NS, "affine-transform-parameter");
				StringBuilder tmp = new StringBuilder();
				for (double affineItem : affineTransformParameter) {
					if (tmp.length() > 0) {
						tmp.append(" ");
					}
					tmp.append(Double.toString(affineItem));
				}
				nodeAffineTransform.setTextContent(tmp.toString());
				nodePreset.appendChild(nodeAffineTransform);
			}

			// categories
			for (Map.Entry<PartsCategory, List<PartsIdentifier>> entry : partsSet.entrySet()) {
				PartsCategory partsCategory = entry.getKey();
				
				// category
				Element nodeCategory = doc.createElementNS(NS, "category");
				nodeCategory.setAttribute("refid", partsCategory.getCategoryId());
				nodePreset.appendChild(nodeCategory);
				
				List<PartsIdentifier> partsIdentifiers = entry.getValue();
				for (PartsIdentifier partsIdentifier : partsIdentifiers) {
					String partsName = partsIdentifier.getPartsName();
					Element nodeParts = doc.createElementNS(NS, "parts");
					nodeParts.setAttribute("name", partsName);
					nodeCategory.appendChild(nodeParts);

					PartsColorInfo partsColorInfo = partsSet.getColorInfo(partsIdentifier);
					if (partsColorInfo != null) {
						Element nodeColor = doc.createElementNS(NS, "color");
						nodeParts.appendChild(nodeColor);

						for (Map.Entry<Layer, ColorInfo> colorInfoEntry : partsColorInfo.entrySet()) {
							Layer layer = colorInfoEntry.getKey();
							ColorInfo colorInfo = colorInfoEntry.getValue();
							
							Element nodeLayer = doc.createElementNS(NS, "layer");
							nodeLayer.setAttribute("refid", layer.getId());
							nodeColor.appendChild(nodeLayer);
							
							// ColorGroup
							ColorGroup colorGroup = colorInfo.getColorGroup();
							boolean colorSync = colorInfo.isSyncColorGroup();
							
							if (colorGroup.isEnabled()) {
								Element nodeColorGroup = doc.createElementNS(NS, "color-group");
								nodeColorGroup.setAttribute("group", colorGroup.getId());
								nodeColorGroup.setAttribute("synchronized", colorSync ? "true" : "false");
								nodeLayer.appendChild(nodeColorGroup);
							}
							
							// RGB
							ColorConvertParameter param = colorInfo.getColorParameter();

							Element nodeRGB = doc.createElementNS(NS, "rgb");
							Object[][] rgbArgss = {
									{"red", param.getOffsetR(), param.getFactorR(), param.getGammaR()},
									{"green", param.getOffsetG(), param.getFactorG(), param.getGammaG()},
									{"blue", param.getOffsetB(), param.getFactorB(), param.getGammaB()},
									{"alpha", param.getOffsetA(), param.getFactorA(), param.getGammaA()},
									};
							for (Object[] rgbArgs : rgbArgss) {
								Element nodeRGBItem = doc.createElementNS(NS, rgbArgs[0].toString());
								nodeRGBItem.setAttribute("offset", rgbArgs[1].toString());
								nodeRGBItem.setAttribute("factor", rgbArgs[2].toString());
								nodeRGBItem.setAttribute("gamma", rgbArgs[3].toString());
								nodeRGB.appendChild(nodeRGBItem);
							}
							nodeLayer.appendChild(nodeRGB);
							
							// HSB
							Element nodeHSB = doc.createElementNS(NS, "hsb");
							nodeHSB.setAttribute("hue", Float.toString(param.getHue()));
							nodeHSB.setAttribute("saturation", Float.toString(param.getSaturation()));
							nodeHSB.setAttribute("brightness", Float.toString(param.getBrightness()));
							nodeLayer.appendChild(nodeHSB);
							
							// RGB Replace
							Element nodeRGBReplace = doc.createElementNS(NS, "rgb-replace");
							ColorConv colorConv = param.getColorReplace();
							if (colorConv == null) {
								colorConv = ColorConv.NONE;
							}
							nodeRGBReplace.setAttribute("replace-type", colorConv.name());
							nodeRGBReplace.setAttribute("gray", Float.toString(param.getGrayLevel()));
							nodeLayer.appendChild(nodeRGBReplace);
						}
					}
				}
			}
		}
		
		// プリセット登録時はデフォルトのプリセットIDがあれば、それも登録する.
		// (ただし、該当パーツセットが書き込み済みである場合のみ)
		if (writePresets) {
			String defaultPresetId = characterData.getDefaultPartsSetId();
			if (defaultPresetId != null && defaultPresetId.length() > 0) {
				PartsSet defaultPartsSet = registeredPartsSetMap.get(defaultPresetId);
				if (defaultPartsSet != null && defaultPartsSet.isPresetParts()) {
					baseElement.setAttribute("default-preset", defaultPresetId);
				}
			}
		}
		
		return registeredPartsSetMap.size();
	}
	
	/**
	 * CharacterDataのプリセットまたはFavoritesのパーツセットのXMLからパーツセットを読み取って登録する.<br>
	 * @param characterData キャラクターデータ
	 * @param nodePartssets パーツセットのノード、プリセットまたはパーツセットノード
	 * @param presetParts ロードしたパーツセットにプリセットフラグをたてる場合はtrue
	 * @throws XPathExpressionException
	 */
	protected void loadPartsSet(CharacterData characterData,
			Node nodePartssets, boolean presetParts)
			throws XPathExpressionException {
		
		logger.log(Level.INFO, "loadPartsSet: " + characterData + " /presetParts=" + presetParts);
		
		XPath xpath = createXPath();
		
		// language
		Locale locale = Locale.getDefault();
		String lang = locale.getLanguage();

		XPathExpression expPresets = xpath.compile("pre:preset");
		XPathExpression expDisplayNameByLocale = xpath.compile("pre:display-name[lang('" + lang + "')]");
		XPathExpression expDisplayNameByDefault = xpath.compile("pre:display-name[position() = 1]");
		XPathExpression expId = xpath.compile("@id");
		XPathExpression expRefId = xpath.compile("@refid");
		XPathExpression expName = xpath.compile("@name");
		XPathExpression expCategories = xpath.compile("pre:category");
		XPathExpression expParts = xpath.compile("pre:parts");
		XPathExpression expLayers = xpath.compile("pre:color/pre:layer");
		XPathExpression expColorGroup = xpath.compile("pre:color-group");
		XPathExpression expHsb = xpath.compile("pre:hsb");
		XPathExpression expRgb = xpath.compile("pre:rgb/pre:red|pre:rgb/pre:green|pre:rgb/pre:blue|pre:rgb/pre:alpha");
		XPathExpression expRgbReplace = xpath.compile("pre:rgb-replace");
		XPathExpression expBgColor = xpath.compile("pre:background-color/@color");
		XPathExpression expAffineTrns = xpath.compile("pre:affine-transform-parameter");

		String defaultPresetId = null;
		Node nodeDefaultPreset = (Node) xpath.evaluate("@default-preset", nodePartssets, XPathConstants.NODE);
		if (nodeDefaultPreset != null) {
			defaultPresetId = nodeDefaultPreset.getTextContent().trim();
		}

		for (Node nodePreset : iterable((NodeList) expPresets.evaluate(nodePartssets, XPathConstants.NODESET))) {
			String partsSetId = ((Node) expId.evaluate(nodePreset, XPathConstants.NODE)).getTextContent().trim();
			if (defaultPresetId == null) {
				defaultPresetId = partsSetId; 
			}

			Node nodeDisplayName = (Node) expDisplayNameByLocale.evaluate(nodePreset, XPathConstants.NODE);
			if (nodeDisplayName == null) {
				nodeDisplayName = (Node) expDisplayNameByDefault.evaluate(nodePreset, XPathConstants.NODE);
			}
			String displayName = nodeDisplayName.getTextContent();
			
			PartsSet partsSet = new PartsSet();

			partsSet.setPartsSetId(partsSetId);
			partsSet.setLocalizedName(displayName);
			partsSet.setPresetParts(presetParts);
			
			// bg-color
			Node nodeBgColor = (Node) expBgColor.evaluate(nodePreset, XPathConstants.NODE);
			if (nodeBgColor != null) {
				String bgColorStr = nodeBgColor.getTextContent().trim();
				try {
					Color bgColor = Color.decode(bgColorStr);
					partsSet.setBgColor(bgColor);
				} catch (Exception ex) {
					logger.log(Level.WARNING, "bgColor parameter is invalid. :" + bgColorStr, ex);
					// 無視する
				}
			}
			
			// affine-transform-parameter
			Node nodeAffineTrans = (Node) expAffineTrns.evaluate(nodePreset, XPathConstants.NODE);
			if (nodeAffineTrans != null) {
				String strParams = nodeAffineTrans.getTextContent();
				if (strParams != null && strParams.trim().length() > 0) {
					try {
						ArrayList<Double> affineTransformParameterArr = new ArrayList<Double>();
						for (String strParam : strParams.split("\\s+")) {
							affineTransformParameterArr.add(Double.valueOf(strParam));
						}
						double[] affineTransformParameter = new double[affineTransformParameterArr.size()];
						int idx = 0;
						for (double aaffineItem : affineTransformParameterArr) {
							affineTransformParameter[idx++] = aaffineItem;
						}
						partsSet.setAffineTransformParameter(affineTransformParameter);
					} catch (Exception ex) {
						logger.log(Level.WARNING, "affine transform parameter is invalid. :" + strParams, ex);
						// 無視する.
					}
				}
			}
			
			// Category
			for (Node nodeCategory : iterable((NodeList) expCategories.evaluate(nodePreset, XPathConstants.NODESET))) {
				String categoryId = ((Node) expRefId.evaluate(nodeCategory, XPathConstants.NODE)).getTextContent().trim();
				PartsCategory category = characterData.getPartsCategory(categoryId);
				if (category == null) {
					logger.log(Level.WARNING, "undefined category: " + categoryId);
					continue;
				}
				
				// Parts
				for (Node nodeParts : iterable((NodeList) expParts.evaluate(nodeCategory, XPathConstants.NODESET))) {
					String partsName = ((Node) expName.evaluate(nodeParts, XPathConstants.NODE)).getTextContent().trim();

					PartsIdentifier partsIdentifier = new PartsIdentifier(category, partsName, partsName);

					// Color/Layer
					PartsColorInfo partsColorInfo = null;
					for (Node nodeLayer : iterable((NodeList) expLayers.evaluate(nodeParts, XPathConstants.NODESET))) {
						String layerId = ((Node) expRefId.evaluate(nodeLayer, XPathConstants.NODE)).getTextContent().trim();
						Layer layer = category.getLayer(layerId);
						if (layer == null) {
							logger.log(Level.WARNING, "undefined layer: " + layerId);
							continue;
						}

						if (partsColorInfo == null) {
							partsColorInfo = new PartsColorInfo(category);
						}
						ColorInfo colorInfo = partsColorInfo.get(layer);
						
						// color-group
						Element elmColorGroup = (Element) expColorGroup.evaluate(nodeLayer, XPathConstants.NODE);
						if (elmColorGroup != null) {
							ColorGroup colorGroup = characterData.getColorGroup(elmColorGroup.getAttribute("group"));
							boolean syncColorGroup = Boolean.parseBoolean(elmColorGroup.getAttribute("synchronized"));
							colorInfo.setColorGroup(colorGroup);
							colorInfo.setSyncColorGroup(syncColorGroup);
						}
						
						// rgb
						ColorConvertParameter param = colorInfo.getColorParameter();
						for (Node nodeRgb : iterable((NodeList) expRgb.evaluate(nodeLayer, XPathConstants.NODESET))) {
							Element elmRgb = (Element) nodeRgb;
							String rgbName = elmRgb.getNodeName();
							int offset = Integer.parseInt(elmRgb.getAttribute("offset"));
							float factor = Float.parseFloat(elmRgb.getAttribute("factor"));
							float gamma = Float.parseFloat(elmRgb.getAttribute("gamma"));
							if ("red".equals(rgbName)) {
								param.setOffsetR(offset);
								param.setFactorR(factor);
								param.setGammaR(gamma);
							} else if ("green".equals(rgbName)) {
								param.setOffsetG(offset);
								param.setFactorG(factor);
								param.setGammaG(gamma);
							} else if ("blue".equals(rgbName)) {
								param.setOffsetB(offset);
								param.setFactorB(factor);
								param.setGammaB(gamma);
							} else if ("alpha".equals(rgbName)) {
								param.setOffsetA(offset);
								param.setFactorA(factor);
								param.setGammaA(gamma);
							}
						}
						
						// hsb
						Element elmHsb = (Element) expHsb.evaluate(nodeLayer, XPathConstants.NODE);
						if (elmHsb != null) {
							float hue = Float.parseFloat(elmHsb.getAttribute("hue"));
							float saturation = Float.parseFloat(elmHsb.getAttribute("saturation"));
							float brightness = Float.parseFloat(elmHsb.getAttribute("brightness"));
							param.setHue(hue);
							param.setSaturation(saturation);
							param.setBrightness(brightness);
						}
						
						// rgb-replace
						Element elmRgbReplace = (Element) expRgbReplace.evaluate(nodeLayer, XPathConstants.NODE);
						if (elmRgbReplace != null) {
							Float grayLevel = Float.parseFloat(elmRgbReplace.getAttribute("gray"));
							ColorConv colorType = ColorConv.valueOf(elmRgbReplace.getAttribute("replace-type"));
							param.setGrayLevel(grayLevel);
							param.setColorReplace(colorType);
						}
					}

					partsSet.appendParts(category, partsIdentifier, partsColorInfo);
				}
			}
			
			characterData.addPartsSet(partsSet);
		}
		
		if (presetParts) {
			characterData.setDefaultPartsSetId(defaultPresetId);
		}
	}
	
	/**
	 * お気に入り(Favorites)を読み込む.
	 * 
	 * @param characterData キャラクターデータ
	 * @throws IOException 読み込みに失敗した場合
	 */
	public void loadFavorites(CharacterData characterData) throws IOException {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		
		// serialize形式
		UserData favoritesSer = getFavoritesUserData(characterData, "ser");
		// xml形式
		UserData favoritesData = getFavoritesUserData(characterData, "xml");

		try {
			// serialize形式が存在し、更新日がxmlよりも等しいか新しければ、こちらを優先する.
			if (favoritesSer.exists() && favoritesSer.lastModified() >= favoritesData.lastModified()) {
				// serialize形式の読み込み
				@SuppressWarnings("unchecked")
				Collection<PartsSet> partsSets = (Collection<PartsSet>) favoritesSer.load();
				for (PartsSet partsSet : partsSets) {
					characterData.addPartsSet(partsSet);
				}
				return;
			}
			
		} catch (Exception ex) {
			logger.log(Level.WARNING, "cached favorites loading failed. :" + characterData, ex);
			// シリアライズ形式の読み込みに失敗した場合はxmlで継続する
		}

		// xml形式の読み込み
		if (favoritesData.exists()) {
			InputStream is = favoritesData.openStream();
			try {
				loadPartsSet(characterData, is);
			} finally {
				is.close();
			}
		}
		
		// xml形式の読み込みに成功したらシリアライズする.
		try {
			Collection<PartsSet> partsSetsOrg = characterData.getPartsSets().values(); 
			ArrayList<PartsSet> partsSets = new ArrayList<PartsSet>(partsSetsOrg);
			favoritesSer.save(partsSets);
			
		} catch (Exception ex) {
			logger.log(Level.WARNING, "cached favorites creation failed. :" + characterData, ex);
			// シリアライズに失敗しても継続する.
		}
	}
	
	/**
	 * 入力ストリームからパーツセット定義(Favorites.xml)を読み込んで、characterDataに追加登録する.<br>
	 * @param characterData お気に入りを登録されるキャラクターデータ
	 * @param inpstm お気に入りのxmlへの入力ストリーム
	 * @throws IOException 読み込みに失敗した場合
	 */
	protected void loadPartsSet(CharacterData characterData, InputStream inpstm) throws IOException {
		if (characterData == null || inpstm == null) {
			throw new IllegalArgumentException();
		}
		
		Schema schema = loadSchema(PARTSSET_XML_SCHEMA);
		
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		factory.setSchema(schema);
		
		Document doc;
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			final ArrayList<SAXParseException> errors = new ArrayList<SAXParseException>();
			builder.setErrorHandler(new ErrorHandler() {
				public void error(SAXParseException exception) throws SAXException {
					errors.add(exception);
				}
				public void fatalError(SAXParseException exception) throws SAXException {
					errors.add(exception);
				}
				public void warning(SAXParseException exception) throws SAXException {
					errors.add(exception);
				}
			});
			doc = builder.parse(inpstm);
			if (errors.size() > 0) {
				throw errors.get(0);
			}

		} catch (ParserConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration Failed.", ex);

		} catch (SAXException ex) {
			IOException ex2 = new IOException("PartsSet invalid format");
			ex2.initCause(ex);
			throw ex2;
		}

		try {
			XPath xpath = createXPath();
			Node nodePartssets = (Node) xpath.evaluate("/pre:partssets", doc, XPathConstants.NODE);
			if (nodePartssets != null) {
				loadPartsSet(characterData, nodePartssets, false);
			}

		} catch (XPathExpressionException ex) {
			IOException ex2 = new IOException("PartsSet invalid format");
			ex2.initCause(ex2);
			throw ex2;
		}
	}

	/**
	 * 既存のキャラクター定義を削除する.<br>
	 * 有効なdocBaseがあり、そのxmlファイルが存在するものについて、削除を行う.<br>
	 * forceRemoveがtrueでない場合はキャラクター定義 character.xmlファイルの拡張子を
	 * リネームすることでキャラクター定義として認識させなくする.<br>
	 * forceRevmoeがtrueの場合は実際にファイルを削除する.<br>
	 * character.xml、favorites、workingsetのキャッシュも削除される.<br>
	 * @param cd キャラクター定義
	 * @param forceRemove ファイルを削除する場合はtrue、リネームして無効にするだけならfalse
	 * @throws IOException 削除またはリネームできなかった場合
	 */
	public void remove(CharacterData cd, boolean forceRemove) throws IOException {
		if (cd == null || cd.getDocBase() == null) {
			throw new IllegalArgumentException();
		}

		URL docBase = cd.getDocBase();
		File xmlFile = new File(docBase.getPath());
		if (!xmlFile.exists() || !xmlFile.isFile()) {
			// すでに存在しない場合
			return;
		}
		
		// 排他的なアクセス権のチェック
		RandomAccessFile fileChk = new RandomAccessFile(xmlFile, "rws");
		try {
			FileChannel channel = fileChk.getChannel();
			FileLock lock = channel.tryLock();
			if (lock == null) {
				throw new IOException("can not lock the configuration file. " + xmlFile);
			}
			lock.release();
		} finally {
			fileChk.close();
		}
		
		// character.xml キャッシュの削除
		UserData serializedFile = getCharacterDataCacheUserFile(docBase);
		if (serializedFile.exists()) {
			serializedFile.delete();
		}
		
		// favories.xml/serの削除
		UserData[] favoritesDatas = new UserData[] {
				!forceRemove ? null : getFavoritesUserData(cd, "xml"),
				getFavoritesUserData(cd, "ser"),
		};
		for (UserData favoriteData : favoritesDatas) {
			if (favoriteData != null && favoriteData.exists()) {
				favoriteData.delete();
			}
		}
		
		// ワーキングセットの削除
		UserData workingSetSer = MainFrame.getWorkingSetUserData(cd);
		if (workingSetSer != null && workingSetSer.exists()) {
			workingSetSer.delete();
		}
		
		// xmlファイルの拡張子を変更することでキャラクター定義として認識させない.
		// (削除に失敗するケースに備えて先にリネームする.)
		String suffix = "." + System.currentTimeMillis() + ".deleted";
		File bakFile = new File(xmlFile.getPath() + suffix);
		if (!xmlFile.renameTo(bakFile)) {
			throw new IOException("can not rename configuration file.:" + xmlFile);
		}

		// ディレクトリ
		File baseDir = xmlFile.getParentFile();

		if (!forceRemove) {
			// 削除されたディレクトリであることを識別できるようにディレクトリ名も変更する.
			File parentBak = new File(baseDir.getPath() + suffix);
			baseDir.renameTo(parentBak);

		} else {
			// 完全に削除する
			removeRecursive(baseDir);
		}
	}
	
	/**
	 * 指定したファイルを削除します.<br>
	 * 指定したファイルがディレクトリを示す場合、このディレクトリを含む配下のすべてのファイルとディレクトリを削除します.<br>
	 * @param file ファイル、またはディレクトリ
	 */
	protected void removeRecursive(File file) {
		if (file == null) {
			throw new IllegalArgumentException();
		}
		if (file.isDirectory()) {
			for (File child : file.listFiles()) {
				removeRecursive(child);
			}
		}
		file.delete();
	}
	
	protected Iterable<Node> iterable(final NodeList nodeList) {
		final int mx;
		if (nodeList == null) {
			mx = 0;
		} else {
			mx = nodeList.getLength();
		}
		return new Iterable<Node>() {
			public Iterator<Node> iterator() {
				return new Iterator<Node>() {
					private int idx = 0;
					public boolean hasNext() {
						return idx < mx;
					}
					public Node next() {
						if (idx >= mx) {
							throw new NoSuchElementException();
						}
						return nodeList.item(idx++);
					}
					public void remove() {
						throw new UnsupportedOperationException();
					}
				};
			}
		};
	}
	
	protected Schema loadSchema(String schemaName) {
		Schema schema = schemaMap.get(schemaName);
		if (schema != null) {
			return schema;
		}

		URL schemaURL = null;
		try {
			SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
			schemaFactory.setErrorHandler(errorHandler);
			schemaURL = getSchemaURL(schemaName);
			schema = schemaFactory.newSchema(schemaURL);
			schemaMap.put(schemaName, schema);
			return schema;
			
		} catch (Exception ex) {
			throw new RuntimeException("schema creation failed. :" + schemaURL, ex);
		}
	}
		
	protected URL getSchemaURL(String schemaName) {
		return this.getClass().getResource(schemaName);
	}
	
	protected XPath createXPath() {
		XPathFactory xpathFactory = XPathFactory.newInstance();
		XPath xpath = xpathFactory.newXPath();
		xpath.setNamespaceContext(new NamespaceContext() {
			public String getNamespaceURI(String prefix) {
				if (prefix == null) {
					throw new IllegalArgumentException();
				}
				if (prefix.equals("pre")) {
					return NS;
				}
				if (prefix.equals("xml")) {
					return XMLConstants.XML_NS_URI;
				}
				return XMLConstants.NULL_NS_URI;
			}
			public Iterator<?> getPrefixes(String namespaceURI) {
				throw new UnsupportedOperationException();
			}
			
			public String getPrefix(String namespaceURI) {
				throw new UnsupportedOperationException();
			}
		});
		return xpath;
	}

	/**
	 * サンプルピクチャを読み込む.<br>
	 * ピクチャが存在しなければnullを返す.
	 * キャラクター定義がValidでない場合は常にnullを返す.<br>
	 * @param characterData キャラクター定義、null不可
	 * @param loader イメージのローダー、null不可
	 * @return ピクチャのイメージ、もしくはnull
	 * @throws IOException ピクチャの読み取りに失敗した場合
	 */
	public BufferedImage loadSamplePicture(CharacterData characterData, ImageLoader loader) throws IOException {
		if (characterData == null || loader == null) {
			throw new IllegalArgumentException();
		}
		if (!characterData.isValid()) {
			return null;
		}
		
		File sampleImageFile = getSamplePictureFile(characterData);
		if (sampleImageFile != null && sampleImageFile.exists()) {
			return loader.load(new FileImageResource(sampleImageFile));
		}
		return null;
	}
	
	/**
	 * キャラクターのサンプルピクチャが登録可能であるか?<br>
	 * キャラクターデータが有効であり、且つ、ファイルの書き込みが可能であればtrueを返す.<br>
	 * キャラクターデータがnullもしくは無効であるか、ファイルプロトコルでないか、ファイルが書き込み禁止であればfalseょ返す.<br>
	 * @param characterData キャラクターデータ
	 * @return 書き込み可能であればtrue、そうでなければfalse
	 */
	public boolean canSaveSamplePicture(CharacterData characterData) {
		if (characterData == null || !characterData.isValid()) {
			return false;
		}
		File sampleImageFile = getSamplePictureFile(characterData);
		if (sampleImageFile != null) {
			if (sampleImageFile.exists() && sampleImageFile.canWrite()) {
				return true;
			}
			if (!sampleImageFile.exists()) {
				File parentDir = sampleImageFile.getParentFile();
				if (parentDir != null) {
					return parentDir.canWrite();
				}
			}
		}
		return false;
	}
	
	/**
	 * サンプルピクチャとして認識されるファイル位置を返す.<br>
	 * ファイルが実在するかは問わない.<br>
	 * DocBaseが未設定であるか、ファィルプロトコルとして返せない場合はnullを返す.<br>
	 * @param characterData キャラクター定義
	 * @return サンプルピクチャの保存先のファイル位置、もしくはnull
	 */
	protected File getSamplePictureFile(CharacterData characterData) {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		URL docBase = characterData.getDocBase();
		if (docBase != null && docBase.getProtocol().equals("file")) {
			File dir = new File(docBase.getPath());
			return new File(dir.getParentFile(), SAMPLE_IMAGE_FILENAME);
		}
		return null;
	}
	
	/**
	 * サンプルピクチャを保存する.
	 * @param characterData キャラクターデータ
	 * @param samplePicture サンプルピクチャ
	 * @throws IOException 保存に失敗した場合
	 */
	public void saveSamplePicture(CharacterData characterData, BufferedImage samplePicture) throws IOException {
		if (!canSaveSamplePicture(characterData)) {
			throw new IOException("can not write a sample picture.:" + characterData);
		}
		File sampleImageFile = getSamplePictureFile(characterData); // canSaveSamplePictureで書き込み先検証済み
		
		if (samplePicture != null) {
			// 登録または更新

			// pngで保存するので背景色は透過になるが、一応、コードとしては入れておく。
			AppConfig appConfig = AppConfig.getInstance();
			Color sampleImageBgColor = appConfig.getSampleImageBgColor();
			
			ImageSaveHelper imageSaveHelper = new ImageSaveHelper();
			imageSaveHelper.savePicture(samplePicture, sampleImageBgColor, sampleImageFile, null);

		} else {
			// 削除
			if (sampleImageFile.exists()) {
				if (!sampleImageFile.delete()) {
					throw new IOException("sample pucture delete failed. :" + sampleImageFile);
				}
			}
		}

	}

}
