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 15 import org.unicode.cldr.util.CLDRConfig; 16 import org.unicode.cldr.util.CldrUtility; 17 import org.unicode.cldr.util.DtdData; 18 import org.unicode.cldr.util.DtdData.Attribute; 19 import org.unicode.cldr.util.DtdData.Element; 20 import org.unicode.cldr.util.DtdType; 21 import org.unicode.cldr.util.SupplementalDataInfo; 22 23 import com.google.common.base.MoreObjects; 24 import com.google.common.collect.ImmutableMultimap; 25 import com.google.common.collect.ImmutableSet; 26 import com.google.common.collect.Multimap; 27 import com.ibm.icu.dev.util.CollectionUtilities; 28 import com.ibm.icu.impl.Utility; 29 import com.ibm.icu.util.VersionInfo; 30 31 /** 32 * Changed ShowDtdDiffs into a chart. 33 * @author markdavis 34 */ 35 public class ChartDtdDelta extends Chart { 36 37 private static final String DEPRECATED_PREFIX = "⊖"; 38 39 private static final String NEW_PREFIX = "+"; 40 41 private static final Set<String> OMITTED_ATTRIBUTES = Collections.singleton("⊕"); 42 main(String[] args)43 public static void main(String[] args) { 44 new ChartDtdDelta().writeChart(null); 45 } 46 47 @Override getDirectory()48 public String getDirectory() { 49 return FormattedFileWriter.CHART_TARGET_DIR; 50 } 51 52 @Override getTitle()53 public String getTitle() { 54 return "DTD Deltas"; 55 } 56 57 @Override getExplanation()58 public String getExplanation() { 59 return "<p>Shows changes to the LDML dtds over time. " 60 + "New elements or attributes are indicated with a + sign, and newly deprecated ones with a ⊖ sign. " 61 + "Element attributes are abbreviated as ⊕ if where is no change to them, but the element is newly the child of another. " 62 + "<p>"; 63 } 64 65 @Override writeContents(FormattedFileWriter pw)66 public void writeContents(FormattedFileWriter pw) throws IOException { 67 TablePrinter tablePrinter = new TablePrinter() 68 .addColumn("Version", "class='source'", CldrUtility.getDoubleLinkMsg(), "class='source'", true) 69 .setSortPriority(0) 70 .setSortAscending(false) 71 .setBreakSpans(true) 72 .addColumn("Dtd Type", "class='source'", null, "class='source'", true) 73 .setSortPriority(1) 74 75 .addColumn("Intermediate Path", "class='source'", null, "class='target'", true) 76 .setSortPriority(2) 77 78 .addColumn("Element", "class='target'", null, "class='target'", true) 79 .setSpanRows(false) 80 .addColumn("Attributes", "class='target'", null, "class='target'", true) 81 .setSpanRows(false); 82 83 String last = null; 84 LinkedHashSet<String> allVersions = new LinkedHashSet<>(ToolConstants.CLDR_VERSIONS); 85 allVersions.add(ToolConstants.LAST_CHART_VERSION); 86 for (String current : allVersions) { 87 System.out.println("DTD delta: " + current); 88 final boolean finalVersion = current.equals(ToolConstants.LAST_CHART_VERSION); 89 String currentName = finalVersion ? ToolConstants.CHART_DISPLAY_VERSION : current; 90 for (DtdType type : TYPES) { 91 String firstVersion = type.firstVersion; // FIRST_VERSION.get(type); 92 if (firstVersion != null && current != null && current.compareTo(firstVersion) < 0) { 93 continue; 94 } 95 DtdData dtdCurrent = null; 96 try { 97 dtdCurrent = DtdData.getInstance(type, 98 finalVersion && ToolConstants.CHART_STATUS != ToolConstants.ChartStatus.release ? null : current); 99 } catch (Exception e) { 100 if (!(e.getCause() instanceof FileNotFoundException)) { 101 throw e; 102 } 103 System.out.println(e.getMessage() + ", " + e.getCause().getMessage()); 104 continue; 105 } 106 DtdData dtdLast = null; 107 if (last != null) { 108 try { 109 dtdLast = DtdData.getInstance(type, last); 110 } catch (Exception e) { 111 if (!(e.getCause() instanceof FileNotFoundException)) { 112 throw e; 113 } 114 } 115 } 116 diff(currentName, dtdLast, dtdCurrent); 117 } 118 last = current; 119 } 120 121 for (DiffElement datum : data) { 122 tablePrinter.addRow() 123 .addCell(datum.getVersionString()) 124 .addCell(datum.dtdType) 125 .addCell(datum.newPath) 126 .addCell(datum.newElement) 127 .addCell(datum.attributeNames) 128 .finishRow(); 129 } 130 pw.write(tablePrinter.toTable()); 131 pw.write(Utility.repeat("<br>", 50)); 132 } 133 134 static final String NONE = " "; 135 136 static final SupplementalDataInfo SDI = CLDRConfig.getInstance().getSupplementalDataInfo(); 137 138 static Set<DtdType> TYPES = EnumSet.allOf(DtdType.class); 139 static { 140 TYPES.remove(DtdType.ldmlICU); 141 } 142 143 static final Map<DtdType, String> FIRST_VERSION = new EnumMap<>(DtdType.class); 144 static { FIRST_VERSION.put(DtdType.ldmlBCP47, "1.7.2")145 FIRST_VERSION.put(DtdType.ldmlBCP47, "1.7.2"); FIRST_VERSION.put(DtdType.keyboard, "22.1")146 FIRST_VERSION.put(DtdType.keyboard, "22.1"); FIRST_VERSION.put(DtdType.platform, "22.1")147 FIRST_VERSION.put(DtdType.platform, "22.1"); 148 } 149 diff(String prefix, DtdData dtdLast, DtdData dtdCurrent)150 private void diff(String prefix, DtdData dtdLast, DtdData dtdCurrent) { 151 Map<String, Element> oldNameToElement = dtdLast == null ? Collections.emptyMap() : dtdLast.getElementFromName(); 152 checkNames(prefix, dtdCurrent, dtdLast, oldNameToElement, "/", dtdCurrent.ROOT, new HashSet<Element>(), false); 153 } 154 155 static final DtdType DEBUG_DTD = null; // set to enable 156 static final String DEBUG_ELEMENT = "lias"; 157 static final boolean SHOW = false; 158 159 @SuppressWarnings("unused") checkNames(String version, DtdData dtdCurrent, DtdData dtdLast, Map<String, Element> oldNameToElement, String path, Element element, HashSet<Element> seen, boolean showAnyway)160 private void checkNames(String version, DtdData dtdCurrent, DtdData dtdLast, Map<String, Element> oldNameToElement, String path, Element element, 161 HashSet<Element> seen, boolean showAnyway) { 162 String name = element.getName(); 163 164 if (SKIP_ELEMENTS.contains(name)) { 165 return; 166 } 167 if (SKIP_TYPE_ELEMENTS.containsEntry(dtdCurrent.dtdType, name)) { 168 return; 169 } 170 171 String newPath = path + "/" + element.name; 172 173 // if an element is newly a child of another but has already been seen, you'll have special indication 174 if (seen.contains(element)) { 175 if (showAnyway) { 176 addData(dtdCurrent, NEW_PREFIX + name, version, newPath, OMITTED_ATTRIBUTES); 177 } 178 return; 179 } 180 181 seen.add(element); 182 if (SHOW && ToolConstants.CHART_DISPLAY_VERSION.equals(version)) { 183 System.out.println(dtdCurrent.dtdType + "\t" + name); 184 } 185 if (DEBUG_DTD == dtdCurrent.dtdType && name.contains(DEBUG_ELEMENT)) { 186 int debug = 0; 187 } 188 189 190 Element oldElement = null; 191 192 if (!oldNameToElement.containsKey(name)) { 193 Set<String> attributeNames = getAttributeNames(dtdCurrent, dtdLast, name, Collections.emptyMap(), element.getAttributes()); 194 addData(dtdCurrent, NEW_PREFIX + name, version, newPath, attributeNames); 195 } else { 196 oldElement = oldNameToElement.get(name); 197 Set<String> attributeNames = getAttributeNames(dtdCurrent, dtdLast, name, oldElement.getAttributes(), element.getAttributes()); 198 boolean currentDeprecated = element.isDeprecated(); 199 boolean lastDeprecated = dtdLast == null ? false : oldElement.isDeprecated(); // + (currentDeprecated ? "ⓓ" : "") 200 boolean newlyDeprecated = currentDeprecated && !lastDeprecated; 201 if (newlyDeprecated) { 202 addData(dtdCurrent, DEPRECATED_PREFIX + name, version, newPath, Collections.emptySet()); 203 } 204 if (!attributeNames.isEmpty()) { 205 addData(dtdCurrent, (newlyDeprecated ? DEPRECATED_PREFIX : "") + name, version, newPath, attributeNames); 206 } 207 } 208 if (element.getName().equals("coordinateUnit")) { 209 System.out.println(version + "\toordinateUnit\t" + element.getChildren().keySet()); 210 } 211 Set<Element> oldChildren = oldElement == null ? Collections.emptySet() : oldElement.getChildren().keySet(); 212 for (Element child : element.getChildren().keySet()) { 213 showAnyway = true; 214 for (Element oldChild : oldChildren) { 215 if (oldChild.getName().equals(child.getName())) { 216 showAnyway = false; 217 break; 218 } 219 } 220 checkNames(version, dtdCurrent, dtdLast, oldNameToElement, newPath, child, seen, showAnyway); 221 } 222 } 223 224 enum DiffType { 225 Element, Attribute, AttributeValue 226 } 227 228 private static class DiffElement { 229 230 final VersionInfo version; 231 final DtdType dtdType; 232 final boolean isBeta; 233 final String newPath; 234 final String newElement; 235 final String attributeNames; 236 DiffElement(DtdData dtdCurrent, String version, String newPath, String newElement, Set<String> attributeNames2)237 public DiffElement(DtdData dtdCurrent, String version, String newPath, String newElement, Set<String> attributeNames2) { 238 isBeta = version.endsWith("β"); 239 try { 240 this.version = isBeta ? VersionInfo.getInstance(version.substring(0, version.length() - 1)) : VersionInfo.getInstance(version); 241 } catch (Exception e) { 242 e.printStackTrace(); 243 throw e; 244 } 245 dtdType = dtdCurrent.dtdType; 246 this.newPath = fix(newPath); 247 this.attributeNames = attributeNames2.isEmpty() ? NONE : CollectionUtilities.join(attributeNames2, ", "); 248 this.newElement = newElement; 249 } 250 fix(String substring)251 private String fix(String substring) { 252 int base = substring.indexOf('/', 2); 253 if (base < 0) return ""; 254 int last = substring.lastIndexOf('/'); 255 if (last <= base) return "/"; 256 substring = substring.substring(base, last); 257 return substring.replace("/", "\u200B/") + "/"; 258 } 259 260 @Override toString()261 public String toString() { 262 return MoreObjects.toStringHelper(this) 263 .add("version", getVersionString()) 264 .add("dtdType", dtdType) 265 .add("newPath", newPath) 266 .add("newElement", newElement) 267 .add("attributeNames", attributeNames) 268 .toString(); 269 } 270 getVersionString()271 private String getVersionString() { 272 return version.getVersionString(2, 4) + (isBeta ? "β" : ""); 273 } 274 } 275 276 List<DiffElement> data = new ArrayList<>(); 277 addData(DtdData dtdCurrent, String element, String prefix, String newPath, Set<String> attributeNames)278 private void addData(DtdData dtdCurrent, String element, String prefix, String newPath, Set<String> attributeNames) { 279 DiffElement item = new DiffElement(dtdCurrent, prefix, newPath, element, attributeNames); 280 data.add(item); 281 } 282 283 static final Set<String> SKIP_ELEMENTS = ImmutableSet.of("generation", "identity", "special"); // , "telephoneCodeData" 284 285 static final Multimap<DtdType, String> SKIP_TYPE_ELEMENTS = ImmutableMultimap.of(DtdType.ldml, "alias"); 286 287 static final Set<String> SKIP_ATTRIBUTES = ImmutableSet.of("references", "standard", "draft", "alt"); 288 getAttributeNames(DtdData dtdCurrent, DtdData dtdLast, String elementName, Map<Attribute, Integer> attributesOld, Map<Attribute, Integer> attributes)289 private static Set<String> getAttributeNames(DtdData dtdCurrent, DtdData dtdLast, String elementName, 290 Map<Attribute, Integer> attributesOld, 291 Map<Attribute, Integer> attributes) { 292 Set<String> names = new LinkedHashSet<>(); 293 if (elementName.equals("coordinateUnit")) { 294 int debug = 0; 295 } 296 297 main: 298 // we want to add a name that is new or that becomes deprecated 299 for (Attribute attribute : attributes.keySet()) { 300 String name = attribute.getName(); 301 if (SKIP_ATTRIBUTES.contains(name)) { 302 continue; 303 } 304 String display = NEW_PREFIX + name; 305 // if (isDeprecated(dtdCurrent, elementName, name)) { // SDI.isDeprecated(dtdCurrent, elementName, name, "*")) { 306 // continue; 307 // } 308 for (Attribute attributeOld : attributesOld.keySet()) { 309 if (attributeOld.name.equals(name)) { 310 if (attribute.isDeprecated() && !attributeOld.isDeprecated()) { 311 display = DEPRECATED_PREFIX + name; 312 } else { 313 continue main; 314 } 315 } 316 } 317 names.add(display); 318 } 319 return names; 320 } 321 322 // private static boolean isDeprecated(DtdData dtdCurrent, String elementName, String attributeName) { 323 // try { 324 // return dtdCurrent.isDeprecated(elementName, attributeName, "*"); 325 // } catch (DtdData.IllegalByDtdException e) { 326 // return true; 327 // } 328 // } 329 } 330