// Persist library : Persistence layer
// Copyright (C) 2003 Vincent Daron
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Text;
using System.Data;
using System.Collections;
using System.Reflection;
using System.Collections.Specialized;
using Persist.Sql;
using Persist.Config;
using Persist.Tools;
namespace Persist.Maps{
/// <summary>
/// Manage the mapping between a Type and a Table
/// This class should not be used externally except when creating another configurationStore
/// </summary>
public class ClassMap
{
/// <summary>
/// The Name Of the ClassMap = Type Name with Namespaces
/// </summary>
private string theTypeName;
/// <summary>
/// The Name of the ClassMap, used in associations
/// </summary>
private string theName;
/// <summary>
/// The name of the relational database used by this ClassMap
/// </summary>
private string theRelationalDatabaseName;
/// <summary>
/// The Select Statement with all attribute (included inherited)
/// Ex : SELECT ATT1,ATT2,ATT3,ATT4 FROM TableName [WHERE ... (Inheritance)]
/// </summary>
private SqlStatement theSelectStatement = null;
/// <summary>
/// The Select Statement with Proxy Attributes
/// </summary>
private SqlStatement theSelectProxyStatement = null;
/// <summary>
/// The Select Statement to retreive the Optimistic lock DateTime
/// Ex : Select Table.TimeStamp FROM Table
/// </summary>
private SqlStatement theSelectTimestampStatement = null;
/// <summary>
/// The Insert Statement INSERT INTO Table VALUES(?,?,?,?,?,?,?,?)
/// </summary>
private SqlStatement theInsertStatement = null;
/// <summary>
/// The DeleteStatement
/// Ex : DELETE FROM Table WHERE (ID = ?)
/// </summary>
private SqlStatement theDeleteStatement = null;
/// <summary>
/// The Update Statement
/// Ex : UPDATE table SET Table.Column = ? ,Table.Column2 = ? WHERE ID = ?
/// </summary>
private SqlStatement theUpdateStatement = null;
/// <summary>
/// The Associated TableMap
/// </summary>
private TableMap theTableMap = new TableMap();
/// <summary>
/// The Attributes used in update statement
/// </summary>
private ArrayList theUpdateAttributeMaps = new ArrayList();
/// <summary>
/// All Attribute Maps
/// </summary>
private ArrayList theAttributeMaps = new ArrayList();
/// <summary>
/// The Attributes used in the Proxy Statement
/// </summary>
private ArrayList theProxyAttributeMaps = new ArrayList();
/// <summary>
/// The Attributes composing the PrimaryKey
/// </summary>
private ArrayList theKeyAttributeMap = new ArrayList();
/// <summary>
/// ...
/// </summary>
private ArrayList theReferenceAttributeMaps = new ArrayList();
/// <summary>
/// AssociationMap ... TODO: Complete description
/// </summary>
private ArrayList theInverseAssociationMaps = new ArrayList();
/// <summary>
/// AssociationMap ... TODO: Complete description
/// </summary>
private ArrayList theStraightAssociationMaps = new ArrayList();
private Hashtable theAssociationMaps = new Hashtable();
private Hashtable theHashedAttributeMaps = new Hashtable();
private Type theMapObjectType = null;
/// <summary>
/// Base Class, (Unused for now), Inheritance support incomplete
/// </summary>
private ClassMap theSuperClass = null;
/// <summary>
/// The Corresponding RelationalDatabaseObject
/// </summary>
private RelationalDatabase theRelationalDatabase = null;
/// <summary>
/// The Attribute Map used for Optimist Lock
/// </summary>
private AttributeMap theOptimistLockAttributeMap = null;
/// <summary>
/// Boolean value indicating if the ClassMap has been initialized
/// </summary>
private bool isInitialized = false;
/// <summary>
/// ClassMap Constructor
/// </summary>
/// <param name="name">the ClassMap name</param>
/// <param name="typeName">the Type Name</param>
/// <param name="relationalDatabaseName">the Relational Database name</param>
public ClassMap(string name,string typeName,string relationalDatabaseName)
{
isInitialized = false;
theTypeName = typeName;
theName = name;
theRelationalDatabaseName = relationalDatabaseName;
}
/// <summary>
/// Return the TypeName
/// </summary>
public string TypeName
{
get{return theTypeName;}
}
/// <summary>
/// return the Mapping Name
/// </summary>
public string Name
{
get{return theName;}
}
/// <summary>
/// Return the RelationalDatabase
/// </summary>
public RelationalDatabase RelationalDatabase
{
get
{
return theRelationalDatabase;
}
}
/// <summary>
/// Add an AttributeMap to the ClassMap
/// </summary>
/// <param name="attributeMap"></param>
public void AddAttributeMap(AttributeMap attributeMap)
{
theHashedAttributeMaps.Add(attributeMap.Name, attributeMap);
}
/// <summary>
/// Add an Association to the ClassMap
/// </summary>
/// <param name="map"></param>
public void AddAssociationMap(UniDirectionalAssociationMap map)
{
theAssociationMaps.Add(map.TargetName, map);
if(map.IsInverse)
{
theInverseAssociationMaps.Add(map);
}
else
{
theStraightAssociationMaps.Add(map);
}
}
/// <summary>
/// All AttributeMaps
/// </summary>
public Hashtable AttributeMaps
{
get{return theHashedAttributeMaps;}
}
/// <summary>
/// All Associations
/// </summary>
public Hashtable AssociationMaps
{
get
{
return theAssociationMaps;
}
}
/// <summary>
/// Associated TableMap
/// </summary>
public TableMap Table
{
get
{
return theTableMap;
}
set
{
theTableMap = value;
}
}
#region Internal Members
/// <summary>
/// Return the Optimist lock Attribute Map if exists
/// </summary>
internal AttributeMap OptimistLockAttributeMap
{
get
{
return theOptimistLockAttributeMap;
}
set
{
theOptimistLockAttributeMap = value;
}
}
/// <summary>
/// The Mapped object type
/// </summary>
internal Type MapObjectType
{
get
{
return theMapObjectType;
}
}
/// <summary>
/// Retreive the Super Class (Inheritance support incomplete)
/// </summary>
internal ClassMap SuperClass
{
get
{
return theSuperClass;
}
}
internal SqlStatement GetSelectSql(bool distinct)
{
// Create new statement
SqlStatement statement = new SqlStatement(theRelationalDatabase);
// Add 'SELECT' clause to the select statement
statement.AddSqlClause(RelationalDatabase.ClauseStringSelect + " ");
if(distinct)
{
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringDistinct + " ");
}
// Add clauses for all attributes. Do not add ", " before the first attribute
bool isFirst = true;
ClassMap classMap = this;
do
{
for (int i = 0; i < classMap.AttributeCount; i++)
{
statement.AddSqlClause((isFirst ? "" : ", ") + classMap.GetAttributeMap(i).ColumnMap.FullyQualifiedName);
isFirst = false;
}
classMap = classMap.SuperClass;
}while(classMap != null);
return statement;
}
internal SqlStatement GetSelectSqlFor(PersistentObject obj)
{
// Clone statement
SqlStatement statement = (SqlStatement)theSelectStatement.Copy();
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
// Add the Primary Key value
object keyValue = keyMap.ColumnMap.Converter.ConvertFrom(keyMap.GetValue(obj));
statement.AddParameter(keyValue, keyMap.ColumnMap.DbType);
}
return statement;
}
internal SqlStatement GetSelectCountSql(bool distinct)
{
// Create new statement
SqlStatement statement = new SqlStatement(theRelationalDatabase);
// Add 'SELECT' clause to the select statement
statement.AddSqlClause(RelationalDatabase.ClauseStringSelect + " ");
if(distinct)
{
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringDistinct + " ");
}
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringCount + "(*)");
return statement;
}
internal SqlStatement GetFromAndWhereSql()
{
// Create new statement
SqlStatement statement = new SqlStatement(theRelationalDatabase);
// Add 'FROM' clause to the select statement
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringFrom + " ");
bool isFirst = true;
ClassMap classMap = this;
do
{
AttributeMap map = classMap.GetAttributeMap(0);
if (map != null)
{
statement.AddSqlClause((isFirst ? "" : ", ") + map.ColumnMap.TableMap.Name);
}
classMap = classMap.SuperClass;
isFirst = false;
}
while(classMap != null);
// Add part for keys and inheritance support
string inheritanceAssociations = GetInheritanceAssociations();
// Add 'WHERE key= <ParameterName>' to the select statement
if(PrimaryKeyAttributeCount > 0 || inheritanceAssociations.Length > 0)
{
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringWhere + " ");
isFirst = true;
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
if(!isFirst)
{
statement.AddSqlClause(" " +RelationalDatabase.ClauseStringAnd+ " ");
}
isFirst = false;
statement.AddSqlClause(keyMap.ColumnMap.FullyQualifiedName+"=");
statement.AddSqlParameter();
}
// Add part for inheritance support
if(inheritanceAssociations.Length > 0)
{
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringAnd + " " + inheritanceAssociations);
}
}
return statement;
}
internal SqlStatement GetSelectProxySql(bool distinct)
{
// Create new statement
SqlStatement statement = new SqlStatement(theRelationalDatabase);
// Add 'SELECT' clause to the select statement
statement.AddSqlClause(RelationalDatabase.ClauseStringSelect + " ");
if(distinct)
{
statement.AddSqlClause(" " + RelationalDatabase.ClauseStringDistinct + " ");
}
// Add clauses for all attributes. Do not add ", " before the first attribute
bool isFirst = true;
ClassMap classMap = this;
do
{
for (int i = 0; i < classMap.ProxyAttributesCount; i++)
{
statement.AddSqlClause((isFirst ? "" : ", ") + classMap.GetProxyAttributeMap(i).ColumnMap.FullyQualifiedName);
isFirst = false;
}
classMap = classMap.SuperClass;
}
while(classMap != null);
return statement;
}
internal SqlStatement GetSelectProxySqlFor(PersistentObject obj)
{
// Clone statement
SqlStatement statement = (SqlStatement)theSelectProxyStatement.Copy();
// Fill statement with key values
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
object keyValue = keyMap.ColumnMap.Converter.ConvertFrom(keyMap.GetValue(obj));
statement.AddParameter(keyValue, keyMap.ColumnMap.DbType);
}
return statement;
}
internal SqlStatement GetDeleteSqlFor(PersistentObject obj)
{
// Clone statement
SqlStatement statement = (SqlStatement)theDeleteStatement.Copy();
// Fill statement with key values
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
object keyValue = keyMap.ColumnMap.Converter.ConvertFrom(keyMap.GetValue(obj));
statement.AddParameter(keyValue, keyMap.ColumnMap.DbType);
}
return statement;
}
internal SqlStatement GetUpdateSqlFor(PersistentObject obj)
{
// Clone statement
SqlStatement statement = (SqlStatement)theUpdateStatement.Copy();
// Fill statement with values
AttributeMap aMap;
object aValue;
for(int i = 0; i < UpdateAttributeCount; i++)
{
aMap = GetUpdateAttributeMap(i);
aValue = aMap.ColumnMap.Converter.ConvertFrom(aMap.GetValue(obj));
statement.AddParameter(aValue, aMap.ColumnMap.DbType);
}
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
object keyValue = keyMap.ColumnMap.Converter.ConvertFrom(keyMap.GetValue(obj));
statement.AddParameter(keyValue, keyMap.ColumnMap.DbType);
}
return statement;
}
internal SqlStatement GetInsertSqlFor(PersistentObject obj)
{
// Clone statement
SqlStatement statement = (SqlStatement)theInsertStatement.Copy();
// Fill statement with values
AttributeMap aMap;
object attributeValue;
for(int i = 0; i < AttributeCount; i++)
{
aMap = GetAttributeMap(i);
attributeValue = aMap.ColumnMap.Converter.ConvertFrom(aMap.GetValue(obj));
statement.AddParameter(attributeValue, aMap.ColumnMap.DbType);
}
return statement;
}
internal SqlStatement GetSelectTimestampSqlFor(PersistentObject obj)
{
// Check if optimistic lock is supported by the object
// Try to find timestamp attribute map
if(theSelectTimestampStatement == null)
{
throw new Exception("Optimistic lock is not supported by the object");
}
// Clone statement
SqlStatement statement = (SqlStatement)theSelectTimestampStatement.Copy();
// Fill statement with key value
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
object keyValue = keyMap.ColumnMap.Converter.ConvertFrom(keyMap.GetValue(obj));
statement.AddParameter(keyValue, keyMap.ColumnMap.DbType);
}
return statement;
}
internal void Init(Configuration configuration)
{
lock(typeof(ClassMap))
{
// We don't have to Init class map twice
if(isInitialized)
{
return;
}
theRelationalDatabase = configuration.GetRelationalDatabase(theRelationalDatabaseName);
// Load class for this map object
try
{
theMapObjectType = Type.GetType(theTypeName, true,true);
// Try to find superclass map for this class map
Type sc = theMapObjectType.BaseType;
if(sc != typeof(PersistentObject) && typeof(PersistentObject).IsAssignableFrom(sc))
{
// Try to find class map for the superclass
ClassMap superClassMap = configuration.GetClassMap(sc.Name);
if(superClassMap != null)
theSuperClass = superClassMap;
}
}
catch
{
throw new Exception("Class " + TypeName + " not found");
}
// Initialize Attributes
foreach(AttributeMap attributeMap in theHashedAttributeMaps.Values)
{
attributeMap.Init(configuration);
if(attributeMap.ColumnMap != null)
{
theAttributeMaps.Add(attributeMap);
if(attributeMap.ColumnMap.KeyType == ColumnMap.KeyTypeEnum.Primary)
{
theKeyAttributeMap.Add(attributeMap);
}
else
{
theUpdateAttributeMaps.Add(attributeMap);
}
if(attributeMap.Reference != null)
{
theReferenceAttributeMaps.Add(attributeMap);
}
// Add attributeMap table to the table map collection
theTableMap = attributeMap.ColumnMap.TableMap;
if(attributeMap.IsProxy || attributeMap.ColumnMap.KeyType != ColumnMap.KeyTypeEnum.None)
{
theProxyAttributeMaps.Add(attributeMap);
}
}
// Init accessors
attributeMap.InitAccessors(theMapObjectType);
}
// Init all statements
//
// Init SELECT statement
//
theSelectStatement = GetSelectSql(false);
// Add 'FROM' and 'WHERE' clauses to the select statement
theSelectStatement.AddSqlClause(" ");
theSelectStatement.AddSqlStatement(GetFromAndWhereSql());
//
// Init SELECT statement for proxy
//
theSelectProxyStatement = GetSelectProxySql(false);
// Add 'FROM' and 'WHERE' clauses to the select statement
theSelectProxyStatement.AddSqlClause(" ");
theSelectProxyStatement.AddSqlStatement(GetFromAndWhereSql());
//
// Init OptimistLock SELECT statement
//
ClassMap cm = this;
AttributeMap am = null;
while(cm != null)
{
am = cm.OptimistLockAttributeMap;
if(am != null)
break;
cm = cm.SuperClass;
}
if(am != null)
{
// Create new statement
theSelectTimestampStatement = new SqlStatement(theRelationalDatabase);
// Add 'SELECT' clause to the select statement
theSelectTimestampStatement.AddSqlClause(RelationalDatabase.ClauseStringSelect + " ");
theSelectTimestampStatement.AddSqlClause(am.ColumnMap.FullyQualifiedName);
// Add 'FROM' and 'WHERE' clauses to the select statement
theSelectTimestampStatement.AddSqlStatement(GetFromAndWhereSql());
// Add FOR UPDATE clause if object needs to be locked
theSelectTimestampStatement.AddSqlClause(" " + RelationalDatabase.ClauseStringForUpdate);
}
//
// Init UPDATE statement
//
theUpdateStatement = new SqlStatement(theRelationalDatabase);
// Add 'UPDATE' clause to the select statement
theUpdateStatement.AddSqlClause(RelationalDatabase.ClauseStringUpdate + " ");
theUpdateStatement.AddSqlClause(theTableMap.Name + " ");
// Add 'SET' clause to the update statement
theUpdateStatement.AddSqlClause(RelationalDatabase.ClauseStringSet + " ");
// Add clauses for all attributes. Do not add ", " before the first attribute
for (int i = 0; i < UpdateAttributeCount; i++)
{
theUpdateStatement.AddSqlClause((i > 0 ? ", " : "") + GetUpdateAttributeMap(i).ColumnMap.Name + "=");
theUpdateStatement.AddSqlParameter();
}
// Add 'WHERE key=?' to the update statement
if(PrimaryKeyAttributeCount > 0)
{
theUpdateStatement.AddSqlClause(" " + RelationalDatabase.ClauseStringWhere + " ");
bool isFirst = true;
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
if(!isFirst)
{
theUpdateStatement.AddSqlClause(" " + RelationalDatabase.ClauseStringAnd + " ");
}
isFirst = false;
theUpdateStatement.AddSqlClause(keyMap.ColumnMap.Name+"=");
theUpdateStatement.AddSqlParameter();
}
}
//
// Init INSERT statement
//
theInsertStatement = new SqlStatement(theRelationalDatabase);
// Add 'INSERT INTO' clause to the select statement
theInsertStatement.AddSqlClause(RelationalDatabase.ClauseStringInsert + " ");
theInsertStatement.AddSqlClause(theTableMap.Name + " ");
// Add clauses for all attributes. Do not add ", " before the first attribute
theInsertStatement.AddSqlClause("(");
for (int i = 0; i < AttributeCount; i++)
{
theInsertStatement.AddSqlClause((i > 0 ? ", " : "") + GetAttributeMap(i).ColumnMap.Name);
}
theInsertStatement.AddSqlClause(") ");
// Add 'VALUES' clause to the select statement
theInsertStatement.AddSqlClause(RelationalDatabase.ClauseStringValues + " ");
theInsertStatement.AddSqlClause("(");
for (int i = 0; i < AttributeCount; i++)
{
theInsertStatement.AddSqlClause((i > 0 ? ", " : ""));
theInsertStatement.AddSqlParameter();
}
theInsertStatement.AddSqlClause(") ");
//
// Init DELETE statement
//
theDeleteStatement = new SqlStatement(theRelationalDatabase);
// Add 'DELETE FROM' clause to the select statement
theDeleteStatement.AddSqlClause(RelationalDatabase.ClauseStringDelete + " " + RelationalDatabase.ClauseStringFrom + " ");
theDeleteStatement.AddSqlClause(theTableMap.Name + " ");
// Add 'WHERE key=<parametername>' to the select statement
// Add 'WHERE key=?' to the update statement
if(PrimaryKeyAttributeCount > 0)
{
theDeleteStatement.AddSqlClause(" " + RelationalDatabase.ClauseStringWhere + " ");
bool isFirst = true;
foreach(AttributeMap keyMap in theKeyAttributeMap)
{
if(!isFirst)
{
theDeleteStatement.AddSqlClause(" " + RelationalDatabase.ClauseStringAnd + " ");
}
isFirst = false;
theDeleteStatement.AddSqlClause(keyMap.ColumnMap.Name+"=");
theDeleteStatement.AddSqlParameter();
}
}
isInitialized = true;
}
}
#region Counters
internal int ProxyAttributesCount
{
get
{
return theProxyAttributeMaps.Count;
}
}
internal int AttributeCount
{
get
{
return theAttributeMaps.Count;
}
}
internal int UpdateAttributeCount
{
get
{
return theUpdateAttributeMaps.Count;
}
}
internal int PrimaryKeyAttributeCount
{
get
{
return theKeyAttributeMap.Count;
}
}
internal int ReferenceAttributesCount
{
get
{
return theReferenceAttributeMaps.Count;
}
}
internal int StraightAssociationMapCount
{
get
{
return theStraightAssociationMaps.Count;
}
}
internal int InverseAssociationMapCount
{
get
{
return theInverseAssociationMaps.Count;
}
}
#endregion
/// <summary>
/// Unused for now .... Inheritance support incomplete
/// </summary>
/// <returns></returns>
internal string GetInheritanceAssociations()
{
StringBuilder result = new StringBuilder();
ClassMap classMap = this;
do
{
for(int i = 0; i < classMap.ReferenceAttributesCount; i++)
{
result.Append((i > 0 ? (" " + RelationalDatabase.ClauseStringAnd + " ") : "") +
classMap.GetReferenceAttributeMap(i).ColumnMap.FullyQualifiedName + "=" +
classMap.GetReferenceAttributeMap(i).Reference.ColumnMap.FullyQualifiedName);
}
classMap = classMap.SuperClass;
}
while(classMap != null);
return result.ToString();
}
internal AttributeMap GetProxyAttributeMap(int index)
{
return (AttributeMap)theProxyAttributeMaps[index];
}
internal AttributeMap GetAttributeMap(int index)
{
return (AttributeMap)theAttributeMaps[index];
}
internal AttributeMap GetAttributeMap(string name)
{
return GetAttributeMap(name, false);
}
internal AttributeMap GetAttributeMap(string name, bool areSuperClassesIncluded)
{
AttributeMap am = null;
ClassMap cm = this;
do
{
am = (AttributeMap)cm.theHashedAttributeMaps[name];
cm = cm.SuperClass;
}
while(areSuperClassesIncluded && am == null && cm != null);
return am;
}
internal AttributeMap GetUpdateAttributeMap(int index)
{
return (AttributeMap)theUpdateAttributeMaps[index];
}
internal AttributeMap GetKeyAttributeMap(int index)
{
return (AttributeMap)theKeyAttributeMap[index];
}
internal AttributeMap GetReferenceAttributeMap(int index)
{
return (AttributeMap)theReferenceAttributeMaps[index];
}
internal UniDirectionalAssociationMap GetStraightAssociationMap(int index)
{
return (UniDirectionalAssociationMap)theStraightAssociationMaps[index];
}
internal UniDirectionalAssociationMap GetInverseAssociationMap(int index)
{
return (UniDirectionalAssociationMap)theInverseAssociationMaps[index];
}
internal UniDirectionalAssociationMap GetAssociationMap(string name)
{
return (UniDirectionalAssociationMap)theAssociationMaps[name];
}
internal void RetrieveObject(PersistentObject obj, IDataReader dataReader)
{
ClassMap classMap = this;
object value = null;
int index = 1;
do
{
for (int i = 0; i < classMap.AttributeCount; i++)
{
value = dataReader[classMap.GetAttributeMap(i).ColumnMap.Name];
classMap.GetAttributeMap(i).SetValue(obj,classMap.GetAttributeMap(i).ColumnMap.Converter.ConvertTo(value));
index++;
}
classMap = classMap.SuperClass;
}
while (classMap != null);
if(this.theOptimistLockAttributeMap != null)
{
obj.isOptimisticLock = true;
}
obj.isPersistent = true;
obj.isProxy = false;
}
internal void RetrieveProxyObject(PersistentObject obj, IDataReader dataReader)
{
ClassMap classMap = this;
object value = null;
int index = 1;
do
{
for (int i = 0; i < classMap.ProxyAttributesCount; i++)
{
value = dataReader[classMap.GetAttributeMap(i).ColumnMap.Name];
classMap.GetProxyAttributeMap(i).SetValue(obj,classMap.GetProxyAttributeMap(i).ColumnMap.Converter.ConvertTo(value));
index++;
}
classMap = classMap.SuperClass;
}
while (classMap != null);
if(this.OptimistLockAttributeMap != null)
{
obj.isOptimisticLock = true;
}
obj.isPersistent = true;
obj.isProxy = true;
}
#endregion
}
}
|