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