• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.tool;
2 
3 import java.io.FileNotFoundException;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.Collections;
7 import java.util.EnumMap;
8 import java.util.EnumSet;
9 import java.util.HashSet;
10 import java.util.LinkedHashSet;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Set;
14 import java.util.regex.Matcher;
15 
16 import org.unicode.cldr.util.CLDRConfig;
17 import org.unicode.cldr.util.CldrUtility;
18 import org.unicode.cldr.util.DtdData;
19 import org.unicode.cldr.util.DtdData.Attribute;
20 import org.unicode.cldr.util.DtdData.AttributeStatus;
21 import org.unicode.cldr.util.DtdData.Element;
22 import org.unicode.cldr.util.DtdType;
23 import org.unicode.cldr.util.SupplementalDataInfo;
24 
25 import com.google.common.base.Joiner;
26 import com.google.common.base.MoreObjects;
27 import com.google.common.base.Splitter;
28 import com.google.common.collect.ImmutableMultimap;
29 import com.google.common.collect.ImmutableSet;
30 import com.google.common.collect.Multimap;
31 import com.ibm.icu.impl.Utility;
32 import com.ibm.icu.util.VersionInfo;
33 
34 /**
35  * Changed ShowDtdDiffs into a chart.
36  * @author markdavis
37  */
38 public class ChartDtdDelta extends Chart {
39 
40     private static final Splitter SPLITTER_SPACE = Splitter.on(' ');
41 
42     private static final String DEPRECATED_PREFIX = "⊖";
43 
44     private static final String NEW_PREFIX = "+";
45     private static final String ORDERED_SIGN = "⇣";
46     private static final String UNORDERED_SIGN = "⇟";
47 
48 
49     private static final Set<String> OMITTED_ATTRIBUTES = Collections.singleton("⊕");
50 
main(String[] args)51     public static void main(String[] args) {
52         new ChartDtdDelta().writeChart(null);
53     }
54 
55     @Override
getDirectory()56     public String getDirectory() {
57         return FormattedFileWriter.CHART_TARGET_DIR;
58     }
59 
60     @Override
getTitle()61     public String getTitle() {
62         return "DTD Deltas";
63     }
64 
65     @Override
getExplanation()66     public String getExplanation() {
67         return "<p>Changes to the LDML DTDs over time.</p>\n"
68             + "<ul>\n"
69             + "<li>New elements or attributes are indicated with a + sign, and newly deprecated ones with a ⊖ sign.</li>\n"
70             + "<li>Element attributes are abbreviated as ⊕ where is no change to them, "
71             + "but the element is newly the child of another.</li>\n"
72             + "<li>LDML DTDs have augmented data:\n"
73             + "<ul><li>Attribute status is marked by: "
74             + AttributeStatus.distinguished.shortName + "=" + AttributeStatus.distinguished + ", "
75             + AttributeStatus.value.shortName + "=" + AttributeStatus.value + ", or "
76             + AttributeStatus.metadata.shortName + "=" + AttributeStatus.metadata + ".</li>\n"
77             + "<li>Attribute value constraints are marked with ⟨…⟩ (for DTD constraints) and ⟪…⟫ (for augmented constraints, added in v35.0).</li>\n"
78             + "<li>Changes in status or constraints are shown with ➠, with identical sections shown with ….</li>\n"
79             + "<li>Newly ordered elements are indicated with " + ORDERED_SIGN + "; newly unordered with " + UNORDERED_SIGN + ".</li>\n"
80             + "</ul></li></ul>\n"
81             + "<p>For more information, see the LDML spec.</p>";
82     }
83 
84     @Override
writeContents(FormattedFileWriter pw)85     public void writeContents(FormattedFileWriter pw) throws IOException {
86         TablePrinter tablePrinter = new TablePrinter()
87             .addColumn("Version", "class='source'", CldrUtility.getDoubleLinkMsg(), "class='source'", true)
88             .setSortPriority(0)
89             .setSortAscending(false)
90             .setBreakSpans(true)
91             .addColumn("Dtd Type", "class='source'", null, "class='source'", true)
92             .setSortPriority(1)
93 
94             .addColumn("Intermediate Path", "class='source'", null, "class='target'", true)
95             .setSortPriority(2)
96 
97             .addColumn("Element", "class='target'", null, "class='target'", true)
98             .setSpanRows(false)
99             .addColumn("Attributes", "class='target'", null, "class='target'", true)
100             .setSpanRows(false);
101 
102         String last = null;
103 
104         for (String current : ToolConstants.CLDR_RELEASE_AND_DEV_VERSION_SET) {
105             System.out.println("DTD delta: " + current);
106             final boolean finalVersion = current.equals(ToolConstants.DEV_VERSION);
107             String currentName = finalVersion ? ToolConstants.CHART_DISPLAY_VERSION : current;
108             for (DtdType type : TYPES) {
109                 String firstVersion = type.firstVersion; // FIRST_VERSION.get(type);
110                 if (firstVersion != null && current != null && current.compareTo(firstVersion) < 0) {
111                     continue;
112                 }
113                 DtdData dtdCurrent = null;
114                 try {
115                     dtdCurrent = DtdData.getInstance(type,
116                         finalVersion
117                         // && ToolConstants.CHART_STATUS != ToolConstants.ChartStatus.release
118                         ? null
119                             : current);
120                 } catch (Exception e) {
121                     if (!(e.getCause() instanceof FileNotFoundException)) {
122                         throw e;
123                     }
124                     System.out.println(e.getMessage() + ", " + e.getCause().getMessage());
125                     continue;
126                 }
127                 DtdData dtdLast = null;
128                 if (last != null) {
129                     try {
130                         dtdLast = DtdData.getInstance(type, last);
131                     } catch (Exception e) {
132                         if (!(e.getCause() instanceof FileNotFoundException)) {
133                             throw e;
134                         }
135                     }
136                 }
137                 diff(currentName, dtdLast, dtdCurrent);
138             }
139             last = current;
140             if (current.contentEquals(ToolConstants.CHART_VERSION)) {
141                 break;
142             }
143         }
144 
145         for (DiffElement datum : data) {
146             tablePrinter.addRow()
147             .addCell(datum.getVersionString())
148             .addCell(datum.dtdType)
149             .addCell(datum.newPath)
150             .addCell(datum.newElement)
151             .addCell(datum.attributeNames)
152             .finishRow();
153         }
154         pw.write(tablePrinter.toTable());
155         pw.write(Utility.repeat("<br>", 50));
156     }
157 
158     static final String NONE = " ";
159 
160     static final SupplementalDataInfo SDI = CLDRConfig.getInstance().getSupplementalDataInfo();
161 
162     static Set<DtdType> TYPES = EnumSet.allOf(DtdType.class);
163     static {
164         TYPES.remove(DtdType.ldmlICU);
165     }
166 
167     static final Map<DtdType, String> FIRST_VERSION = new EnumMap<>(DtdType.class);
168     static {
FIRST_VERSION.put(DtdType.ldmlBCP47, "1.7.2")169         FIRST_VERSION.put(DtdType.ldmlBCP47, "1.7.2");
FIRST_VERSION.put(DtdType.keyboard, "22.1")170         FIRST_VERSION.put(DtdType.keyboard, "22.1");
FIRST_VERSION.put(DtdType.platform, "22.1")171         FIRST_VERSION.put(DtdType.platform, "22.1");
172     }
173 
diff(String prefix, DtdData dtdLast, DtdData dtdCurrent)174     private void diff(String prefix, DtdData dtdLast, DtdData dtdCurrent) {
175         Map<String, Element> oldNameToElement = dtdLast == null ? Collections.emptyMap() : dtdLast.getElementFromName();
176         checkNames(prefix, dtdCurrent, dtdLast, oldNameToElement, "/", dtdCurrent.ROOT, new HashSet<Element>(), false);
177     }
178 
179     static final DtdType DEBUG_DTD = null; // set to enable
180     static final String DEBUG_ELEMENT = "lias";
181     static final boolean SHOW = false;
182 
183     @SuppressWarnings("unused")
checkNames(String version, DtdData dtdCurrent, DtdData dtdLast, Map<String, Element> oldNameToElement, String path, Element element, HashSet<Element> seen, boolean showAnyway)184     private void checkNames(String version, DtdData dtdCurrent, DtdData dtdLast, Map<String, Element> oldNameToElement, String path, Element element,
185         HashSet<Element> seen, boolean showAnyway) {
186         String name = element.getName();
187 
188         if (SKIP_ELEMENTS.contains(name)) {
189             return;
190         }
191         if (SKIP_TYPE_ELEMENTS.containsEntry(dtdCurrent.dtdType, name)) {
192             return;
193         }
194 
195         String newPath = path + "/" + element.name;
196 
197         // if an element is newly a child of another but has already been seen, you'll have special indication
198         if (seen.contains(element)) {
199             if (showAnyway) {
200                 addData(dtdCurrent, NEW_PREFIX + name, version, newPath, OMITTED_ATTRIBUTES);
201             }
202             return;
203         }
204 
205         seen.add(element);
206         if (SHOW && ToolConstants.CHART_DISPLAY_VERSION.equals(version)) {
207             System.out.println(dtdCurrent.dtdType + "\t" + name);
208         }
209         if (DEBUG_DTD == dtdCurrent.dtdType && name.contains(DEBUG_ELEMENT)) {
210             int debug = 0;
211         }
212 
213 
214         Element oldElement = null;
215         boolean ordered = element.isOrdered();
216 
217         if (!oldNameToElement.containsKey(name)) {
218             Set<String> attributeNames = getAttributeNames(dtdCurrent, dtdLast, name, Collections.emptyMap(), element.getAttributes());
219             addData(dtdCurrent, NEW_PREFIX + name + (ordered ? ORDERED_SIGN : ""), version, newPath, attributeNames);
220         } else {
221             oldElement = oldNameToElement.get(name);
222             boolean oldOrdered = oldElement.isOrdered();
223             Set<String> attributeNames = getAttributeNames(dtdCurrent, dtdLast, name, oldElement.getAttributes(), element.getAttributes());
224             boolean currentDeprecated = element.isDeprecated();
225             boolean lastDeprecated = dtdLast == null ? false : oldElement.isDeprecated(); //  + (currentDeprecated ? "ⓓ" : "")
226             boolean newlyDeprecated = currentDeprecated && !lastDeprecated;
227             String orderingStatus = (ordered == oldOrdered || currentDeprecated) ? "" : ordered ? ORDERED_SIGN : UNORDERED_SIGN;
228             if (newlyDeprecated) {
229                 addData(dtdCurrent, DEPRECATED_PREFIX + name + orderingStatus, version, newPath, Collections.emptySet());
230             }
231             if (!attributeNames.isEmpty()) {
232                 addData(dtdCurrent, (newlyDeprecated ? DEPRECATED_PREFIX : "") + name + orderingStatus, version, newPath, attributeNames);
233             }
234         }
235         if (element.getName().equals("coordinateUnit")) {
236             System.out.println(version + "\toordinateUnit\t" + element.getChildren().keySet());
237         }
238         Set<Element> oldChildren = oldElement == null ? Collections.emptySet() : oldElement.getChildren().keySet();
239         for (Element child : element.getChildren().keySet()) {
240             showAnyway = true;
241             for (Element oldChild : oldChildren) {
242                 if (oldChild.getName().equals(child.getName())) {
243                     showAnyway = false;
244                     break;
245                 }
246             }
247             checkNames(version, dtdCurrent, dtdLast, oldNameToElement, newPath, child, seen, showAnyway);
248         }
249     }
250 
251     enum DiffType {
252         Element, Attribute, AttributeValue
253     }
254 
255     private static class DiffElement {
256 
257         private static final String START_ATTR = "<div>";
258         private static final String END_ATTR = "</div>";
259         final VersionInfo version;
260         final DtdType dtdType;
261         final boolean isBeta;
262         final String newPath;
263         final String newElement;
264         final String attributeNames;
265 
DiffElement(DtdData dtdCurrent, String version, String newPath, String newElement, Set<String> attributeNames2)266         public DiffElement(DtdData dtdCurrent, String version, String newPath, String newElement, Set<String> attributeNames2) {
267             isBeta = version.endsWith("β");
268             try {
269                 this.version = isBeta ? VersionInfo.getInstance(version.substring(0, version.length() - 1)) : VersionInfo.getInstance(version);
270             } catch (Exception e) {
271                 e.printStackTrace();
272                 throw e;
273             }
274             dtdType = dtdCurrent.dtdType;
275             this.newPath = fix(newPath);
276             this.attributeNames = attributeNames2.isEmpty() ? NONE :
277                 START_ATTR + Joiner.on(END_ATTR + START_ATTR).join(attributeNames2) + END_ATTR;
278             this.newElement = newElement;
279         }
280 
fix(String substring)281         private String fix(String substring) {
282             int base = substring.indexOf('/', 2);
283             if (base < 0) return "";
284             int last = substring.lastIndexOf('/');
285             if (last <= base) return "/";
286             substring = substring.substring(base, last);
287             return substring.replace("/", "\u200B/") + "/";
288         }
289 
290         @Override
toString()291         public String toString() {
292             return MoreObjects.toStringHelper(this)
293                 .add("version", getVersionString())
294                 .add("dtdType", dtdType)
295                 .add("newPath", newPath)
296                 .add("newElement", newElement)
297                 .add("attributeNames", attributeNames)
298                 .toString();
299         }
300 
getVersionString()301         private String getVersionString() {
302             return version.getVersionString(2, 4) + (isBeta ? "β" : "");
303         }
304     }
305 
306     List<DiffElement> data = new ArrayList<>();
307 
addData(DtdData dtdCurrent, String element, String prefix, String newPath, Set<String> attributeNames)308     private void addData(DtdData dtdCurrent, String element, String prefix, String newPath, Set<String> attributeNames) {
309         DiffElement item = new DiffElement(dtdCurrent, prefix, newPath, element, attributeNames);
310         data.add(item);
311     }
312 
313     static final Set<String> SKIP_ELEMENTS = ImmutableSet.of("generation", "identity", "special"); // , "telephoneCodeData"
314 
315     static final Multimap<DtdType, String> SKIP_TYPE_ELEMENTS = ImmutableMultimap.of(DtdType.ldml, "alias");
316 
317     static final Set<String> SKIP_ATTRIBUTES = ImmutableSet.of("references", "standard", "draft", "alt");
318 
getAttributeNames(DtdData dtdCurrent, DtdData dtdLast, String elementName, Map<Attribute, Integer> attributesOld, Map<Attribute, Integer> attributes)319     private static Set<String> getAttributeNames(DtdData dtdCurrent, DtdData dtdLast, String elementName,
320         Map<Attribute, Integer> attributesOld,
321         Map<Attribute, Integer> attributes) {
322         Set<String> names = new LinkedHashSet<>();
323         if (elementName.equals("coordinateUnit")) {
324             int debug = 0;
325         }
326 
327         main:
328             // we want to add a name that is new or that becomes deprecated
329             for (Attribute attribute : attributes.keySet()) {
330                 String name = attribute.getName();
331                 if (SKIP_ATTRIBUTES.contains(name)) {
332                     continue;
333                 }
334                 String match = attribute.getMatchString();
335                 AttributeStatus status = attribute.attributeStatus;
336                 String display = NEW_PREFIX + name;
337 //            if (isDeprecated(dtdCurrent, elementName, name)) { // SDI.isDeprecated(dtdCurrent, elementName, name, "*")) {
338 //                continue;
339 //            }
340                 String oldMatch = "?";
341                 String pre, post;
342                 Attribute attributeOld = attribute.getMatchingName(attributesOld);
343                 if (attributeOld == null) {
344                     display = NEW_PREFIX + name +  " " + AttributeStatus.getShortName(status) + " " + match;
345                 } else if (attribute.isDeprecated() && !attributeOld.isDeprecated()) {
346                     display = DEPRECATED_PREFIX + name;
347                 } else {
348                     oldMatch = attributeOld.getMatchString();
349                     AttributeStatus oldStatus = attributeOld.attributeStatus;
350 
351                     boolean matchEquals = match.equals(oldMatch);
352                     if (status != oldStatus) {
353                         pre = AttributeStatus.getShortName(oldStatus);
354                         post = AttributeStatus.getShortName(status);
355                         if (!matchEquals) {
356                             pre += " " + oldMatch;
357                             post += " " + match;
358                         }
359                     } else if (!matchEquals) {
360                         pre = oldMatch;
361                         post = match;
362                     } else {
363                         continue main; // skip attribute entirely;
364                     }
365                     display = name + " " + diff(pre, post);
366                 }
367                 names.add(display);
368             }
369         return names;
370     }
371 
diff(String pre, String post)372     public static String diff(String pre, String post) {
373         Matcher matcherPre = Attribute.LEAD_TRAIL.matcher(pre);
374         Matcher matcherPost = Attribute.LEAD_TRAIL.matcher(post);
375         if (matcherPre.matches() && matcherPost.matches()) {
376             List<String> preParts = SPLITTER_SPACE.splitToList(matcherPre.group(2));
377             List<String> postParts = SPLITTER_SPACE.splitToList(matcherPost.group(2));
378             pre = matcherPre.group(1) + remove(preParts, postParts) + matcherPre.group(3);
379             post = matcherPost.group(1) + remove(postParts, preParts) + matcherPost.group(3);
380         }
381         return pre + "➠" + post;
382     }
383 
remove(List<String> main, List<String> toRemove)384     private static String remove(List<String> main, List<String> toRemove) {
385         List<String> result = new ArrayList<>();
386         boolean removed = false;
387         for (String s : main) {
388             if (toRemove.contains(s)) {
389                 removed = true;
390             } else {
391                 if (removed) {
392                     result.add("…");
393                     removed = false;
394                 }
395                 result.add(s);
396             }
397         }
398         if (removed) {
399             result.add("…");
400         }
401         return Joiner.on(" ").join(result);
402     }
403 
404 //    private static boolean isDeprecated(DtdData dtdCurrent, String elementName, String attributeName) {
405 //        try {
406 //            return dtdCurrent.isDeprecated(elementName, attributeName, "*");
407 //        } catch (DtdData.IllegalByDtdException e) {
408 //            return true;
409 //        }
410 //    }
411 }
412