James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
commit | author | age
5e3521 1 /*
JM 2  * Copyright 2014 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.tickets;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.text.MessageFormat;
21 import java.text.ParseException;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Date;
25 import java.util.LinkedHashSet;
26 import java.util.List;
27 import java.util.Set;
28
29 import org.apache.lucene.analysis.standard.StandardAnalyzer;
30 import org.apache.lucene.document.Document;
31 import org.apache.lucene.document.Field.Store;
32 import org.apache.lucene.document.IntField;
33 import org.apache.lucene.document.LongField;
34 import org.apache.lucene.document.TextField;
35 import org.apache.lucene.index.DirectoryReader;
36 import org.apache.lucene.index.IndexWriter;
37 import org.apache.lucene.index.IndexWriterConfig;
38 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
39 import org.apache.lucene.queryparser.classic.QueryParser;
40 import org.apache.lucene.search.BooleanClause.Occur;
41 import org.apache.lucene.search.BooleanQuery;
42 import org.apache.lucene.search.IndexSearcher;
43 import org.apache.lucene.search.Query;
44 import org.apache.lucene.search.ScoreDoc;
45 import org.apache.lucene.search.Sort;
46 import org.apache.lucene.search.SortField;
47 import org.apache.lucene.search.SortField.Type;
48 import org.apache.lucene.search.TopFieldDocs;
49 import org.apache.lucene.search.TopScoreDocCollector;
50 import org.apache.lucene.store.Directory;
51 import org.apache.lucene.store.FSDirectory;
52 import org.apache.lucene.util.Version;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import com.gitblit.Keys;
57 import com.gitblit.manager.IRuntimeManager;
58 import com.gitblit.models.RepositoryModel;
59 import com.gitblit.models.TicketModel;
60 import com.gitblit.models.TicketModel.Attachment;
61 import com.gitblit.models.TicketModel.Patchset;
62 import com.gitblit.models.TicketModel.Status;
63 import com.gitblit.utils.FileUtils;
64 import com.gitblit.utils.StringUtils;
65
66 /**
67  * Indexes tickets in a Lucene database.
68  *
69  * @author James Moger
70  *
71  */
72 public class TicketIndexer {
73
74     /**
75      * Fields in the Lucene index
76      */
77     public static enum Lucene {
78
79         rid(Type.STRING),
80         did(Type.STRING),
81         project(Type.STRING),
82         repository(Type.STRING),
83         number(Type.LONG),
84         title(Type.STRING),
85         body(Type.STRING),
86         topic(Type.STRING),
87         created(Type.LONG),
88         createdby(Type.STRING),
89         updated(Type.LONG),
90         updatedby(Type.STRING),
91         responsible(Type.STRING),
92         milestone(Type.STRING),
93         status(Type.STRING),
94         type(Type.STRING),
95         labels(Type.STRING),
96         participants(Type.STRING),
97         watchedby(Type.STRING),
98         mentions(Type.STRING),
99         attachments(Type.INT),
100         content(Type.STRING),
101         patchset(Type.STRING),
102         comments(Type.INT),
103         mergesha(Type.STRING),
104         mergeto(Type.STRING),
105         patchsets(Type.INT),
f9c78c 106         votes(Type.INT),
PM 107         //NOTE: Indexing on the underlying value to allow flexibility on naming
108         priority(Type.INT),
109         severity(Type.INT);
5e3521 110
JM 111         final Type fieldType;
112
113         Lucene(Type fieldType) {
114             this.fieldType = fieldType;
115         }
116
117         public String colon() {
118             return name() + ":";
119         }
120
121         public String matches(String value) {
122             if (StringUtils.isEmpty(value)) {
123                 return "";
124             }
125             boolean not = value.charAt(0) == '!';
126             if (not) {
127                 return "!" + name() + ":" + escape(value.substring(1));
128             }
129             return name() + ":" + escape(value);
130         }
131
132         public String doesNotMatch(String value) {
133             if (StringUtils.isEmpty(value)) {
134                 return "";
135             }
136             return "NOT " + name() + ":" + escape(value);
137         }
138
139         public String isNotNull() {
140             return matches("[* TO *]");
141         }
142
143         public SortField asSortField(boolean descending) {
144             return new SortField(name(), fieldType, descending);
145         }
146
147         private String escape(String value) {
148             if (value.charAt(0) != '"') {
a4fa1b 149                 for (char c : value.toCharArray()) {
JM 150                     if (!Character.isLetterOrDigit(c)) {
151                         return "\"" + value + "\"";
152                     }
5e3521 153                 }
JM 154             }
155             return value;
156         }
157
158         public static Lucene fromString(String value) {
159             for (Lucene field : values()) {
160                 if (field.name().equalsIgnoreCase(value)) {
161                     return field;
162                 }
163             }
164             return created;
165         }
166     }
167
168     private final Logger log = LoggerFactory.getLogger(getClass());
169
60110f 170     private final Version luceneVersion = Version.LUCENE_46;
5e3521 171
JM 172     private final File luceneDir;
173
174     private IndexWriter writer;
175
176     private IndexSearcher searcher;
177
178     public TicketIndexer(IRuntimeManager runtimeManager) {
179         this.luceneDir = runtimeManager.getFileOrFolder(Keys.tickets.indexFolder, "${baseFolder}/tickets/lucene");
180     }
181
182     /**
183      * Close all writers and searchers used by the ticket indexer.
184      */
185     public void close() {
186         closeSearcher();
187         closeWriter();
188     }
189
190     /**
191      * Deletes the entire ticket index for all repositories.
192      */
193     public void deleteAll() {
194         close();
195         FileUtils.delete(luceneDir);
196     }
197
198     /**
199      * Deletes all tickets for the the repository from the index.
200      */
201     public boolean deleteAll(RepositoryModel repository) {
202         try {
203             IndexWriter writer = getWriter();
60110f 204             StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
JM 205             QueryParser qp = new QueryParser(luceneVersion, Lucene.rid.name(), analyzer);
5e3521 206             BooleanQuery query = new BooleanQuery();
JM 207             query.add(qp.parse(repository.getRID()), Occur.MUST);
208
209             int numDocsBefore = writer.numDocs();
210             writer.deleteDocuments(query);
211             writer.commit();
212             closeSearcher();
213             int numDocsAfter = writer.numDocs();
214             if (numDocsBefore == numDocsAfter) {
215                 log.debug(MessageFormat.format("no records found to delete in {0}", repository));
216                 return false;
217             } else {
218                 log.debug(MessageFormat.format("deleted {0} records in {1}", numDocsBefore - numDocsAfter, repository));
219                 return true;
220             }
221         } catch (Exception e) {
222             log.error("error", e);
223         }
224         return false;
225     }
226
227     /**
228      * Bulk Add/Update tickets in the Lucene index
229      *
230      * @param tickets
231      */
232     public void index(List<TicketModel> tickets) {
233         try {
234             IndexWriter writer = getWriter();
235             for (TicketModel ticket : tickets) {
236                 Document doc = ticketToDoc(ticket);
237                 writer.addDocument(doc);
238             }
239             writer.commit();
240             closeSearcher();
241         } catch (Exception e) {
242             log.error("error", e);
243         }
244     }
245
246     /**
247      * Add/Update a ticket in the Lucene index
248      *
249      * @param ticket
250      */
251     public void index(TicketModel ticket) {
252         try {
253             IndexWriter writer = getWriter();
254             delete(ticket.repository, ticket.number, writer);
255             Document doc = ticketToDoc(ticket);
256             writer.addDocument(doc);
257             writer.commit();
258             closeSearcher();
259         } catch (Exception e) {
260             log.error("error", e);
261         }
262     }
263
264     /**
265      * Delete a ticket from the Lucene index.
266      *
267      * @param ticket
268      * @throws Exception
269      * @return true, if deleted, false if no record was deleted
270      */
271     public boolean delete(TicketModel ticket) {
272         try {
273             IndexWriter writer = getWriter();
274             return delete(ticket.repository, ticket.number, writer);
275         } catch (Exception e) {
276             log.error("Failed to delete ticket " + ticket.number, e);
277         }
278         return false;
279     }
280
281     /**
282      * Delete a ticket from the Lucene index.
283      *
284      * @param repository
285      * @param ticketId
286      * @throws Exception
287      * @return true, if deleted, false if no record was deleted
288      */
289     private boolean delete(String repository, long ticketId, IndexWriter writer) throws Exception {
60110f 290         StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
JM 291         QueryParser qp = new QueryParser(luceneVersion, Lucene.did.name(), analyzer);
5e3521 292         BooleanQuery query = new BooleanQuery();
JM 293         query.add(qp.parse(StringUtils.getSHA1(repository + ticketId)), Occur.MUST);
294
295         int numDocsBefore = writer.numDocs();
296         writer.deleteDocuments(query);
297         writer.commit();
298         closeSearcher();
299         int numDocsAfter = writer.numDocs();
300         if (numDocsBefore == numDocsAfter) {
301             log.debug(MessageFormat.format("no records found to delete in {0}", repository));
302             return false;
303         } else {
304             log.debug(MessageFormat.format("deleted {0} records in {1}", numDocsBefore - numDocsAfter, repository));
305             return true;
306         }
307     }
308
309     /**
310      * Returns true if the repository has tickets in the index.
311      *
312      * @param repository
313      * @return true if there are indexed tickets
314      */
315     public boolean hasTickets(RepositoryModel repository) {
316         return !queryFor(Lucene.rid.matches(repository.getRID()), 1, 0, null, true).isEmpty();
317     }
318
319     /**
320      * Search for tickets matching the query.  The returned tickets are
321      * shadows of the real ticket, but suitable for a results list.
322      *
323      * @param repository
324      * @param text
325      * @param page
326      * @param pageSize
327      * @return search results
328      */
329     public List<QueryResult> searchFor(RepositoryModel repository, String text, int page, int pageSize) {
330         if (StringUtils.isEmpty(text)) {
331             return Collections.emptyList();
332         }
333         Set<QueryResult> results = new LinkedHashSet<QueryResult>();
60110f 334         StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
5e3521 335         try {
JM 336             // search the title, description and content
337             BooleanQuery query = new BooleanQuery();
338             QueryParser qp;
339
60110f 340             qp = new QueryParser(luceneVersion, Lucene.title.name(), analyzer);
5e3521 341             qp.setAllowLeadingWildcard(true);
JM 342             query.add(qp.parse(text), Occur.SHOULD);
343
60110f 344             qp = new QueryParser(luceneVersion, Lucene.body.name(), analyzer);
5e3521 345             qp.setAllowLeadingWildcard(true);
JM 346             query.add(qp.parse(text), Occur.SHOULD);
347
60110f 348             qp = new QueryParser(luceneVersion, Lucene.content.name(), analyzer);
5e3521 349             qp.setAllowLeadingWildcard(true);
JM 350             query.add(qp.parse(text), Occur.SHOULD);
351
352             IndexSearcher searcher = getSearcher();
353             Query rewrittenQuery = searcher.rewrite(query);
354
355             log.debug(rewrittenQuery.toString());
356
60110f 357             TopScoreDocCollector collector = TopScoreDocCollector.create(5000, true);
5e3521 358             searcher.search(rewrittenQuery, collector);
JM 359             int offset = Math.max(0, (page - 1) * pageSize);
360             ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;
361             for (int i = 0; i < hits.length; i++) {
362                 int docId = hits[i].doc;
363                 Document doc = searcher.doc(docId);
364                 QueryResult result = docToQueryResult(doc);
365                 if (repository != null) {
366                     if (!result.repository.equalsIgnoreCase(repository.name)) {
367                         continue;
368                     }
369                 }
370                 results.add(result);
371             }
372         } catch (Exception e) {
373             log.error(MessageFormat.format("Exception while searching for {0}", text), e);
374         }
375         return new ArrayList<QueryResult>(results);
376     }
377
378     /**
379      * Search for tickets matching the query.  The returned tickets are
380      * shadows of the real ticket, but suitable for a results list.
381      *
382      * @param text
383      * @param page
384      * @param pageSize
385      * @param sortBy
386      * @param desc
387      * @return
388      */
389     public List<QueryResult> queryFor(String queryText, int page, int pageSize, String sortBy, boolean desc) {
390         if (StringUtils.isEmpty(queryText)) {
391             return Collections.emptyList();
392         }
393
394         Set<QueryResult> results = new LinkedHashSet<QueryResult>();
60110f 395         StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
5e3521 396         try {
60110f 397             QueryParser qp = new QueryParser(luceneVersion, Lucene.content.name(), analyzer);
5e3521 398             Query query = qp.parse(queryText);
JM 399
400             IndexSearcher searcher = getSearcher();
401             Query rewrittenQuery = searcher.rewrite(query);
402
403             log.debug(rewrittenQuery.toString());
404
405             Sort sort;
406             if (sortBy == null) {
407                 sort = new Sort(Lucene.created.asSortField(desc));
408             } else {
409                 sort = new Sort(Lucene.fromString(sortBy).asSortField(desc));
410             }
411             int maxSize = 5000;
412             TopFieldDocs docs = searcher.search(rewrittenQuery, null, maxSize, sort, false, false);
413             int size = (pageSize <= 0) ? maxSize : pageSize;
414             int offset = Math.max(0, (page - 1) * size);
415             ScoreDoc[] hits = subset(docs.scoreDocs, offset, size);
416             for (int i = 0; i < hits.length; i++) {
417                 int docId = hits[i].doc;
418                 Document doc = searcher.doc(docId);
419                 QueryResult result = docToQueryResult(doc);
420                 result.docId = docId;
421                 result.totalResults = docs.totalHits;
422                 results.add(result);
423             }
424         } catch (Exception e) {
425             log.error(MessageFormat.format("Exception while searching for {0}", queryText), e);
426         }
427         return new ArrayList<QueryResult>(results);
428     }
429
430     private ScoreDoc [] subset(ScoreDoc [] docs, int offset, int size) {
431         if (docs.length >= (offset + size)) {
432             ScoreDoc [] set = new ScoreDoc[size];
433             System.arraycopy(docs, offset, set, 0, set.length);
434             return set;
435         } else if (docs.length >= offset) {
436             ScoreDoc [] set = new ScoreDoc[docs.length - offset];
437             System.arraycopy(docs, offset, set, 0, set.length);
438             return set;
439         } else {
440             return new ScoreDoc[0];
441         }
442     }
443
444     private IndexWriter getWriter() throws IOException {
445         if (writer == null) {
60110f 446             Directory directory = FSDirectory.open(luceneDir);
5e3521 447
JM 448             if (!luceneDir.exists()) {
449                 luceneDir.mkdirs();
450             }
451
60110f 452             StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
JM 453             IndexWriterConfig config = new IndexWriterConfig(luceneVersion, analyzer);
5e3521 454             config.setOpenMode(OpenMode.CREATE_OR_APPEND);
JM 455             writer = new IndexWriter(directory, config);
456         }
457         return writer;
458     }
459
460     private synchronized void closeWriter() {
461         try {
462             if (writer != null) {
463                 writer.close();
464             }
465         } catch (Exception e) {
466             log.error("failed to close writer!", e);
467         } finally {
468             writer = null;
469         }
470     }
471
472     private IndexSearcher getSearcher() throws IOException {
473         if (searcher == null) {
474             searcher = new IndexSearcher(DirectoryReader.open(getWriter(), true));
475         }
476         return searcher;
477     }
478
479     private synchronized void closeSearcher() {
480         try {
481             if (searcher != null) {
482                 searcher.getIndexReader().close();
483             }
484         } catch (Exception e) {
485             log.error("failed to close searcher!", e);
486         } finally {
487             searcher = null;
488         }
489     }
490
491     /**
492      * Creates a Lucene document from a ticket.
493      *
494      * @param ticket
495      * @return a Lucene document
496      */
497     private Document ticketToDoc(TicketModel ticket) {
498         Document doc = new Document();
499         // repository and document ids for Lucene querying
500         toDocField(doc, Lucene.rid, StringUtils.getSHA1(ticket.repository));
501         toDocField(doc, Lucene.did, StringUtils.getSHA1(ticket.repository + ticket.number));
502
503         toDocField(doc, Lucene.project, ticket.project);
504         toDocField(doc, Lucene.repository, ticket.repository);
505         toDocField(doc, Lucene.number, ticket.number);
506         toDocField(doc, Lucene.title, ticket.title);
507         toDocField(doc, Lucene.body, ticket.body);
508         toDocField(doc, Lucene.created, ticket.created);
509         toDocField(doc, Lucene.createdby, ticket.createdBy);
510         toDocField(doc, Lucene.updated, ticket.updated);
511         toDocField(doc, Lucene.updatedby, ticket.updatedBy);
512         toDocField(doc, Lucene.responsible, ticket.responsible);
513         toDocField(doc, Lucene.milestone, ticket.milestone);
514         toDocField(doc, Lucene.topic, ticket.topic);
515         toDocField(doc, Lucene.status, ticket.status.name());
516         toDocField(doc, Lucene.comments, ticket.getComments().size());
517         toDocField(doc, Lucene.type, ticket.type == null ? null : ticket.type.name());
518         toDocField(doc, Lucene.mergesha, ticket.mergeSha);
519         toDocField(doc, Lucene.mergeto, ticket.mergeTo);
520         toDocField(doc, Lucene.labels, StringUtils.flattenStrings(ticket.getLabels(), ";").toLowerCase());
521         toDocField(doc, Lucene.participants, StringUtils.flattenStrings(ticket.getParticipants(), ";").toLowerCase());
522         toDocField(doc, Lucene.watchedby, StringUtils.flattenStrings(ticket.getWatchers(), ";").toLowerCase());
523         toDocField(doc, Lucene.mentions, StringUtils.flattenStrings(ticket.getMentions(), ";").toLowerCase());
524         toDocField(doc, Lucene.votes, ticket.getVoters().size());
f9c78c 525         toDocField(doc, Lucene.priority, ticket.priority.getValue());
PM 526         toDocField(doc, Lucene.severity, ticket.severity.getValue());
5e3521 527
JM 528         List<String> attachments = new ArrayList<String>();
529         for (Attachment attachment : ticket.getAttachments()) {
530             attachments.add(attachment.name.toLowerCase());
531         }
532         toDocField(doc, Lucene.attachments, StringUtils.flattenStrings(attachments, ";"));
533
534         List<Patchset> patches = ticket.getPatchsets();
535         if (!patches.isEmpty()) {
536             toDocField(doc, Lucene.patchsets, patches.size());
537             Patchset patchset = patches.get(patches.size() - 1);
538             String flat =
539                     patchset.number + ":" +
540                     patchset.rev + ":" +
541                     patchset.tip + ":" +
542                     patchset.base + ":" +
543                     patchset.commits;
544             doc.add(new org.apache.lucene.document.Field(Lucene.patchset.name(), flat, TextField.TYPE_STORED));
545         }
546
547         doc.add(new TextField(Lucene.content.name(), ticket.toIndexableString(), Store.NO));
548
549         return doc;
550     }
551
552     private void toDocField(Document doc, Lucene lucene, Date value) {
553         if (value == null) {
554             return;
555         }
556         doc.add(new LongField(lucene.name(), value.getTime(), Store.YES));
557     }
558
559     private void toDocField(Document doc, Lucene lucene, long value) {
560         doc.add(new LongField(lucene.name(), value, Store.YES));
561     }
562
563     private void toDocField(Document doc, Lucene lucene, int value) {
564         doc.add(new IntField(lucene.name(), value, Store.YES));
565     }
566
567     private void toDocField(Document doc, Lucene lucene, String value) {
568         if (StringUtils.isEmpty(value)) {
569             return;
570         }
571         doc.add(new org.apache.lucene.document.Field(lucene.name(), value, TextField.TYPE_STORED));
572     }
573
574     /**
575      * Creates a query result from the Lucene document.  This result is
576      * not a high-fidelity representation of the real ticket, but it is
577      * suitable for display in a table of search results.
578      *
579      * @param doc
580      * @return a query result
581      * @throws ParseException
582      */
583     private QueryResult docToQueryResult(Document doc) throws ParseException {
584         QueryResult result = new QueryResult();
585         result.project = unpackString(doc, Lucene.project);
586         result.repository = unpackString(doc, Lucene.repository);
587         result.number = unpackLong(doc, Lucene.number);
588         result.createdBy = unpackString(doc, Lucene.createdby);
589         result.createdAt = unpackDate(doc, Lucene.created);
590         result.updatedBy = unpackString(doc, Lucene.updatedby);
591         result.updatedAt = unpackDate(doc, Lucene.updated);
592         result.title = unpackString(doc, Lucene.title);
593         result.body = unpackString(doc, Lucene.body);
594         result.status = Status.fromObject(unpackString(doc, Lucene.status), Status.New);
595         result.responsible = unpackString(doc, Lucene.responsible);
596         result.milestone = unpackString(doc, Lucene.milestone);
597         result.topic = unpackString(doc, Lucene.topic);
598         result.type = TicketModel.Type.fromObject(unpackString(doc, Lucene.type), TicketModel.Type.defaultType);
599         result.mergeSha = unpackString(doc, Lucene.mergesha);
600         result.mergeTo = unpackString(doc, Lucene.mergeto);
601         result.commentsCount = unpackInt(doc, Lucene.comments);
602         result.votesCount = unpackInt(doc, Lucene.votes);
603         result.attachments = unpackStrings(doc, Lucene.attachments);
604         result.labels = unpackStrings(doc, Lucene.labels);
605         result.participants = unpackStrings(doc, Lucene.participants);
606         result.watchedby = unpackStrings(doc, Lucene.watchedby);
607         result.mentions = unpackStrings(doc, Lucene.mentions);
f9c78c 608         result.priority = TicketModel.Priority.fromObject(unpackInt(doc, Lucene.priority), TicketModel.Priority.defaultPriority);
PM 609         result.severity = TicketModel.Severity.fromObject(unpackInt(doc, Lucene.severity), TicketModel.Severity.defaultSeverity);
5e3521 610
JM 611         if (!StringUtils.isEmpty(doc.get(Lucene.patchset.name()))) {
612             // unpack most recent patchset
613             String [] values = doc.get(Lucene.patchset.name()).split(":", 5);
614
615             Patchset patchset = new Patchset();
616             patchset.number = Integer.parseInt(values[0]);
617             patchset.rev = Integer.parseInt(values[1]);
618             patchset.tip = values[2];
619             patchset.base = values[3];
620             patchset.commits = Integer.parseInt(values[4]);
621
622             result.patchset = patchset;
623         }
624
625         return result;
626     }
627
628     private String unpackString(Document doc, Lucene lucene) {
629         return doc.get(lucene.name());
630     }
631
632     private List<String> unpackStrings(Document doc, Lucene lucene) {
633         if (!StringUtils.isEmpty(doc.get(lucene.name()))) {
634             return StringUtils.getStringsFromValue(doc.get(lucene.name()), ";");
635         }
636         return null;
637     }
638
639     private Date unpackDate(Document doc, Lucene lucene) {
640         String val = doc.get(lucene.name());
641         if (!StringUtils.isEmpty(val)) {
642             long time = Long.parseLong(val);
643             Date date = new Date(time);
644             return date;
645         }
646         return null;
647     }
648
649     private long unpackLong(Document doc, Lucene lucene) {
650         String val = doc.get(lucene.name());
651         if (StringUtils.isEmpty(val)) {
652             return 0;
653         }
654         long l = Long.parseLong(val);
655         return l;
656     }
657
658     private int unpackInt(Document doc, Lucene lucene) {
659         String val = doc.get(lucene.name());
660         if (StringUtils.isEmpty(val)) {
661             return 0;
662         }
663         int i = Integer.parseInt(val);
664         return i;
665     }
666 }