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.FileInputStream;
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.LinkedList;
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.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
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.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

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.PartsAuthorInfo;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsColorInfo;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.model.PartsManageData;
import charactermanaj.model.PartsSet;
import charactermanaj.model.PartsManageData.PartsKey;
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://charactermanaj.sourceforge.jp/schema/charactermanaj";

	/**
	 * パーツ定義XMLファイルの名前空間
	 */
	public static final String NS_PARTSDEF = "http://charactermanaj.sourceforge.jp/schema/charactermanaj-partsdef";

	
	/**
	 * キャラクター定義XML用のスキーマ定義リソース名
	 */
	private static final String CHARACTER_XML_SCHEMA = "/schema/character.xsd";
	
	/**
	 * キャラクター定義XML用のスキーマ定義リソース名
	 */
	private static final String CHARACTER_XML_SCHEMA_0_8 = "/schema/0.8/character.xsd";

	/**
	 * パーツセット定義XMLのスキーマ定義リソース名
	 */
	private static final String PARTSSET_XML_SCHEMA = "/schema/partsset.xsd";
	
	/**
	 * パーツセット定義XMLのスキーマ定義リソース名
	 */
	private static final String PARTSSET_XML_SCHEMA_0_8 = "/schema/0.8/partsset.xsd";

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

	
	/**
	 * XMLのデータ形式.<br>
	 * SchemaのバリデージョンチェックとDOMの解析を始める前にSAXで流し込んで、
	 * 最初のエレメント名や使用している名前空間、バージョンを読み込んで、
	 * スキーマをきりかえられるようにするためのもの.
	 * @author seraphy
	 */
	public static class DocInfo {
		
		private String firstElementName;
		
		private String version;
		
		private String namespace;
		
		public void setFirstElementName(String firstElementName) {
			this.firstElementName = firstElementName;
		}

		/**
		 * 最初の要素のqName
		 * @return
		 */
		public String getFirstElementName() {
			return firstElementName;
		}
		
		public void setNamespace(String namespace) {
			this.namespace = namespace;
		}
		
		public void setVersion(String version) {
			this.version = version;
		}
		
		/**
		 * 最初の要素に指定されているxmlns属性の値
		 * @param namespace
		 */
		public String getNamespace() {
			return namespace;
		}

		/**
		 * 最初の要素に指定されているversion属性の値
		 * @return
		 */
		public String getVersion() {
			return version;
		}
		
		@Override
		public String toString() {
			return firstElementName + " /version: " + version + " /namespace:" + namespace;
		}
	}

	/**
	 * プロファイルの列挙時のエラーハンドラ.<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();
		}
		
		DocInfo docInfo;
		InputStream is = docBase.openStream();
		try {
			docInfo = readDocumentType(is);
			logger.log(Level.INFO, "docinfo: " + docInfo);
		} finally {
			is.close();
		}
		if (docInfo == null) {
			throw new IOException("unknown document type.");
		}
		
		CharacterData cd;
		is = docBase.openStream();
		try {
			cd = loadCharacterDataFromXML(is, docBase, docInfo);
		} finally {
			is.close();
		}
		return cd;
	}
	
	/**
	 * XMLの最初の要素と、その要素の属性にあるversionとxmlnsを取得して返す.<br>
	 * XMLとして不正などの理由で取得できない場合はnullを返す.<br>
	 * 読み込みそのものに失敗した場合は例外を返す.<br>
	 * 
	 * @param is XMLコンテンツと想定される入力ストリーム
	 * @return DocInfo情報、もしくはnull
	 * @throws IOException 読み込みに失敗した場合
	 */
	public DocInfo readDocumentType(InputStream is) throws IOException {
		
		SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
		SAXParser saxParser;
		try {
			saxParser = saxParserFactory.newSAXParser();
		} catch (ParserConfigurationException ex) {
			throw new RuntimeException("JAXP Configuration Exception.", ex);
		} catch (SAXException ex) {
			throw new RuntimeException("JAXP Configuration Exception.", ex);
		}
		
		try {
			final DocInfo[] result = new DocInfo[1];
			saxParser.parse(is, new DefaultHandler() {
				private int elmCount = 0;
				@Override
				public void startElement(String uri, String localName,
						String qName, Attributes attributes)
						throws SAXException {
					if (elmCount == 0) {
						String version = attributes.getValue("version");
						String namespace = attributes.getValue("xmlns");
						DocInfo docInfo = new DocInfo();
						docInfo.setFirstElementName(qName);
						docInfo.setVersion(version);
						docInfo.setNamespace(namespace);
						result[0] = docInfo;
					}
					elmCount++;
				}
			});
			return result[0];

		} catch (SAXException ex) {
			logger.log(Level.INFO, "character.xml check failed.", ex);
		}
		return null;
	}
	
	
	/**
	 * デフォルトのキャラクター定義を生成して返す.<br>
	 * 生成された伽セクター定義のdocBaseはnullであるため、使用する場合はdocBaseをセットすること.<br>
	 * @return キャラクター定義
	 */
	public CharacterData createDefaultCharacterData() {
		try {
			URL defaultCharacter = getSchemaURL(DEFAULT_CHARACTER_XML); // スキーマではなくドキュメントxmlであるが同じ扱いでロードできる

			CharacterData cd;
			InputStream is = defaultCharacter.openStream();
			try {
				DocInfo docInfo = new DocInfo();
				docInfo.setFirstElementName("character");
				docInfo.setNamespace(NS);
				docInfo.setVersion("1.0");
				
				cd = loadCharacterDataFromXML(is, null, docInfo);
			} 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>
	 * @param is 入力ストリーム
	 * @param docBase XMLファイルの位置を示すURL、nullの場合はnullが設定される。
	 * @param docInfo ドキュメントのタイプ
	 * @return 読み取られたプロファイル
	 * @throws IOException 読み取りに失敗
	 */
	public CharacterData loadCharacterDataFromXML(InputStream is, URL docBase, DocInfo docInfo) throws IOException {
		if (is == null || docInfo == null) {
			throw new IllegalArgumentException();
		}
		Schema schema = loadSchema(docInfo);
		
		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(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(docInfo);
		
		CharacterData characterData = new CharacterData();
		characterData.setDocBase(docBase);
		
		try {
			// check version
			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, docInfo);
			}

		} 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
	 * @param docInfo ドキュメントタイプ
	 * @throws XPathExpressionException
	 */
	protected void loadPartsSet(CharacterData characterData,
			Node nodePartssets, boolean presetParts, DocInfo docInfo)
			throws XPathExpressionException {
		
		logger.log(Level.INFO, "loadPartsSet: " + characterData + " /presetParts=" + presetParts);
		
		XPath xpath = createXPath(docInfo);
		
		// 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()) {
			DocInfo docInfo;
			InputStream is = favoritesData.openStream();
			try {
				docInfo = readDocumentType(is);
			} finally {
				is.close();
			}
			if (docInfo == null) {
				throw new IOException("unknown document type :" + characterData);
			}
			is = favoritesData.openStream();
			try {
				loadPartsSet(characterData, is, docInfo);
			} 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への入力ストリーム
	 * @param docInfo ドキュメントタイプ
	 * @throws IOException 読み込みに失敗した場合
	 */
	protected void loadPartsSet(CharacterData characterData, InputStream inpstm, DocInfo docInfo) throws IOException {
		if (characterData == null || inpstm == null) {
			throw new IllegalArgumentException();
		}
		
		Schema schema = loadSchema(docInfo);
		
		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(docInfo);
			Node nodePartssets = (Node) xpath.evaluate("/pre:partssets", doc, XPathConstants.NODE);
			if (nodePartssets != null) {
				loadPartsSet(characterData, nodePartssets, false, docInfo);
			}

		} 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(DocInfo docInfo) throws IOException {
		if (docInfo == null) {
			throw new IllegalArgumentException();
		}
		
		String schemaName = null;
		if ("character".equals(docInfo.getFirstElementName())) {
			if ("http://com.exmaple/charactermanaj".equals(docInfo.getNamespace())) {
				schemaName = CHARACTER_XML_SCHEMA_0_8;
			} else if ("http://charactermanaj.sourceforge.jp/schema/charactermanaj".equals(docInfo.getNamespace())) {
				schemaName = CHARACTER_XML_SCHEMA;
			}
		} else if ("partssets".equals(docInfo.getFirstElementName())) {
			if ("http://com.exmaple/charactermanaj".equals(docInfo.getNamespace())) {
				schemaName = PARTSSET_XML_SCHEMA_0_8;
			} else if ("http://charactermanaj.sourceforge.jp/schema/charactermanaj".equals(docInfo.getNamespace())) {
				schemaName = PARTSSET_XML_SCHEMA;
			}
		}
		if (schemaName == null) {
			throw new IOException("unsupported namespace: " + docInfo);
		}
		
		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(DocInfo docInfo) {
		if (docInfo == null) {
			throw new IllegalArgumentException();
		}
		
		final String namespace;
		if (docInfo.getNamespace() != null && docInfo.getNamespace().length() > 0) {
			namespace = docInfo.getNamespace();
		} else {
			namespace = NS;
		}
		
		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 namespace;
				}
				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);
				}
			}
		}
	}
	
	/**
	 * パーツ管理情報をDocBaseと同じフォルダ上のparts-info.xmlに書き出す.<br>
	 * XML生成中に失敗した場合は既存の管理情報は残される.<br>
	 * (管理情報の書き込み中にI/O例外が発生した場合は管理情報は破壊される.)<br>
	 * 
	 * @param docBase character.xmlの位置
	 * @param partsManageData パーツ管理情報
	 * @throws IOException 出力に失敗した場合
	 */
	public void savePartsManageData(URL docBase, PartsManageData partsManageData) throws IOException {
		if (docBase == null || partsManageData == null) {
			throw new IllegalArgumentException();
		}

		if (!docBase.getProtocol().equals("file")) {
			throw new IOException("not file protocol: " + docBase);
		}
		File docBaseFile = new File(docBase.getFile());
		File baseDir = docBaseFile.getParentFile();
		
		// データからXMLを構築してストリームに出力する.
		// 完全に成功したXMLのみ書き込むようにするため、一旦バッファする。
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		try {
			savePartsManageData(partsManageData, bos);
		} finally {
			bos.close();
		}
		
		// バッファされたXMLデータを実際のファイルに書き込む
		File partsInfoXML = new File(baseDir, "parts-info.xml");
		FileOutputStream os = new FileOutputStream(partsInfoXML);
		try {
			os.write(bos.toByteArray());
		} finally {
			os.close();
		}
	}
	
	/**
	 * パーツ管理情報をXMLとしてストリームに書き出す.<br>
	 * @param partsManageData パーツ管理データ
	 * @param outstm 出力先ストリーム
	 * @throws IOException 出力に失敗した場合
	 */
	public void savePartsManageData(PartsManageData partsManageData, OutputStream outstm) throws IOException {
		if (partsManageData == 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);
		}

		Locale locale = Locale.getDefault();
		String lang = locale.getLanguage();
		
		Element root = doc.createElementNS(NS_PARTSDEF, "parts-definition");
		
		root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI);
		root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
		root.setAttribute("xsi:schemaLocation", NS_PARTSDEF + " parts-definition.xsd");
		doc.appendChild(root);

		// 作者情報を取得する
		Collection<PartsAuthorInfo> partsAuthors = partsManageData.getAuthorInfos();
		for (PartsAuthorInfo partsAuthorInfo : partsAuthors) {
			String author = partsAuthorInfo.getAuthor();
			if (author == null || author.length() == 0) {
				continue;
			}
			
			// 作者情報の登録
			Element nodeAuthor = doc.createElementNS(NS_PARTSDEF, "author");
			Element nodeAuthorName = doc.createElementNS(NS_PARTSDEF, "name");
			Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
			attrLang.setValue(lang);
			nodeAuthorName.setAttributeNodeNS(attrLang);
			nodeAuthorName.setTextContent(author);
			nodeAuthor.appendChild(nodeAuthorName);
			
			String homepageURL = partsAuthorInfo.getHomePage();
			if (homepageURL != null && homepageURL.length() > 0) {
				Element nodeHomepage = doc.createElementNS(NS_PARTSDEF, "home-page");
				Attr attrHomepageLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
				attrHomepageLang.setValue(lang);
				nodeHomepage.setAttributeNodeNS(attrHomepageLang);
				nodeHomepage.setTextContent(homepageURL);
				nodeAuthor.appendChild(nodeHomepage);
			}
			
			root.appendChild(nodeAuthor);
			
			Collection<PartsKey> partsKeys = partsManageData.getPartsKeysByAuthor(author);

			// ダウンロード別にパーツキーの集約
			HashMap<String, List<PartsKey>> downloadMap = new HashMap<String, List<PartsKey>>();
			for (PartsKey partsKey : partsKeys) {
				PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersionStrict(partsKey);
				String downloadURL = versionInfo.getDownloadURL();
				if (downloadURL == null) {
					downloadURL = "";
				}
				List<PartsKey> partsKeyGrp = downloadMap.get(downloadURL);
				if (partsKeyGrp == null) {
					partsKeyGrp = new ArrayList<PartsKey>();
					downloadMap.put(downloadURL, partsKeyGrp);
				}
				partsKeyGrp.add(partsKey);
			}
			
			// ダウンロード別にパーツ情報の登録
			ArrayList<String> downloadURLs = new ArrayList<String>(downloadMap.keySet());
			Collections.sort(downloadURLs);
			
			for (String downloadURL : downloadURLs) {
				List<PartsKey> partsKeyGrp = downloadMap.get(downloadURL);
				Collections.sort(partsKeyGrp);
				
				Element nodeDownload = doc.createElementNS(NS_PARTSDEF, "download-url");
				nodeDownload.setTextContent(downloadURL);
				root.appendChild(nodeDownload);
				
				for (PartsKey partsKey : partsKeyGrp) {
					PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersionStrict(partsKey);

					Element nodeParts = doc.createElementNS(NS_PARTSDEF, "parts");

					nodeParts.setAttribute("name", partsKey.getPartsName());
					if (partsKey.getCategoryId() != null) {
						nodeParts.setAttribute("category", partsKey.getCategoryId());
					}
					if (versionInfo.getVersion() > 0) {
						nodeParts.setAttribute("version", Double.toString(versionInfo.getVersion()));
					}
					
					String localizedName = partsManageData.getLocalizedName(partsKey);
					if (localizedName != null && localizedName.trim().length() > 0) {
						Element nodeLocalizedName = doc.createElementNS(NS_PARTSDEF, "local-name");
						Attr attrLocalizedNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang");
						attrLocalizedNameLang.setValue(lang);
						nodeLocalizedName.setAttributeNodeNS(attrLocalizedNameLang);
						nodeLocalizedName.setTextContent(localizedName);
						nodeParts.appendChild(nodeLocalizedName);
					}
					
					root.appendChild(nodeParts);
				}
			}
		}
		
		// 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;
		}
	}
	
	/**
	 * 指定したDocBaseと同じフォルダにあるparts-info.xmlからパーツ管理情報を取得して返す.<br>
	 * ファイルが存在しない場合は空のインスタンスを返す.<br>
	 * 返されるインスタンスは編集可能です.<br>
	 * @param docBase character.xmlの位置
	 * @return パーツ管理情報、存在しない場合は空のインスタンス
	 * @throws IOException 読み込み中に失敗した場合
	 */
	public PartsManageData loadPartsManageData(URL docBase) throws IOException {
		if (docBase == null) {
			throw new IllegalArgumentException();
		}
		if (!docBase.getProtocol().equals("file")) {
			throw new IOException("not file protocol: " + docBase);
		}
		File docBaseFile = new File(docBase.getFile());
		File baseDir = docBaseFile.getParentFile();

		// パーツ管理情報ファイルの確認
		final File partsInfoXML = new File(baseDir, "parts-info.xml");
		if (!partsInfoXML.exists()) {
			// ファイルが存在しなければ空を返す.
			return new PartsManageData();
		}
		
		PartsManageData partsManageData;
		InputStream is = new FileInputStream(partsInfoXML);
		try {
			partsManageData = loadPartsManageData(is);
		} finally {
			is.close();
		}
		return partsManageData;
	}
	
	public PartsManageData loadPartsManageData(InputStream is) throws IOException {
		if (is == null) {
			throw new IllegalArgumentException();
		}
		
		// パーツ管理情報
		final PartsManageData partsManageData = new PartsManageData();

		// SAXParserの準備
		SAXParser saxParser;
		try {
			SAXParserFactory saxPartserFactory = SAXParserFactory.newInstance();
			saxPartserFactory.setNamespaceAware(true);
			saxParser = saxPartserFactory.newSAXParser();
		} catch (Exception ex) {
			throw new RuntimeException("JAXP Configuration failed.", ex);
		}

		// デフォルトのロケールから言語を取得
		final Locale locale = Locale.getDefault();
		final String lang = locale.getLanguage();
		
		try {
			// 要素のスタック
			final LinkedList<String> stack = new LinkedList<String>();

			// DOMではなくSAXで読み流す.
			saxParser.parse(is, new DefaultHandler() {
				private StringBuilder buf = new StringBuilder();

				private PartsAuthorInfo partsAuthorInfo;
				
				private String authorName;
				private String homepageURL;
				private String authorNameLang;
				private String homepageLang;
				private String downloadURL;
				
				private String partsLocalNameLang;
				private String partsLocalName;
				private String partsCategoryId;
				private String partsName;
				private double partsVersion;
				
				@Override
				public void startDocument() throws SAXException {
					logger.log(Level.INFO, "parts-info : start");
				}
				
				@Override
				public void endDocument() throws SAXException {
					logger.log(Level.INFO, "parts-info : end");
				}
				
				@Override
				public void characters(char[] ch, int start, int length)
						throws SAXException {
					buf.append(ch, start, length);
				}
				@Override
				public void startElement(String uri, String localName,
						String qName, Attributes attributes) throws SAXException {
					stack.addFirst(qName);
					int mx = stack.size();
					if (mx >= 2 && stack.get(1).equals("parts")) {
						if ("local-name".equals(qName)) {
							partsLocalNameLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang");
						}

					} else if (mx >= 2 && stack.get(1).equals("author")) {
						if ("name".equals(qName)) {
							authorNameLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang");
						
						} else if ("home-page".equals(qName)) {
							homepageLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang");
						}
					
					} else if ("author".equals(qName)) {
						partsAuthorInfo = null;
						authorName = null;
						authorNameLang = null;
						homepageURL = null;
						homepageLang = null;
					
					} else if ("download-url".equals(qName)) {
						downloadURL = null;
					
					} else if ("parts".equals(qName)) {
						partsLocalName = null;
						partsLocalNameLang = null;
						partsCategoryId = attributes.getValue("category");
						partsName = attributes.getValue("name");
						String strVersion = attributes.getValue("version");
						try {
							if (strVersion == null || strVersion.length() == 0) {
								partsVersion = 0.;
							
							} else {
								partsVersion = Double.parseDouble(strVersion);
								if (partsVersion < 0) {
									partsVersion = 0;
								}
							}
							
						} catch (Exception ex) {
							logger.log(Level.INFO, "parts-info.xml: invalid version." + strVersion);
							partsVersion = 0;
						}
					}

					buf = new StringBuilder();
				}
				@Override
				public void endElement(String uri, String localName, String qName)
						throws SAXException {
					
					int mx = stack.size();
					
					if (mx >= 2 && "parts".equals(stack.get(1))) {
						if ("local-name".equals(qName)) {
							if (partsLocalName == null || lang.equals(partsLocalNameLang)) {
								partsLocalName = buf.toString();
							}
						}
					
					} else if (mx >= 2 && "author".equals(stack.get(1))) {
						if ("name".equals(qName)) {
							if (authorName == null || lang.equals(authorNameLang)) {
								authorName = buf.toString();
							}
					
						} else if ("home-page".equals(qName)) {
							if (homepageURL == null || lang.equals(homepageLang)) {
								homepageURL = buf.toString();
							}
						}
					
					} else if ("author".equals(qName)) {
						logger.log(Level.INFO, "parts-info: author: " + authorName + " /homepage:" + homepageURL);
						if (authorName != null && authorName.length() > 0) {
							partsAuthorInfo = new PartsAuthorInfo();
							partsAuthorInfo.setAuthor(authorName);
							partsAuthorInfo.setHomePage(homepageURL);
						
						} else {
							partsAuthorInfo = null;
						}
					
					} else if ("download-url".equals(qName)) {
						downloadURL = buf.toString();
						logger.log(Level.INFO, "parts-info: download-url: " + downloadURL);

					} else if ("parts".equals(qName)) {
						if (logger.isLoggable(Level.FINE)) {
							logger.log(Level.FINE,
									"parts-info.xml: parts-name: " + partsName
									+ " /category: " + partsCategoryId
									+ " /parts-local-name: " + partsLocalName
									+ " /version:" + partsVersion);
						}

						PartsManageData.PartsVersionInfo versionInfo = new PartsManageData.PartsVersionInfo();
						versionInfo.setVersion(partsVersion);
						versionInfo.setDownloadURL(downloadURL);
						
						PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsName, partsCategoryId);
						
						partsManageData.putPartsInfo(partsKey, partsLocalName, partsAuthorInfo, versionInfo);

					}
					stack.removeFirst();
				}
			});

		} catch (SAXException ex) {
			IOException ex2 = new IOException("parts-info.xml read failed.");
			ex2.initCause(ex);
			throw ex2;
		}
		
		return partsManageData;
	}

}
