Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Signature.java
* PROJECT: JDigiDoc
* DESCRIPTION: Digi Doc functions for creating
* and reading signed documents.
* AUTHOR: Veiko Sinivee, Sunset Software OÜ
*==================================================
* Copyright (C) AS Sertifitseerimiskeskus
* 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.
* GNU Lesser General Public Licence is available at
* http://www.gnu.org/copyleft/lesser.html
*==================================================
*/
package ee.sk.digidoc;
import java.io.Serializable;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import ee.sk.digidoc.factory.NotaryFactory;
import ee.sk.digidoc.factory.TimestampFactory;
import ee.sk.digidoc.factory.DigiDocFactory;
import ee.sk.digidoc.factory.DigiDocXmlGenFactory;
import ee.sk.digidoc.factory.DigiDocGenFactory;
import ee.sk.digidoc.factory.DigiDocVerifyFactory;
import ee.sk.utils.ConfigManager;
import java.security.cert.CertStore;
import java.security.cert.X509Certificate;
import ee.sk.utils.ConvertUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import org.apache.log4j.Logger;
import org.bouncycastle.tsp.TSPAlgorithms;
import org.bouncycastle.tsp.TimeStampResponse;
/**
* Models an XML-DSIG/ETSI Signature. A signature
* can contain references SignedInfo (truly signed data)
* and signed and unsigned properties.
* @author Veiko Sinivee
* @version 1.0
*/
public class Signature implements Serializable
{
private static final long serialVersionUID = 1L;
/** reference to the parent SignedDoc object */
private SignedDoc m_sigDoc;
/** signature id */
private String m_id;
/** SignedInfo object */
private SignedInfo m_signedInfo;
/** SignatureValue object */
private SignatureValue m_signatureValue;
/** container comment (bdoc2 lib ver and name) */
private String m_comment;
/** KeyInfo object */
private KeyInfo m_keyInfo;
/** SignedProperties object */
private SignedProperties m_sigProp;
/** QualifyingProperties object */
private QualifyingProperties m_qualProp;
/** UnsignedProperties object */
private UnsignedProperties m_unsigProp;
/** original bytes read from XML file */
private byte[] m_origContent;
/** CertID elements */
private ArrayList m_certIds;
/** CertValue elements */
private ArrayList m_certValues;
/** TimestampInfo elements */
private ArrayList m_timestamps;
/** cached list of errors */
private ArrayList m_errs;
/** path in bdoc container */
private String m_path;
private boolean m_bAltDigMatch;
/** signature profile - used in bdoc */
private String m_profile, m_httpFrom;
private static Logger m_logger = Logger.getLogger(Signature.class);
/**
* Creates new Signature
*/
public Signature(SignedDoc sigDoc) {
m_sigDoc = sigDoc;
m_id = null;
m_signedInfo = null;
m_signatureValue = null;
m_keyInfo = null;
m_sigProp = null;
m_unsigProp = null;
m_origContent = null;
m_certIds = null;
m_certValues = null;
m_timestamps = null;
m_path = null;
m_profile = null;
m_errs = null;
m_httpFrom = null;
m_bAltDigMatch = false;
m_comment = null;
}
/**
* Accessor for sigDoc attribute
* @return value of sigDoc attribute
*/
public SignedDoc getSignedDoc() {
return m_sigDoc;
}
/**
* Mutator for sigDoc attribute
* @param sigDoc new value for sigDoc attribute
*/
public void setSignedDoc(SignedDoc sigDoc)
{
m_sigDoc = sigDoc;
}
/**
* Accessor for id attribute
* @return value of id attribute
*/
public String getId() {
return m_id;
}
/**
* Mutator for id attribute
* @param str new value for id attribute
* @throws DigiDocException for validation errors
*/
public void setId(String str)
throws DigiDocException
{
DigiDocException ex = validateId(str);
if(ex != null)
throw ex;
m_id = str;
}
/**
* Accessor for origContent attribute
* @return value of origContent attribute
*/
public byte[] getOrigContent() {
return m_origContent;
}
/**
* Mutator for origContent attribute
* @param str new value for origContent attribute
*/
public void setOrigContent(byte[] data)
{
m_origContent = data;
}
/**
* Helper method to validate an id
* @param str input data
* @return exception or null for ok
*/
private DigiDocException validateId(String str)
{
DigiDocException ex = null;
if(str == null)
ex = new DigiDocException(DigiDocException.ERR_SIGNATURE_ID,
"Id is a required attribute", null);
return ex;
}
/**
* Accessor for signedInfo attribute
* @return value of signedInfo attribute
*/
public SignedInfo getSignedInfo() {
return m_signedInfo;
}
/**
* Mutator for signedInfo attribute
* @param str new value for signedInfo attribute
* @throws DigiDocException for validation errors
*/
public void setSignedInfo(SignedInfo si)
throws DigiDocException
{
//ArrayList errs = si.validate();
//if(!errs.isEmpty())
// throw (DigiDocException)errs.get(0);
m_signedInfo = si;
}
/**
* Checks if this signature uses EC key
* @return true if EC signature
*/
public boolean isEllipticCurveSiganture()
{
return (m_signedInfo != null) && (m_signedInfo.getSignatureMethod() != null) &&
(m_signedInfo.getSignatureMethod().equals(SignedDoc.ECDSA_SHA1_SIGNATURE_METHOD) ||
m_signedInfo.getSignatureMethod().equals(SignedDoc.ECDSA_SHA224_SIGNATURE_METHOD) ||
m_signedInfo.getSignatureMethod().equals(SignedDoc.ECDSA_SHA256_SIGNATURE_METHOD) ||
m_signedInfo.getSignatureMethod().equals(SignedDoc.ECDSA_SHA384_SIGNATURE_METHOD) ||
m_signedInfo.getSignatureMethod().equals(SignedDoc.ECDSA_SHA512_SIGNATURE_METHOD));
}
/**
* Returnes true if alternate digest matches instead of the real one
* @return true if alternate digest matches instead of the real one
* @deprecated alternate digest is still calculated but it is no longer optional check.
* Error is allways egenrated if real digest doesn't match.
*/
@Deprecated
public boolean getAltDigestMatch() { return m_bAltDigMatch; }
/**
* Set flag to indicate that alternate digest matches instead of the real one
* @param b flag to indicate that alternate digest matches instead of the real one
*/
public void setAltDigestMatch(boolean b) { m_bAltDigMatch = b; }
/**
* Calculates the SignedInfo digest
* @return SignedInfo digest
*/
public byte[] calculateSignedInfoDigest()
throws DigiDocException
{
return m_signedInfo.calculateDigest();
}
/**
* Calculates the SignedInfo xml to sign
* @return SignedInfo xml
*/
public byte[] calculateSignedInfoXML()
throws DigiDocException
{
DigiDocXmlGenFactory genFac = new DigiDocXmlGenFactory(m_sigDoc);
return genFac.signedInfoToXML(this, m_signedInfo);
}
/**
* Returns HTTP_FROM value. This value is used
* as a http header during ocsp requests. It must be
* set before calling DigiDocGenFactory.finalizeSignature()
* @return HTTP_FROM value
*/
public String getHttpFrom() { return m_httpFrom; }
/**
* Sets HTTP_FROM value. This value is used
* as a http header during ocsp requests. It must be
* set before calling DigiDocGenFactory.finalizeSignature()
* @param s HTTP_FROM value
*/
public void setHttpFrom(String s) { m_httpFrom = s; }
/**
* Accessor for signatureValue attribute
* @return value of signatureValue attribute
*/
public SignatureValue getSignatureValue() {
return m_signatureValue;
}
/**
* Mutator for signatureValue attribute
* @param str new value for signatureValue attribute
* @throws DigiDocException for validation errors
*/
public void setSignatureValue(SignatureValue sv)
throws DigiDocException
{
//ArrayList errs = sv.validate();
//if(!errs.isEmpty())
// throw (DigiDocException)errs.get(0);
m_signatureValue = sv;
// VS: bug fix on 14.05.2008
m_origContent = null;
}
/**
* Creates a new SignatureValue object
* of this signature
* @param sigv signatures byte data
* @throws DigiDocException for validation errors
*/
public void setSignatureValue(byte[] sigv)
throws DigiDocException
{
SignatureValue sv = new SignatureValue(this, sigv);
setSignatureValue(sv);
}
/**
* Accessor for keyInfo attribute
* @return value of keyInfo attribute
*/
public KeyInfo getKeyInfo() {
return m_keyInfo;
}
/**
* Mutator for keyInfo attribute
* @param str new value for keyInfo attribute
* @throws DigiDocException for validation errors
*/
public void setKeyInfo(KeyInfo ki)
throws DigiDocException
{
//ArrayList errs = ki.validate();
//if(!errs.isEmpty())
// throw (DigiDocException)errs.get(0);
m_keyInfo = ki;
}
/**
* Accessor for signedProperties attribute
* @return value of signedProperties attribute
*/
public SignedProperties getSignedProperties() {
return m_sigProp;
}
/**
* Mutator for signedProperties attribute
* @param str new value for signedProperties attribute
* @throws DigiDocException for validation errors
*/
public void setSignedProperties(SignedProperties sp)
throws DigiDocException
{
//ArrayList errs = sp.validate();
//if(!errs.isEmpty())
// throw (DigiDocException)errs.get(0);
m_sigProp = sp;
}
/**
* Accessor for unsignedProperties attribute
* @return value of unsignedProperties attribute
*/
public UnsignedProperties getUnsignedProperties() {
return m_unsigProp;
}
/**
* Mutator for unsignedProperties attribute
* @param str new value for unsignedProperties attribute
* @throws DigiDocException for validation errors
*/
public void setUnsignedProperties(UnsignedProperties usp)
throws DigiDocException
{
//ArrayList errs = usp.validate();
//if(!errs.isEmpty())
// throw (DigiDocException)errs.get(0);
m_unsigProp = usp;
}
/**
* return the count of CertID objects
* @return count of CertID objects
*/
public int countCertIDs()
{
return ((m_certIds == null) ? 0 : m_certIds.size());
}
/**
* Adds a new CertID object
* @param cid new object to be added
*/
public void addCertID(CertID cid)
{
if(m_certIds == null)
m_certIds = new ArrayList();
cid.setSignature(this);
m_certIds.add(cid);
}
/**
* Retrieves CertID element with the desired index
* @param idx CertID index
* @return CertID element or null if not found
*/
public CertID getCertID(int idx)
{
if(m_certIds != null && idx < m_certIds.size()) {
return (CertID)m_certIds.get(idx);
}
return null; // not found
}
/**
* Retrieves the last CertID element
* @return CertID element or null if not found
*/
public CertID getLastCertId()
{
if(m_certIds != null && m_certIds.size() > 0) {
return (CertID)m_certIds.get(m_certIds.size()-1);
}
return null; // not found
}
/**
* Retrieves CertID element with the desired type
* @param type CertID type
* @return CertID element or null if not found
*/
public CertID getCertIdOfType(int type)
{
for(int i = 0; (m_certIds != null) && (i < m_certIds.size()); i++) {
CertID cid = (CertID)m_certIds.get(i);
if(cid.getType() == type)
return cid;
}
return null; // not found
}
/**
* Retrieves CertID element with the desired type.
* If not found creates a new one with this type.
* @param type CertID type
* @return CertID element
* @throws DigiDocException for validation errors
*/
public CertID getOrCreateCertIdOfType(int type)
throws DigiDocException
{
CertID cid = getCertIdOfType(type);
if(cid == null) {
cid = new CertID();
cid.setType(type);
addCertID(cid);
}
return cid; // not found
}
/**
* return the count of CertValue objects
* @return count of CertValues objects
*/
public int countCertValues()
{
return ((m_certValues == null) ? 0 : m_certValues.size());
}
/**
* Adds a new CertValue object
* @param cval new object to be added
*/
public void addCertValue(CertValue cval)
{
if(m_certValues == null)
m_certValues = new ArrayList();
cval.setSignature(this);
m_certValues.add(cval);
}
/**
* Retrieves CertValue element with the desired index
* @param idx CertValue index
* @return CertValue element or null if not found
*/
public CertValue getCertValue(int idx)
{
if(m_certValues != null && idx < m_certValues.size()) {
return (CertValue)m_certValues.get(idx);
} else
return null; // not found
}
/**
* Retrieves the last CertValue element
* @return CertValue element or null if not found
*/
public CertValue getLastCertValue()
{
if(m_certValues != null && m_certValues.size() > 0) {
return (CertValue)m_certValues.get(m_certValues.size()-1);
} else
return null; // not found
}
/**
* Retrieves CertValue element with the desired type
* @param type CertValue type
* @return CertValue element or null if not found
*/
public CertValue getCertValueOfType(int type)
{
for(int i = 0; (m_certValues != null) && (i < m_certValues.size()); i++) {
CertValue cval = (CertValue)m_certValues.get(i);
if(cval.getType() == type)
return cval;
}
return null; // not found
}
/**
* Retrieves CertValue element with the desired type.
* If not found creates a new one with this type.
* @param type CertValue type
* @return CertValue element
* @throws DigiDocException for validation errors
*/
public CertValue getOrCreateCertValueOfType(int type)
throws DigiDocException
{
CertValue cval = getCertValueOfType(type);
if(cval == null) {
cval = new CertValue();
cval.setType(type);
addCertValue(cval);
}
return cval; // not found
}
/**
* Returns the first CertValue with the given serial
* number that has been attached to this signature in
* digidoc document. This could be either the signers
* cert, OCSP responders cert or one of the TSA certs.
* @param serNo certificates serial number
* @return found CertValue or null
*/
public CertValue findCertValueWithSerial(BigInteger serNo)
{
for(int i = 0; (m_certValues != null) && (i < m_certValues.size()); i++) {
CertValue cval = (CertValue)m_certValues.get(i);
if(cval.getCert().getSerialNumber().equals(serNo))
return cval;
}
return null;
}
/**
* Retrieves OCSP respoinders certificate
* @return OCSP respoinders certificate
*/
public X509Certificate findResponderCert()
{
CertValue cval = getCertValueOfType(CertValue.CERTVAL_TYPE_RESPONDER);
if(cval != null)
return cval.getCert();
else
return null;
}
/**
* Retrieves TSA certificates
* @return TSA certificates
*/
public ArrayList findTSACerts()
{
ArrayList vec = new ArrayList();
for(int i = 0; (m_certValues != null) && (i < m_certValues.size()); i++) {
CertValue cval = (CertValue)m_certValues.get(i);
if(cval.getType() == CertValue.CERTVAL_TYPE_TSA)
vec.add(cval.getCert());
}
return vec;
}
/**
* return the count of TimestampInfo objects
* @return count of TimestampInfo objects
*/
public int countTimestampInfos()
{
return ((m_timestamps == null) ? 0 : m_timestamps.size());
}
/**
* Adds a new TimestampInfo object
* @param ts new object to be added
*/
public void addTimestampInfo(TimestampInfo ts)
{
if(m_timestamps == null)
m_timestamps = new ArrayList();
ts.setSignature(this);
m_timestamps.add(ts);
}
/**
* Retrieves TimestampInfo element with the desired index
* @param idx TimestampInfo index
* @return TimestampInfo element or null if not found
*/
public TimestampInfo getTimestampInfo(int idx)
{
if(m_timestamps != null && idx < m_timestamps.size()) {
return (TimestampInfo)m_timestamps.get(idx);
} else
return null; // not found
}
/**
* Retrieves the last TimestampInfo element
* @return TimestampInfo element or null if not found
*/
public TimestampInfo getLastTimestampInfo()
{
if(m_timestamps != null && m_timestamps.size() > 0) {
return (TimestampInfo)m_timestamps.get(m_timestamps.size()-1);
} else
return null; // not found
}
/**
* Retrieves TimestampInfo element with the desired type
* @param type TimestampInfo type
* @return TimestampInfo element or null if not found
*/
public TimestampInfo getTimestampInfoOfType(int type)
{
for(int i = 0; (m_timestamps != null) && (i < m_timestamps.size()); i++) {
TimestampInfo ts = (TimestampInfo)m_timestamps.get(i);
if(ts.getType() == type)
return ts;
}
return null; // not found
}
/**
* Retrieves TimestampInfo element with the desired type.
* If not found creates a new one with this type.
* @param type TimestampInfo type
* @return TimestampInfo element
* @throws DigiDocException for validation errors
*/
public TimestampInfo getOrCreateTimestampInfoOfType(int type)
throws DigiDocException
{
TimestampInfo ts = getTimestampInfoOfType(type);
if(ts == null) {
ts = new TimestampInfo();
ts.setType(type);
addTimestampInfo(ts);
}
return ts; // not found
}
/**
* Accessor for path attribute
* @return value of path attribute
*/
public String getPath()
{
return m_path;
}
/**
* Mutator for path attribute
* @param s new value for path attribute
*/
public void setPath(String s)
{
m_path = s;
}
/**
* Accessor for profile attribute
* @return value of profile attribute
*/
public String getProfile()
{
return m_profile;
}
/**
* Mutator for profile attribute
* @param s new value for profile attribute
*/
public void setProfile(String s)
{
m_profile = s;
}
/**
* Accessor for comment attribute
* @return value of comment attribute
*/
public String getComment()
{
return m_comment;
}
/**
* Mutator for comment attribute
* @param s new value for comment attribute
*/
public void setComment(String s)
{
m_comment = s;
}
/**
* Gets confirmation and adds the corresponding
* members that carry the returned info to
* this signature
* @throws DigiDocException for all errors
*/
public void getConfirmation()
throws DigiDocException
{
if(m_sigDoc.getFormat().equals(SignedDoc.FORMAT_DIGIDOC_XML) ||
m_sigDoc.getFormat().equals(SignedDoc.FORMAT_SK_XML)) {
if(m_profile == null || m_profile.equalsIgnoreCase(SignedDoc.BDOC_PROFILE_TM)) {
DigiDocGenFactory.finalizeXadesT(m_sigDoc, this);
DigiDocGenFactory.finalizeXadesC(m_sigDoc, this);
DigiDocGenFactory.finalizeXadesXL_TM(m_sigDoc, this);
}
} else {
String profile = m_profile;
if(profile == null)
profile = m_sigDoc.getProfile();
if(profile == null || profile.trim().length() == 0)
profile = ConfigManager.instance().getStringProperty("DIGIDOC_DEFAULT_PROFILE", SignedDoc.BDOC_PROFILE_TM);
DigiDocGenFactory.finalizeSignature(m_sigDoc, this, m_signatureValue.getValue(), profile);
}
// reset original content since we just added to confirmation
if(m_origContent != null) {
DigiDocXmlGenFactory genFac = new DigiDocXmlGenFactory(m_sigDoc);
String str = new String(m_origContent);
int idx1 = str.indexOf("");
if(idx1 != -1) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(m_origContent, 0, idx1);
bos.write("".getBytes());
bos.write(genFac.unsignedPropertiesToXML(this, m_unsigProp));
bos.write("".getBytes());
m_origContent = bos.toByteArray();
} catch(java.io.IOException ex) {
DigiDocException.handleException(ex, DigiDocException.ERR_OCSP_GET_CONF);
}
}
}
}
/**
* Checks if this signature defines that if complies with bdoc 2.0 nonce
* @return true if this signature defines bdoc 2.0 nonce policy compliance
*/
public boolean hasBdoc2NoncePolicy()
{
if(m_sigProp != null &&
m_sigProp.getSignaturePolicyIdentifier() != null &&
m_sigProp.getSignaturePolicyIdentifier().getSignaturePolicyId() != null &&
m_sigProp.getSignaturePolicyIdentifier().getSignaturePolicyId().getSigPolicyId() != null &&
m_sigProp.getSignaturePolicyIdentifier().getSignaturePolicyId().getSigPolicyId().getIdentifier() != null) {
Identifier id = m_sigProp.getSignaturePolicyIdentifier().getSignaturePolicyId().getSigPolicyId().getIdentifier();
if(id.getQualifier().equals(Identifier.OIDAsURN) &&
id.getUri().equals(Identifier.BDOC_210_OID))
return true;
}
return false;
}
/**
* Verifies and validates this signature. Returns a list of both
* validation and verification errors.
* @param lerrs list to be filled with DigiDocException objects
* @return true if signature is ok
*/
public boolean verify(SignedDoc sdoc, ArrayList lerrs)
{
boolean bOk = true;
// validation
ArrayList lerrs1 = validate();
if(lerrs1 != null && lerrs1.size() > 0) {
bOk = false;
if(lerrs != null)
lerrs.addAll(lerrs1);
}
// verification
lerrs1 = new ArrayList();
boolean bOk1 = DigiDocVerifyFactory.verifySignature(sdoc, this, lerrs1);
if(!bOk1) bOk = false;
if(lerrs1 != null && lerrs1.size() > 0) {
bOk = false;
if(lerrs != null)
lerrs.addAll(lerrs1);
}
return bOk;
}
/**
* Verifies this signature
* @param sdoc parent doc object
* @param checkDate Date on which to check the signature validity
* @param demandConfirmation true if you demand OCSP confirmation from
* every signature
* @return a possibly empty list of DigiDocException objects
*/
public ArrayList verify(SignedDoc sdoc, boolean checkDate, boolean demandConfirmation)
{
Date do1 = null, dt1 = null, dt2 = null;
ArrayList lerrs = new ArrayList();
boolean bOk = DigiDocVerifyFactory.verifySignature(sdoc, this, lerrs);
return lerrs;
}
/**
* Helper method to validate the whole
* Signature object
* @return a possibly empty list of DigiDocException objects
*/
public ArrayList validate()
{
ArrayList errs = new ArrayList();
DigiDocException ex = validateId(m_id);
if(ex != null)
errs.add(ex);
ArrayList e = null;
if(m_signedInfo != null) {
e = m_signedInfo.validate();
} else {
errs.add(new DigiDocException(DigiDocException.ERR_PARSE_XML, "Missing SignedInfo element", null));
}
if(e != null && !e.isEmpty())
errs.addAll(e);
if(m_signatureValue != null)
e = m_signatureValue.validate();
if(e != null && !e.isEmpty())
errs.addAll(e);
if(m_keyInfo != null) {
e = m_keyInfo.validate();
} else {
errs.add(new DigiDocException(DigiDocException.ERR_PARSE_XML, "Missing KeyInfo element", null));
}
if(e != null && !e.isEmpty())
errs.addAll(e);
if(m_sigProp != null) {
e = m_sigProp.validate();
if(!e.isEmpty())
errs.addAll(e);
}
if(m_unsigProp != null) {
e = m_unsigProp.validate();
if(!e.isEmpty())
errs.addAll(e);
}
return errs;
}
/** returns QualifyingProperties object */
public QualifyingProperties getQualifyingProperties() {
return m_qualProp;
}
/** sets QualifyingProperties object */
public void setQualifyingProperties(QualifyingProperties prop) {
m_qualProp = prop;
}
/**
* Accessor for signedInfo attribute
* @return value of signedInfo attribute
*/
public String getSubject() {
return m_keyInfo.getSubjectFirstName() + " " + m_keyInfo.getSubjectLastName() + " " + m_keyInfo.getSubjectPersonalCode();
}
public ArrayList getErrors() { return m_errs; }
public void setErrors(ArrayList l) { m_errs = l; }
public String getStatus() {
if(m_errs == null || m_errs.size() == 0) {
if(m_signatureValue != null && m_signatureValue.getValue() != null)
return "OK";
else
return "INCOMPLETE";
} else
return "ERROR";
}
/**
* Retrieves the signature production timestamp from first ocsp response
* @return ocsp response produced-at time if exists
*/
public Date getSignatureProducedAtTime()
{
if(m_unsigProp != null) {
Notary not = m_unsigProp.getNotary();
if(not != null)
return not.getProducedAt();
}
return null;
}
/**
* Converts Signature object to String representation
* mainly for debugging purposes
*/
public String toString()
{
try {
DigiDocXmlGenFactory genFac = new DigiDocXmlGenFactory(m_sigDoc);
return new String(genFac.signatureToXML(this), "UTF-8");
} catch(Exception ex) {
m_logger.error("Error converting Signature to string: " + ex);
}
return null;
}
}