From 6e464ea251cab63c776ece0b2a356f1498ffa294 Mon Sep 17 00:00:00 2001 From: Thog Date: Wed, 8 Mar 2017 08:17:04 +0100 Subject: Follow Fabric guidelines --- .../java/cuchaz/enigma/convert/ClassIdentity.java | 821 ++++++++++----------- 1 file changed, 410 insertions(+), 411 deletions(-) (limited to 'src/main/java/cuchaz/enigma/convert/ClassIdentity.java') diff --git a/src/main/java/cuchaz/enigma/convert/ClassIdentity.java b/src/main/java/cuchaz/enigma/convert/ClassIdentity.java index f72bf70..a395b75 100644 --- a/src/main/java/cuchaz/enigma/convert/ClassIdentity.java +++ b/src/main/java/cuchaz/enigma/convert/ClassIdentity.java @@ -8,18 +8,10 @@ * Contributors: * Jeff Martin - initial API and implementation ******************************************************************************/ + package cuchaz.enigma.convert; import com.google.common.collect.*; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; -import java.util.Set; - import cuchaz.enigma.analysis.ClassImplementationsTreeNode; import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.analysis.JarIndex; @@ -33,408 +25,415 @@ import javassist.*; import javassist.bytecode.*; import javassist.expr.*; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; + public class ClassIdentity { - private ClassEntry classEntry; - private SidedClassNamer namer; - private Multiset fields; - private Multiset methods; - private Multiset constructors; - private String staticInitializer; - private String extendz; - private Multiset implementz; - private Set stringLiterals; - private Multiset implementations; - private Multiset references; - private String outer; - - private final ClassNameReplacer classNameReplacer = new ClassNameReplacer() { - - private Map classNames = Maps.newHashMap(); - - @Override - public String replace(String className) { - - // classes not in the none package can be passed through - ClassEntry classEntry = new ClassEntry(className); - if (classEntry.getPackageName() != null) { - return className; - } - - // is this class ourself? - if (className.equals(classEntry.getName())) { - return "CSelf"; - } - - // try the namer - if (namer != null) { - String newName = namer.getName(className); - if (newName != null) { - return newName; - } - } - - // otherwise, use local naming - if (!classNames.containsKey(className)) { - classNames.put(className, getNewClassName()); - } - return classNames.get(className); - } - - private String getNewClassName() { - return String.format("C%03d", classNames.size()); - } - }; - - public ClassIdentity(CtClass c, SidedClassNamer namer, JarIndex index, boolean useReferences) { - this.namer = namer; - - // stuff from the bytecode - - this.classEntry = EntryFactory.getClassEntry(c); - this.fields = HashMultiset.create(); - for (CtField field : c.getDeclaredFields()) { - this.fields.add(scrubType(field.getSignature())); - } - this.methods = HashMultiset.create(); - for (CtMethod method : c.getDeclaredMethods()) { - this.methods.add(scrubSignature(method.getSignature()) + "0x" + getBehaviorSignature(method)); - } - this.constructors = HashMultiset.create(); - for (CtConstructor constructor : c.getDeclaredConstructors()) { - this.constructors.add(scrubSignature(constructor.getSignature()) + "0x" + getBehaviorSignature(constructor)); - } - this.staticInitializer = ""; - if (c.getClassInitializer() != null) { - this.staticInitializer = getBehaviorSignature(c.getClassInitializer()); - } - this.extendz = ""; - if (c.getClassFile().getSuperclass() != null) { - this.extendz = scrubClassName(Descriptor.toJvmName(c.getClassFile().getSuperclass())); - } - this.implementz = HashMultiset.create(); - for (String interfaceName : c.getClassFile().getInterfaces()) { - this.implementz.add(scrubClassName(Descriptor.toJvmName(interfaceName))); - } - - this.stringLiterals = Sets.newHashSet(); - ConstPool constants = c.getClassFile().getConstPool(); - for (int i = 1; i < constants.getSize(); i++) { - if (constants.getTag(i) == ConstPool.CONST_String) { - this.stringLiterals.add(constants.getStringInfo(i)); - } - } - - // stuff from the jar index - - this.implementations = HashMultiset.create(); - ClassImplementationsTreeNode implementationsNode = index.getClassImplementations(null, this.classEntry); - if (implementationsNode != null) { - @SuppressWarnings("unchecked") - Enumeration implementations = implementationsNode.children(); - while (implementations.hasMoreElements()) { - ClassImplementationsTreeNode node = implementations.nextElement(); - this.implementations.add(scrubClassName(node.getClassEntry().getName())); - } - } - - this.references = HashMultiset.create(); - if (useReferences) { - for (CtField field : c.getDeclaredFields()) { - FieldEntry fieldEntry = EntryFactory.getFieldEntry(field); - index.getFieldReferences(fieldEntry).forEach(this::addReference); - } - for (CtBehavior behavior : c.getDeclaredBehaviors()) { - BehaviorEntry behaviorEntry = EntryFactory.getBehaviorEntry(behavior); - index.getBehaviorReferences(behaviorEntry).forEach(this::addReference); - } - } - - this.outer = null; - if (this.classEntry.isInnerClass()) { - this.outer = this.classEntry.getOuterClassName(); - } - } - - private void addReference(EntryReference reference) { - if (reference.context.getSignature() != null) { - this.references.add(String.format("%s_%s", - scrubClassName(reference.context.getClassName()), - scrubSignature(reference.context.getSignature()) - )); - } else { - this.references.add(String.format("%s_", - scrubClassName(reference.context.getClassName()) - )); - } - } - - public ClassEntry getClassEntry() { - return this.classEntry; - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(); - buf.append("class: "); - buf.append(this.classEntry.getName()); - buf.append(" "); - buf.append(hashCode()); - buf.append("\n"); - for (String field : this.fields) { - buf.append("\tfield "); - buf.append(field); - buf.append("\n"); - } - for (String method : this.methods) { - buf.append("\tmethod "); - buf.append(method); - buf.append("\n"); - } - for (String constructor : this.constructors) { - buf.append("\tconstructor "); - buf.append(constructor); - buf.append("\n"); - } - if (this.staticInitializer.length() > 0) { - buf.append("\tinitializer "); - buf.append(this.staticInitializer); - buf.append("\n"); - } - if (this.extendz.length() > 0) { - buf.append("\textends "); - buf.append(this.extendz); - buf.append("\n"); - } - for (String interfaceName : this.implementz) { - buf.append("\timplements "); - buf.append(interfaceName); - buf.append("\n"); - } - for (String implementation : this.implementations) { - buf.append("\timplemented by "); - buf.append(implementation); - buf.append("\n"); - } - for (String reference : this.references) { - buf.append("\treference "); - buf.append(reference); - buf.append("\n"); - } - buf.append("\touter "); - buf.append(this.outer); - buf.append("\n"); - return buf.toString(); - } - - private String scrubClassName(String className) { - return classNameReplacer.replace(className); - } - - private String scrubType(String typeName) { - return scrubType(new Type(typeName)).toString(); - } - - private Type scrubType(Type type) { - if (type.hasClass()) { - return new Type(type, classNameReplacer); - } else { - return type; - } - } - - private String scrubSignature(String signature) { - return scrubSignature(new Signature(signature)).toString(); - } - - private Signature scrubSignature(Signature signature) { - return new Signature(signature, classNameReplacer); - } - - private boolean isClassMatchedUniquely(String className) { - return this.namer != null && this.namer.getName(Descriptor.toJvmName(className)) != null; - } - - private String getBehaviorSignature(CtBehavior behavior) { - try { - // does this method have an implementation? - if (behavior.getMethodInfo().getCodeAttribute() == null) { - return "(none)"; - } - - // compute the hash from the opcodes - ConstPool constants = behavior.getMethodInfo().getConstPool(); - final MessageDigest digest = MessageDigest.getInstance("MD5"); - CodeIterator iter = behavior.getMethodInfo().getCodeAttribute().iterator(); - while (iter.hasNext()) { - int pos = iter.next(); - - // update the hash with the opcode - int opcode = iter.byteAt(pos); - digest.update((byte) opcode); - int constIndex; - switch (opcode) { - case Opcode.LDC: - constIndex = iter.byteAt(pos + 1); - updateHashWithConstant(digest, constants, constIndex); - break; - - case Opcode.LDC_W: - case Opcode.LDC2_W: - constIndex = (iter.byteAt(pos + 1) << 8) | iter.byteAt(pos + 2); - updateHashWithConstant(digest, constants, constIndex); - break; - default: - break; - } - } - - // update hash with method and field accesses - behavior.instrument(new ExprEditor() { - @Override - public void edit(MethodCall call) { - updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName()))); - updateHashWithString(digest, scrubSignature(call.getSignature())); - if (isClassMatchedUniquely(call.getClassName())) { - updateHashWithString(digest, call.getMethodName()); - } - } - - @Override - public void edit(FieldAccess access) { - updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(access.getClassName()))); - updateHashWithString(digest, scrubType(access.getSignature())); - if (isClassMatchedUniquely(access.getClassName())) { - updateHashWithString(digest, access.getFieldName()); - } - } - - @Override - public void edit(ConstructorCall call) { - updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName()))); - updateHashWithString(digest, scrubSignature(call.getSignature())); - } - - @Override - public void edit(NewExpr expr) { - updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(expr.getClassName()))); - } - }); - - // convert the hash to a hex string - return toHex(digest.digest()); - } catch (BadBytecode | NoSuchAlgorithmException | CannotCompileException ex) { - throw new Error(ex); - } - } - - private void updateHashWithConstant(MessageDigest digest, ConstPool constants, int index) { - ConstPoolEditor editor = new ConstPoolEditor(constants); - ConstInfoAccessor item = editor.getItem(index); - if (item.getType() == InfoType.StringInfo) { - updateHashWithString(digest, constants.getStringInfo(index)); - } - // TODO: other constants - } - - private void updateHashWithString(MessageDigest digest, String val) { - try { - digest.update(val.getBytes("UTF8")); - } catch (UnsupportedEncodingException ex) { - throw new Error(ex); - } - } - - private String toHex(byte[] bytes) { - // function taken from: - // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java - final char[] hexArray = "0123456789ABCDEF".toCharArray(); - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - @Override - public boolean equals(Object other) { - return other instanceof ClassIdentity && equals((ClassIdentity) other); - } - - public boolean equals(ClassIdentity other) { - return this.fields.equals(other.fields) - && this.methods.equals(other.methods) - && this.constructors.equals(other.constructors) - && this.staticInitializer.equals(other.staticInitializer) - && this.extendz.equals(other.extendz) - && this.implementz.equals(other.implementz) - && this.implementations.equals(other.implementations) - && this.references.equals(other.references); - } - - @Override - public int hashCode() { - List objs = Lists.newArrayList(); - objs.addAll(this.fields); - objs.addAll(this.methods); - objs.addAll(this.constructors); - objs.add(this.staticInitializer); - objs.add(this.extendz); - objs.addAll(this.implementz); - objs.addAll(this.implementations); - objs.addAll(this.references); - return Utils.combineHashesOrdered(objs); - } - - public int getMatchScore(ClassIdentity other) { - return 2 * getNumMatches(this.extendz, other.extendz) - + 2 * getNumMatches(this.outer, other.outer) - + 2 * getNumMatches(this.implementz, other.implementz) - + getNumMatches(this.stringLiterals, other.stringLiterals) - + getNumMatches(this.fields, other.fields) - + getNumMatches(this.methods, other.methods) - + getNumMatches(this.constructors, other.constructors); - } - - public int getMaxMatchScore() { - return 2 + 2 + 2 * this.implementz.size() + this.stringLiterals.size() + this.fields.size() + this.methods.size() + this.constructors.size(); - } - - public boolean matches(CtClass c) { - // just compare declaration counts - return this.fields.size() == c.getDeclaredFields().length - && this.methods.size() == c.getDeclaredMethods().length - && this.constructors.size() == c.getDeclaredConstructors().length; - } - - private int getNumMatches(Set a, Set b) { - int numMatches = 0; - for (String val : a) { - if (b.contains(val)) { - numMatches++; - } - } - return numMatches; - } - - private int getNumMatches(Multiset a, Multiset b) { - int numMatches = 0; - for (String val : a) { - if (b.contains(val)) { - numMatches++; - } - } - return numMatches; - } - - private int getNumMatches(String a, String b) { - if (a == null && b == null) { - return 1; - } else if (a != null && b != null && a.equals(b)) { - return 1; - } - return 0; - } + private ClassEntry classEntry; + private SidedClassNamer namer; + private final ClassNameReplacer classNameReplacer = new ClassNameReplacer() { + + private Map classNames = Maps.newHashMap(); + + @Override + public String replace(String className) { + + // classes not in the none package can be passed through + ClassEntry classEntry = new ClassEntry(className); + if (classEntry.getPackageName() != null) { + return className; + } + + // is this class ourself? + if (className.equals(classEntry.getName())) { + return "CSelf"; + } + + // try the namer + if (namer != null) { + String newName = namer.getName(className); + if (newName != null) { + return newName; + } + } + + // otherwise, use local naming + if (!classNames.containsKey(className)) { + classNames.put(className, getNewClassName()); + } + return classNames.get(className); + } + + private String getNewClassName() { + return String.format("C%03d", classNames.size()); + } + }; + private Multiset fields; + private Multiset methods; + private Multiset constructors; + private String staticInitializer; + private String extendz; + private Multiset implementz; + private Set stringLiterals; + private Multiset implementations; + private Multiset references; + private String outer; + + public ClassIdentity(CtClass c, SidedClassNamer namer, JarIndex index, boolean useReferences) { + this.namer = namer; + + // stuff from the bytecode + + this.classEntry = EntryFactory.getClassEntry(c); + this.fields = HashMultiset.create(); + for (CtField field : c.getDeclaredFields()) { + this.fields.add(scrubType(field.getSignature())); + } + this.methods = HashMultiset.create(); + for (CtMethod method : c.getDeclaredMethods()) { + this.methods.add(scrubSignature(method.getSignature()) + "0x" + getBehaviorSignature(method)); + } + this.constructors = HashMultiset.create(); + for (CtConstructor constructor : c.getDeclaredConstructors()) { + this.constructors.add(scrubSignature(constructor.getSignature()) + "0x" + getBehaviorSignature(constructor)); + } + this.staticInitializer = ""; + if (c.getClassInitializer() != null) { + this.staticInitializer = getBehaviorSignature(c.getClassInitializer()); + } + this.extendz = ""; + if (c.getClassFile().getSuperclass() != null) { + this.extendz = scrubClassName(Descriptor.toJvmName(c.getClassFile().getSuperclass())); + } + this.implementz = HashMultiset.create(); + for (String interfaceName : c.getClassFile().getInterfaces()) { + this.implementz.add(scrubClassName(Descriptor.toJvmName(interfaceName))); + } + + this.stringLiterals = Sets.newHashSet(); + ConstPool constants = c.getClassFile().getConstPool(); + for (int i = 1; i < constants.getSize(); i++) { + if (constants.getTag(i) == ConstPool.CONST_String) { + this.stringLiterals.add(constants.getStringInfo(i)); + } + } + + // stuff from the jar index + + this.implementations = HashMultiset.create(); + ClassImplementationsTreeNode implementationsNode = index.getClassImplementations(null, this.classEntry); + if (implementationsNode != null) { + @SuppressWarnings("unchecked") + Enumeration implementations = implementationsNode.children(); + while (implementations.hasMoreElements()) { + ClassImplementationsTreeNode node = implementations.nextElement(); + this.implementations.add(scrubClassName(node.getClassEntry().getName())); + } + } + + this.references = HashMultiset.create(); + if (useReferences) { + for (CtField field : c.getDeclaredFields()) { + FieldEntry fieldEntry = EntryFactory.getFieldEntry(field); + index.getFieldReferences(fieldEntry).forEach(this::addReference); + } + for (CtBehavior behavior : c.getDeclaredBehaviors()) { + BehaviorEntry behaviorEntry = EntryFactory.getBehaviorEntry(behavior); + index.getBehaviorReferences(behaviorEntry).forEach(this::addReference); + } + } + + this.outer = null; + if (this.classEntry.isInnerClass()) { + this.outer = this.classEntry.getOuterClassName(); + } + } + + private void addReference(EntryReference reference) { + if (reference.context.getSignature() != null) { + this.references.add(String.format("%s_%s", + scrubClassName(reference.context.getClassName()), + scrubSignature(reference.context.getSignature()) + )); + } else { + this.references.add(String.format("%s_", + scrubClassName(reference.context.getClassName()) + )); + } + } + + public ClassEntry getClassEntry() { + return this.classEntry; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("class: "); + buf.append(this.classEntry.getName()); + buf.append(" "); + buf.append(hashCode()); + buf.append("\n"); + for (String field : this.fields) { + buf.append("\tfield "); + buf.append(field); + buf.append("\n"); + } + for (String method : this.methods) { + buf.append("\tmethod "); + buf.append(method); + buf.append("\n"); + } + for (String constructor : this.constructors) { + buf.append("\tconstructor "); + buf.append(constructor); + buf.append("\n"); + } + if (!this.staticInitializer.isEmpty()) { + buf.append("\tinitializer "); + buf.append(this.staticInitializer); + buf.append("\n"); + } + if (!this.extendz.isEmpty()) { + buf.append("\textends "); + buf.append(this.extendz); + buf.append("\n"); + } + for (String interfaceName : this.implementz) { + buf.append("\timplements "); + buf.append(interfaceName); + buf.append("\n"); + } + for (String implementation : this.implementations) { + buf.append("\timplemented by "); + buf.append(implementation); + buf.append("\n"); + } + for (String reference : this.references) { + buf.append("\treference "); + buf.append(reference); + buf.append("\n"); + } + buf.append("\touter "); + buf.append(this.outer); + buf.append("\n"); + return buf.toString(); + } + + private String scrubClassName(String className) { + return classNameReplacer.replace(className); + } + + private String scrubType(String typeName) { + return scrubType(new Type(typeName)).toString(); + } + + private Type scrubType(Type type) { + if (type.hasClass()) { + return new Type(type, classNameReplacer); + } else { + return type; + } + } + + private String scrubSignature(String signature) { + return scrubSignature(new Signature(signature)).toString(); + } + + private Signature scrubSignature(Signature signature) { + return new Signature(signature, classNameReplacer); + } + + private boolean isClassMatchedUniquely(String className) { + return this.namer != null && this.namer.getName(Descriptor.toJvmName(className)) != null; + } + + private String getBehaviorSignature(CtBehavior behavior) { + try { + // does this method have an implementation? + if (behavior.getMethodInfo().getCodeAttribute() == null) { + return "(none)"; + } + + // compute the hash from the opcodes + ConstPool constants = behavior.getMethodInfo().getConstPool(); + final MessageDigest digest = MessageDigest.getInstance("MD5"); + CodeIterator iter = behavior.getMethodInfo().getCodeAttribute().iterator(); + while (iter.hasNext()) { + int pos = iter.next(); + + // update the hash with the opcode + int opcode = iter.byteAt(pos); + digest.update((byte) opcode); + int constIndex; + switch (opcode) { + case Opcode.LDC: + constIndex = iter.byteAt(pos + 1); + updateHashWithConstant(digest, constants, constIndex); + break; + + case Opcode.LDC_W: + case Opcode.LDC2_W: + constIndex = (iter.byteAt(pos + 1) << 8) | iter.byteAt(pos + 2); + updateHashWithConstant(digest, constants, constIndex); + break; + default: + break; + } + } + + // update hash with method and field accesses + behavior.instrument(new ExprEditor() { + @Override + public void edit(MethodCall call) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName()))); + updateHashWithString(digest, scrubSignature(call.getSignature())); + if (isClassMatchedUniquely(call.getClassName())) { + updateHashWithString(digest, call.getMethodName()); + } + } + + @Override + public void edit(FieldAccess access) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(access.getClassName()))); + updateHashWithString(digest, scrubType(access.getSignature())); + if (isClassMatchedUniquely(access.getClassName())) { + updateHashWithString(digest, access.getFieldName()); + } + } + + @Override + public void edit(ConstructorCall call) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName()))); + updateHashWithString(digest, scrubSignature(call.getSignature())); + } + + @Override + public void edit(NewExpr expr) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(expr.getClassName()))); + } + }); + + // convert the hash to a hex string + return toHex(digest.digest()); + } catch (BadBytecode | NoSuchAlgorithmException | CannotCompileException ex) { + throw new Error(ex); + } + } + + private void updateHashWithConstant(MessageDigest digest, ConstPool constants, int index) { + ConstPoolEditor editor = new ConstPoolEditor(constants); + ConstInfoAccessor item = editor.getItem(index); + if (item.getType() == InfoType.StringInfo) { + updateHashWithString(digest, constants.getStringInfo(index)); + } + // TODO: other constants + } + + private void updateHashWithString(MessageDigest digest, String val) { + try { + digest.update(val.getBytes("UTF8")); + } catch (UnsupportedEncodingException ex) { + throw new Error(ex); + } + } + + private String toHex(byte[] bytes) { + // function taken from: + // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java + final char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + @Override + public boolean equals(Object other) { + return other instanceof ClassIdentity && equals((ClassIdentity) other); + } + + public boolean equals(ClassIdentity other) { + return this.fields.equals(other.fields) + && this.methods.equals(other.methods) + && this.constructors.equals(other.constructors) + && this.staticInitializer.equals(other.staticInitializer) + && this.extendz.equals(other.extendz) + && this.implementz.equals(other.implementz) + && this.implementations.equals(other.implementations) + && this.references.equals(other.references); + } + + @Override + public int hashCode() { + List objs = Lists.newArrayList(); + objs.addAll(this.fields); + objs.addAll(this.methods); + objs.addAll(this.constructors); + objs.add(this.staticInitializer); + objs.add(this.extendz); + objs.addAll(this.implementz); + objs.addAll(this.implementations); + objs.addAll(this.references); + return Utils.combineHashesOrdered(objs); + } + + public int getMatchScore(ClassIdentity other) { + return 2 * getNumMatches(this.extendz, other.extendz) + + 2 * getNumMatches(this.outer, other.outer) + + 2 * getNumMatches(this.implementz, other.implementz) + + getNumMatches(this.stringLiterals, other.stringLiterals) + + getNumMatches(this.fields, other.fields) + + getNumMatches(this.methods, other.methods) + + getNumMatches(this.constructors, other.constructors); + } + + public int getMaxMatchScore() { + return 2 + 2 + 2 * this.implementz.size() + this.stringLiterals.size() + this.fields.size() + this.methods.size() + this.constructors.size(); + } + + public boolean matches(CtClass c) { + // just compare declaration counts + return this.fields.size() == c.getDeclaredFields().length + && this.methods.size() == c.getDeclaredMethods().length + && this.constructors.size() == c.getDeclaredConstructors().length; + } + + private int getNumMatches(Set a, Set b) { + int numMatches = 0; + for (String val : a) { + if (b.contains(val)) { + numMatches++; + } + } + return numMatches; + } + + private int getNumMatches(Multiset a, Multiset b) { + int numMatches = 0; + for (String val : a) { + if (b.contains(val)) { + numMatches++; + } + } + return numMatches; + } + + private int getNumMatches(String a, String b) { + if (a == null && b == null) { + return 1; + } else if (a != null && b != null && a.equals(b)) { + return 1; + } + return 0; + } } -- cgit v1.2.3