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