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