Skip to content

Commit 6893889

Browse files
authored
Implement equals, hashCode, and toString() methods. (firebase#1595)
* Implement equals, hashCode, and toString() methods. * gJF * Add docs on equals and hashCode.
1 parent 376c25c commit 6893889

File tree

7 files changed

+433
-10
lines changed

7 files changed

+433
-10
lines changed

encoders/firebase-encoders-processor/src/main/java/com/google/firebase/encoders/processor/annotations/AnnotationBuilder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,9 @@ private static AnnotationImpl createAnnotationImpl(TypeElement annotation) {
222222
}
223223
annotationImpl.addMethod(constructorBuilder.build());
224224

225-
// TODO(vkryachko): generate proper equals() and hashCode().
226-
// annotationImpl.addMethod(EqualsMethod.generate(annotation));
227-
// annotationImpl.addMethod(HashCodeMethod.generate(annotation));
225+
annotationImpl.addMethod(EqualsMethod.generate(annotation));
226+
annotationImpl.addMethod(HashCodeMethod.generate(annotation));
227+
annotationImpl.addMethod(ToStringMethod.generate(annotation));
228228

229229
return new AnnotationImpl(annotationImpl.build(), defaults);
230230
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.encoders.processor.annotations;
16+
17+
import com.squareup.javapoet.CodeBlock;
18+
import com.squareup.javapoet.MethodSpec;
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import javax.lang.model.element.ExecutableElement;
23+
import javax.lang.model.element.Modifier;
24+
import javax.lang.model.element.TypeElement;
25+
import javax.lang.model.type.TypeKind;
26+
import javax.lang.model.util.ElementFilter;
27+
28+
/**
29+
* Generates compliant equals() method implementation.
30+
*
31+
* @see <a
32+
* href="https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/Annotation.html#equals(java.lang.Object)">Annotation#equals()</a>
33+
* requirements.
34+
*/
35+
class EqualsMethod {
36+
static MethodSpec generate(TypeElement annotation) {
37+
MethodSpec.Builder equalsBuilder =
38+
MethodSpec.methodBuilder("equals")
39+
.addModifiers(Modifier.PUBLIC)
40+
.addAnnotation(Override.class)
41+
.returns(boolean.class)
42+
.addParameter(Object.class, "other");
43+
equalsBuilder.addCode(
44+
CodeBlock.builder()
45+
.add("if (this == other) return true;\n")
46+
.add("if (!(other instanceof $T)) return false;\n", annotation)
47+
.add("$1T that = ($1T) other;\n\n", annotation)
48+
.build());
49+
50+
List<CodeBlock> equalExpressions = new ArrayList<>();
51+
52+
for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {
53+
TypeKind resultKind = method.getReturnType().getKind();
54+
if (resultKind.isPrimitive()) {
55+
if (resultKind.equals(TypeKind.FLOAT)) {
56+
equalExpressions.add(
57+
CodeBlock.builder()
58+
.add(
59+
"(Float.floatToIntBits($1L) == Float.floatToIntBits(that.$1L()))",
60+
method.getSimpleName().toString())
61+
.build());
62+
continue;
63+
}
64+
if (resultKind.equals(TypeKind.DOUBLE)) {
65+
equalExpressions.add(
66+
CodeBlock.builder()
67+
.add(
68+
"(Double.doubleToLongBits($1L) == Double.doubleToLongBits(that.$1L()))",
69+
method.getSimpleName().toString())
70+
.build());
71+
continue;
72+
}
73+
equalExpressions.add(
74+
CodeBlock.builder()
75+
.add("($1L == that.$1L())", method.getSimpleName().toString())
76+
.build());
77+
} else {
78+
if (resultKind.equals(TypeKind.ARRAY)) {
79+
equalExpressions.add(
80+
CodeBlock.builder()
81+
.add(
82+
"($1T.equals($2L, that.$2L()))",
83+
Arrays.class,
84+
method.getSimpleName().toString())
85+
.build());
86+
}
87+
equalExpressions.add(
88+
CodeBlock.builder()
89+
.add("($1L.equals(that.$1L()))", method.getSimpleName().toString())
90+
.build());
91+
}
92+
}
93+
if (equalExpressions.isEmpty()) {
94+
equalsBuilder.addCode("return true;\n");
95+
} else {
96+
equalsBuilder.addCode("return $L;\n", CodeBlock.join(equalExpressions, "\n && "));
97+
}
98+
return equalsBuilder.build();
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.encoders.processor.annotations;
16+
17+
import com.squareup.javapoet.CodeBlock;
18+
import com.squareup.javapoet.MethodSpec;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import javax.lang.model.element.ExecutableElement;
22+
import javax.lang.model.element.Modifier;
23+
import javax.lang.model.element.TypeElement;
24+
import javax.lang.model.type.TypeKind;
25+
import javax.lang.model.util.ElementFilter;
26+
27+
/**
28+
* Generates compliant hashCode() method implementation.
29+
*
30+
* @see <a
31+
* href="https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/Annotation.html#hashCode()">Annotation#hashCode()</a>
32+
* requirements.
33+
*/
34+
class HashCodeMethod {
35+
static MethodSpec generate(TypeElement annotation) {
36+
MethodSpec.Builder hashCodeBuilder =
37+
MethodSpec.methodBuilder("hashCode")
38+
.addModifiers(Modifier.PUBLIC)
39+
.addAnnotation(Override.class)
40+
.returns(int.class);
41+
42+
List<ExecutableElement> methods = ElementFilter.methodsIn(annotation.getEnclosedElements());
43+
if (methods.isEmpty()) {
44+
hashCodeBuilder.addCode("return 0;\n");
45+
return hashCodeBuilder.build();
46+
}
47+
48+
CodeBlock.Builder code = CodeBlock.builder().add("return ");
49+
for (ExecutableElement method : methods) {
50+
code.add(
51+
"+ ($L ^ $L)\n",
52+
127 * method.getSimpleName().toString().hashCode(),
53+
hashExpression(method));
54+
}
55+
code.add(";\n");
56+
hashCodeBuilder.addCode(code.build());
57+
58+
return hashCodeBuilder.build();
59+
}
60+
61+
private static CodeBlock hashExpression(ExecutableElement method) {
62+
TypeKind returnKind = method.getReturnType().getKind();
63+
if (returnKind.isPrimitive()) {
64+
if (returnKind.equals(TypeKind.FLOAT)) {
65+
return CodeBlock.builder()
66+
.add("(Float.floatToIntBits($L))", method.getSimpleName())
67+
.build();
68+
}
69+
if (returnKind.equals(TypeKind.DOUBLE)) {
70+
return CodeBlock.builder()
71+
.add(
72+
"((int) ((Double.doubleToLongBits($1L) >>> 32) ^ Double.doubleToLongBits($1L)))",
73+
method.getSimpleName())
74+
.build();
75+
}
76+
if (returnKind.equals(TypeKind.BOOLEAN)) {
77+
return CodeBlock.builder().add("($L ? 1231 : 1237)", method.getSimpleName()).build();
78+
}
79+
if (returnKind.equals(TypeKind.LONG)) {
80+
return CodeBlock.builder()
81+
.add("((int)($1L ^ ($1L >>> 32)))", method.getSimpleName())
82+
.build();
83+
}
84+
return CodeBlock.builder().add("((int)$L)", method.getSimpleName()).build();
85+
}
86+
if (returnKind.equals(TypeKind.ARRAY)) {
87+
return CodeBlock.builder()
88+
.add("$T.hashCode($L)", Arrays.class, method.getSimpleName())
89+
.build();
90+
}
91+
return CodeBlock.builder().add("$L.hashCode()", method.getSimpleName()).build();
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.encoders.processor.annotations;
16+
17+
import com.squareup.javapoet.ClassName;
18+
import com.squareup.javapoet.CodeBlock;
19+
import com.squareup.javapoet.MethodSpec;
20+
import com.squareup.javapoet.MethodSpec.Builder;
21+
import java.util.List;
22+
import javax.lang.model.element.ExecutableElement;
23+
import javax.lang.model.element.Modifier;
24+
import javax.lang.model.element.TypeElement;
25+
import javax.lang.model.util.ElementFilter;
26+
27+
final class ToStringMethod {
28+
static MethodSpec generate(TypeElement element) {
29+
ClassName.get(element).reflectionName();
30+
Builder result =
31+
MethodSpec.methodBuilder("toString")
32+
.addModifiers(Modifier.PUBLIC)
33+
.returns(String.class)
34+
.addAnnotation(Override.class)
35+
.addCode(
36+
CodeBlock.builder()
37+
.add(
38+
"StringBuilder sb = new StringBuilder(\"@$L\");\n",
39+
ClassName.get(element).reflectionName().replace('$', '.'))
40+
.build());
41+
List<ExecutableElement> methods = ElementFilter.methodsIn(element.getEnclosedElements());
42+
if (!methods.isEmpty()) {
43+
result.addCode("sb.append('(');\n");
44+
}
45+
for (ExecutableElement method : methods) {
46+
result.addCode("sb.append(\"$1L=\").append($1L);\n", method.getSimpleName());
47+
}
48+
if (!methods.isEmpty()) {
49+
result.addCode("sb.append(')');\n");
50+
}
51+
result.addCode("return sb.toString();\n");
52+
return result.build();
53+
}
54+
}

encoders/firebase-encoders-processor/src/test/java/com/google/firebase/encoders/processor/ExtraPropertyProcessorTest.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ public void test() {
3232
.withProcessors(new ExtraPropertyProcessor())
3333
.compile(
3434
JavaFileObjects.forSourceLines(
35-
"EmptyAnnotation",
35+
"com.example.EmptyAnnotation",
36+
"package com.example;",
3637
"import com.google.firebase.encoders.annotations.ExtraProperty;",
3738
"@ExtraProperty",
3839
"public @interface EmptyAnnotation {}"));
3940

4041
assertThat(result).succeededWithoutWarnings();
4142
assertThat(result)
42-
.generatedSourceFile("AtEmptyAnnotation")
43+
.generatedSourceFile("com/example/AtEmptyAnnotation")
4344
.hasSourceEquivalentTo(JavaFileObjects.forResource("ExpectedAtEmptyAnnotation.java"));
4445
}
4546

@@ -50,19 +51,26 @@ public void test2() {
5051
.withProcessors(new ExtraPropertyProcessor())
5152
.compile(
5253
JavaFileObjects.forSourceLines(
53-
"MyAnnotation",
54+
"com.example.MyAnnotation",
55+
"package com.example;",
5456
"import com.google.firebase.encoders.annotations.ExtraProperty;",
5557
"@ExtraProperty",
5658
"public @interface MyAnnotation {",
5759
"int intVal();",
60+
"long longVal();",
61+
"boolean boolVal();",
62+
"short shortVal();",
63+
"float floatVal();",
64+
"double doubleVal();",
65+
"double[] doubleArrayVal();",
5866
"String strVal() default \"default\";",
5967
"MyEnum enumVal() default MyEnum.VALUE1;",
6068
"enum MyEnum { VALUE1, VALUE2 }",
6169
"}"));
6270

6371
assertThat(result).succeededWithoutWarnings();
6472
assertThat(result)
65-
.generatedSourceFile("AtMyAnnotation")
73+
.generatedSourceFile("com/example/AtMyAnnotation")
6674
.hasSourceEquivalentTo(JavaFileObjects.forResource("ExpectedAtMyAnnotation.java"));
6775
}
6876
}

encoders/firebase-encoders-processor/src/test/resources/ExpectedAtEmptyAnnotation.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
package com.example;
16+
1517
import java.lang.Class;
18+
import java.lang.Object;
1619
import java.lang.Override;
20+
import java.lang.String;
1721
import java.lang.annotation.Annotation;
1822

1923
public final class AtEmptyAnnotation {
@@ -40,5 +44,25 @@ private static final class EmptyAnnotationImpl implements EmptyAnnotation {
4044
public Class<? extends Annotation> annotationType() {
4145
return EmptyAnnotation.class;
4246
}
47+
48+
@Override
49+
public boolean equals(Object other) {
50+
if (this == other) return true;
51+
if (!(other instanceof EmptyAnnotation)) return false;
52+
EmptyAnnotation that = (EmptyAnnotation) other;
53+
54+
return true;
55+
}
56+
57+
@Override
58+
public int hashCode() {
59+
return 0;
60+
}
61+
62+
@Override
63+
public String toString() {
64+
StringBuilder sb = new StringBuilder("@com.example.EmptyAnnotation");
65+
return sb.toString();
66+
}
4367
}
4468
}

0 commit comments

Comments
 (0)