View Javadoc
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 }