1 /*
2 * Copyright (c) 2012-2024, jcabi.com
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met: 1) Redistributions of source code must retain the above
8 * copyright notice, this list of conditions and the following
9 * disclaimer. 2) Redistributions in binary form must reproduce the above
10 * copyright notice, this list of conditions and the following
11 * disclaimer in the documentation and/or other materials provided
12 * with the distribution. 3) Neither the name of the jcabi.com nor
13 * the names of its contributors may be used to endorse or promote
14 * products derived from this software without specific prior written
15 * permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28 * OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 package com.jcabi.aspects.aj;
31
32 import com.jcabi.aspects.Immutable;
33 import com.jcabi.aspects.version.Version;
34 import com.jcabi.log.Logger;
35 import java.lang.reflect.Field;
36 import java.lang.reflect.Modifier;
37 import java.util.Collection;
38 import java.util.HashSet;
39 import org.aspectj.lang.JoinPoint;
40 import org.aspectj.lang.annotation.After;
41 import org.aspectj.lang.annotation.Aspect;
42
43 /**
44 * Checks for class immutability.
45 *
46 * <p>The class is thread-safe.
47 *
48 * @since 0.7.8
49 */
50 @Aspect
51 public final class ImmutabilityChecker {
52
53 /**
54 * Already checked immutable classes.
55 */
56 private final transient Collection<Class<?>> immutable = new HashSet<>();
57
58 /**
59 * Catch instantiation and validate class.
60 *
61 * <p>Try NOT to change the signature of this method, in order to keep
62 * it backward compatible.
63 * @param point Joint point
64 */
65 @After("initialization((@com.jcabi.aspects.Immutable *).new(..))")
66 public void after(final JoinPoint point) {
67 final Class<?> type = point.getTarget().getClass();
68 try {
69 this.check(type);
70 } catch (final ImmutabilityChecker.Violation ex) {
71 throw new IllegalStateException(
72 String.format(
73 // @checkstyle LineLength (1 line)
74 "%s is not immutable, can't use it (jcabi-aspects %s/%s)",
75 type,
76 Version.CURRENT.projectVersion(),
77 Version.CURRENT.buildNumber()
78 ),
79 ex
80 );
81 }
82 }
83
84 /**
85 * This class is immutable?
86 * @param type The class to check
87 * @throws ImmutabilityChecker.Violation If it is mutable
88 */
89 private void check(final Class<?> type)
90 throws ImmutabilityChecker.Violation {
91 synchronized (this.immutable) {
92 if (!this.ignore(type)) {
93 if (type.isInterface()
94 && !type.isAnnotationPresent(Immutable.class)) {
95 throw new ImmutabilityChecker.Violation(
96 String.format(
97 "Interface '%s' is not annotated with @Immutable",
98 type.getName()
99 )
100 );
101 }
102 if (!type.isInterface()
103 && !Modifier.isFinal(type.getModifiers())) {
104 throw new ImmutabilityChecker.Violation(
105 String.format(
106 "Class '%s' is not final",
107 type.getName()
108 )
109 );
110 }
111 try {
112 this.fields(type);
113 } catch (final ImmutabilityChecker.Violation ex) {
114 throw new ImmutabilityChecker.Violation(
115 String.format("Class '%s' is mutable", type.getName()),
116 ex
117 );
118 }
119 this.immutable.add(type);
120 Logger.debug(this, "#check(%s): immutability checked", type);
121 }
122 }
123 }
124
125 /**
126 * This class should be ignored and never checked any more?
127 * @param type The type to check
128 * @return TRUE if this class shouldn't be checked
129 */
130 private boolean ignore(final Class<?> type) {
131 // @checkstyle BooleanExpressionComplexity (5 lines)
132 return type.equals(Object.class)
133 || type.equals(String.class)
134 || type.isPrimitive()
135 || type.getName().startsWith("org.aspectj.runtime.reflect.")
136 || this.immutable.contains(type);
137 }
138
139 /**
140 * All its fields are safe?
141 * @param type Type to check
142 * @throws ImmutabilityChecker.Violation If it is mutable
143 */
144 private void fields(final Class<?> type)
145 throws ImmutabilityChecker.Violation {
146 final Field[] fields = type.getDeclaredFields();
147 for (final Field field : fields) {
148 if (Modifier.isStatic(field.getModifiers())) {
149 continue;
150 }
151 if (!Modifier.isFinal(field.getModifiers())) {
152 throw new ImmutabilityChecker.Violation(
153 String.format(
154 "field '%s' is not final in %s",
155 field, type.getName()
156 )
157 );
158 }
159 try {
160 if (field.getType().isArray()) {
161 this.checkArray(field);
162 }
163 } catch (final ImmutabilityChecker.Violation ex) {
164 throw new ImmutabilityChecker.Violation(
165 String.format(
166 "field '%s' is mutable",
167 field
168 ),
169 ex
170 );
171 }
172 }
173 }
174
175 /**
176 * This array field immutable?
177 * @param field The field to check
178 * @throws ImmutabilityChecker.Violation If it is mutable.
179 */
180 private void checkArray(final Field field)
181 throws ImmutabilityChecker.Violation {
182 if (!field.isAnnotationPresent(Immutable.Array.class)) {
183 throw new ImmutabilityChecker.Violation(
184 String.format(
185 // @checkstyle LineLength (1 line)
186 "Field '%s' is an array and is not annotated with @Immutable.Array",
187 field.getName()
188 )
189 );
190 }
191 final Class<?> type = field.getType().getComponentType();
192 try {
193 this.check(type);
194 } catch (final ImmutabilityChecker.Violation ex) {
195 throw new ImmutabilityChecker.Violation(
196 String.format(
197 "Field array component type '%s' is mutable",
198 type.getName()
199 ),
200 ex
201 );
202 }
203 }
204
205 /**
206 * Immutability violation.
207 * @since 0.0.0
208 */
209 private static final class Violation extends Exception {
210
211 /**
212 * Serialization marker.
213 */
214 private static final long serialVersionUID = 1L;
215
216 /**
217 * Public ctor.
218 * @param msg Message
219 */
220 private Violation(final String msg) {
221 super(msg);
222 }
223
224 /**
225 * Public ctor.
226 * @param msg Message
227 * @param cause Cause of it
228 */
229 private Violation(final String msg, final Exception cause) {
230 super(msg, cause);
231 }
232 }
233
234 }