com.alachisoft.ncache.serialization.util.SerializationUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nc-serialization Show documentation
Show all versions of nc-serialization Show documentation
Internal package of Alachisoft.
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.alachisoft.ncache.serialization.util;
import com.alachisoft.ncache.runtime.JSON.JsonObject;
import com.alachisoft.ncache.runtime.JSON.JsonValue;
import com.alachisoft.ncache.runtime.JSON.JsonValueBase;
import com.alachisoft.ncache.runtime.datasourceprovider.DistributedDataStructureType;
import com.alachisoft.ncache.runtime.exceptions.CompactSerializationException;
import com.alachisoft.ncache.runtime.exceptions.GeneralFailureException;
import com.alachisoft.ncache.runtime.exceptions.OperationFailedException;
import com.alachisoft.ncache.serialization.JSON.JsonBinaryFormatter;
import com.alachisoft.ncache.serialization.core.io.ICompactSerializable;
import com.alachisoft.ncache.serialization.standard.FormatterServices;
import com.alachisoft.ncache.serialization.standard.io.ObjectInputStream;
import com.alachisoft.ncache.serialization.standard.io.ObjectOutputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
/**
* @author Administrator
*/
public class SerializationUtil {
public static short userdefinedArrayTypeHandle = 5000;
static HashMap _attributeOrder = new HashMap();
static HashMap _portibilaty = new HashMap();
static HashMap _subTypeHandle = new HashMap();
private static FormatterServices impl = null;
//contains CacheContext just to mae sure when unregistering cache, we don't attempt to unrigester a never existing cacheContext
private static List cacheList;
////*****************
private static HashMap typeInfoMapTable = new HashMap();
private static ClassInfoPool classInfoPool;
/**
* Called to initialize Compact Framework, all input parameters must be provided, null or empty not supported
*
* @param compactTypeInfoString info String containing compactYpe information
* @param cacheContext Cache name
* @throws Exception
*/
public static void initialize(String compactTypeInfoString, String cacheContext) throws Exception {
try {
impl = FormatterServices.getDefault();
HashMap type = GetTypeMapFromProtocolString(compactTypeInfoString, new int[1], new int[1]);
Map compactType = getCompactTypes(type, false, cacheContext);
if (compactType != null) {
if (!compactType.isEmpty() || compactType.size() != 0) {
impl.register(compactType, getAttributeOrder(cacheContext), cacheContext, getPortability(cacheContext));
}
}
if (cacheList == null) {
cacheList = new ArrayList();
}
if (!cacheList.contains(cacheContext)) {
cacheList.add(cacheContext);
}
} catch (Exception ex) {
throw ex;
}
}
/**
* Simply serializes any object unless one of the supported default types or Dynamically registered with respect to CacheContext
*
* @param value object to serialize
* @param cacheContext Name of the Cache that object is registered to else null if default type
* @return returns serialized byte array of the object
* @throws IOException
*/
public static byte[] safeSerialize(Object value, String cacheContext, SerializationBitSet flag) throws IOException {
// if (value != null && value instanceof ICompactSerializable)
// {
if ((value instanceof byte[]) && flag != null) {
flag.SetBit((byte) SerializationBitSetConstant.BinaryData);
return (byte[]) value;
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutput objectOutput = new ObjectOutputStream(byteArrayOutputStream, cacheContext);
objectOutput.writeObject(value);
objectOutput.flush();
return byteArrayOutputStream.toByteArray();
// }
// return value;
}
/**
* Deserializes byte[] into an object castable into the one actually expected if it is one of the supported default types or Dynamically registered with respect to CacheContext
*
* @param value byte[] to deserialize
* @param cacheContext Name of the cache that object is registered to else null if default type
* @return expected Object boxed
* @throws IOException
* @throws ClassNotFoundException if Class was not dynamically registered or the value is not one of the default types
*/
public static Object safeDeserialize(Object value, String cacheContext, SerializationBitSet flag) throws IOException, ClassNotFoundException {
if (value != null && value instanceof byte[]) {
if (flag != null && flag.IsBitSet((byte) SerializationBitSetConstant.BinaryData)) return value;
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream((byte[]) value);
ObjectInput objectInput = new ObjectInputStream(byteArrayInputStream, cacheContext);
return objectInput.readObject();
}
return value;
}
public static Object compactSerialize(Object value, String cacheContext) throws IOException {
if (value != null && value instanceof ICompactSerializable) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutput objectOutput = new ObjectOutputStream(byteArrayOutputStream, cacheContext);
objectOutput.writeObject(value);
objectOutput.flush();
return byteArrayOutputStream.toByteArray();
}
return value;
}
/**
* Deletes only the stored cacheContext, dynamically created classes are not destroyed
* since once a class/file has been loaded into JVM, its instance can be deleted but the class/file cannot
*
* @param cacheContext CacheName
*/
public static void unRegisterCache(String cacheContext) {
if (cacheList != null && cacheList.contains(cacheContext)) {
impl.unregisterKnownType(null, cacheContext, true);
cacheList.remove(cacheContext);
} else {
if (impl == null) {
impl = FormatterServices.getDefault();
}
impl.unregisterKnownType(null, cacheContext, true);
}
}
public static HashMap getCompactTypes(Map typeInfo, boolean throwExceptions, String cacheContext) throws CompactSerializationException, Exception {
String typeName = "";
short typeHandle = 0;
HashMap types = null;
HashMap knownTypes = null;
HashMap framework = new HashMap();
HashMap typeTable = new HashMap();
HashMap portability = new HashMap();
Iterator ide = typeInfo.entrySet().iterator();
try {
while (ide.hasNext()) {
Map.Entry pairs = (Map.Entry) ide.next();
HashMap info = (HashMap) pairs.getValue();
// Jar files do not have any Assembly Names, so no need to parse them
// If a known type, try to load it, at exception throw only when throwable is true otherwise consider the class not available in classpath
if (info != null) {
Class type = null;
typeHandle = (short) Integer.parseInt((String) pairs.getKey());
Iterator ide2 = info.entrySet().iterator();
while (ide2.hasNext()) {
Map.Entry pairs2 = (Map.Entry) ide2.next();
{
if (pairs.getValue() instanceof HashMap) {
types = (HashMap) pairs2.getValue();
if (types.containsKey("name") && types.get("type").toString().toLowerCase().equals("java")) {
// typeHandle = (short) Integer.parseInt((String) types.get("id"));
typeName = (String) types.get("name");
HashMap attributeOrder = (HashMap) types.get("attribute");
boolean portable = false;
try {
portable = Boolean.parseBoolean((String) types.get("portable"));
} catch (Exception ex) {
portable = false;
}
String[] typeNameArray = null;
if (portable) {
typeNameArray = typeName.split(":");
typeName = typeNameArray[0];
for (int i = 1; i < typeNameArray.length - 1; i++) {
typeName += "." + typeNameArray[i];
}
}
try {
// To load Class, class path & name are CASE SENSITIVE, i.e. . e.g. testapp1.Customers
type = null;
type = Class.forName(typeName);
String ImplementationVersion = "";
if (portable) {
String SpecificationVersion = "";
String className = type.getSimpleName() + ".class";
String classPath = type.getResource(className).toString();
// classPath = type.getProtectionDomain().getCodeSource().getLocation().getPath();
if (!classPath.startsWith("jar")) {
} else {
String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF";
Manifest manifest = new Manifest(new URL(manifestPath).openStream());
Attributes attr = manifest.getMainAttributes();
Attributes.Name Implementation = new Name("Implementation-Version");
Attributes.Name Specification = new Name("Specification-Version");
if (attr.get(Implementation) != null) {
ImplementationVersion = (String) attr.get(Implementation);
}
if (attr.get(Specification) != null) {
SpecificationVersion = (String) attr.get(Specification);
}
String version = typeNameArray[typeNameArray.length - 1];
if (typeNameArray != null && !(version.equals("0.0") || ImplementationVersion.equals(version) || SpecificationVersion.equals(version))) {
type = null;
throw new Exception("ImplementationVersion or SpecificationVersion does not match");
}
}
}
} catch (Exception e) {
if (throwExceptions) {
throw new CompactSerializationException(e.getMessage());
}
}
if (type != null) {
HashMap handleNonCompactFieldsMap = new HashMap();
handleNonCompactFieldsMap.put("handle", typeHandle);
if (types.containsKey("non-compact-fields")) {
handleNonCompactFieldsMap.put("non-compact-fields", types.get("non-compact-fields"));
}
framework.put(type, handleNonCompactFieldsMap);
// framework.put(type,typeHandle);
} else {
//if (throwExceptions)
//{
//throw new Exception(typeName + " can not be registered with compact framework, unable to load class");
continue;
//}
}
HashMap attributeUnion = null;
HashMap[] orderedUnion = null;
if (portable) {
attributeUnion = (HashMap) info.get("Alachisoft.NCache.AttributeUnion");
orderedUnion = new HashMap[((HashMap) (attributeUnion.get("attribute"))).size()];
//Order AttributeUnion with orderNumber
if (attributeUnion != null && attributeUnion.get("attribute") != null) {
Iterator union = ((HashMap) attributeUnion.get("attribute")).entrySet().iterator();
while (union.hasNext()) {
Map.Entry p = (Map.Entry) union.next();
HashMap attribute = (HashMap) p.getValue();
int order = Integer.parseInt((String) attribute.get("order"));
orderedUnion[order - 1] = (HashMap) attribute;
}
}
}
// Order of 0 means skip, -1 means it comes after EOF
if (attributeOrder != null && attributeOrder.size() > 0 && portable) {
String[][] temp = new String[2][];
//Attribute Name
temp[0] = new String[orderedUnion.length];
//Order Number
temp[1] = new String[orderedUnion.length];
//Create a List of attributes of a class
HashMap[] attributearray = (HashMap[]) attributeOrder.values().toArray(new HashMap[attributeOrder.size()]);
ArrayList attributeList = new ArrayList();
attributeList.addAll(Arrays.asList(attributearray));
int count = 0;
HashMap toRemove = null;
while (count < orderedUnion.length) {
String attrib = (String) ((HashMap) orderedUnion[count]).get("order");
//Serach for mapped-to in attributeList
for (int i = 0; i < attributeList.size(); i++) {
HashMap attribute = (HashMap) attributeList.get(i);
if ((attrib).equals((String) attribute.get("order"))) {
temp[0][count] = (String) attribute.get("name");
if (attribute.get("type").toString().contains("unsigned")) {
temp[0][count] = attribute.get("type").toString() + "." + temp[0][count];
}
temp[1][count] = String.valueOf(count + 1);
toRemove = attribute;
break;
}
}
if (toRemove != null) {
attributeList.remove(toRemove);
toRemove = null;
}
if (temp[0][count] == null) {
temp[0][count] = "skip.attribute";
temp[1][count] = "0";
}
count++;
}
//Add in all thats left of attributeList as after EOF
if (attributeList != null && attributeList.size() > 0) {
String[][] temporary = new String[2][];
temporary = (String[][]) temp.clone();
temp[0] = new String[((HashMap) attributeUnion.get("attribute")).size() + attributeList.size()];
temp[1] = new String[((HashMap) attributeUnion.get("attribute")).size() + attributeList.size()];
for (int i = 0; i < temporary[0].length; i++) {
temp[0][i] = temporary[0][i];
temp[1][i] = temporary[1][i];
}
for (int i = temporary[0].length; i < temporary[0].length + attributeList.size(); i++) {
temp[0][i] = (String) ((HashMap) (attributeList.toArray()[i - temporary[0].length])).get("name");
temp[1][i] = "-1";
}
}
if (CheckAlreadyRegistered(portable, cacheContext, (String) pairs.getKey(), type, typeName))
continue;
typeTable.put(typeName, temp);
_attributeOrder.put(cacheContext, typeTable);
portability.put(Short.parseShort((String) pairs.getKey()), portable);
_portibilaty.put(cacheContext, portability);
PopulateSubHandle(portable, cacheContext, (String) pairs.getKey(), (String) types.get("handle-id"), type);
} else {
if (CheckAlreadyRegistered(portable, cacheContext, (String) pairs.getKey(), type, typeName))
continue;
typeTable.put(typeName, null);
_attributeOrder.put(cacheContext, typeTable);
portability.put(Short.parseShort((String) pairs.getKey()), portable);
_portibilaty.put(cacheContext, portability);
PopulateSubHandle(portable, cacheContext, (String) pairs.getKey(), (String) types.get("handle-id"), type);
}
//Perform after loading of Classes
//
// if (attributeOrder != null)
// {
// if (attributeOrder.size() > 0)
// {
// String[][] temp = new String[2][];
// //Attribute Name
// temp[0] = new String[attributeOrder.size()];
// //Order Number
// temp[1] = new String[attributeOrder.size()];
//
// Iterator attrib = attributeOrder.entrySet().iterator();
// int lastIndex = attributeOrder.size() - 1;
// while (attrib.hasNext())
// {
// Map.Entry attribPair = (Map.Entry) attrib.next();
// try
// {
// int order = Integer.parseInt((String) ((HashMap) attribPair.getValue()).get("order"));
// if (order == -1)
// {
// temp[0][lastIndex] = ((HashMap) attribPair.getValue()).get("name").toString();
// temp[1][lastIndex] = String.valueOf(order);
// lastIndex--;
// }
// else
// {
// temp[0][order - 1] = ((HashMap) attribPair.getValue()).get("name").toString();
// if (((HashMap)attribPair.getValue()).get("type").toString().contains("unsigned"))
// {
// temp[0][order-1] = ((HashMap)attribPair.getValue()).get("type").toString() + "." + temp[0][order-1];
// }
// temp[1][order - 1] = String.valueOf(order);
// }
//
// }
// catch (Exception ex)
// {
// throw new CompactSerializationException("Order Numbering of Dynamic Compact Serialization should start from 1 and should increment by a difference of +1; Error in config.ncconf");
// }
// }
//
// for (int idx = 0; idx < temp.length; idx++)
// {
// Object elem = temp[idx];
// if (temp[idx] == null || temp[idx].length == 0)
// {
// throw new CompactSerializationException("Order information/Numbering not correct of Dynamic Compact Serialization in config.ncconf");
// }
// }
// typeTable.put(typeName, temp);
//
// _attributeOrder.put(cacheContext, typeTable);
//
// portability.put(Short.valueOf((String)pairs.getKey()), Boolean.parseBoolean((String)types.get("portable")));
// _portable.put(cacheContext,portability);
// }
// }
//
}
}
}
}
}
}
} catch (Exception exception) {
//System.out.println(exception.getMessage());
}
return framework;
}
public static short getSubTypeHandle(String cacheContext, String handle, java.lang.Class type) {
if (_subTypeHandle == null) {
return 0;
}
if (!_subTypeHandle.containsKey(cacheContext)) {
return 0;
}
if (!((HashMap) _subTypeHandle.get(cacheContext)).containsKey(handle)) {
return 0;
}
return Short.parseShort((String) ((HashMap) ((HashMap) _subTypeHandle.get(cacheContext)).get(handle)).get(type));
}
private static boolean CheckAlreadyRegistered(boolean portable, String cacheContext, String handle, Class type, String typeName) {
if (portable) {
if (_subTypeHandle.containsKey(cacheContext))
if (((HashMap) _subTypeHandle.get(cacheContext.toLowerCase())).containsKey(handle))
if (((HashMap) ((HashMap) _subTypeHandle.get(cacheContext.toLowerCase())).get(handle)).containsKey(type))
return true;
} else {
if (_attributeOrder.containsKey(cacheContext.toLowerCase()))
if (((HashMap) _attributeOrder.get(cacheContext.toLowerCase())).containsKey(typeName))
return true;
}
return false;
}
/**
* Get Type Map of Dynamic Compact Serialization From Protocol String (Recursion Logic inside to parse whole string)
*
* @param protocolString String returned by Server containing complete information of which Class(es) to Dynamically Serialize
* @param startIndex Used for String manipulation, For start of Recursion pass 0
* @param endIndex Used for String manipulation, For start of Recursion pass 0
* @return HasMap containing Class name, ID and other data related to the class to be initialized
*/
public static HashMap GetTypeMapFromProtocolString(String protocolString, int[] startIndex, int[] endIndex) {
endIndex[0] = protocolString.indexOf("\"", startIndex[0] + 1);
HashMap tbl = new HashMap();
String token = protocolString.substring(startIndex[0], (endIndex[0]));
if (token.equals("__dictionary")) {
startIndex[0] = endIndex[0] + 1;
endIndex[0] = protocolString.indexOf("\"", endIndex[0] + 1);
int dicCount = Integer.parseInt(protocolString.substring(startIndex[0], (endIndex[0])));
for (int i = 0; i < dicCount; i++) {
startIndex[0] = endIndex[0] + 1;
endIndex[0] = protocolString.indexOf("\"", endIndex[0] + 1);
String key = protocolString.substring(startIndex[0], (endIndex[0]));
startIndex[0] = endIndex[0] + 1;
endIndex[0] = protocolString.indexOf("\"", endIndex[0] + 1);
// If value to be stored is _dictionary then call the function recursively other wise its an attribute
String value = protocolString.substring(startIndex[0], (endIndex[0]));
if (value.equals("__dictionary")) {
HashMap temp = new HashMap();
temp = GetTypeMapFromProtocolString(protocolString, startIndex, endIndex);
tbl.put(key, temp);
} else {
tbl.put(key, value);
}
}
}
return tbl;
}
public static String GetProtocolStringFromTypeMap(HashMap typeMap) {
Stack st = new Stack();
StringBuilder protocolString = new StringBuilder();
protocolString.append("__dictionary").append("\"");
protocolString.append(typeMap.size()).append("\"");
Iterator ide = typeMap.entrySet().iterator();
while (ide.hasNext()) {
Map.Entry mapDic = (Map.Entry) ide.next();
if (mapDic.getValue() instanceof HashMap) {
st.push(mapDic.getValue());
st.push(mapDic.getKey());
} else {
protocolString.append(mapDic.getKey()).append("\"");
protocolString.append(mapDic.getValue()).append("\"");
}
}
while (st.size() != 0 && st.size() % 2 == 0) {
Object obj1 = st.pop();
protocolString.append((obj1 instanceof String) ? obj1.toString() : "").append("\"");
Object obj2 = st.pop();
protocolString.append(GetProtocolStringFromTypeMap(obj2 instanceof HashMap ? (HashMap) obj2 : null));
}
return protocolString.toString();
}
public static HashMap getAttributeOrder(String cacheContext) {
return (HashMap) _attributeOrder.get(cacheContext);
}
public static HashMap getPortability(String cacheContext) {
return ((HashMap) _portibilaty.get(cacheContext));
}
public static boolean getPortability(short type, String cacheContext) {
return (Boolean) ((HashMap) _portibilaty.get(cacheContext)).get(type);
}
public static void PopulateSubHandle(boolean portable, String cacheContext, String handle, String subHandle, Class type) {
if (portable) {
if (!_subTypeHandle.containsKey(cacheContext)) {
_subTypeHandle.put(cacheContext, new HashMap());
}
if (!((HashMap) _subTypeHandle.get(cacheContext)).containsKey(handle)) {
((HashMap) _subTypeHandle.get(cacheContext)).put(handle, new HashMap());
}
if (!((HashMap) ((HashMap) _subTypeHandle.get(cacheContext)).get(handle)).containsKey(type)) {
((HashMap) ((HashMap) _subTypeHandle.get(cacheContext)).get(handle)).put(type, subHandle);
} else {
throw new IllegalArgumentException("Sub-Handle '" + subHandle + "' already present in " + cacheContext + " in class " + type.getName() + " with Handle " + handle);
}
}
}
public static short GetSubTypeHandle(String cacheContext, String handle, Class type) {
if (!_subTypeHandle.containsKey(cacheContext)) {
return 0;
}
if (!(((HashMap) _subTypeHandle.get(cacheContext)).containsKey(handle))) {
return 0;
}
return Short.parseShort((String) ((HashMap) ((HashMap) _subTypeHandle.get(cacheContext)).get(handle)).get(type));
}
public static void registerTypeInfoMap(String cacheContext, TypeInfoMap typeInfoMap) {
classInfoPool = new ClassInfoPool();
if (!typeInfoMapTable.containsKey(cacheContext)) {
typeInfoMapTable.put(cacheContext, typeInfoMap);
}
}
public static void UnRegisterTypeInfoMap(String cacheContext) {
if (!typeInfoMapTable.containsKey(cacheContext)) {
typeInfoMapTable.remove(cacheContext);
}
}
public static HashMap getQueryInfo(Object value, String cacheContext) throws Exception {
TypeInfoMap typeInfoMap = (TypeInfoMap) typeInfoMapTable.get(cacheContext);
if (typeInfoMap == null) {
return null;
}
return getQueryInfo(value, typeInfoMap);
}
public static HashMap getQueryInfo(Object value, TypeInfoMap typeMap)
throws Exception {
HashMap queryInfo = null;
if (typeMap == null) {
return null;
}
try {
String typeName= value.getClass().getCanonicalName();
boolean isJson=false;
if(JsonObject.class.isAssignableFrom(value.getClass()))
{
JsonObject jsonObject = (JsonObject) value;
if(!(jsonObject.getType() == null || jsonObject.getType().isEmpty()))
{
typeName=jsonObject.getType();
isJson=true;
}
}
int handleId = typeMap.getHandleId(typeName);
if (handleId != -1) {
queryInfo = new HashMap();
ArrayList attribValues = new ArrayList();
ArrayList attributes = typeMap.getAttribList(handleId);
for (int i = 0; i < attributes.size(); i++) {
if(!isJson) {
Field fieldAttrib = value.getClass().getDeclaredField((String) attributes.get(i));
if (fieldAttrib != null) {
fieldAttrib.setAccessible(true);
Object attribValue = fieldAttrib.get(value);
if (attribValue instanceof java.lang.String) //add all strings as lower case in index tree
{
attribValue = (Object) (attribValue.toString()).toLowerCase();
}
attribValues.add(attribValue);
} else {
throw new Exception("Unable to extract query information from user object.");
}
}
else
{
JsonObject jsonObject= (JsonObject)value;
JsonValue jsonValue= (JsonValue) jsonObject.getAttributeValue((String)attributes.get(i));
if(jsonValue!=null)
{
attribValues.add(jsonValue.getValue());
}
else
throw new Exception ("Unable to extract query information from user object for attribute \""+(String)attributes.get(i)+"\"");
}
}
queryInfo.put("" + handleId, attribValues);
}
} catch (Exception e) {
throw new GeneralFailureException(e.getMessage());
}
return queryInfo;
}
public static byte[] serializeDataStructureItem(Object serializableObject) {
String json = new String();
JsonValueBase possibleJsonValue = serializableObject instanceof JsonValueBase? (JsonValueBase)serializableObject:null;
if (possibleJsonValue != null)
json = possibleJsonValue.toString();
else {
try {
json = JsonBinaryFormatter.serializeObject(serializableObject, false);
} catch (OperationFailedException e) {
return null;
}
}
return json.getBytes(JsonBinaryFormatter.SERIALIZATION_CHARSET);
}
}
class ClassInfoPool {
static int i = 1;
HashMap pool = new HashMap();
public ClassInfo getClassInf(Class cls) {
ClassInfo classInfo = pool.get(cls);
if (classInfo != null) {
return classInfo;
}
classInfo = new ClassInfo(cls.getCanonicalName());
pool.put(cls, classInfo);
return classInfo;
}
public Field getField(Class cls, String fieldName) throws NoSuchFieldException {
ClassInfo clsInfo = pool.get(cls);
Field field = clsInfo.getField(fieldName);
if (field != null) {
return field;
}
field = cls.getField(fieldName);
clsInfo.addField(fieldName, field);
return field;
}
}
class ClassInfo {
String canonicalName;
HashMap fieldMap;
ClassInfo(String canonicalName) {
this.canonicalName = canonicalName;
fieldMap = new HashMap();
}
public String getCanonicalName() {
return this.canonicalName;
}
public void setCanonicalName(String canonicalName) {
this.canonicalName = canonicalName;
}
public Field getField(String fieldName) {
return fieldMap.get(fieldName);
}
public void addField(String fieldName, Field field) {
fieldMap.put(fieldName, field);
}
}