diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java index c59e1d2ea..491f2ef0a 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java @@ -2532,79 +2532,74 @@ public class SolrInformationServer implements InformationServer { long start = System.nanoTime(); - // Expensive call to be done with ContentTracker - GetTextContentResponse response = repositoryClient.getTextContent(dbId, propertyQName, null); - - addContentPropertyMetadata(doc, propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_STATUS, - response); - addContentPropertyMetadata(doc, propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_EXCEPTION, - response); - addContentPropertyMetadata(doc, propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_TIME, - response); - - InputStream ris = response.getContent(); - if (Objects.equals(response.getContentEncoding(), "gzip")) - { - ris = new GZIPInputStream(ris); - } - String textContent = ""; - try + try (GetTextContentResponse response = repositoryClient.getTextContent(dbId, propertyQName, null)) { + // Expensive call to be done with ContentTracker + + addContentPropertyMetadata(doc, propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_STATUS, + response); + addContentPropertyMetadata(doc, propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_EXCEPTION, + response); + addContentPropertyMetadata(doc, propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_TIME, + response); + + InputStream ris = response.getContent(); + if (Objects.equals(response.getContentEncoding(), "gzip")) + { + ris = new GZIPInputStream(ris); + } + String textContent = ""; + if (ris != null) { // Get and copy content byte[] bytes = FileCopyUtils.copyToByteArray(new BoundedInputStream(ris, contentStreamLimit)); textContent = new String(bytes, StandardCharsets.UTF_8); } - } - finally - { - // release the response only when the content has been read - response.release(); - } - if(minHash && textContent.length() > 0) - { - Analyzer analyzer = core.getLatestSchema().getFieldType("min_hash").getIndexAnalyzer(); - TokenStream ts = analyzer.tokenStream("min_hash", textContent); - CharTermAttribute termAttribute = ts.getAttribute(CharTermAttribute.class); - ts.reset(); - while (ts.incrementToken()) + if(minHash && textContent.length() > 0) { - StringBuilder tokenBuff = new StringBuilder(); - char[] buff = termAttribute.buffer(); - - for(int i=0; i. - * #L% - */ +/* + * #%L + * Alfresco Solr Client + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco 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 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + * #L% + */ package org.alfresco.solr.client; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.httpclient.AlfrescoHttpClient; -import org.alfresco.httpclient.AuthenticationException; -import org.alfresco.httpclient.GetRequest; -import org.alfresco.httpclient.PostRequest; -import org.alfresco.httpclient.Response; -import org.alfresco.repo.dictionary.M2Model; -import org.alfresco.repo.dictionary.NamespaceDAO; -import org.alfresco.repo.index.shard.ShardState; -import org.alfresco.repo.tenant.TenantService; -import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.dictionary.PropertyDefinition; -import org.alfresco.service.cmr.repository.AssociationRef; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.MLText; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.Path; -import org.alfresco.service.cmr.repository.Path.AttributeElement; -import org.alfresco.service.cmr.repository.Path.ChildAssocElement; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; -import org.alfresco.service.cmr.repository.datatype.TypeConversionException; -import org.alfresco.service.cmr.repository.datatype.TypeConverter; -import org.alfresco.service.cmr.repository.datatype.TypeConverter.Converter; -import org.alfresco.service.namespace.QName; -import org.alfresco.util.ISO8601DateFormat; -import org.alfresco.util.Pair; -import org.apache.commons.codec.EncoderException; -import org.apache.commons.codec.net.URLCodec; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.util.DateUtil; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONTokener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.extensions.surf.util.URLEncoder; - -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.httpclient.AlfrescoHttpClient; +import org.alfresco.httpclient.AuthenticationException; +import org.alfresco.httpclient.GetRequest; +import org.alfresco.httpclient.PostRequest; +import org.alfresco.httpclient.Response; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.NamespaceDAO; +import org.alfresco.repo.index.shard.ShardState; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.MLText; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.Path.AttributeElement; +import org.alfresco.service.cmr.repository.Path.ChildAssocElement; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; +import org.alfresco.service.cmr.repository.datatype.TypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConverter.Converter; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO8601DateFormat; +import org.alfresco.util.Pair; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.net.URLCodec; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.util.DateUtil; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.extensions.surf.util.URLEncoder; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; // TODO error handling, including dealing with a repository that is not responsive (ConnectException in sendRemoteRequest) @@ -103,8 +103,8 @@ public class SOLRAPIClient private static final String GET_NODES_URL = "api/solr/nodes"; private static final String GET_CONTENT = "api/solr/textContent"; private static final String GET_MODEL = "api/solr/model"; - private static final String GET_MODELS_DIFF = "api/solr/modelsdiff"; - private static final String GET_NEXT_TX_COMMIT_TIME = "api/solr/nextTransaction"; + private static final String GET_MODELS_DIFF = "api/solr/modelsdiff"; + private static final String GET_NEXT_TX_COMMIT_TIME = "api/solr/nextTransaction"; private static final String GET_TX_INTERVAL_COMMIT_TIME = "api/solr/transactionInterval"; private static final String CHECKSUM_HEADER = "XAlfresco-modelChecksum"; @@ -113,33 +113,33 @@ public class SOLRAPIClient private SOLRDeserializer deserializer; private DictionaryService dictionaryService; private JsonFactory jsonFactory; - private NamespaceDAO namespaceDAO; - - /** - * This option enables ("Accept-Encoding": "gzip") header for compression - * in GET_CONTENT requests. Additional configuration is required in - * Alfresco Repository Tomcat Connector or HTTP Web Proxy to deal - * with compressed requests. - */ - private boolean compression; + private NamespaceDAO namespaceDAO; + + /** + * This option enables ("Accept-Encoding": "gzip") header for compression + * in GET_CONTENT requests. Additional configuration is required in + * Alfresco Repository Tomcat Connector or HTTP Web Proxy to deal + * with compressed requests. + */ + private boolean compression; - public SOLRAPIClient(AlfrescoHttpClient repositoryHttpClient, - DictionaryService dictionaryService, - NamespaceDAO namespaceDAO) - { - this(repositoryHttpClient, dictionaryService, namespaceDAO, false); - } - public SOLRAPIClient(AlfrescoHttpClient repositoryHttpClient, DictionaryService dictionaryService, - NamespaceDAO namespaceDAO, + NamespaceDAO namespaceDAO) + { + this(repositoryHttpClient, dictionaryService, namespaceDAO, false); + } + + public SOLRAPIClient(AlfrescoHttpClient repositoryHttpClient, + DictionaryService dictionaryService, + NamespaceDAO namespaceDAO, boolean compression) { this.repositoryHttpClient = repositoryHttpClient; this.dictionaryService = dictionaryService; this.namespaceDAO = namespaceDAO; this.deserializer = new SOLRDeserializer(namespaceDAO); - this.jsonFactory = new JsonFactory(); + this.jsonFactory = new JsonFactory(); this.compression = compression; } @@ -420,7 +420,7 @@ public class SOLRAPIClient } public Transactions getTransactions(Long fromCommitTime, Long minTxnId, Long toCommitTime, Long maxTxnId, int maxResults, ShardState shardState) throws AuthenticationException, IOException, JSONException, EncoderException - { + { log.debug("### get transactions ###"); URLCodec encoder = new URLCodec(); @@ -447,7 +447,7 @@ public class SOLRAPIClient args.append(args.length() == 0 ? "?" : "&").append("maxResults").append("=").append(maxResults); } if(shardState != null) - { + { log.debug("### Shard state exists ###"); args.append(args.length() == 0 ? "?" : "&"); args.append(encoder.encode("baseUrl")).append("=").append(encoder.encode(shardState.getShardInstance().getBaseUrl())); @@ -560,7 +560,7 @@ public class SOLRAPIClient } finally - { + { log.debug("## end getTransactions"); if(response != null) { @@ -625,16 +625,16 @@ public class SOLRAPIClient } body.put("maxResults", maxResults); - - if(parameters.getShardProperty() != null) - { - body.put("shardProperty", parameters.getShardProperty().toString()); - } - - if (parameters.getCoreName() != null){ - body.put("coreName", parameters.getCoreName()); - } - + + if(parameters.getShardProperty() != null) + { + body.put("shardProperty", parameters.getShardProperty().toString()); + } + + if (parameters.getCoreName() != null){ + body.put("coreName", parameters.getCoreName()); + } + PostRequest req = new PostRequest(url.toString(), body.toString(), "application/json"); @@ -688,16 +688,16 @@ public class SOLRAPIClient if(jsonNodeInfo.has("aclId")) { nodeInfo.setAclId(jsonNodeInfo.getLong("aclId")); - } - - if(jsonNodeInfo.has("shardPropertyValue")) - { - nodeInfo.setShardPropertyValue(jsonNodeInfo.getString("shardPropertyValue")); - } - - if(jsonNodeInfo.has("explicitShardId")) - { - nodeInfo.setExplicitShardId(jsonNodeInfo.getInt("explicitShardId")); + } + + if(jsonNodeInfo.has("shardPropertyValue")) + { + nodeInfo.setShardPropertyValue(jsonNodeInfo.getString("shardPropertyValue")); + } + + if(jsonNodeInfo.has("explicitShardId")) + { + nodeInfo.setExplicitShardId(jsonNodeInfo.getInt("explicitShardId")); } if(jsonNodeInfo.has("tenant")) @@ -972,21 +972,21 @@ public class SOLRAPIClient if(jsonNodeInfo.has("paths")) { JSONArray jsonPaths = jsonNodeInfo.getJSONArray("paths"); - List> paths = new ArrayList>(jsonPaths.length()); + List> paths = new ArrayList>(jsonPaths.length()); List ancestorPaths = new ArrayList(); for(int j = 0; j < jsonPaths.length(); j++) { JSONObject path = jsonPaths.getJSONObject(j); String pathValue = path.getString("path"); QName qname = path.has("qname") ? deserializer.deserializeValue(QName.class, path.getString("qname")) : null; - paths.add(new Pair(pathValue, qname)); - if(path.has("apath")) - { - String ancestorPath = path.getString("apath"); - ancestorPaths.add(ancestorPath); + paths.add(new Pair(pathValue, qname)); + if(path.has("apath")) + { + String ancestorPath = path.getString("apath"); + ancestorPaths.add(ancestorPath); } } - metaData.setPaths(paths); + metaData.setPaths(paths); metaData.setAncestorPaths(ancestorPaths); } @@ -1101,8 +1101,7 @@ public class SOLRAPIClient return nodes; } - public GetTextContentResponse getTextContent(Long nodeId, QName propertyQName, Long modifiedSince) throws AuthenticationException, IOException - { + public GetTextContentResponse getTextContent(Long nodeId, QName propertyQName, Long modifiedSince) throws AuthenticationException, IOException { StringBuilder url = new StringBuilder(128); url.append(GET_CONTENT); @@ -1137,24 +1136,26 @@ public class SOLRAPIClient GetRequest req = new GetRequest(url.toString()); - Map headers = new HashMap<>(); + Map headers = new HashMap<>(); if(modifiedSince != null) { headers.put("If-Modified-Since", String.valueOf(DateUtil.formatDate(new Date(modifiedSince)))); } - if (compression) - { - headers.put("Accept-Encoding", "gzip"); - } - req.setHeaders(headers); - + if (compression) + { + headers.put("Accept-Encoding", "gzip"); + } + req.setHeaders(headers); + Response response = repositoryHttpClient.sendRequest(req); if(response.getStatus() != Status.STATUS_NOT_MODIFIED && response.getStatus() != Status.STATUS_NO_CONTENT && response.getStatus() != Status.STATUS_OK) { - throw new AlfrescoRuntimeException("GetTextContentResponse return status is " + response.getStatus()); - } - + int status = response.getStatus(); + response.release(); + throw new AlfrescoRuntimeException("GetTextContentResponse return status is " + status); + } + return new GetTextContentResponse(response); } @@ -1251,99 +1252,99 @@ public class SOLRAPIClient } return diffs; - } - - /** - * Returns the next commit time from a given commit time. - * - * @param coreName alfresco, archive - * @param fromCommitTime initial transaction commit time - * @return Time of the next transaction - * @throws IOException - * @throws AuthenticationException - * @throws NoSuchMethodException - */ - public Long getNextTxCommitTime(String coreName, Long fromCommitTime) throws AuthenticationException, IOException, NoSuchMethodException - { - StringBuilder url = new StringBuilder(GET_NEXT_TX_COMMIT_TIME); - url.append("?").append("fromCommitTime").append("=").append(fromCommitTime); - GetRequest get = new GetRequest(url.toString()); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(get); - if (response.getStatus() != HttpStatus.SC_OK) - { - throw new NoSuchMethodException(coreName + " - GetNextTxCommitTime return status is " - + response.getStatus() + " when invoking " + url); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if (response != null) - { - response.release(); - } - } - if (log.isDebugEnabled()) - { - log.debug(json.toString()); - } - - return Long.parseLong(json.get("nextTransactionCommitTimeMs").toString()); - } - - /** - * Returns the minimum and the maximum commit time for transactions in a node id range. - * - * @param coreName alfresco, archive - * @param fromNodeId Id of the initial node - * @param toNodeId Id of the final node - * @return Time of the first transaction, time of the last transaction - * @throws IOException - * @throws AuthenticationException - * @throws NoSuchMethodException - */ - public Pair getTxIntervalCommitTime(String coreName, Long fromNodeId, Long toNodeId) - throws AuthenticationException, IOException, NoSuchMethodException - { - StringBuilder url = new StringBuilder(GET_TX_INTERVAL_COMMIT_TIME); - url.append("?").append("fromNodeId").append("=").append(fromNodeId); - url.append("&").append("toNodeId").append("=").append(toNodeId); - GetRequest get = new GetRequest(url.toString()); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(get); - if (response.getStatus() != HttpStatus.SC_OK) - { - throw new NoSuchMethodException(coreName + " - GetTxIntervalCommitTime return status is " - + response.getStatus() + " when invoking " + url); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if (response != null) - { - response.release(); - } - } - if (log.isDebugEnabled()) - { - log.debug(json.toString()); - } - - return new Pair(Long.parseLong(json.get("minTransactionCommitTimeMs").toString()), - Long.parseLong(json.get("maxTransactionCommitTimeMs").toString())); - } + } + + /** + * Returns the next commit time from a given commit time. + * + * @param coreName alfresco, archive + * @param fromCommitTime initial transaction commit time + * @return Time of the next transaction + * @throws IOException + * @throws AuthenticationException + * @throws NoSuchMethodException + */ + public Long getNextTxCommitTime(String coreName, Long fromCommitTime) throws AuthenticationException, IOException, NoSuchMethodException + { + StringBuilder url = new StringBuilder(GET_NEXT_TX_COMMIT_TIME); + url.append("?").append("fromCommitTime").append("=").append(fromCommitTime); + GetRequest get = new GetRequest(url.toString()); + Response response = null; + JSONObject json = null; + try + { + response = repositoryHttpClient.sendRequest(get); + if (response.getStatus() != HttpStatus.SC_OK) + { + throw new NoSuchMethodException(coreName + " - GetNextTxCommitTime return status is " + + response.getStatus() + " when invoking " + url); + } + + Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); + json = new JSONObject(new JSONTokener(reader)); + } + finally + { + if (response != null) + { + response.release(); + } + } + if (log.isDebugEnabled()) + { + log.debug(json.toString()); + } + + return Long.parseLong(json.get("nextTransactionCommitTimeMs").toString()); + } + + /** + * Returns the minimum and the maximum commit time for transactions in a node id range. + * + * @param coreName alfresco, archive + * @param fromNodeId Id of the initial node + * @param toNodeId Id of the final node + * @return Time of the first transaction, time of the last transaction + * @throws IOException + * @throws AuthenticationException + * @throws NoSuchMethodException + */ + public Pair getTxIntervalCommitTime(String coreName, Long fromNodeId, Long toNodeId) + throws AuthenticationException, IOException, NoSuchMethodException + { + StringBuilder url = new StringBuilder(GET_TX_INTERVAL_COMMIT_TIME); + url.append("?").append("fromNodeId").append("=").append(fromNodeId); + url.append("&").append("toNodeId").append("=").append(toNodeId); + GetRequest get = new GetRequest(url.toString()); + Response response = null; + JSONObject json = null; + try + { + response = repositoryHttpClient.sendRequest(get); + if (response.getStatus() != HttpStatus.SC_OK) + { + throw new NoSuchMethodException(coreName + " - GetTxIntervalCommitTime return status is " + + response.getStatus() + " when invoking " + url); + } + + Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); + json = new JSONObject(new JSONTokener(reader)); + } + finally + { + if (response != null) + { + response.release(); + } + } + if (log.isDebugEnabled()) + { + log.debug(json.toString()); + } + + return new Pair(Long.parseLong(json.get("minTransactionCommitTimeMs").toString()), + Long.parseLong(json.get("maxTransactionCommitTimeMs").toString())); + } /* * type conversions from serialized JSON values to SOLR-consumable objects @@ -1363,13 +1364,13 @@ public class SOLRAPIClient this.namespaceDAO = namespaceDAO; // add all default converters to this converter - for (Entry, Map, Converter>> source : DefaultTypeConverter.INSTANCE.getConverters().entrySet()) - { - for (Entry, Converter> dest : source.getValue().entrySet()) - { - instance.addConverter((Class) source.getKey(), (Class) dest.getKey(), dest.getValue()); - } - } + for (Entry, Map, Converter>> source : DefaultTypeConverter.INSTANCE.getConverters().entrySet()) + { + for (Entry, Converter> dest : source.getValue().entrySet()) + { + instance.addConverter((Class) source.getKey(), (Class) dest.getKey(), dest.getValue()); + } + } // dates instance.addConverter(String.class, Date.class, new TypeConverter.Converter() @@ -1378,7 +1379,7 @@ public class SOLRAPIClient { try { - return ISO8601DateFormat.parse(source); + return ISO8601DateFormat.parse(source); } catch (Exception e) { @@ -1590,13 +1591,13 @@ public class SOLRAPIClient } // TODO register a stream close listener that release the response when the response has been read - public static class GetTextContentResponse extends SOLRResponse + public static class GetTextContentResponse extends SOLRResponse implements AutoCloseable { private InputStream content; private SolrApiContentStatus status; private String transformException; private String transformStatusStr; - private Long transformDuration; + private Long transformDuration; private String contentEncoding; public GetTextContentResponse(Response response) throws IOException @@ -1607,7 +1608,7 @@ public class SOLRAPIClient this.transformStatusStr = response.getHeader("X-Alfresco-transformStatus"); this.transformException = response.getHeader("X-Alfresco-transformException"); String tmp = response.getHeader("X-Alfresco-transformDuration"); - this.transformDuration = (tmp != null ? Long.valueOf(tmp) : null); + this.transformDuration = (tmp != null ? Long.valueOf(tmp) : null); this.contentEncoding = response.getHeader("Content-Encoding"); setStatus(); } @@ -1674,11 +1675,16 @@ public class SOLRAPIClient public Long getTransformDuration() { return transformDuration; - } - - public String getContentEncoding() - { - return contentEncoding; + } + + public String getContentEncoding() + { + return contentEncoding; + } + + @Override + public void close() { + response.release(); } }