/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.beans.dataset;

import java.util.*;
import java.io.*;

import jp.ossc.nimbus.core.*;

/**
 * レコードスキーマ。<p>
 * {@link PropertySchema プロパティスキーマ}の集合で、複数のプロパティを持ったBeanのスキーマを表現する。<br>
 * レコードスキーマは、{@link PropertySchema プロパティスキーマ}の集合であり、<br>
 * <pre>
 *   プロパティスキーマの実装クラス名:プロパティスキーマ定義
 *   プロパティスキーマの実装クラス名:プロパティスキーマ定義
 *                   :
 * </pre>
 * というように、プロパティの数だけ改行区切りで定義する。<br>
 * また、プロパティスキーマの実装クラス名は省略可能で、省略した場合は、{@link DefaultPropertySchema}が適用される。<br>
 * また、プロパティスキーマの実装クラスに、{@link RecordListPropertySchema}を指定したい場合は、エイリアス名を使って"LIST:...."と定義できる。<br>
 * また、レコードスキーマ、プロパティスキーマのインスタンスを管理し、同じスキーマ定義のインスタンスは生成しないようにしている。<br>
 * 
 * @author M.Takata
 */
public class RecordSchema{
    
    /**
     * プロパティスキーマの実装クラス名のエイリアス {@link RecordListPropertySchema}のエイリアス。<p>
     */
    public static final String PROPERTY_SCHEMA_ALIAS_NAME_LIST = "LIST";
    
    /**
     * プロパティスキーマの実装クラス名のエイリアス {@link RecordPropertySchema}のエイリアス。<p>
     */
    public static final String PROPERTY_SCHEMA_ALIAS_NAME_RECORD = "RECORD";
    
    /**
     * プロパティスキーマの実装クラス名のエイリアス {@link XpathPropertySchema}のエイリアス。<p>
     */
    public static final String PROPERTY_SCHEMA_ALIAS_NAME_XPATH = "XPATH";
    
    /**
     * プロパティスキーマの実装クラス名のエイリアス {@link RowVersionPropertySchema}のエイリアス。<p>
     */
    public static final String PROPERTY_SCHEMA_ALIAS_NAME_ROW_VERSION = "ROWVERSION";
    
    private static final String PROP_SCHEMA_CLASS_DELIMETER = ":";
    
    protected static final Map<String, RecordSchema> recordSchemaManager
         = Collections.synchronizedMap(new HashMap<String, RecordSchema>());
    
    protected static final Map<String, PropertySchema> propertySchemaManager
         = Collections.synchronizedMap(new HashMap<String, PropertySchema>());
    
    protected static final Map<String, Class<?>> propertySchemaAliasMap
         = Collections.synchronizedMap(new HashMap<String, Class<?>>());
    
    protected Map<String, PropertySchema> propertySchemaMap = new LinkedHashMap<String, PropertySchema>();
    protected List<String> propertyNames = new ArrayList<String>();
    protected List<PropertySchema> primaryKeyProperties;
    
    static{
        propertySchemaAliasMap.put(
            PROPERTY_SCHEMA_ALIAS_NAME_LIST,
            RecordListPropertySchema.class
        );
        propertySchemaAliasMap.put(
            PROPERTY_SCHEMA_ALIAS_NAME_RECORD,
            RecordPropertySchema.class
        );
        propertySchemaAliasMap.put(
            PROPERTY_SCHEMA_ALIAS_NAME_ROW_VERSION,
            RowVersionPropertySchema.class
        );
        propertySchemaAliasMap.put(
            PROPERTY_SCHEMA_ALIAS_NAME_XPATH,
            XpathPropertySchema.class
        );
    }
    
    /**
     * スキーマ文字列。<p>
     */
    protected String schema;
    
    /**
     * 空のレコードスキーマを生成する。<p>
     */
    public RecordSchema(){
    }
    
    /**
     * レコードスキーマを取得する。<p>
     * 同じスキーマ定義のレコードスキーマ、及びプロパティスキーマのインスタンスを新しく生成しないように、内部で管理している。<br>
     *
     * @param schema レコードスキーマ文字列
     * @return レコードスキーマ
     */
    public static RecordSchema getInstance(String schema)
     throws PropertySchemaDefineException{
        RecordSchema recordSchema = recordSchemaManager.get(schema);
        if(recordSchema == null){
            recordSchema = new RecordSchema();
            recordSchema.setSchema(schema);
            recordSchemaManager.put(schema, recordSchema);
        }
        return recordSchema;
    }
    
    /**
     * レコードスキーマを取得する。<p>
     * 同じスキーマ定義のレコードスキーマ、及びプロパティスキーマのインスタンスを新しく生成しないように、内部で管理している。<br>
     *
     * @param schemata レコードのスキーマ定義を表すプロパティスキーマ配列
     * @return レコードスキーマ
     */
    public static RecordSchema getInstance(PropertySchema[] schemata)
     throws PropertySchemaDefineException{
        final StringBuilder buf = new StringBuilder();
        final String lineSep = System.getProperty("line.separator");
        for(int i = 0; i < schemata.length; i++){
            PropertySchema propertySchema = schemata[i];
            buf.append(propertySchema.getSchema());
            if(i != schemata.length - 1){
                buf.append(lineSep);
            }
        }
        final String schema = buf.toString();
        RecordSchema recordSchema = recordSchemaManager.get(schema);
        if(recordSchema == null){
            recordSchema = new RecordSchema();
            recordSchema.setPropertySchemata(schemata);
            recordSchemaManager.put(schema, recordSchema);
        }
        return recordSchema;
    }
    
    /**
     * スキーマ文字列を追加したレコードスキーマを取得する。<p>
     * 同じスキーマ定義のレコードスキーマ、及びプロパティスキーマのインスタンスを新しく生成しないように、内部で管理している。<br>
     *
     * @param schema レコードスキーマ文字列
     * @return レコードスキーマ
     */
    public RecordSchema appendSchema(String schema)
     throws PropertySchemaDefineException{
        final StringBuilder buf = new StringBuilder();
        if(this.schema != null){
            buf.append(this.schema);
            buf.append(System.getProperty("line.separator"));
        }
        buf.append(schema);
        final String newSchema = buf.toString();
        RecordSchema recordSchema = recordSchemaManager.get(newSchema);
        if(recordSchema == null){
            recordSchema = new RecordSchema();
            recordSchema.setSchema(newSchema);
            recordSchemaManager.put(newSchema, recordSchema);
        }
        return recordSchema;
    }
    
    /**
     * レコードのスキーマ定義を設定する。<p>
     *
     * @param schema レコードのスキーマ定義
     * @exception PropertySchemaDefineException レコードのスキーマ定義に失敗した場合
     */
    public void setSchema(String schema) throws PropertySchemaDefineException{
        propertySchemaMap.clear();
        propertyNames.clear();
        if(primaryKeyProperties != null){
            primaryKeyProperties = null;
        }
        BufferedReader reader = new BufferedReader(new StringReader(schema));
        String propertySchemaStr = null;
        try{
            while((propertySchemaStr = reader.readLine()) != null){
                PropertySchema propertySchema
                    = createPropertySchema(propertySchemaStr);
                if(propertySchema == null){
                    continue;
                }
                if(propertySchemaMap.containsKey(propertySchema.getName())){
                    throw new PropertySchemaDefineException(
                        propertySchemaStr,
                        "Property name is duplicated."
                    );
                }
                propertySchemaMap.put(propertySchema.getName(), propertySchema);
                propertyNames.add(propertySchema.getName());
                if(propertySchema.isPrimaryKey()){
                    if(primaryKeyProperties == null){
                        primaryKeyProperties = new ArrayList<PropertySchema>();
                    }
                    primaryKeyProperties.add(propertySchema);
                }
            }
        }catch(IOException e){
            // 起きないはず
            throw new PropertySchemaDefineException(schema, e);
        }
        this.schema = schema;
    }
    
    /**
     * プロパティのスキーマ定義を生成する。<p>
     *
     * @param schema プロパティのスキーマ定義
     * @return プロパティのスキーマ定義
     * @exception PropertySchemaDefineException プロパティのスキーマ定義に失敗した場合
     */
    protected PropertySchema createPropertySchema(String schema)
     throws PropertySchemaDefineException{
        if(schema == null || schema.length() == 0){
            return null;
        }
        Class<?> propertySchemaClass = DefaultPropertySchema.class;
        final int index = schema.indexOf(PROP_SCHEMA_CLASS_DELIMETER);
        if(index == -1 || index == schema.length() - 1){
            throw new PropertySchemaDefineException(
                schema,
                "The class name of PropertySchema is not specified."
            );
        }else if(index != 0){
            String propertySchemaClassName
                 = schema.substring(0, index);
            if(propertySchemaAliasMap.containsKey(propertySchemaClassName)){
                propertySchemaClass = propertySchemaAliasMap.get(propertySchemaClassName);
            }else{
                try{
                    propertySchemaClass = Class.forName(
                        propertySchemaClassName,
                        true,
                        NimbusClassLoader.getInstance()
                    );
                }catch(ClassNotFoundException e){
                    throw new PropertySchemaDefineException(
                        schema,
                        "The class name of PropertySchema is illegal.",
                        e
                    );
                }
            }
        }
        schema = schema.substring(index + 1);
        final String propertySchemaKey
             = propertySchemaClass.getName() + schema;
        PropertySchema propertySchema
             = propertySchemaManager.get(propertySchemaKey);
        if(propertySchema == null){
            try{
                propertySchema = (PropertySchema)propertySchemaClass.newInstance();
            }catch(InstantiationException e){
                throw new PropertySchemaDefineException(
                    schema,
                    e
                );
            }catch(IllegalAccessException e){
                throw new PropertySchemaDefineException(
                    schema,
                    e
                );
            }
            propertySchema.setSchema(schema);
            propertySchemaManager.put(propertySchemaKey, propertySchema);
        }
        return propertySchema;
    }
    
    /**
     * レコードのスキーマ文字列を取得する。<p>
     *
     * @return レコードのスキーマ文字列
     */
    public String getSchema(){
        return schema;
    }
    
    /**
     * レコードのスキーマ定義を設定する。<p>
     *
     * @param schemata レコードのスキーマ定義を表すプロパティスキーマ配列
     */
    public void setPropertySchemata(PropertySchema[] schemata){
        propertySchemaMap.clear();
        propertyNames.clear();
        if(primaryKeyProperties != null){
            primaryKeyProperties = null;
        }
        final StringBuilder buf = new StringBuilder();
        final String lineSep = System.getProperty("line.separator");
        for(int i = 0; i < schemata.length; i++){
            PropertySchema propertySchema = schemata[i];
            buf.append(propertySchema.getSchema());
            if(i != schemata.length - 1){
                buf.append(lineSep);
            }
            final String propertySchemaKey
                 = propertySchema.getClass().getName() + propertySchema.getSchema();
            propertySchemaManager.put(propertySchemaKey, propertySchema);
            
            propertySchemaMap.put(propertySchema.getName(), propertySchema);
            propertyNames.add(propertySchema.getName());
            if(propertySchema.isPrimaryKey()){
                if(primaryKeyProperties == null){
                    primaryKeyProperties = new ArrayList<PropertySchema>();
                }
                primaryKeyProperties.add(propertySchema);
            }
        }
        
        schema = buf.toString();
    }
    
    /**
     * プロパティスキーマ配列を取得する。<p>
     *
     * @return プロパティスキーマ配列
     */
    public PropertySchema[] getPropertySchemata(){
        return propertySchemaMap.values().toArray(
            new PropertySchema[propertySchemaMap.size()]
        );
    }
    
    /**
     * プライマリキーとなるプロパティスキーマ配列を取得する。<p>
     *
     * @return プロパティスキーマ配列
     */
    public PropertySchema[] getPrimaryKeyPropertySchemata(){
        return (primaryKeyProperties == null
            || primaryKeyProperties.size() == 0) ? null
                : primaryKeyProperties.toArray(
                    new PropertySchema[primaryKeyProperties.size()]);
    }
    
    /**
     * 指定されたインデックスのプロパティス名を取得する。<p>
     *
     * @param index インデックス
     * @return プロパティス名
     */
    public String getPropertyName(int index){
        if(index < 0 || index >= propertyNames.size()){
            return null;
        }
        return propertyNames.get(index);
    }
    
    /**
     * 指定されたプロパティス名のインデックスを取得する。<p>
     *
     * @param name インデックス
     * @return インデックス
     */
    public int getPropertyIndex(String name){
        return propertyNames.indexOf(name);
    }
    
    /**
     * 指定されたインデックスのプロパティスキーマを取得する。<p>
     *
     * @param index インデックス
     * @return プロパティスキーマ
     */
    public PropertySchema getPropertySchema(int index){
        if(index < 0 || index >= propertyNames.size()){
            return null;
        }
        return propertySchemaMap.get(propertyNames.get(index));
    }
    
    /**
     * 指定されたプロパティ名のプロパティスキーマを取得する。<p>
     *
     * @param name プロパティ名
     * @return プロパティスキーマ
     */
    public PropertySchema getPropertySchema(String name){
        if(name == null){
            return null;
        }
        return propertySchemaMap.get(name);
    }
    
    /**
     * プロパティの数を取得する。<p>
     *
     * @return プロパティの数
     */
    public int getPropertySize(){
        return propertyNames.size();
    }
    
    /**
     * このレコードスキーマの文字列表現を取得する。<p>
     *
     * @return 文字列表現
     */
    @Override
    public String toString(){
        final StringBuilder buf = new StringBuilder();
        buf.append('{');
        if(propertySchemaMap != null){
            final Iterator<PropertySchema> schemata = propertySchemaMap.values().iterator();
            while(schemata.hasNext()){
                buf.append(schemata.next());
                if(schemata.hasNext()){
                    buf.append(';');
                }
            }
        }
        buf.append('}');
        return buf.toString();
    }
}