diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 552c1d0..2d87202 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -77,10 +77,9 @@
android:name="ca.dracode.ais.service.SearchService"
android:exported="true"
android:label="@string/app_name"
- android:process=":search"
+ android:process=":index"
android:permission="ca.dracode.permission.AIS_SEARCH">
-
diff --git a/pom.xml b/pom.xml
index 48dc4f3..ebc1784 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,7 +110,6 @@
package
true
-
my-release-key.keystore
@@ -141,7 +140,9 @@
${project.build.directory}/${project.artifactId}-${project.version}-RELEASE.apk
-
+
+ false
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8484f43..8bbb37d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -32,4 +32,5 @@
Allows the app to search content stored in the Lucene
index created by the Android Indexing Service. All data stored in the index will be
available to the service.
+ Connect to AIS Indexer
diff --git a/src/ca/dracode/ais/indexclient/MClientService.aidl b/src/ca/dracode/ais/indexclient/MClientService.aidl
index 2d59a18..64af49e 100644
--- a/src/ca/dracode/ais/indexclient/MClientService.aidl
+++ b/src/ca/dracode/ais/indexclient/MClientService.aidl
@@ -1,14 +1,24 @@
-
+/**
+ * Copyright 2014 Benjamin Winger
+ *
+ * This file is part of The Android Indexing Service Client Library.
+ *
+ * The Android Indexing Service Client 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Android Indexing Service Client 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 The Android Indexing Service Client Library. If not, see .
+ */
package ca.dracode.ais.indexclient;
-/*
- * MClientService.aidl
- *
- * Service interface file implemented by the client application that allows the
- * index service to parse files using the client application
- *
- */
interface MClientService {
/**
* Load libraries to access the file here so that it only has to be done once.
@@ -23,11 +33,11 @@ interface MClientService {
* @param page - the page to be returned from the file
* @return - A string containing all of the words on the page
*/
- String getWordsForPage(int page);
+ String getWordsForPage(int page, String path);
/**
- * Gets the number of pages in the file
+ * Gets the number of pages in the file previously specified in loadFile(String path)
* @return - the number of pages in the file specified at loadFile(String path)
*/
- int getPageCount();
+ int getPageCount(String path);
}
diff --git a/src/ca/dracode/ais/indexdata/SearchResult.java b/src/ca/dracode/ais/indexdata/SearchResult.java
index d84dd09..d520ee2 100644
--- a/src/ca/dracode/ais/indexdata/SearchResult.java
+++ b/src/ca/dracode/ais/indexdata/SearchResult.java
@@ -83,7 +83,7 @@ public LinkedHashMap> getFirstResult(){
*/
public LinkedHashMap> getResultAtIndex(int index){
if(this.results.size() > 0)
- return (LinkedHashMap>)this.results.entrySet().toArray()[index];
+ return (LinkedHashMap>)(this.results.values().toArray()[index]);
else return null;
}
diff --git a/src/ca/dracode/ais/indexer/FileIndexer.java b/src/ca/dracode/ais/indexer/FileIndexer.java
index 27633a5..2bd7dc1 100644
--- a/src/ca/dracode/ais/indexer/FileIndexer.java
+++ b/src/ca/dracode/ais/indexer/FileIndexer.java
@@ -104,7 +104,7 @@ public static void Build(IndexWriter writer, File file, int page,
writer.updateDocument(new Term("id", file.getPath() + ":"
+ page), doc);
}
- //Log.i(TAG, "Done Indexing file: " + file.getName() + " " + page);
+ Log.i(TAG, "Done Indexing file: " + file.getName() + " " + page);
} catch(Exception e) {
Log.e(TAG, "Error ", e);
}
@@ -228,7 +228,7 @@ public int buildIndex(String filename, int pages) {
writer.updateDocument(new Term("id", file.getPath() + ":meta"),
doc);
}
- Log.i(TAG, "Done creating metadata for file " + filename);
+ //Log.i(TAG, "Done creating metadata for file " + filename);
// Must only call ForceMerge and Commit once per document as they are very resource heavy operations
writer.commit();
} catch(Exception e) {
@@ -271,10 +271,12 @@ public int buildIndex(List contents, File file) {
*/
public void close() {
try {
- writer.commit();
- // TODO - Determine how much of a speed increase is gained while searching after ForceMerge
- writer.forceMerge(1);
- writer.close();
+ if(writer != null) {
+ writer.commit();
+ // TODO - Determine how much of a speed increase is gained while searching after ForceMerge
+ writer.forceMerge(1);
+ writer.close();
+ }
} catch(IOException e) {
Log.e(TAG, "Error while closing indexwriter", e);
}
diff --git a/src/ca/dracode/ais/indexer/FileSearcher.java b/src/ca/dracode/ais/indexer/FileSearcher.java
index d652f17..3716256 100644
--- a/src/ca/dracode/ais/indexer/FileSearcher.java
+++ b/src/ca/dracode/ais/indexer/FileSearcher.java
@@ -272,11 +272,13 @@ public SearchResult findInFiles(int id, String term, String field,
public SearchResult findInFile(int id, String term, String field, String constrainValue,
String constrainField, int maxResults, int set, int type,
final int page) {
+
+ Query qry = this.getQuery(term, field, type);
+ Log.i(TAG, "Query: " + term + " " + field + " " + type + " " + constrainValue);
if(this.interrupt == id) {
this.interrupt = -1;
return null;
}
- Query qry = this.getQuery(term, field, type);
if(qry != null){
String[] values = {constrainValue};
Filter filter;
@@ -301,8 +303,8 @@ public SearchResult findInFile(int id, String term, String field, String constra
filter = this.getFilter(constrainField, Arrays.asList(values), type, 0,
page - 1);
hits = concat(hits, indexSearcher.search(qry, filter,
- maxResults * set + maxResults -hits.length,
- new Sort(new SortField("page", SortField.Type.INT))).scoreDocs);
+ maxResults,
+ sort).scoreDocs);
}
} else {
sort = new Sort(new SortField("page", SortField.Type.INT, true));
@@ -314,7 +316,7 @@ public SearchResult findInFile(int id, String term, String field, String constra
filter = this.getFilter(constrainField, Arrays.asList(values), type, page,
Integer.MAX_VALUE);
hits = concat(hits, indexSearcher.search(qry, filter,
- maxResults * -(set + 1) + maxResults - hits.length,
+ maxResults - hits.length,
sort).scoreDocs);
} else {
ScoreDoc[] tmp = hits;
@@ -364,8 +366,18 @@ private Query getQuery(String term, String field, int type) {
Query qry = null;
if(type == FileSearcher.QUERY_BOOLEAN) {
qry = new BooleanQuery();
- ((BooleanQuery) qry).add(new WildcardQuery(new Term(field, "*" + term + "*")),
+ String[] words = term.split(" ");
+ ((BooleanQuery) qry).add(new WildcardQuery(new Term(field, "*" + words[0])),
BooleanClause.Occur.MUST);
+ if(words.length > 1) {
+ for(int i = 1; i < words.length - 1; i++) {
+ ((BooleanQuery) qry).add(new WildcardQuery(new Term(field, words[i])),
+ BooleanClause.Occur.MUST);
+ }
+ ((BooleanQuery) qry).add(new WildcardQuery(new Term(field,
+ words[words.length - 1] + "*")),
+ BooleanClause.Occur.MUST);
+ }
} else if(type == FileSearcher.QUERY_STANDARD) {
try {
qry = new QueryParser(Version.LUCENE_47, field,
@@ -387,9 +399,14 @@ private Filter getFilter(String constrainField, List constrainValues,
int type, int startPage,
int endPage){
BooleanQuery cqry = new BooleanQuery();
- for(String s : constrainValues) {
- cqry.add(new TermQuery(new Term(constrainField, s)),
- BooleanClause.Occur.SHOULD);
+ if(constrainValues.size() == 1){
+ cqry.add(new TermQuery(new Term(constrainField, constrainValues.get(0))),
+ BooleanClause.Occur.MUST);
+ } else {
+ for(String s : constrainValues) {
+ cqry.add(new TermQuery(new Term(constrainField, s)),
+ BooleanClause.Occur.SHOULD);
+ }
}
if(type == FileSearcher.QUERY_BOOLEAN && startPage != -1 && endPage != -1) {
cqry.add(NumericRangeQuery.newIntRange("page", startPage, endPage, true, true),
@@ -410,7 +427,7 @@ private Filter getFilter(String constrainField, List constrainValues,
* @return A SearchResult containing the results sorted by relevance and page
*/
private SearchResult getHighlightedResults(List docs, Query qry, int type,
- String term, int maxResults){
+ String term, int maxResults){
try {
int numResults = 0;
LinkedHashMap>> results = new LinkedHashMap>>();
@@ -469,9 +486,11 @@ private SearchResult getHighlightedResults(List docs, Query qry, int t
*/
private ArrayList getDocs(int maxResults, int set, ScoreDoc[] hits){
ArrayList docs = new ArrayList();
- if(set > 0) {
- for(int i = maxResults * set; i < hits.length && i < (maxResults * set +
- maxResults);
+ int max = maxResults;
+ if(max > hits.length)max = hits.length;
+ Log.i(TAG, "Max: " + maxResults + " Set: " + set);
+ if(set >= 0) {
+ for(int i = 0; i < hits.length;
i++) {
try {
Document tmp = indexSearcher.doc(hits[i].doc);
@@ -481,8 +500,8 @@ private ArrayList getDocs(int maxResults, int set, ScoreDoc[] hits){
}
}
} else {
- for(int i = 0; i < hits.length && i < (maxResults * -(set + 1) +
- maxResults);
+ for(int i = 0; i < hits.length && i < (max * -(set + 1) +
+ max);
i++) {
try {
Document tmp = indexSearcher.doc(hits[i].doc);
@@ -492,6 +511,7 @@ private ArrayList getDocs(int maxResults, int set, ScoreDoc[] hits){
}
}
}
+ Log.i(TAG, "doc size" + docs.size());
return docs;
}
diff --git a/src/ca/dracode/ais/service/BSearchService1_0.aidl b/src/ca/dracode/ais/service/BSearchService1_0.aidl
index 8afc7d8..8c7ea71 100644
--- a/src/ca/dracode/ais/service/BSearchService1_0.aidl
+++ b/src/ca/dracode/ais/service/BSearchService1_0.aidl
@@ -72,22 +72,14 @@ interface BSearchService1_0 {
int resultSet);
/**
- * used to send file contents to the indexing service. Because of the limitations of
- * the service communicsation system the information may have to be sent in chunks as
- * there can only be a maximum of about 1MB in the buffer at a time (which is shared
- * among all applications). The client class sends data in chunks that do not exceed 256KB,
- * currently pages cannot exceed 256KB as the data transfer will fail
+ * Tells the indexer to try to build the given file
* @param filePath - the location of the file to be built; used by the indexer to identify the file
- * text - the text to be added to the index
- * page - the page upon which the chunk of the file that is being transferred starts.
- * It is a double to allow the transfer of parts of a single page if the page is too large
- * maxPage - the total number of pages in the entire file
* @return 0 if index was built successfully;
* 1 if the file lock was in place due to another build operation being in progress;
* 2 if the Service is still waiting for the rest of the pages
* -1 on error
*/
- int buildIndex(int id, String filePath, in List text, double page, int maxPage);
+ int buildIndex(int id, String filePath);
/**
* Tells the indexer to load a file's metadata into memory for use in searches.
diff --git a/src/ca/dracode/ais/service/FileListener.java b/src/ca/dracode/ais/service/FileListener.java
index ac639e6..6272d05 100644
--- a/src/ca/dracode/ais/service/FileListener.java
+++ b/src/ca/dracode/ais/service/FileListener.java
@@ -130,7 +130,7 @@ public void onEvent(int event, String path) {
}
// If a file was changed or created, re-index file or index the new file
mBoundService.createIndex(new File(Environment.getExternalStorageDirectory()
- .getAbsolutePath() + path));
+ .getAbsolutePath() + path), null);
timerHandler.postDelayed(timerRunnable, DELAY);
Log.i(TAG, "File changed: " + path);
} else if(event == FileObserver.DELETE || event == FileObserver.MOVED_FROM) {
diff --git a/src/ca/dracode/ais/service/IndexService.java b/src/ca/dracode/ais/service/IndexService.java
index 3ab4f3b..5972658 100644
--- a/src/ca/dracode/ais/service/IndexService.java
+++ b/src/ca/dracode/ais/service/IndexService.java
@@ -123,7 +123,9 @@ private static void getServices(File directory, List services) {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- this.crawl = intent.getBooleanExtra("crawl", false);
+ if(intent != null) {
+ this.crawl = intent.getBooleanExtra("crawl", false);
+ }
if(this.crawl) {
canStop = true;
doneCrawling = false;
@@ -180,14 +182,19 @@ public void run() {
}
Indexable tmp = pIndexes.poll();
if(tmp != null) {
- Log.i(TAG, "Indexing: " + tmp.file.getAbsolutePath());
+ //Log.i(TAG, "Indexing: " + tmp.file.getAbsolutePath());
if(tmp.tmpData == null || tmp.tmpData.size() == 0) {
indexer.buildIndex(tmp.file.getAbsolutePath(), -1);
tasks--;
} else {
try {
- indexer.buildIndex(tmp.tmpData,
- tmp.file);
+ if(tmp.callback != null) {
+ tmp.callback.indexCreated(tmp.file, indexer.buildIndex(tmp.tmpData,
+ tmp.file));
+ } else {
+ indexer.buildIndex(tmp.tmpData,
+ tmp.file);
+ }
tasks--;
} catch(Exception e) {
Log.e(TAG, "Error ", e);
@@ -260,7 +267,7 @@ public void crawl(File directory) throws IOException {
}
}
if(content.canRead()) {
- createIndex(content);
+ createIndex(content, null);
}
}
for(File content : contents) {
@@ -275,14 +282,17 @@ public void crawl(File directory) throws IOException {
}
}
+ public interface IndexCallback{
+ public void indexCreated(File content, int retval);
+ }
+
/**
* calls for an index to be created for the given file
* @param content The file to be stored in the index
*/
- public void createIndex(File content) {
+ public void createIndex(File content, IndexCallback callback) {
String serviceName = null;
if(content.isFile()) {
- int size = services.size();
for(ParserService service : services) {
int mLoc = content.getName().lastIndexOf(".") + 1;
if(mLoc != 0) {
@@ -299,23 +309,23 @@ public void createIndex(File content) {
try {
int state = indexer.checkForIndex(content.getAbsolutePath() + ":meta", content.lastModified());
if(state == 0) {
- Log.i(TAG, "Found index for " + content.getName() + "; skipping.");
+ //Log.i(TAG, "Found index for " + content.getName() + "; skipping.");
} else if(state == 1) {
- Log.i(TAG, "Index for " + content.getName() + " out of date, building index");
+ //Log.i(TAG, "Index for " + content.getName() + " out of date, building index");
try {
new RemoteBuilder(
content,
- serviceName);
+ serviceName, null);
tasks++;
} catch(Exception e) {
Log.e(TAG, "" + e.getMessage());
}
} else if(state == -1) {
- Log.i(TAG, "Index for " + content.getName() + " not found, building index");
+ //Log.i(TAG, "Index for " + content.getName() + " not found, building index");
try {
new RemoteBuilder(
content,
- serviceName);
+ serviceName, null);
tasks++;
} catch(Exception e) {
Log.e(TAG, "" + e.getMessage());
@@ -376,15 +386,18 @@ private class Indexable {
private IBinder mService;
private String serviceName = null;
private RemoteBuilder builder;
+ private IndexCallback callback;
public Indexable(ArrayList tmpData, File file,
- IBinder mService, String serviceName, RemoteBuilder builder) {
+ IBinder mService, String serviceName, RemoteBuilder builder,
+ IndexCallback callback) {
super();
this.tmpData = tmpData;
this.file = file;
this.mService = mService;
this.serviceName = serviceName;
this.builder = builder;
+ this.callback = callback;
}
}
@@ -394,15 +407,17 @@ private class RemoteBuilder {
private File file;
private IBinder mService;
private String serviceName = null;
+ IndexCallback callback;
- public RemoteBuilder(File file, String serviceName) {
+ public RemoteBuilder(File file, String serviceName, IndexCallback callback) {
this.file = file;
this.serviceName = serviceName;
+ this.callback = callback;
if(serviceName != null) {
this.doBindService(getApplicationContext());
} else {
pIndexes.add(new Indexable(null, file, null,
- null, RemoteBuilder.this));
+ null, RemoteBuilder.this, callback));
}
}
@@ -450,16 +465,16 @@ public void onServiceConnected(ComponentName className,
.asInterface(mService);
tmp.loadFile(file.getAbsolutePath());
tmpData = new ArrayList();
- int pages = tmp.getPageCount();
+ int pages = tmp.getPageCount(file.getAbsolutePath());
for(int i = 0; i < pages; i++) {
- tmpData.add(tmp.getWordsForPage(i));
+ tmpData.add(tmp.getWordsForPage(i, file.getAbsolutePath()));
}
} catch(RemoteException e) {
- e.printStackTrace();
+ Log.e(TAG, "error", e);
}
// build();
pIndexes.add(new Indexable(tmpData, file, mService,
- serviceName, RemoteBuilder.this));
+ serviceName, RemoteBuilder.this, callback));
doUnbindService(getApplicationContext());
}
@@ -478,5 +493,4 @@ IndexService getService() {
return IndexService.this;
}
}
-
}
diff --git a/src/ca/dracode/ais/service/SearchService.java b/src/ca/dracode/ais/service/SearchService.java
index 45f1a3e..bd5fa5d 100644
--- a/src/ca/dracode/ais/service/SearchService.java
+++ b/src/ca/dracode/ais/service/SearchService.java
@@ -20,7 +20,10 @@
package ca.dracode.ais.service;
import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
@@ -32,7 +35,6 @@
import java.util.List;
import ca.dracode.ais.indexdata.SearchResult;
-import ca.dracode.ais.indexer.FileIndexer;
import ca.dracode.ais.indexer.FileSearcher;
/**
@@ -45,9 +47,10 @@
// for single file searches (create new index in RAMDirectory and add appropriate documents in
// the load function).
-public class SearchService extends Service {
+public class SearchService extends Service implements IndexService.IndexCallback {
private static final String TAG = "ca.dracode.ais.service.SearchService";
int currentId = 0;
+ HashMap builtIndexes;
private final BSearchService1_0.Stub mBinder = new BSearchService1_0.Stub() {
public SearchResult find(int id, String doc, int type, String text, int numHits, int set,
int page) {
@@ -64,9 +67,8 @@ public List findName(int id, List docs, int type, String text, i
return sm.findName(id, text, docs, numHits, set, type);
}
- public int buildIndex(int id, String filePath, List text, double page,
- int maxPage) {
- return SearchService.this.buildIndex(id, filePath, text, page, maxPage);
+ public int buildIndex(int id, String filePath) {
+ return SearchService.this.buildIndex(id, filePath);
}
public int load(String filePath) {
@@ -85,7 +87,18 @@ public int getId(){
return ++currentId;
}
};
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mBoundService = ((IndexService.LocalBinder) service).getService();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mBoundService = null;
+ }
+ };
private SearchManager sm;
+ private IndexService mBoundService;
+ private boolean mIsBound = false;
// private final IBinder mBinder = new LocalBinder();
private HashMap data;
@@ -100,56 +113,42 @@ public void onCreate() {
super.onCreate();
this.sm = new SearchManager();
this.data = new HashMap();
+ this.builtIndexes = new HashMap();
+ }
+
+ private void doBindService() {
+ bindService(new Intent(SearchService.this,
+ IndexService.class), mConnection, Context.BIND_AUTO_CREATE);
+ mIsBound = true;
+ }
+
+ private void doUnbindService() {
+ if(mIsBound) {
+ // Detach our existing connection.
+ unbindService(mConnection);
+ mIsBound = false;
+ }
}
/**
- * used to send file contents to the indexing service. Because of the limitations of
- * the service communicsation system the information may have to be sent in chunks as
- * there can only be a maximum of about 1MB in the buffer at a time (which is shared
- * among all applications). The client class sends data in chunks that do not exceed 256KB,
- * currently pages cannot exceed 256KB as the data transfer will fail
+ * Tells the indexer to try to build the given file
* @param filePath - the location of the file to be built; used by the indexer to identify the file
- * @param text - the text to be added to the index
- * @param page - the page upon which the chunk of the file that is being transferred
- * starts.
- * It is a double to allow the transfer of parts of a single page if the page is too large
- * maxPage - the total number of pages in the entire file
* @return 0 if index was built successfully;
* 1 if the file lock was in place due to another build operation being in progress;
* 2 if the Service is still waiting for the rest of the pages
* -1 on error
*/
- public int buildIndex(int id, String filePath, List text, double page,
- int maxPage) {
- File indexDirFile = new File(FileIndexer.getRootStorageDir());
- File[] dirContents = indexDirFile.listFiles();
- if(dirContents != null) {
- for(File dirContent : dirContents) {
- if(dirContent.getName().equals("write.lock")) {
- return 1;
- }
- }
- }
- FileIndexer indexer = new FileIndexer();
- SearchData tmpData = this.data.get(filePath);
- if(page == 0) {
- tmpData.text.clear();
+ public int buildIndex(int id, String filePath) {
+ if(!mIsBound){
+ doBindService();
}
- if(page + text.size() != maxPage) {
- tmpData.text.addAll(text);
- } else {
- tmpData.text.addAll(text);
- try {
- indexer.buildIndex(tmpData.text, new File(filePath));
- } catch(Exception ex) {
- Log.v("PDFIndex", "" + ex.getMessage());
- ex.printStackTrace();
- } finally {
- Log.i(TAG, "Built Index");
- }
- return 0;
+ while(!mIsBound){
+
}
- return 2;
+ mBoundService.createIndex(new File(filePath), this);
+ mBoundService.stopWhenReady();
+ unbindService(mConnection);
+ return waitForIndexer(new File(filePath));
}
/**
@@ -172,7 +171,8 @@ public int load(final String filePath) {
Log.e(TAG, "Searcher is null");
return -1;
}
- if((tmp = this.sm.searcher.getMetaFile(filePath)) != null) {
+ Log.i(TAG, "Loading: " + filePath + " " + new File(filePath).getAbsolutePath());
+ if((tmp = this.sm.searcher.getMetaFile(new File(filePath).getAbsolutePath())) != null) {
try {
IndexableField f = tmp.getField("pages");
if(f == null) {
@@ -223,7 +223,7 @@ private boolean interrupt(int id) {
* Used to search for file names
* @param directory - A list containing directories to search
* @param type - allows the client to specify how to filter the files
- * @param text - the search term
+ * @param term - the search term
* @param numHits - the maximum number of results to return
*/
private List findName(int id, String term, List directory, int numHits,
@@ -253,7 +253,7 @@ private SearchResult findIn(int id, String term, List documents, int num
}
private SearchResult find(int id, String term, String constrainValue, int maxResults,
- int set, int type, int page) {
+ int type, int set, int page) {
/**
* TODO - Preload information about the index in the load function for use here
* **/
@@ -263,4 +263,18 @@ private SearchResult find(int id, String term, String constrainValue, int maxRes
}
}
+ public int waitForIndexer(File content){
+ while(!this.builtIndexes.containsKey(content)){
+ try{
+ wait(5);
+ } catch(InterruptedException e){
+ Log.e(TAG, "Error", e);
+ }
+ }
+ return this.builtIndexes.get(content);
+ }
+
+ public void indexCreated(File content, int retval){
+ this.builtIndexes.put(content, retval);
+ }
}