|
| 1 | +package org.jgroups.util; |
| 2 | + |
| 3 | +import org.jgroups.annotations.Component; |
| 4 | +import org.jgroups.annotations.Property; |
| 5 | +import org.jgroups.stack.Protocol; |
| 6 | + |
| 7 | +import java.io.*; |
| 8 | +import java.lang.reflect.Field; |
| 9 | +import java.lang.reflect.Method; |
| 10 | +import java.lang.reflect.Modifier; |
| 11 | +import java.util.*; |
| 12 | +import java.util.concurrent.ConcurrentSkipListMap; |
| 13 | +import java.util.concurrent.ConcurrentSkipListSet; |
| 14 | +import java.util.stream.Collectors; |
| 15 | + |
| 16 | +/** |
| 17 | + * Tools to (1) dump all protocols and the names of their attributes ({@link org.jgroups.annotations.ManagedAttribute}) |
| 18 | + * and properties ({@link Property}) to file, (2) read from that file ('old) and compare whether old is a proper |
| 19 | + * subset of new, ie. if all protocols and attributes/properties still have the same names in new.<br/> |
| 20 | + * To be run before releasing a new version (mainly minor and micro). |
| 21 | + * @author Bela Ban |
| 22 | + * @since 5.4.4 |
| 23 | + */ |
| 24 | +public class CompareMetrics { |
| 25 | + protected static final String ROOT_PACKAGE="org.jgroups"; |
| 26 | + protected static final String[] PACKAGES={ |
| 27 | + "org.jgroups.protocols", |
| 28 | + "org.jgroups.protocols.pbcast", |
| 29 | + "org.jgroups.protocols.dns", |
| 30 | + "org.jgroups.protocols.relay" |
| 31 | + }; |
| 32 | + |
| 33 | + public static void main(String[] args) throws IOException, ClassNotFoundException { |
| 34 | + String from_file=null, to_file=null; |
| 35 | + for(int i=0; i < args.length; i++) { |
| 36 | + if("-from".equals(args[i])) { |
| 37 | + from_file=args[++i]; |
| 38 | + continue; |
| 39 | + } |
| 40 | + if("-to".equals(args[i])) { |
| 41 | + to_file=args[++i]; |
| 42 | + continue; |
| 43 | + } |
| 44 | + System.out.printf("%s (-from <read from file> | -to <dump to file>)\n", CompareMetrics.class.getSimpleName()); |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + // Sanity check |
| 49 | + if((from_file == null && to_file == null) || (from_file != null && to_file != null)) |
| 50 | + throw new IllegalArgumentException("(only) one of '-from' or '-to' has to be defined"); |
| 51 | + |
| 52 | + Map<String,Collection<String>> old_metrics=null; |
| 53 | + if(from_file != null) { |
| 54 | + old_metrics=readOldMetrics(from_file); |
| 55 | + long total_attrs=old_metrics.values().stream().mapToLong(Collection::size).sum(); |
| 56 | + System.out.printf("-- read old metrics: %d protocols and %,d attributes\n", old_metrics.size(), total_attrs); |
| 57 | + } |
| 58 | + |
| 59 | + Map<String,Collection<String>> new_metrics=readCurrentMetrics(); |
| 60 | + // System.out.printf("current metrics:\n%s\n", print(new_metrics)); |
| 61 | + long total_attrs=new_metrics.values().stream().mapToLong(Collection::size).sum(); |
| 62 | + System.out.printf("-- read current metrics: %d protocols and %,d attributes\n", new_metrics.size(), total_attrs); |
| 63 | + if(to_file != null) { |
| 64 | + // read the current metrics, dump to file and exit |
| 65 | + writeMetricsToFile(new_metrics, to_file); |
| 66 | + return; |
| 67 | + } |
| 68 | + |
| 69 | + // from_file was used to read the old metrics; compare with the current and then exit |
| 70 | + compareMetrics(new_metrics, old_metrics); |
| 71 | + |
| 72 | + old_metrics.put("UDP", List.of("number_of_messages")); |
| 73 | + new_metrics.put("UDP", List.of("num_msgs")); |
| 74 | + |
| 75 | + if(old_metrics.isEmpty() && new_metrics.isEmpty()) { |
| 76 | + System.out.println("\n** Success: both old and new metrics are the same"); |
| 77 | + return; |
| 78 | + } |
| 79 | + if(!old_metrics.isEmpty()) { |
| 80 | + System.out.printf("\n** Failure: the following protocols/attributes are only found in old " + |
| 81 | + "metrics, but not in new:\n%s\n", print(old_metrics)); |
| 82 | + } |
| 83 | + if(!new_metrics.isEmpty()) { |
| 84 | + System.out.printf("\n** The following protocols/attributes are only found in new, but not in old " + |
| 85 | + "(this may not be an error, e.g. when new protocols or attributes have been added):\n" + |
| 86 | + "%s\n", print(new_metrics)); |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + protected static Map<String,Collection<String>> readOldMetrics(String from_file) throws IOException { |
| 91 | + Map<String,Collection<String>> map=new ConcurrentSkipListMap<>(); |
| 92 | + try(InputStream in=new FileInputStream(from_file)) { |
| 93 | + int i=1; |
| 94 | + for(;;) { |
| 95 | + String line=Util.readLine(in); |
| 96 | + if(line == null) |
| 97 | + break; |
| 98 | + int index=line.indexOf(":"); |
| 99 | + String protocol=line.substring(0, index).trim(); |
| 100 | + index=line.indexOf("["); |
| 101 | + int end=line.indexOf("]"); |
| 102 | + String attributes=line.substring(index + 1, end); |
| 103 | + StringTokenizer tok=new StringTokenizer(attributes, ","); |
| 104 | + Collection<String> attrs=new ConcurrentSkipListSet<>(); |
| 105 | + while(tok.hasMoreTokens()) { |
| 106 | + String attr=tok.nextToken().trim(); |
| 107 | + attrs.add(attr); |
| 108 | + } |
| 109 | + map.put(protocol, attrs); |
| 110 | + } |
| 111 | + } |
| 112 | + return map; |
| 113 | + } |
| 114 | + |
| 115 | + protected static Map<String,Collection<String>> readCurrentMetrics() throws IOException, ClassNotFoundException { |
| 116 | + SortedMap<String,Collection<String>> map=new ConcurrentSkipListMap<>(); |
| 117 | + Set<Class<?>> classes=getProtocols(); |
| 118 | + for(Class<?> cl: classes) { |
| 119 | + Collection<String> attrs=getAttributes(cl, null); |
| 120 | + if(!attrs.isEmpty()) |
| 121 | + map.put(cl.getSimpleName(), attrs); |
| 122 | + } |
| 123 | + return map; |
| 124 | + } |
| 125 | + |
| 126 | + protected static Set<Class<?>> getProtocols() throws IOException, ClassNotFoundException { |
| 127 | + ClassLoader cl=Thread.currentThread().getContextClassLoader(); |
| 128 | + Set<Class<?>> s=new HashSet<>(); |
| 129 | + for(String p: PACKAGES) { |
| 130 | + Set<Class<?>> tmp=Util.findClassesAssignableFrom(p, Protocol.class, cl); |
| 131 | + s.addAll(tmp); |
| 132 | + } |
| 133 | + return s; |
| 134 | + } |
| 135 | + |
| 136 | + protected static void writeMetricsToFile(Map<String,Collection<String>> metrics, String to_file) throws IOException { |
| 137 | + try(OutputStream out=new FileOutputStream(to_file)) { |
| 138 | + for(Map.Entry<String,Collection<String>> e: metrics.entrySet()) { |
| 139 | + String s=String.format("%s: %s\n", e.getKey(), e.getValue()); |
| 140 | + out.write(s.getBytes()); |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Compares the new to the old metrics by removing 'old' from 'new' (if present in both 'new' and 'old').<br/> |
| 147 | + * If protocols/attributes remain, then either new protocols or attributes were added in new, or attributes / |
| 148 | + * protocols changed. E.g. an attribute changes from "number_of_messages" -> "num_msgs", then "number_of_attributes" |
| 149 | + * will remain in 'old' and "num_msgs" in 'new'.<br/> |
| 150 | + * The goal is that all attributes of 'old' also need to be in 'new', or else we have an incompatible change in that |
| 151 | + * an attribute was renamed or removed. If an attribute or protocol is only in 'new', that's acceptable and means |
| 152 | + * that it was added in 'new. |
| 153 | + */ |
| 154 | + protected static void compareMetrics(Map<String,Collection<String>> new_metrics, Map<String,Collection<String>> old_metrics) { |
| 155 | + for(Iterator<Map.Entry<String,Collection<String>>> it=old_metrics.entrySet().iterator(); it.hasNext();) { |
| 156 | + Map.Entry<String,Collection<String>> old_entry=it.next(); |
| 157 | + String key=old_entry.getKey(); |
| 158 | + Collection<String> old_attrs=old_entry.getValue(); |
| 159 | + Collection<String> new_attrs=new_metrics.get(key); |
| 160 | + boolean rc=compareAttributes(new_attrs, old_attrs); |
| 161 | + if(rc) { |
| 162 | + it.remove(); |
| 163 | + new_metrics.remove(key); |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + // Remove all attributes both in new and old |
| 169 | + // If both old and new are empty -> return true, else false |
| 170 | + protected static boolean compareAttributes(Collection<String> new_attrs, Collection<String> old_attrs) { |
| 171 | + for(Iterator<String> it=old_attrs.iterator(); it.hasNext();) { |
| 172 | + String old_attr=it.next(); |
| 173 | + if(new_attrs.contains(old_attr)) { |
| 174 | + it.remove(); |
| 175 | + new_attrs.remove(old_attr); |
| 176 | + } |
| 177 | + } |
| 178 | + return old_attrs.isEmpty() && new_attrs.isEmpty(); |
| 179 | + } |
| 180 | + |
| 181 | + protected static String print(Map<String,Collection<String>> map) { |
| 182 | + return map.entrySet().stream().map(e -> String.format("%s: %s", e.getKey(), e.getValue())) |
| 183 | + .collect(Collectors.joining("\n")); |
| 184 | + } |
| 185 | + |
| 186 | + protected static Collection<String> getAttributes(Class<?> clazz, String prefix) |
| 187 | + throws IOException, ClassNotFoundException { |
| 188 | + Collection<String> ret=new ConcurrentSkipListSet<>(); |
| 189 | + Field[] fields=clazz.getDeclaredFields(); |
| 190 | + for(Field field: fields) { |
| 191 | + if(field.isAnnotationPresent(Property.class)) { |
| 192 | + String property=field.getName(); |
| 193 | + Property annotation=field.getAnnotation(Property.class); |
| 194 | + String name=annotation.name(); |
| 195 | + if(name != null && !name.trim().isEmpty()) |
| 196 | + property=name.trim(); |
| 197 | + if(prefix != null && !prefix.isEmpty()) |
| 198 | + property=prefix + "." + property; |
| 199 | + ret.add(property); |
| 200 | + } |
| 201 | + |
| 202 | + // is the field annotated with @Component? |
| 203 | + if(field.isAnnotationPresent(Component.class)) { |
| 204 | + Component ann=field.getAnnotation(Component.class); |
| 205 | + Class<?> type=field.getType(); |
| 206 | + if(type.isInterface() || Modifier.isAbstract(type.getModifiers())) { |
| 207 | + Set<Class<?>> implementations=Util.findClassesAssignableFrom(ROOT_PACKAGE, type, Thread.currentThread().getContextClassLoader()); |
| 208 | + for(Class<?> impl: implementations) |
| 209 | + ret.addAll(getAttributes(impl, ann.name())); |
| 210 | + } |
| 211 | + else |
| 212 | + ret.addAll(getAttributes(type, ann.name())); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + Method[] methods=clazz.getDeclaredMethods(); |
| 217 | + for(Method method: methods) { |
| 218 | + if(method.isAnnotationPresent(Property.class)) { |
| 219 | + Property annotation=method.getAnnotation(Property.class); |
| 220 | + String name=annotation.name(); |
| 221 | + if(name.isEmpty()) |
| 222 | + name=Util.methodNameToAttributeName(method.getName()); |
| 223 | + if(prefix != null && !prefix.isEmpty()) |
| 224 | + name=prefix + "." + name; |
| 225 | + ret.add(name); |
| 226 | + } |
| 227 | + } |
| 228 | + return ret; |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Reads from the input stream and replaces occurrences of ${PROT} with p.get("PROT") and writes this to the |
| 233 | + * output stream. If no value is found, then the ${PROT} will simple be omitted from the output. |
| 234 | + * Escaped values of the form \${PROT} are not looked up and the value without the backslash will be written |
| 235 | + * to the output stream. |
| 236 | + */ |
| 237 | + protected static void replaceVariables(InputStream in, OutputStream out, Properties p) { |
| 238 | + boolean looping=true; |
| 239 | + while(looping) { |
| 240 | + try { |
| 241 | + int ch=in.read(), n1, n2; |
| 242 | + if(ch == -1) |
| 243 | + break; |
| 244 | + switch(ch) { |
| 245 | + case '\\': |
| 246 | + n1=in.read(); |
| 247 | + n2=in.read(); |
| 248 | + if(n1 == -1 || n2 == -1) { |
| 249 | + looping=false; |
| 250 | + if(n1 != -1) |
| 251 | + out.write(n1); |
| 252 | + break; |
| 253 | + } |
| 254 | + |
| 255 | + if(n1 == '$' && n2 == '{') { |
| 256 | + String s=readUntilBracket(in); |
| 257 | + out.write(n1); |
| 258 | + out.write(n2); |
| 259 | + out.write(s.getBytes()); |
| 260 | + out.write('}'); |
| 261 | + } |
| 262 | + else { |
| 263 | + out.write(ch); |
| 264 | + out.write(n1); |
| 265 | + out.write(n2); |
| 266 | + } |
| 267 | + break; |
| 268 | + case '$': |
| 269 | + n1=in.read(); |
| 270 | + if(n1 == -1) { |
| 271 | + out.write(ch); |
| 272 | + looping=false; |
| 273 | + } |
| 274 | + else { |
| 275 | + if(n1 == '{') { |
| 276 | + String s=readUntilBracket(in); |
| 277 | + writeVarToStream(s, p, out); |
| 278 | + } |
| 279 | + else { |
| 280 | + out.write(ch); |
| 281 | + out.write(n1); |
| 282 | + } |
| 283 | + } |
| 284 | + break; |
| 285 | + default: |
| 286 | + out.write(ch); |
| 287 | + } |
| 288 | + } |
| 289 | + catch(IOException e) { |
| 290 | + break; |
| 291 | + } |
| 292 | + } |
| 293 | + Util.close(in, out); |
| 294 | + } |
| 295 | + |
| 296 | + |
| 297 | + protected static void writeVarToStream(String var, Properties p, OutputStream out) throws IOException { |
| 298 | + String val=(String)p.get(var); |
| 299 | + if(val != null) |
| 300 | + out.write(val.getBytes()); |
| 301 | + } |
| 302 | + |
| 303 | + /** Reads until the next bracket '}' and returns the string excluding the bracket, or throws an exception if |
| 304 | + * no bracket has been found */ |
| 305 | + protected static String readUntilBracket(InputStream in) throws IOException { |
| 306 | + StringBuilder sb=new StringBuilder(); |
| 307 | + while(true) { |
| 308 | + int ch=in.read(); |
| 309 | + switch(ch) { |
| 310 | + case -1: throw new EOFException("no matching } found"); |
| 311 | + case '}': |
| 312 | + return sb.toString(); |
| 313 | + default: |
| 314 | + sb.append((char)ch); |
| 315 | + } |
| 316 | + } |
| 317 | + } |
| 318 | + |
| 319 | + private static String fileToString(File f) throws Exception { |
| 320 | + StringWriter output = new StringWriter(); |
| 321 | + try (FileReader input = new FileReader(f)) { |
| 322 | + char[] buffer = new char[8 * 1024]; |
| 323 | + int n; |
| 324 | + while (-1 != (n = input.read(buffer))) { |
| 325 | + output.write(buffer, 0, n); |
| 326 | + } |
| 327 | + } |
| 328 | + return output.toString(); |
| 329 | + } |
| 330 | + |
| 331 | + public static int copy(Reader input, Writer output) throws IOException { |
| 332 | + char[] buffer = new char[8 * 1024]; |
| 333 | + int count = 0; |
| 334 | + int n = 0; |
| 335 | + try { |
| 336 | + while (-1 != (n = input.read(buffer))) { |
| 337 | + output.write(buffer, 0, n); |
| 338 | + count += n; |
| 339 | + } |
| 340 | + } finally { |
| 341 | + output.flush(); |
| 342 | + output.close(); |
| 343 | + } |
| 344 | + return count; |
| 345 | + } |
| 346 | +} |
0 commit comments