1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id$ */
19 
20 package org.apache.fop.complexscripts.fonts;
21 
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.LinkedHashMap;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.ListIterator;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.TreeSet;
32 
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 
36 import org.apache.fop.complexscripts.scripts.ScriptProcessor;
37 import org.apache.fop.complexscripts.util.GlyphSequence;
38 import org.apache.fop.complexscripts.util.ScriptContextTester;
39 
40 // CSOFF: LineLengthCheck
41 
42 /**
43  * <p>Base class for all advanced typographic glyph tables.</p>
44  *
45  * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
46  */
47 public class GlyphTable {
48 
49     /** logging instance */
50     private static final Log log = LogFactory.getLog(GlyphTable.class);
51 
52     /** substitution glyph table type */
53     public static final int GLYPH_TABLE_TYPE_SUBSTITUTION = 1;
54     /** positioning glyph table type */
55     public static final int GLYPH_TABLE_TYPE_POSITIONING = 2;
56     /** justification glyph table type */
57     public static final int GLYPH_TABLE_TYPE_JUSTIFICATION = 3;
58     /** baseline glyph table type */
59     public static final int GLYPH_TABLE_TYPE_BASELINE = 4;
60     /** definition glyph table type */
61     public static final int GLYPH_TABLE_TYPE_DEFINITION = 5;
62 
63     // (optional) glyph definition table in table types other than glyph definition table
64     private GlyphTable gdef;
65 
66     // map from lookup specs to lists of strings, each of which identifies a lookup table (consisting of one or more subtables)
67     private Map<LookupSpec, List<String>> lookups;
68 
69     // map from lookup identifiers to lookup tables
70     private Map<String, LookupTable> lookupTables;
71 
72     // cache for lookups matching
73     private Map<LookupSpec, Map<LookupSpec, List<LookupTable>>> matchedLookups;
74 
75     // if true, then prevent further subtable addition
76     private boolean frozen;
77 
78     protected Map<String, ScriptProcessor> processors;
79 
80     /**
81      * Instantiate glyph table with specified lookups.
82      * @param gdef glyph definition table that applies
83      * @param lookups map from lookup specs to lookup tables
84      */
GlyphTable(GlyphTable gdef, Map<LookupSpec, List<String>> lookups, Map<String, ScriptProcessor> processors)85     public GlyphTable(GlyphTable gdef, Map<LookupSpec, List<String>> lookups,
86                       Map<String, ScriptProcessor> processors) {
87         this.processors = processors;
88         if ((gdef != null) && !(gdef instanceof GlyphDefinitionTable)) {
89             throw new AdvancedTypographicTableFormatException("bad glyph definition table");
90         } else if (lookups == null) {
91             throw new AdvancedTypographicTableFormatException("lookups must be non-null map");
92         } else {
93             this.gdef = gdef;
94             this.lookups = lookups;
95             this.lookupTables = new LinkedHashMap<String, LookupTable>();
96             this.matchedLookups = new HashMap<LookupSpec, Map<LookupSpec, List<LookupTable>>>();
97         }
98     }
99 
100     /**
101      * Obtain glyph definition table.
102      * @return (possibly null) glyph definition table
103      */
getGlyphDefinitions()104     public GlyphDefinitionTable getGlyphDefinitions() {
105         return (GlyphDefinitionTable) gdef;
106     }
107 
108     /**
109      * Obtain list of all lookup specifications.
110      * @return (possibly empty) list of all lookup specifications
111      */
getLookups()112     public List<LookupSpec> getLookups() {
113         return matchLookupSpecs("*", "*", "*");
114     }
115 
116     /**
117      * Obtain ordered list of all lookup tables, where order is by lookup identifier, which
118      * lexicographic ordering follows the lookup list order.
119      * @return (possibly empty) ordered list of all lookup tables
120      */
getLookupTables()121     public List<LookupTable> getLookupTables() {
122         TreeSet<String> lids = new TreeSet<String>(lookupTables.keySet());
123         List<LookupTable> ltl = new ArrayList<LookupTable>(lids.size());
124         for (Object lid1 : lids) {
125             String lid = (String) lid1;
126             ltl.add(lookupTables.get(lid));
127         }
128         return ltl;
129     }
130 
131     /**
132      * Obtain lookup table by lookup id. This method is used by test code, and provides
133      * access to embedded lookups not normally accessed by {script, language, feature} lookup spec.
134      * @param lid lookup id
135      * @return table associated with lookup id or null if none
136      */
getLookupTable(String lid)137     public LookupTable getLookupTable(String lid) {
138         return lookupTables.get(lid);
139     }
140 
141     /**
142      * Add a subtable.
143      * @param subtable a (non-null) glyph subtable
144      */
addSubtable(GlyphSubtable subtable)145     protected void addSubtable(GlyphSubtable subtable) {
146         // ensure table is not frozen
147         if (frozen) {
148             throw new IllegalStateException("glyph table is frozen, subtable addition prohibited");
149         }
150         // set subtable's table reference to this table
151         subtable.setTable(this);
152         // add subtable to this table's subtable collection
153         String lid = subtable.getLookupId();
154         if (lookupTables.containsKey(lid)) {
155             LookupTable lt = lookupTables.get(lid);
156             lt.addSubtable(subtable);
157         } else {
158             LookupTable lt = new LookupTable(lid, subtable);
159             lookupTables.put(lid, lt);
160         }
161     }
162 
163     /**
164      * Freeze subtables, i.e., do not allow further subtable addition, and
165      * create resulting cached state.
166      */
freezeSubtables()167     protected void freezeSubtables() {
168         if (!frozen) {
169             for (Object o : lookupTables.values()) {
170                 LookupTable lt = (LookupTable) o;
171                 lt.freezeSubtables(lookupTables);
172             }
173             frozen = true;
174         }
175     }
176 
177     /**
178      * Match lookup specifications according to &lt;script,language,feature&gt; tuple, where
179      * '*' is a wildcard for a tuple component.
180      * @param script a script identifier
181      * @param language a language identifier
182      * @param feature a feature identifier
183      * @return a (possibly empty) array of matching lookup specifications
184      */
matchLookupSpecs(String script, String language, String feature)185     public List<LookupSpec> matchLookupSpecs(String script, String language, String feature) {
186         Set<LookupSpec> keys = lookups.keySet();
187         List<LookupSpec> matches = new ArrayList<LookupSpec>();
188         for (Object key : keys) {
189             LookupSpec ls = (LookupSpec) key;
190             if (!"*".equals(script)) {
191                 if (!ls.getScript().equals(script)) {
192                     continue;
193                 }
194             }
195             if (!"*".equals(language)) {
196                 if (!ls.getLanguage().equals(language)) {
197                     continue;
198                 }
199             }
200             if (!"*".equals(feature)) {
201                 if (!ls.getFeature().equals(feature)) {
202                     continue;
203                 }
204             }
205             matches.add(ls);
206         }
207         return matches;
208     }
209 
210     /**
211      * Match lookup specifications according to &lt;script,language,feature&gt; tuple, where
212      * '*' is a wildcard for a tuple component.
213      * @param script a script identifier
214      * @param language a language identifier
215      * @param feature a feature identifier
216      * @return a (possibly empty) map from matching lookup specifications to lists of corresponding lookup tables
217      */
matchLookups(String script, String language, String feature)218     public Map<LookupSpec, List<LookupTable>> matchLookups(String script, String language, String feature) {
219         LookupSpec lsm = new LookupSpec(script, language, feature, true, true);
220         Map<LookupSpec, List<LookupTable>> lm = matchedLookups.get(lsm);
221         if (lm == null) {
222             lm = new LinkedHashMap();
223             List<LookupSpec> lsl = matchLookupSpecs(script, language, feature);
224             for (Object aLsl : lsl) {
225                 LookupSpec ls = (LookupSpec) aLsl;
226                 lm.put(ls, findLookupTables(ls));
227             }
228             matchedLookups.put(lsm, lm);
229         }
230         if (lm.isEmpty() && !OTFScript.isDefault(script) && !OTFScript.isWildCard(script)) {
231             return matchLookups(OTFScript.DEFAULT, OTFLanguage.DEFAULT, feature);
232         } else {
233             return lm;
234         }
235     }
236 
237     /**
238      * Obtain ordered list of glyph lookup tables that match a specific lookup specification.
239      * @param ls a (non-null) lookup specification
240      * @return a (possibly empty) ordered list of lookup tables whose corresponding lookup specifications match the specified lookup spec
241      */
findLookupTables(LookupSpec ls)242     public List<LookupTable> findLookupTables(LookupSpec ls) {
243         TreeSet<LookupTable> lts = new TreeSet<LookupTable>();
244         List<String> ids;
245         if ((ids = lookups.get(ls)) != null) {
246             for (Object id : ids) {
247                 String lid = (String) id;
248                 LookupTable lt;
249                 if ((lt = lookupTables.get(lid)) != null) {
250                     lts.add(lt);
251                 }
252             }
253         }
254         return new ArrayList<LookupTable>(lts);
255     }
256 
257     /**
258      * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups,
259      * where the order of the array is in accordance to the order of the applicable lookup list.
260      * @param features array of feature identifiers to apply
261      * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features
262      * @return ordered array of assembled lookup table use specifications
263      */
assembleLookups(String[] features, Map<LookupSpec, List<LookupTable>> lookups)264     public UseSpec[] assembleLookups(String[] features, Map<LookupSpec, List<LookupTable>> lookups) {
265         TreeSet<UseSpec> uss = new TreeSet<UseSpec>();
266         for (String feature : features) {
267             for (Object o : lookups.entrySet()) {
268                 Map.Entry<LookupSpec, List<LookupTable>> e = (Map.Entry<LookupSpec, List<LookupTable>>) o;
269                 LookupSpec ls = e.getKey();
270                 if (ls.getFeature().equals(feature)) {
271                     List<LookupTable> ltl = e.getValue();
272                     if (ltl != null) {
273                         for (Object aLtl : ltl) {
274                             LookupTable lt = (LookupTable) aLtl;
275                             uss.add(new UseSpec(lt, feature));
276                         }
277                     }
278                 }
279             }
280         }
281         return uss.toArray(new UseSpec [ uss.size() ]);
282     }
283 
284     /**
285      * Determine if table supports specific feature, i.e., supports at least one lookup.
286      *
287      * @param script to qualify feature lookup
288      * @param language to qualify feature lookup
289      * @param feature to test
290      * @return true if feature supported (has at least one lookup)
291      */
hasFeature(String script, String language, String feature)292     public boolean hasFeature(String script, String language, String feature) {
293         UseSpec[] usa = assembleLookups(new String[] { feature }, matchLookups(script, language, feature));
294         return usa.length > 0;
295     }
296 
297     /** {@inheritDoc} */
toString()298     public String toString() {
299         StringBuffer sb = new StringBuffer(super.toString());
300         sb.append("{");
301         sb.append("lookups={");
302         sb.append(lookups.toString());
303         sb.append("},lookupTables={");
304         sb.append(lookupTables.toString());
305         sb.append("}}");
306         return sb.toString();
307     }
308 
309     /**
310      * Obtain glyph table type from name.
311      * @param name of table type to map to type value
312      * @return glyph table type (as an integer constant)
313      */
getTableTypeFromName(String name)314     public static int getTableTypeFromName(String name) {
315         int t;
316         String s = name.toLowerCase();
317         if ("gsub".equals(s)) {
318             t = GLYPH_TABLE_TYPE_SUBSTITUTION;
319         } else if ("gpos".equals(s)) {
320             t = GLYPH_TABLE_TYPE_POSITIONING;
321         } else if ("jstf".equals(s)) {
322             t = GLYPH_TABLE_TYPE_JUSTIFICATION;
323         } else if ("base".equals(s)) {
324             t = GLYPH_TABLE_TYPE_BASELINE;
325         } else if ("gdef".equals(s)) {
326             t = GLYPH_TABLE_TYPE_DEFINITION;
327         } else {
328             t = -1;
329         }
330         return t;
331     }
332 
333     /**
334      * Resolve references to lookup tables in a collection of rules sets.
335      * @param rsa array of rule sets
336      * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
337      */
resolveLookupReferences(RuleSet[] rsa, Map<String, LookupTable> lookupTables)338     public static void resolveLookupReferences(RuleSet[] rsa, Map<String, LookupTable> lookupTables) {
339         if ((rsa != null) && (lookupTables != null)) {
340             for (RuleSet rs : rsa) {
341                 if (rs != null) {
342                     rs.resolveLookupReferences(lookupTables);
343                 }
344             }
345         }
346     }
347 
348     /**
349      * A structure class encapsulating a lookup specification as a &lt;script,language,feature&gt; tuple.
350      */
351     public static class LookupSpec implements Comparable {
352 
353         private final String script;
354         private final String language;
355         private final String feature;
356 
357         /**
358          * Instantiate lookup spec.
359          * @param script a script identifier
360          * @param language a language identifier
361          * @param feature a feature identifier
362          */
LookupSpec(String script, String language, String feature)363         public LookupSpec(String script, String language, String feature) {
364             this (script, language, feature, false, false);
365         }
366 
367         /**
368          * Instantiate lookup spec.
369          * @param script a script identifier
370          * @param language a language identifier
371          * @param feature a feature identifier
372          * @param permitEmpty if true then permit empty script, language, or feature
373          * @param permitWildcard if true then permit wildcard script, language, or feature
374          */
LookupSpec(String script, String language, String feature, boolean permitEmpty, boolean permitWildcard)375         LookupSpec(String script, String language, String feature, boolean permitEmpty, boolean permitWildcard) {
376             if ((script == null) || (!permitEmpty && (script.length() == 0))) {
377                 throw new AdvancedTypographicTableFormatException("script must be non-empty string");
378             } else if ((language == null) || (!permitEmpty && (language.length() == 0))) {
379                 throw new AdvancedTypographicTableFormatException("language must be non-empty string");
380             } else if ((feature == null) || (!permitEmpty && (feature.length() == 0))) {
381                 throw new AdvancedTypographicTableFormatException("feature must be non-empty string");
382             } else if (!permitWildcard && script.equals("*")) {
383                 throw new AdvancedTypographicTableFormatException("script must not be wildcard");
384             } else if (!permitWildcard && language.equals("*")) {
385                 throw new AdvancedTypographicTableFormatException("language must not be wildcard");
386             } else if (!permitWildcard && feature.equals("*")) {
387                 throw new AdvancedTypographicTableFormatException("feature must not be wildcard");
388             }
389             this.script = script.trim();
390             this.language = language.trim();
391             this.feature = feature.trim();
392         }
393 
394         /** @return script identifier */
getScript()395         public String getScript() {
396             return script;
397         }
398 
399         /** @return language identifier */
getLanguage()400         public String getLanguage() {
401             return language;
402         }
403 
404         /** @return feature identifier  */
getFeature()405         public String getFeature() {
406             return feature;
407         }
408 
409         /** {@inheritDoc} */
hashCode()410         public int hashCode() {
411             int hc = 0;
412             hc =  7 * hc + (hc ^ script.hashCode());
413             hc = 11 * hc + (hc ^ language.hashCode());
414             hc = 17 * hc + (hc ^ feature.hashCode());
415             return hc;
416         }
417 
418         /** {@inheritDoc} */
equals(Object o)419         public boolean equals(Object o) {
420             if (o instanceof LookupSpec) {
421                 LookupSpec l = (LookupSpec) o;
422                 if (!l.script.equals(script)) {
423                     return false;
424                 } else if (!l.language.equals(language)) {
425                     return false;
426                 } else {
427                     return l.feature.equals(feature);
428                 }
429             } else {
430                 return false;
431             }
432         }
433 
434         /** {@inheritDoc} */
compareTo(Object o)435         public int compareTo(Object o) {
436             int d;
437             if (o instanceof LookupSpec) {
438                 LookupSpec ls = (LookupSpec) o;
439                 if ((d = script.compareTo(ls.script)) == 0) {
440                     if ((d = language.compareTo(ls.language)) == 0) {
441                         if ((d = feature.compareTo(ls.feature)) == 0) {
442                             d = 0;
443                         }
444                     }
445                 }
446             } else {
447                 d = -1;
448             }
449             return d;
450         }
451 
452         /** {@inheritDoc} */
toString()453         public String toString() {
454             StringBuffer sb = new StringBuffer(super.toString());
455             sb.append("{");
456             sb.append("<'" + script + "'");
457             sb.append(",'" + language + "'");
458             sb.append(",'" + feature + "'");
459             sb.append(">}");
460             return sb.toString();
461         }
462 
463     }
464 
465     /**
466      * The <code>LookupTable</code> class comprising an identifier and an ordered list
467      * of glyph subtables, each of which employ the same lookup identifier.
468      */
469     public static class LookupTable implements Comparable {
470 
471         private final String id;                                // lookup identifier
472         private final int idOrdinal;                            // parsed lookup identifier ordinal
473         private final List<GlyphSubtable> subtables;        // list of subtables
474         private boolean doesSub;                                // performs substitutions
475         private boolean doesPos;                                // performs positioning
476         private boolean frozen;                                 // if true, then don't permit further subtable additions
477         // frozen state
478         private GlyphSubtable[] subtablesArray;
479         private static GlyphSubtable[] subtablesArrayEmpty       = new GlyphSubtable[0];
480 
481         /**
482          * Instantiate a LookupTable.
483          * @param id the lookup table's identifier
484          * @param subtable an initial subtable (or null)
485          */
LookupTable(String id, GlyphSubtable subtable)486         public LookupTable(String id, GlyphSubtable subtable) {
487             this (id, makeSingleton(subtable));
488         }
489 
490         /**
491          * Instantiate a LookupTable.
492          * @param id the lookup table's identifier
493          * @param subtables a pre-poplated list of subtables or null
494          */
LookupTable(String id, List<GlyphSubtable> subtables)495         public LookupTable(String id, List<GlyphSubtable> subtables) {
496             assert id != null;
497             assert id.length() != 0;
498             assert id.startsWith("lu");
499             this.id = id;
500             this.idOrdinal = Integer.parseInt(id.substring(2));
501             this.subtables = new LinkedList<GlyphSubtable>();
502             if (subtables != null) {
503                 for (Object subtable : subtables) {
504                     GlyphSubtable st = (GlyphSubtable) subtable;
505                     addSubtable(st);
506                 }
507             }
508         }
509 
510         /** @return the subtables as an array */
getSubtables()511         public GlyphSubtable[] getSubtables() {
512             if (frozen) {
513                 return (subtablesArray != null) ? subtablesArray : subtablesArrayEmpty;
514             } else {
515                 if (doesSub) {
516                     return subtables.toArray(new GlyphSubstitutionSubtable [ subtables.size() ]);
517                 } else if (doesPos) {
518                     return subtables.toArray(new GlyphPositioningSubtable [ subtables.size() ]);
519                 } else {
520                     return null;
521                 }
522             }
523         }
524 
525         /**
526          * Add a subtable into this lookup table's collecion of subtables according to its
527          * natural order.
528          * @param subtable to add
529          * @return true if subtable was not already present, otherwise false
530          */
addSubtable(GlyphSubtable subtable)531         public boolean addSubtable(GlyphSubtable subtable) {
532             boolean added = false;
533             // ensure table is not frozen
534             if (frozen) {
535                 throw new IllegalStateException("glyph table is frozen, subtable addition prohibited");
536             }
537             // validate subtable to ensure consistency with current subtables
538             validateSubtable(subtable);
539             // insert subtable into ordered list
540             for (ListIterator<GlyphSubtable> lit = subtables.listIterator(0); lit.hasNext(); ) {
541                 GlyphSubtable st = lit.next();
542                 int d;
543                 if ((d = subtable.compareTo(st)) < 0) {
544                     // insert within list
545                     lit.set(subtable);
546                     lit.add(st);
547                     added = true;
548                 } else if (d == 0) {
549                     // duplicate entry is ignored
550                     added = false;
551                     subtable = null;
552                 }
553             }
554             // append at end of list
555             if (!added && (subtable != null)) {
556                 subtables.add(subtable);
557                 added = true;
558             }
559             return added;
560         }
561 
validateSubtable(GlyphSubtable subtable)562         private void validateSubtable(GlyphSubtable subtable) {
563             if (subtable == null) {
564                 throw new AdvancedTypographicTableFormatException("subtable must be non-null");
565             }
566             if (subtable instanceof GlyphSubstitutionSubtable) {
567                 if (doesPos) {
568                     throw new AdvancedTypographicTableFormatException("subtable must be positioning subtable, but is: " + subtable);
569                 } else {
570                     doesSub = true;
571                 }
572             }
573             if (subtable instanceof GlyphPositioningSubtable) {
574                 if (doesSub) {
575                     throw new AdvancedTypographicTableFormatException("subtable must be substitution subtable, but is: " + subtable);
576                 } else {
577                     doesPos = true;
578                 }
579             }
580             if (subtables.size() > 0) {
581                 GlyphSubtable st = subtables.get(0);
582                 if (!st.isCompatible(subtable)) {
583                     throw new AdvancedTypographicTableFormatException("subtable " + subtable + " is not compatible with subtable " + st);
584                 }
585             }
586         }
587 
588         /**
589          * Freeze subtables, i.e., do not allow further subtable addition, and
590          * create resulting cached state. In addition, resolve any references to
591          * lookup tables that appear in this lookup table's subtables.
592          * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
593          */
freezeSubtables(Map<String, LookupTable> lookupTables)594         public void freezeSubtables(Map<String, LookupTable> lookupTables) {
595             if (!frozen) {
596                 GlyphSubtable[] sta = getSubtables();
597                 resolveLookupReferences(sta, lookupTables);
598                 this.subtablesArray = sta;
599                 this.frozen = true;
600             }
601         }
602 
resolveLookupReferences(GlyphSubtable[] subtables, Map<String, LookupTable> lookupTables)603         private void resolveLookupReferences(GlyphSubtable[] subtables, Map<String, LookupTable> lookupTables) {
604             if (subtables != null) {
605                 for (GlyphSubtable st : subtables) {
606                     if (st != null) {
607                         st.resolveLookupReferences(lookupTables);
608                     }
609                 }
610             }
611         }
612 
613         /**
614          * Determine if this glyph table performs substitution.
615          * @return true if it performs substitution
616          */
performsSubstitution()617         public boolean performsSubstitution() {
618             return doesSub;
619         }
620 
621         /**
622          * Perform substitution processing using this lookup table's subtables.
623          * @param gs an input glyph sequence
624          * @param script a script identifier
625          * @param language a language identifier
626          * @param feature a feature identifier
627          * @param sct a script specific context tester (or null)
628          * @return the substituted (output) glyph sequence
629          */
substitute(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct)630         public GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) {
631             if (performsSubstitution()) {
632                 return GlyphSubstitutionSubtable.substitute(gs, script, language, feature, (GlyphSubstitutionSubtable[]) subtablesArray, sct);
633             } else {
634                 return gs;
635             }
636         }
637 
638         /**
639          * Perform substitution processing on an existing glyph substitution state object using this lookup table's subtables.
640          * @param ss a glyph substitution state object
641          * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
642          * @return the substituted (output) glyph sequence
643          */
substitute(GlyphSubstitutionState ss, int sequenceIndex)644         public GlyphSequence substitute(GlyphSubstitutionState ss, int sequenceIndex) {
645             if (performsSubstitution()) {
646                 return GlyphSubstitutionSubtable.substitute(ss, (GlyphSubstitutionSubtable[]) subtablesArray, sequenceIndex);
647             } else {
648                 return ss.getInput();
649             }
650         }
651 
652         /**
653          * Determine if this glyph table performs positioning.
654          * @return true if it performs positioning
655          */
performsPositioning()656         public boolean performsPositioning() {
657             return doesPos;
658         }
659 
660         /**
661          * Perform positioning processing using this lookup table's subtables.
662          * @param gs an input glyph sequence
663          * @param script a script identifier
664          * @param language a language identifier
665          * @param feature a feature identifier
666          * @param fontSize size in device units
667          * @param widths array of default advancements for each glyph in font
668          * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
669          * with one 4-tuple for each element of glyph sequence
670          * @param sct a script specific context tester (or null)
671          * @return true if some adjustment is not zero; otherwise, false
672          */
position(GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct)673         public boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) {
674             if (performsPositioning()) {
675                 return GlyphPositioningSubtable.position(gs, script, language, feature, fontSize, (GlyphPositioningSubtable[]) subtablesArray, widths, adjustments, sct);
676             } else {
677                 return false;
678             }
679         }
680 
681         /**
682          * Perform positioning processing on an existing glyph positioning state object using this lookup table's subtables.
683          * @param ps a glyph positioning state object
684          * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
685          * @return true if some adjustment is not zero; otherwise, false
686          */
position(GlyphPositioningState ps, int sequenceIndex)687         public boolean position(GlyphPositioningState ps, int sequenceIndex) {
688             if (performsPositioning()) {
689                 return GlyphPositioningSubtable.position(ps, (GlyphPositioningSubtable[]) subtablesArray, sequenceIndex);
690             } else {
691                 return false;
692             }
693         }
694 
695         /** {@inheritDoc} */
hashCode()696         public int hashCode() {
697             return idOrdinal;
698         }
699 
700         /**
701          * {@inheritDoc}
702          * @return true if identifier of the specified lookup table is the same
703          * as the identifier of this lookup table
704          */
equals(Object o)705         public boolean equals(Object o) {
706             if (o instanceof LookupTable) {
707                 LookupTable lt = (LookupTable) o;
708                 return idOrdinal == lt.idOrdinal;
709             } else {
710                 return false;
711             }
712         }
713 
714         /**
715          * {@inheritDoc}
716          * @return the result of comparing the identifier of the specified lookup table with
717          * the identifier of this lookup table; lookup table identifiers take the form
718          * "lu(DIGIT)+", with comparison based on numerical ordering of numbers expressed by
719          * (DIGIT)+.
720          */
compareTo(Object o)721         public int compareTo(Object o) {
722             if (o instanceof LookupTable) {
723                 LookupTable lt = (LookupTable) o;
724                 int i = idOrdinal;
725                 int j = lt.idOrdinal;
726                 if (i < j) {
727                     return -1;
728                 } else if (i > j) {
729                     return 1;
730                 } else {
731                     return 0;
732                 }
733             } else {
734                 return -1;
735             }
736         }
737 
738         /** {@inheritDoc} */
toString()739         public String toString() {
740             StringBuffer sb = new StringBuffer();
741             sb.append("{ ");
742             sb.append("id = " + id);
743             sb.append(", subtables = " + subtables);
744             sb.append(" }");
745             return sb.toString();
746         }
747 
makeSingleton(GlyphSubtable subtable)748         private static List<GlyphSubtable> makeSingleton(GlyphSubtable subtable) {
749             if (subtable == null) {
750                 return null;
751             } else {
752                 List<GlyphSubtable> stl = new ArrayList<GlyphSubtable>(1);
753                 stl.add(subtable);
754                 return stl;
755             }
756         }
757 
758     }
759 
760     /**
761      * The <code>UseSpec</code> class comprises a lookup table reference
762      * and the feature that selected the lookup table.
763      */
764     public static class UseSpec implements Comparable {
765 
766         /** lookup table to apply */
767         private final LookupTable lookupTable;
768         /** feature that caused selection of the lookup table */
769         private final String feature;
770 
771         /**
772          * Construct a glyph lookup table use specification.
773          * @param lookupTable a glyph lookup table
774          * @param feature a feature that caused lookup table selection
775          */
UseSpec(LookupTable lookupTable, String feature)776         public UseSpec(LookupTable lookupTable, String feature) {
777             this.lookupTable = lookupTable;
778             this.feature = feature;
779         }
780 
781         /** @return the lookup table */
getLookupTable()782         public LookupTable getLookupTable() {
783             return lookupTable;
784         }
785 
786         /** @return the feature that selected this lookup table */
getFeature()787         public String getFeature() {
788             return feature;
789         }
790 
791         /**
792          * Perform substitution processing using this use specification's lookup table.
793          * @param gs an input glyph sequence
794          * @param script a script identifier
795          * @param language a language identifier
796          * @param sct a script specific context tester (or null)
797          * @return the substituted (output) glyph sequence
798          */
substitute(GlyphSequence gs, String script, String language, ScriptContextTester sct)799         public GlyphSequence substitute(GlyphSequence gs, String script, String language, ScriptContextTester sct) {
800             return lookupTable.substitute(gs, script, language, feature, sct);
801         }
802 
803         /**
804          * Perform positioning processing using this use specification's lookup table.
805          * @param gs an input glyph sequence
806          * @param script a script identifier
807          * @param language a language identifier
808          * @param fontSize size in device units
809          * @param widths array of default advancements for each glyph in font
810          * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
811          * with one 4-tuple for each element of glyph sequence
812          * @param sct a script specific context tester (or null)
813          * @return true if some adjustment is not zero; otherwise, false
814          */
position(GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct)815         public boolean position(GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) {
816             return lookupTable.position(gs, script, language, feature, fontSize, widths, adjustments, sct);
817         }
818 
819         /** {@inheritDoc} */
hashCode()820         public int hashCode() {
821             return lookupTable.hashCode();
822         }
823 
824         /** {@inheritDoc} */
equals(Object o)825         public boolean equals(Object o) {
826             if (o instanceof UseSpec) {
827                 UseSpec u = (UseSpec) o;
828                 return lookupTable.equals(u.lookupTable);
829             } else {
830                 return false;
831             }
832         }
833 
834         /** {@inheritDoc} */
compareTo(Object o)835         public int compareTo(Object o) {
836             if (o instanceof UseSpec) {
837                 UseSpec u = (UseSpec) o;
838                 return lookupTable.compareTo(u.lookupTable);
839             } else {
840                 return -1;
841             }
842         }
843 
844     }
845 
846     /**
847      * The <code>RuleLookup</code> class implements a rule lookup record, comprising
848      * a glyph sequence index and a lookup table index (in an applicable lookup list).
849      */
850     public static class RuleLookup {
851 
852         private final int sequenceIndex;                        // index into input glyph sequence
853         private final int lookupIndex;                          // lookup list index
854         private LookupTable lookup;                             // resolved lookup table
855 
856         /**
857          * Instantiate a RuleLookup.
858          * @param sequenceIndex the index into the input sequence
859          * @param lookupIndex the lookup table index
860          */
RuleLookup(int sequenceIndex, int lookupIndex)861         public RuleLookup(int sequenceIndex, int lookupIndex) {
862             this.sequenceIndex = sequenceIndex;
863             this.lookupIndex = lookupIndex;
864             this.lookup = null;
865         }
866 
867         /** @return the sequence index */
getSequenceIndex()868         public int getSequenceIndex() {
869             return sequenceIndex;
870         }
871 
872         /** @return the lookup index */
getLookupIndex()873         public int getLookupIndex() {
874             return lookupIndex;
875         }
876 
877         /** @return the lookup table */
getLookup()878         public LookupTable getLookup() {
879             return lookup;
880         }
881 
882         /**
883          * Resolve references to lookup tables.
884          * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
885          */
resolveLookupReferences(Map<String, LookupTable> lookupTables)886         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
887             if (lookupTables != null) {
888                 String lid = "lu" + Integer.toString(lookupIndex);
889                 LookupTable lt = lookupTables.get(lid);
890                 if (lt != null) {
891                     this.lookup = lt;
892                 } else {
893                     log.warn("unable to resolve glyph lookup table reference '" + lid + "' amongst lookup tables: " + lookupTables.values());
894                 }
895             }
896         }
897 
898         /** {@inheritDoc} */
toString()899         public String toString() {
900             return "{ sequenceIndex = " + sequenceIndex + ", lookupIndex = " + lookupIndex + " }";
901         }
902 
903     }
904 
905     /**
906      * The <code>Rule</code> class implements an array of rule lookup records.
907      */
908     public abstract static class Rule {
909 
910         private final RuleLookup[] lookups;                     // rule lookups
911         private final int inputSequenceLength;                  // input sequence length
912 
913         /**
914          * Instantiate a Rule.
915          * @param lookups the rule's lookups
916          * @param inputSequenceLength the number of glyphs in the input sequence for this rule
917          */
Rule(RuleLookup[] lookups, int inputSequenceLength)918         protected Rule(RuleLookup[] lookups, int inputSequenceLength) {
919             assert lookups != null;
920             this.lookups = lookups;
921             this.inputSequenceLength = inputSequenceLength;
922         }
923 
924         /** @return the lookups */
getLookups()925         public RuleLookup[] getLookups() {
926             return lookups;
927         }
928 
929         /** @return the input sequence length */
getInputSequenceLength()930         public int getInputSequenceLength() {
931             return inputSequenceLength;
932         }
933 
934         /**
935          * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
936          * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
937          */
resolveLookupReferences(Map<String, LookupTable> lookupTables)938         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
939             if (lookups != null) {
940                 for (RuleLookup l : lookups) {
941                     if (l != null) {
942                         l.resolveLookupReferences(lookupTables);
943                     }
944                 }
945             }
946         }
947 
948         /** {@inheritDoc} */
toString()949         public String toString() {
950             return "{ lookups = " + Arrays.toString(lookups) + ", inputSequenceLength = " + inputSequenceLength + " }";
951         }
952 
953     }
954 
955     /**
956      * The <code>GlyphSequenceRule</code> class implements a subclass of <code>Rule</code>
957      * that supports matching on a specific glyph sequence.
958      */
959     public static class GlyphSequenceRule extends Rule {
960 
961         private final int[] glyphs;                             // glyphs
962 
963         /**
964          * Instantiate a GlyphSequenceRule.
965          * @param lookups the rule's lookups
966          * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
967          * @param glyphs the rule's glyph sequence to match, starting with second glyph in sequence
968          */
GlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs)969         public GlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs) {
970             super(lookups, inputSequenceLength);
971             assert glyphs != null;
972             this.glyphs = glyphs;
973         }
974 
975         /**
976          * Obtain glyphs. N.B. that this array starts with the second
977          * glyph of the input sequence.
978          * @return the glyphs
979          */
getGlyphs()980         public int[] getGlyphs() {
981             return glyphs;
982         }
983 
984         /**
985          * Obtain glyphs augmented by specified first glyph entry.
986          * @param firstGlyph to fill in first glyph entry
987          * @return the glyphs augmented by first glyph
988          */
getGlyphs(int firstGlyph)989         public int[] getGlyphs(int firstGlyph) {
990             int[] ga = new int [ glyphs.length + 1 ];
991             ga [ 0 ] = firstGlyph;
992             System.arraycopy(glyphs, 0, ga, 1, glyphs.length);
993             return ga;
994         }
995 
996         /** {@inheritDoc} */
toString()997         public String toString() {
998             StringBuffer sb = new StringBuffer();
999             sb.append("{ ");
1000             sb.append("lookups = " + Arrays.toString(getLookups()));
1001             sb.append(", glyphs = " + Arrays.toString(glyphs));
1002             sb.append(" }");
1003             return sb.toString();
1004         }
1005 
1006     }
1007 
1008     /**
1009      * The <code>ClassSequenceRule</code> class implements a subclass of <code>Rule</code>
1010      * that supports matching on a specific glyph class sequence.
1011      */
1012     public static class ClassSequenceRule extends Rule {
1013 
1014         private final int[] classes;                            // glyph classes
1015 
1016         /**
1017          * Instantiate a ClassSequenceRule.
1018          * @param lookups the rule's lookups
1019          * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
1020          * @param classes the rule's glyph class sequence to match, starting with second glyph in sequence
1021          */
ClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes)1022         public ClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes) {
1023             super(lookups, inputSequenceLength);
1024             assert classes != null;
1025             this.classes = classes;
1026         }
1027 
1028         /**
1029          * Obtain glyph classes. N.B. that this array starts with the class of the second
1030          * glyph of the input sequence.
1031          * @return the classes
1032          */
getClasses()1033         public int[] getClasses() {
1034             return classes;
1035         }
1036 
1037         /**
1038          * Obtain glyph classes augmented by specified first class entry.
1039          * @param firstClass to fill in first class entry
1040          * @return the classes augmented by first class
1041          */
getClasses(int firstClass)1042         public int[] getClasses(int firstClass) {
1043             int[] ca = new int [ classes.length + 1 ];
1044             ca [ 0 ] = firstClass;
1045             System.arraycopy(classes, 0, ca, 1, classes.length);
1046             return ca;
1047         }
1048 
1049         /** {@inheritDoc} */
toString()1050         public String toString() {
1051             StringBuffer sb = new StringBuffer();
1052             sb.append("{ ");
1053             sb.append("lookups = " + Arrays.toString(getLookups()));
1054             sb.append(", classes = " + Arrays.toString(classes));
1055             sb.append(" }");
1056             return sb.toString();
1057         }
1058 
1059     }
1060 
1061     /**
1062      * The <code>CoverageSequenceRule</code> class implements a subclass of <code>Rule</code>
1063      * that supports matching on a specific glyph coverage sequence.
1064      */
1065     public static class CoverageSequenceRule extends Rule {
1066 
1067         private final GlyphCoverageTable[] coverages;           // glyph coverages
1068 
1069         /**
1070          * Instantiate a ClassSequenceRule.
1071          * @param lookups the rule's lookups
1072          * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
1073          * @param coverages the rule's glyph coverage sequence to match, starting with first glyph in sequence
1074          */
CoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages)1075         public CoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages) {
1076             super(lookups, inputSequenceLength);
1077             assert coverages != null;
1078             this.coverages = coverages;
1079         }
1080 
1081         /** @return the coverages */
getCoverages()1082         public GlyphCoverageTable[] getCoverages() {
1083             return coverages;
1084         }
1085 
1086         /** {@inheritDoc} */
toString()1087         public String toString() {
1088             StringBuffer sb = new StringBuffer();
1089             sb.append("{ ");
1090             sb.append("lookups = " + Arrays.toString(getLookups()));
1091             sb.append(", coverages = " + Arrays.toString(coverages));
1092             sb.append(" }");
1093             return sb.toString();
1094         }
1095 
1096     }
1097 
1098     /**
1099      * The <code>ChainedGlyphSequenceRule</code> class implements a subclass of <code>GlyphSequenceRule</code>
1100      * that supports matching on a specific glyph sequence in a specific chained contextual.
1101      */
1102     public static class ChainedGlyphSequenceRule extends GlyphSequenceRule {
1103 
1104         private final int[] backtrackGlyphs;                    // backtrack glyphs
1105         private final int[] lookaheadGlyphs;                    // lookahead glyphs
1106 
1107         /**
1108          * Instantiate a ChainedGlyphSequenceRule.
1109          * @param lookups the rule's lookups
1110          * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
1111          * @param glyphs the rule's input glyph sequence to match, starting with second glyph in sequence
1112          * @param backtrackGlyphs the rule's backtrack glyph sequence to match, starting with first glyph in sequence
1113          * @param lookaheadGlyphs the rule's lookahead glyph sequence to match, starting with first glyph in sequence
1114          */
ChainedGlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs, int[] lookaheadGlyphs)1115         public ChainedGlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs, int[] lookaheadGlyphs) {
1116             super(lookups, inputSequenceLength, glyphs);
1117             assert backtrackGlyphs != null;
1118             assert lookaheadGlyphs != null;
1119             this.backtrackGlyphs = backtrackGlyphs;
1120             this.lookaheadGlyphs = lookaheadGlyphs;
1121         }
1122 
1123         /** @return the backtrack glyphs */
getBacktrackGlyphs()1124         public int[] getBacktrackGlyphs() {
1125             return backtrackGlyphs;
1126         }
1127 
1128         /** @return the lookahead glyphs */
getLookaheadGlyphs()1129         public int[] getLookaheadGlyphs() {
1130             return lookaheadGlyphs;
1131         }
1132 
1133         /** {@inheritDoc} */
toString()1134         public String toString() {
1135             StringBuffer sb = new StringBuffer();
1136             sb.append("{ ");
1137             sb.append("lookups = " + Arrays.toString(getLookups()));
1138             sb.append(", glyphs = " + Arrays.toString(getGlyphs()));
1139             sb.append(", backtrackGlyphs = " + Arrays.toString(backtrackGlyphs));
1140             sb.append(", lookaheadGlyphs = " + Arrays.toString(lookaheadGlyphs));
1141             sb.append(" }");
1142             return sb.toString();
1143         }
1144 
1145     }
1146 
1147     /**
1148      * The <code>ChainedClassSequenceRule</code> class implements a subclass of <code>ClassSequenceRule</code>
1149      * that supports matching on a specific glyph class sequence in a specific chained contextual.
1150      */
1151     public static class ChainedClassSequenceRule extends ClassSequenceRule {
1152 
1153         private final int[] backtrackClasses;                    // backtrack classes
1154         private final int[] lookaheadClasses;                    // lookahead classes
1155 
1156         /**
1157          * Instantiate a ChainedClassSequenceRule.
1158          * @param lookups the rule's lookups
1159          * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
1160          * @param classes the rule's input glyph class sequence to match, starting with second glyph in sequence
1161          * @param backtrackClasses the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
1162          * @param lookaheadClasses the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
1163          */
ChainedClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes, int[] backtrackClasses, int[] lookaheadClasses)1164         public ChainedClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes, int[] backtrackClasses, int[] lookaheadClasses) {
1165             super(lookups, inputSequenceLength, classes);
1166             assert backtrackClasses != null;
1167             assert lookaheadClasses != null;
1168             this.backtrackClasses = backtrackClasses;
1169             this.lookaheadClasses = lookaheadClasses;
1170         }
1171 
1172         /** @return the backtrack classes */
getBacktrackClasses()1173         public int[] getBacktrackClasses() {
1174             return backtrackClasses;
1175         }
1176 
1177         /** @return the lookahead classes */
getLookaheadClasses()1178         public int[] getLookaheadClasses() {
1179             return lookaheadClasses;
1180         }
1181 
1182         /** {@inheritDoc} */
toString()1183         public String toString() {
1184             StringBuffer sb = new StringBuffer();
1185             sb.append("{ ");
1186             sb.append("lookups = " + Arrays.toString(getLookups()));
1187             sb.append(", classes = " + Arrays.toString(getClasses()));
1188             sb.append(", backtrackClasses = " + Arrays.toString(backtrackClasses));
1189             sb.append(", lookaheadClasses = " + Arrays.toString(lookaheadClasses));
1190             sb.append(" }");
1191             return sb.toString();
1192         }
1193 
1194     }
1195 
1196     /**
1197      * The <code>ChainedCoverageSequenceRule</code> class implements a subclass of <code>CoverageSequenceRule</code>
1198      * that supports matching on a specific glyph class sequence in a specific chained contextual.
1199      */
1200     public static class ChainedCoverageSequenceRule extends CoverageSequenceRule {
1201 
1202         private final GlyphCoverageTable[] backtrackCoverages;  // backtrack coverages
1203         private final GlyphCoverageTable[] lookaheadCoverages;  // lookahead coverages
1204 
1205         /**
1206          * Instantiate a ChainedCoverageSequenceRule.
1207          * @param lookups the rule's lookups
1208          * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
1209          * @param coverages the rule's input glyph class sequence to match, starting with first glyph in sequence
1210          * @param backtrackCoverages the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
1211          * @param lookaheadCoverages the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
1212          */
ChainedCoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages, GlyphCoverageTable[] backtrackCoverages, GlyphCoverageTable[] lookaheadCoverages)1213         public ChainedCoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages, GlyphCoverageTable[] backtrackCoverages, GlyphCoverageTable[] lookaheadCoverages) {
1214             super(lookups, inputSequenceLength, coverages);
1215             assert backtrackCoverages != null;
1216             assert lookaheadCoverages != null;
1217             this.backtrackCoverages = backtrackCoverages;
1218             this.lookaheadCoverages = lookaheadCoverages;
1219         }
1220 
1221         /** @return the backtrack coverages */
getBacktrackCoverages()1222         public GlyphCoverageTable[] getBacktrackCoverages() {
1223             return backtrackCoverages;
1224         }
1225 
1226         /** @return the lookahead coverages */
getLookaheadCoverages()1227         public GlyphCoverageTable[] getLookaheadCoverages() {
1228             return lookaheadCoverages;
1229         }
1230 
1231         /** {@inheritDoc} */
toString()1232         public String toString() {
1233             StringBuffer sb = new StringBuffer();
1234             sb.append("{ ");
1235             sb.append("lookups = " + Arrays.toString(getLookups()));
1236             sb.append(", coverages = " + Arrays.toString(getCoverages()));
1237             sb.append(", backtrackCoverages = " + Arrays.toString(backtrackCoverages));
1238             sb.append(", lookaheadCoverages = " + Arrays.toString(lookaheadCoverages));
1239             sb.append(" }");
1240             return sb.toString();
1241         }
1242 
1243     }
1244 
1245     /**
1246      * The <code>RuleSet</code> class implements a collection of rules, which
1247      * may or may not be the same rule type.
1248      */
1249     public static class RuleSet {
1250 
1251         private final Rule[] rules;                             // set of rules
1252 
1253         /**
1254          * Instantiate a Rule Set.
1255          * @param rules the rules
1256          * @throws AdvancedTypographicTableFormatException if rules or some element of rules is null
1257          */
RuleSet(Rule[] rules)1258         public RuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException {
1259             // enforce rules array instance
1260             if (rules == null) {
1261                 throw new AdvancedTypographicTableFormatException("rules[] is null");
1262             }
1263             this.rules = rules;
1264         }
1265 
1266         /** @return the rules */
getRules()1267         public Rule[] getRules() {
1268             return rules;
1269         }
1270 
1271         /**
1272          * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
1273          * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
1274          */
resolveLookupReferences(Map<String, LookupTable> lookupTables)1275         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1276             if (rules != null) {
1277                 for (Rule r : rules) {
1278                     if (r != null) {
1279                         r.resolveLookupReferences(lookupTables);
1280                     }
1281                 }
1282             }
1283         }
1284 
1285         /** {@inheritDoc} */
toString()1286         public String toString() {
1287             return "{ rules = " + Arrays.toString(rules) + " }";
1288         }
1289 
1290     }
1291 
1292     /**
1293      * The <code>HomogenousRuleSet</code> class implements a collection of rules, which
1294      * must be the same rule type (i.e., same concrete rule class) or null.
1295      */
1296     public static class HomogeneousRuleSet extends RuleSet {
1297 
1298         /**
1299          * Instantiate a Homogeneous Rule Set.
1300          * @param rules the rules
1301          * @throws AdvancedTypographicTableFormatException if some rule[i] is not an instance of rule[0]
1302          */
HomogeneousRuleSet(Rule[] rules)1303         public HomogeneousRuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException {
1304             super(rules);
1305             // find first non-null rule
1306             Rule r0 = null;
1307             for (int i = 1, n = rules.length; (r0 == null) && (i < n); i++) {
1308                 if (rules[i] != null) {
1309                     r0 = rules[i];
1310                 }
1311             }
1312             // enforce rule instance homogeneity
1313             if (r0 != null) {
1314                 Class c = r0.getClass();
1315                 for (int i = 1, n = rules.length; i < n; i++) {
1316                     Rule r = rules[i];
1317                     if ((r != null) && !c.isInstance(r)) {
1318                         throw new AdvancedTypographicTableFormatException("rules[" + i + "] is not an instance of " + c.getName());
1319                     }
1320                 }
1321             }
1322 
1323         }
1324 
1325     }
1326 
1327 }
1328