]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java
d0a6e240f27472e3196fb524819362628103d9eb
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / model / TemplateDetailsItem.java
1 /*
2  * Copyright © 2021 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.model;
19
20 import android.content.res.Resources;
21 import android.graphics.Color;
22 import android.graphics.Typeface;
23 import android.text.SpannableString;
24 import android.text.Spanned;
25 import android.text.style.ForegroundColorSpan;
26 import android.text.style.StyleSpan;
27 import android.text.style.UnderlineSpan;
28
29 import androidx.annotation.NonNull;
30
31 import net.ktnx.mobileledger.R;
32 import net.ktnx.mobileledger.db.TemplateAccount;
33 import net.ktnx.mobileledger.db.TemplateBase;
34 import net.ktnx.mobileledger.db.TemplateHeader;
35 import net.ktnx.mobileledger.utils.Misc;
36
37 import org.jetbrains.annotations.Contract;
38 import org.jetbrains.annotations.NotNull;
39
40 import java.util.Objects;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 import java.util.regex.PatternSyntaxException;
44
45 abstract public class TemplateDetailsItem {
46     private final Type type;
47     protected Long id;
48     protected Long position;
49
50     protected TemplateDetailsItem(Type type) {
51         this.type = type;
52     }
53     @Contract(" -> new")
54     public static @NotNull TemplateDetailsItem.Header createHeader() {
55         return new Header();
56     }
57     public static @NotNull TemplateDetailsItem.Header createHeader(Header origin) {
58         return new Header(origin);
59     }
60     @Contract("-> new")
61     public static @NotNull TemplateDetailsItem.AccountRow createAccountRow() {
62         return new AccountRow();
63     }
64     public static TemplateDetailsItem fromRoomObject(TemplateBase p) {
65         if (p instanceof TemplateHeader) {
66             TemplateHeader ph = (TemplateHeader) p;
67             Header header = createHeader();
68             header.setId(ph.getId());
69             header.setName(ph.getName());
70             header.setPattern(ph.getRegularExpression());
71             header.setTestText(ph.getTestText());
72
73             if (ph.getTransactionDescriptionMatchGroup() == null)
74                 header.setTransactionDescription(ph.getTransactionDescription());
75             else
76                 header.setTransactionDescriptionMatchGroup(
77                         ph.getTransactionDescriptionMatchGroup());
78
79             if (ph.getTransactionCommentMatchGroup() == null)
80                 header.setTransactionComment(ph.getTransactionComment());
81             else
82                 header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
83
84             if (ph.getDateDayMatchGroup() == null)
85                 header.setDateDay(ph.getDateDay());
86             else
87                 header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
88
89             if (ph.getDateMonthMatchGroup() == null)
90                 header.setDateMonth(ph.getDateMonth());
91             else
92                 header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
93
94             if (ph.getDateYearMatchGroup() == null)
95                 header.setDateYear(ph.getDateYear());
96             else
97                 header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
98
99             return header;
100         }
101         else if (p instanceof TemplateAccount) {
102             TemplateAccount pa = (TemplateAccount) p;
103             AccountRow acc = createAccountRow();
104             acc.setId(pa.getId());
105             acc.setPosition(pa.getPosition());
106
107             if (pa.getAccountNameMatchGroup() == null)
108                 acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
109             else
110                 acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
111
112             if (pa.getAccountCommentMatchGroup() == null)
113                 acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
114             else
115                 acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
116
117             if (pa.getCurrencyMatchGroup() == null) {
118                 final Integer currencyId = pa.getCurrency();
119                 if (currencyId != null && currencyId > 0)
120                     acc.setCurrency(Currency.loadById(currencyId));
121             }
122             else
123                 acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
124
125             final Integer amountMatchGroup = pa.getAmountMatchGroup();
126             if (amountMatchGroup != null && amountMatchGroup > 0) {
127                 acc.setAmountMatchGroup(amountMatchGroup);
128                 final Boolean negateAmount = pa.getNegateAmount();
129                 acc.setNegateAmount(negateAmount != null && negateAmount);
130             }
131             else
132                 acc.setAmount(pa.getAmount());
133
134             return acc;
135         }
136         else {
137             throw new IllegalStateException("Unexpected item class " + p.getClass());
138         }
139     }
140     public Header asHeaderItem() {
141         ensureType(Type.HEADER);
142         return (Header) this;
143     }
144     public AccountRow asAccountRowItem() {
145         ensureType(Type.ACCOUNT_ITEM);
146         return (AccountRow) this;
147     }
148     private void ensureType(Type type) {
149         if (this.type != type)
150             throw new IllegalStateException(
151                     String.format("Type is %s, but %s is required", this.type.toString(),
152                             type.toString()));
153     }
154     void ensureTrue(boolean flag) {
155         if (!flag)
156             throw new IllegalStateException(
157                     "Literal value requested, but it is matched via a pattern group");
158     }
159     void ensureFalse(boolean flag) {
160         if (flag)
161             throw new IllegalStateException("Matching group requested, but the value is a literal");
162     }
163     public long getId() {
164         return id;
165     }
166     public void setId(Long id) {
167         this.id = id;
168     }
169     public void setId(int id) {
170         this.id = (long) id;
171     }
172     public long getPosition() {
173         return position;
174     }
175     public void setPosition(Long position) {
176         this.position = position;
177     }
178     abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
179     public Type getType() {
180         return type;
181     }
182     public enum Type {
183         HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
184         final int index;
185         Type(int i) {
186             index = i;
187         }
188         public int toInt() {
189             return index;
190         }
191     }
192
193     static class PossiblyMatchedValue<T> {
194         private boolean literalValue;
195         private T value;
196         private int matchGroup;
197         public PossiblyMatchedValue() {
198             literalValue = true;
199             value = null;
200         }
201         public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
202             literalValue = origin.literalValue;
203             value = origin.value;
204             matchGroup = origin.matchGroup;
205         }
206         @NonNull
207         public static PossiblyMatchedValue<Integer> withLiteralInt(Integer initialValue) {
208             PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
209             result.setValue(initialValue);
210             return result;
211         }
212         @NonNull
213         public static PossiblyMatchedValue<Float> withLiteralFloat(Float initialValue) {
214             PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
215             result.setValue(initialValue);
216             return result;
217         }
218         public static PossiblyMatchedValue<Short> withLiteralShort(Short initialValue) {
219             PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
220             result.setValue(initialValue);
221             return result;
222         }
223         @NonNull
224         public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
225             PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
226             result.setValue(initialValue);
227             return result;
228         }
229         public void copyFrom(@NonNull PossiblyMatchedValue<T> origin) {
230             literalValue = origin.literalValue;
231             value = origin.value;
232             matchGroup = origin.matchGroup;
233         }
234         public T getValue() {
235             if (!literalValue)
236                 throw new IllegalStateException("Value is not literal");
237             return value;
238         }
239         public void setValue(T newValue) {
240             value = newValue;
241             literalValue = true;
242         }
243         public boolean hasLiteralValue() {
244             return literalValue;
245         }
246         public int getMatchGroup() {
247             if (literalValue)
248                 throw new IllegalStateException("Value is literal");
249             return matchGroup;
250         }
251         public void setMatchGroup(int group) {
252             this.matchGroup = group;
253             literalValue = false;
254         }
255         public boolean equals(PossiblyMatchedValue<T> other) {
256             if (!other.literalValue == literalValue)
257                 return false;
258             if (literalValue) {
259                 if (value == null)
260                     return other.value == null;
261                 return value.equals(other.value);
262             }
263             else
264                 return matchGroup == other.matchGroup;
265         }
266         public void switchToLiteral() {
267             literalValue = true;
268         }
269         public String toString() {
270             if (literalValue)
271                 if (value == null)
272                     return "<null>";
273                 else
274                     return value.toString();
275             if (matchGroup > 0)
276                 return "grp:" + matchGroup;
277             return "<null>";
278         }
279     }
280
281     public static class TYPE {
282         public static final int header = 0;
283         public static final int accountItem = 1;
284     }
285
286     public static class AccountRow extends TemplateDetailsItem {
287         private final PossiblyMatchedValue<String> accountName =
288                 PossiblyMatchedValue.withLiteralString("");
289         private final PossiblyMatchedValue<String> accountComment =
290                 PossiblyMatchedValue.withLiteralString("");
291         private final PossiblyMatchedValue<Float> amount =
292                 PossiblyMatchedValue.withLiteralFloat(null);
293         private final PossiblyMatchedValue<Currency> currency = new PossiblyMatchedValue<>();
294         private boolean negateAmount;
295         public AccountRow() {
296             super(Type.ACCOUNT_ITEM);
297         }
298         public AccountRow(AccountRow origin) {
299             super(Type.ACCOUNT_ITEM);
300             id = origin.id;
301             position = origin.position;
302             accountName.copyFrom(origin.accountName);
303             accountComment.copyFrom(origin.accountComment);
304             amount.copyFrom(origin.amount);
305             currency.copyFrom(origin.currency);
306             negateAmount = origin.negateAmount;
307         }
308         public boolean isNegateAmount() {
309             return negateAmount;
310         }
311         public void setNegateAmount(boolean negateAmount) {
312             this.negateAmount = negateAmount;
313         }
314         public int getAccountCommentMatchGroup() {
315             return accountComment.getMatchGroup();
316         }
317         public void setAccountCommentMatchGroup(int group) {
318             accountComment.setMatchGroup(group);
319         }
320         public String getAccountComment() {
321             return accountComment.getValue();
322         }
323         public void setAccountComment(String comment) {
324             this.accountComment.setValue(comment);
325         }
326         public int getCurrencyMatchGroup() {
327             return currency.getMatchGroup();
328         }
329         public void setCurrencyMatchGroup(int group) {
330             currency.setMatchGroup(group);
331         }
332         public Currency getCurrency() {
333             return currency.getValue();
334         }
335         public void setCurrency(Currency currency) {
336             this.currency.setValue(currency);
337         }
338         public int getAccountNameMatchGroup() {
339             return accountName.getMatchGroup();
340         }
341         public void setAccountNameMatchGroup(int group) {
342             accountName.setMatchGroup(group);
343         }
344         public String getAccountName() {
345             return accountName.getValue();
346         }
347         public void setAccountName(String accountName) {
348             this.accountName.setValue(accountName);
349         }
350         public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
351         public boolean hasLiteralAmount() {
352             return amount.hasLiteralValue();
353         }
354         public int getAmountMatchGroup() {
355             return amount.getMatchGroup();
356         }
357         public void setAmountMatchGroup(int group) {
358             amount.setMatchGroup(group);
359         }
360         public Float getAmount() {
361             return amount.getValue();
362         }
363         public void setAmount(Float amount) {
364             this.amount.setValue(amount);
365         }
366         public String getProblem(@NonNull Resources r, int patternGroupCount) {
367             if (Misc.emptyIsNull(accountName.getValue()) == null)
368                 return r.getString(R.string.account_name_is_empty);
369             if (!amount.hasLiteralValue() &&
370                 (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
371                 return r.getString(R.string.invalid_matching_group_number);
372
373             return null;
374         }
375         public boolean hasLiteralAccountComment() {
376             return accountComment.hasLiteralValue();
377         }
378         public boolean equalContents(AccountRow o) {
379             return amount.equals(o.amount) && accountName.equals(o.accountName) &&
380                    accountComment.equals(o.accountComment) && negateAmount == o.negateAmount;
381         }
382         public void switchToLiteralAmount() {
383             amount.switchToLiteral();
384         }
385         public void switchToLiteralAccountName() {
386             accountName.switchToLiteral();
387         }
388         public void switchToLiteralAccountComment() {
389             accountComment.switchToLiteral();
390         }
391         public TemplateAccount toDBO(@NonNull Long patternId) {
392             TemplateAccount result = new TemplateAccount(id, patternId, position);
393
394             if (accountName.hasLiteralValue())
395                 result.setAccountName(accountName.getValue());
396             else
397                 result.setAccountNameMatchGroup(accountName.getMatchGroup());
398
399             if (accountComment.hasLiteralValue())
400                 result.setAccountComment(accountComment.getValue());
401             else
402                 result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
403
404             if (amount.hasLiteralValue()) {
405                 result.setAmount(amount.getValue());
406                 result.setNegateAmount(null);
407             }
408             else {
409                 result.setAmountMatchGroup(amount.getMatchGroup());
410                 result.setNegateAmount(negateAmount ? true : null);
411             }
412
413             return result;
414         }
415     }
416
417     public static class Header extends TemplateDetailsItem {
418         private String pattern = "";
419         private String testText = "";
420         private String name = "";
421         private Pattern compiledPattern;
422         private String patternError;
423         private PossiblyMatchedValue<String> transactionDescription =
424                 PossiblyMatchedValue.withLiteralString("");
425         private PossiblyMatchedValue<String> transactionComment =
426                 PossiblyMatchedValue.withLiteralString("");
427         private PossiblyMatchedValue<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
428         private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
429         private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
430         private SpannableString testMatch;
431         private Header() {
432             super(Type.HEADER);
433         }
434         public Header(Header origin) {
435             this();
436             id = origin.id;
437             name = origin.name;
438             testText = origin.testText;
439             testMatch = origin.testMatch;
440             setPattern(origin.pattern);
441
442             transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
443             transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
444
445             dateYear = new PossiblyMatchedValue<>(origin.dateYear);
446             dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
447             dateDay = new PossiblyMatchedValue<>(origin.dateDay);
448         }
449         private static StyleSpan capturedSpan() { return new StyleSpan(Typeface.BOLD); }
450         private static UnderlineSpan matchedSpan() { return new UnderlineSpan(); }
451         private static ForegroundColorSpan notMatchedSpan() {
452             return new ForegroundColorSpan(Color.GRAY);
453         }
454         public String getName() {
455             return name;
456         }
457         public void setName(String name) {
458             this.name = name;
459         }
460         public String getPattern() {
461             return pattern;
462         }
463         public void setPattern(String pattern) {
464             this.pattern = pattern;
465             try {
466                 this.compiledPattern = Pattern.compile(pattern);
467                 checkPatternMatch();
468             }
469             catch (PatternSyntaxException ex) {
470                 patternError = ex.getDescription();
471                 compiledPattern = null;
472
473                 testMatch = new SpannableString(testText);
474                 testMatch.setSpan(notMatchedSpan(), 0, testText.length() - 1,
475                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
476             }
477         }
478         @NonNull
479         @Override
480         public String toString() {
481             return super.toString() +
482                    String.format(" name[%s] pat[%s] test[%s] tran[%s] com[%s]", name, pattern,
483                            testText, transactionDescription, transactionComment);
484         }
485         public String getTestText() {
486             return testText;
487         }
488         public void setTestText(String testText) {
489             this.testText = testText;
490
491             checkPatternMatch();
492         }
493         public String getTransactionDescription() {
494             return transactionDescription.getValue();
495         }
496         public void setTransactionDescription(String transactionDescription) {
497             this.transactionDescription.setValue(transactionDescription);
498         }
499         public String getTransactionComment() {
500             return transactionComment.getValue();
501         }
502         public void setTransactionComment(String transactionComment) {
503             this.transactionComment.setValue(transactionComment);
504         }
505         public Integer getDateYear() {
506             return dateYear.getValue();
507         }
508         public void setDateYear(Integer dateYear) {
509             this.dateYear.setValue(dateYear);
510         }
511         public Integer getDateMonth() {
512             return dateMonth.getValue();
513         }
514         public void setDateMonth(Integer dateMonth) {
515             this.dateMonth.setValue(dateMonth);
516         }
517         public Integer getDateDay() {
518             return dateDay.getValue();
519         }
520         public void setDateDay(Integer dateDay) {
521             this.dateDay.setValue(dateDay);
522         }
523         public int getDateYearMatchGroup() {
524             return dateYear.getMatchGroup();
525         }
526         public void setDateYearMatchGroup(int dateYearMatchGroup) {
527             this.dateYear.setMatchGroup(dateYearMatchGroup);
528         }
529         public int getDateMonthMatchGroup() {
530             return dateMonth.getMatchGroup();
531         }
532         public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
533             this.dateMonth.setMatchGroup(dateMonthMatchGroup);
534         }
535         public int getDateDayMatchGroup() {
536             return dateDay.getMatchGroup();
537         }
538         public void setDateDayMatchGroup(int dateDayMatchGroup) {
539             this.dateDay.setMatchGroup(dateDayMatchGroup);
540         }
541         public boolean hasLiteralDateYear() {
542             return dateYear.hasLiteralValue();
543         }
544         public boolean hasLiteralDateMonth() {
545             return dateMonth.hasLiteralValue();
546         }
547         public boolean hasLiteralDateDay() {
548             return dateDay.hasLiteralValue();
549         }
550         public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
551         public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
552         public String getProblem(@NonNull Resources r, int patternGroupCount) {
553             if (patternError != null)
554                 return r.getString(R.string.pattern_has_errors) + ": " + patternError;
555             if (Misc.emptyIsNull(pattern) == null)
556                 return r.getString(R.string.pattern_is_empty);
557
558             if (!dateYear.hasLiteralValue() && compiledPattern != null &&
559                 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
560                 return r.getString(R.string.invalid_matching_group_number);
561
562             if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
563                 (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
564                 return r.getString(R.string.invalid_matching_group_number);
565
566             if (!dateDay.hasLiteralValue() && compiledPattern != null &&
567                 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
568                 return r.getString(R.string.invalid_matching_group_number);
569
570             return null;
571         }
572
573         public boolean equalContents(Header o) {
574             if (!dateDay.equals(o.dateDay))
575                 return false;
576             if (!dateMonth.equals(o.dateMonth))
577                 return false;
578             if (!dateYear.equals(o.dateYear))
579                 return false;
580             if (!transactionDescription.equals(o.transactionDescription))
581                 return false;
582             if (!transactionComment.equals(o.transactionComment))
583                 return true;
584
585             return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
586                    Misc.equalStrings(testText, o.testText) &&
587                    Misc.equalStrings(patternError, o.patternError) &&
588                    Objects.equals(testMatch, o.testMatch);
589         }
590         public String getMatchGroupText(int group) {
591             if (compiledPattern != null && testText != null) {
592                 Matcher m = compiledPattern.matcher(testText);
593                 if (m.matches())
594                     return m.group(group);
595             }
596
597             return "ø";
598         }
599         public Pattern getCompiledPattern() {
600             return compiledPattern;
601         }
602         public void switchToLiteralTransactionDescription() {
603             transactionDescription.switchToLiteral();
604         }
605         public void switchToLiteralTransactionComment() {
606             transactionComment.switchToLiteral();
607         }
608         public int getTransactionDescriptionMatchGroup() {
609             return transactionDescription.getMatchGroup();
610         }
611         public void setTransactionDescriptionMatchGroup(int group) {
612             transactionDescription.setMatchGroup(group);
613         }
614         public int getTransactionCommentMatchGroup() {
615             return transactionComment.getMatchGroup();
616         }
617         public void setTransactionCommentMatchGroup(int group) {
618             transactionComment.setMatchGroup(group);
619         }
620         public void switchToLiteralDateYear() {
621             dateYear.switchToLiteral();
622         }
623         public void switchToLiteralDateMonth() {
624             dateMonth.switchToLiteral();
625         }
626         public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
627         public TemplateHeader toDBO() {
628             TemplateHeader result = new TemplateHeader(id, name, pattern);
629
630             if (Misc.emptyIsNull(testText) != null)
631                 result.setTestText(testText);
632
633             if (transactionDescription.hasLiteralValue())
634                 result.setTransactionDescription(transactionDescription.getValue());
635             else
636                 result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
637
638             if (transactionComment.hasLiteralValue())
639                 result.setTransactionComment(transactionComment.getValue());
640             else
641                 result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
642
643             if (dateYear.hasLiteralValue())
644                 result.setDateYear(dateYear.getValue());
645             else
646                 result.setDateYearMatchGroup(dateYear.getMatchGroup());
647
648             if (dateMonth.hasLiteralValue())
649                 result.setDateMonth(dateMonth.getValue());
650             else
651                 result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
652
653             if (dateDay.hasLiteralValue())
654                 result.setDateDay(dateDay.getValue());
655             else
656                 result.setDateDayMatchGroup(dateDay.getMatchGroup());
657
658             return result;
659         }
660         public SpannableString getTestMatch() {
661             return testMatch;
662         }
663         public void checkPatternMatch() {
664             patternError = null;
665             testMatch = null;
666
667             if (pattern != null) {
668                 try {
669                     if (Misc.emptyIsNull(testText) != null) {
670                         SpannableString ss = new SpannableString(testText);
671                         Matcher m = compiledPattern.matcher(testText);
672                         if (m.find()) {
673                             if (m.start() > 0)
674                                 ss.setSpan(notMatchedSpan(), 0, m.start(),
675                                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
676                             if (m.end() < testText.length() - 1)
677                                 ss.setSpan(notMatchedSpan(), m.end(), testText.length(),
678                                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
679
680                             ss.setSpan(matchedSpan(), m.start(0), m.end(0),
681                                     Spanned.SPAN_INCLUSIVE_INCLUSIVE);
682
683                             if (m.groupCount() > 0) {
684                                 for (int g = 1; g <= m.groupCount(); g++) {
685                                     ss.setSpan(capturedSpan(), m.start(g), m.end(g),
686                                             Spanned.SPAN_INCLUSIVE_INCLUSIVE);
687                                 }
688                             }
689                         }
690                         else {
691                             patternError = "Pattern does not match";
692                             ss.setSpan(new ForegroundColorSpan(Color.GRAY), 0,
693                                     testText.length() - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
694                         }
695
696                         testMatch = ss;
697                     }
698                 }
699                 catch (PatternSyntaxException e) {
700                     this.compiledPattern = null;
701                     this.patternError = e.getMessage();
702                 }
703             }
704             else {
705                 patternError = "Missing pattern";
706             }
707         }
708         public String getPatternError() {
709             return patternError;
710         }
711         public SpannableString testMatch() {
712             return testMatch;
713         }
714     }
715 }