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