1 /*
2  * Copyright (C) 2014 Google, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package dagger.internal.codegen.writer;
17 
18 import com.google.common.base.Ascii;
19 import com.google.common.base.Joiner;
20 import com.google.common.base.Objects;
21 import com.google.common.base.Optional;
22 import com.google.common.base.Splitter;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.ImmutableSet;
25 import com.google.common.collect.Sets;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Set;
32 import javax.lang.model.SourceVersion;
33 import javax.lang.model.element.Element;
34 import javax.lang.model.element.ElementKind;
35 import javax.lang.model.element.NestingKind;
36 import javax.lang.model.element.PackageElement;
37 import javax.lang.model.element.TypeElement;
38 
39 import static com.google.common.base.Preconditions.checkArgument;
40 import static com.google.common.base.Preconditions.checkNotNull;
41 import static javax.lang.model.element.NestingKind.MEMBER;
42 import static javax.lang.model.element.NestingKind.TOP_LEVEL;
43 
44 /**
45  * Represents a fully-qualified class name for {@link NestingKind#TOP_LEVEL} and
46  * {@link NestingKind#MEMBER} classes.
47  *
48  * @since 2.0
49  */
50 public final class ClassName implements TypeName, Comparable<ClassName> {
51   private String fullyQualifiedName = null;
52   private final String packageName;
53   /* From top to bottom.  E.g.: this field will contain ["A", "B"] for pgk.A.B.C */
54   private final ImmutableList<String> enclosingSimpleNames;
55   private final String simpleName;
56 
ClassName(String packageName, ImmutableList<String> enclosingSimpleNames, String simpleName)57   private ClassName(String packageName, ImmutableList<String> enclosingSimpleNames,
58       String simpleName) {
59     this.packageName = packageName;
60     this.enclosingSimpleNames = enclosingSimpleNames;
61     this.simpleName = simpleName;
62   }
63 
packageName()64   public String packageName() {
65     return packageName;
66   }
67 
enclosingSimpleNames()68   public ImmutableList<String> enclosingSimpleNames() {
69     return enclosingSimpleNames;
70   }
71 
enclosingClassName()72   public Optional<ClassName> enclosingClassName() {
73     return enclosingSimpleNames.isEmpty()
74         ? Optional.<ClassName>absent()
75         : Optional.of(new ClassName(packageName,
76             enclosingSimpleNames.subList(0, enclosingSimpleNames.size() - 1),
77             enclosingSimpleNames.get(enclosingSimpleNames.size() - 1)));
78   }
79 
simpleName()80   public String simpleName() {
81     return simpleName;
82   }
83 
canonicalName()84   public String canonicalName() {
85     if (fullyQualifiedName == null) {
86       StringBuilder builder = new StringBuilder(packageName());
87       if (builder.length() > 0) {
88         builder.append('.');
89       }
90       for (String enclosingSimpleName : enclosingSimpleNames()) {
91         builder.append(enclosingSimpleName).append('.');
92       }
93       fullyQualifiedName = builder.append(simpleName()).toString();
94     }
95     return fullyQualifiedName;
96   }
97 
98   /**
99    * Equivalent to {@link #classFileName(char) classFileName('$')}
100    */
classFileName()101   public String classFileName() {
102     return classFileName('$');
103   }
104 
105   /**
106    * Returns the class name (excluding package).
107    *
108    * <p>The returned value includes the names of its enclosing classes (if any) but not the package
109    * name. e.g. {@code fromClass(Map.Entry.class).classFileName('_')} will return {@code Map_Entry}.
110    */
classFileName(char separator)111   public String classFileName(char separator) {
112     StringBuilder builder = new StringBuilder();
113     for (String enclosingSimpleName : enclosingSimpleNames) {
114       builder.append(enclosingSimpleName).append(separator);
115     }
116     return builder.append(simpleName()).toString();
117   }
118 
topLevelClassName()119   public ClassName topLevelClassName() {
120     Iterator<String> enclosingIterator = enclosingSimpleNames().iterator();
121     return enclosingIterator.hasNext()
122         ? new ClassName(packageName(), ImmutableList.<String>of(),
123             enclosingIterator.next())
124         : this;
125   }
126 
nestedClassNamed(String memberClassName)127   public ClassName nestedClassNamed(String memberClassName) {
128     checkNotNull(memberClassName);
129     checkArgument(SourceVersion.isIdentifier(memberClassName));
130     return new ClassName(packageName(),
131         new ImmutableList.Builder<String>()
132             .addAll(enclosingSimpleNames())
133             .add(simpleName())
134             .build(),
135         memberClassName);
136   }
137 
peerNamed(String peerClassName)138   public ClassName peerNamed(String peerClassName) {
139     checkNotNull(peerClassName);
140     checkArgument(SourceVersion.isIdentifier(peerClassName));
141     return new ClassName(packageName(), enclosingSimpleNames(), peerClassName);
142   }
143 
144   /**
145    * Returns a parameterized type name with this as its raw type if {@code parameters} is not empty.
146    * If {@code parameters} is empty, returns this object.
147    */
withTypeParameters(List<? extends TypeName> parameters)148   public TypeName withTypeParameters(List<? extends TypeName> parameters) {
149     return parameters.isEmpty() ? this : ParameterizedTypeName.create(this, parameters);
150   }
151 
152   private static final ImmutableSet<NestingKind> ACCEPTABLE_NESTING_KINDS =
153       Sets.immutableEnumSet(TOP_LEVEL, MEMBER);
154 
fromTypeElement(TypeElement element)155   public static ClassName fromTypeElement(TypeElement element) {
156     checkNotNull(element);
157     checkArgument(ACCEPTABLE_NESTING_KINDS.contains(element.getNestingKind()));
158     String simpleName = element.getSimpleName().toString();
159     List<String> enclosingNames = new ArrayList<String>();
160     Element current = element.getEnclosingElement();
161     while (current.getKind().isClass() || current.getKind().isInterface()) {
162       checkArgument(ACCEPTABLE_NESTING_KINDS.contains(element.getNestingKind()));
163       enclosingNames.add(current.getSimpleName().toString());
164       current = current.getEnclosingElement();
165     }
166     PackageElement packageElement = getPackage(current);
167     Collections.reverse(enclosingNames);
168     return new ClassName(packageElement.getQualifiedName().toString(),
169         ImmutableList.copyOf(enclosingNames), simpleName);
170   }
171 
fromClass(Class<?> clazz)172   public static ClassName fromClass(Class<?> clazz) {
173     checkNotNull(clazz);
174     List<String> enclosingNames = new ArrayList<String>();
175     Class<?> current = clazz.getEnclosingClass();
176     while (current != null) {
177       enclosingNames.add(current.getSimpleName());
178       current = current.getEnclosingClass();
179     }
180     Collections.reverse(enclosingNames);
181     return create(clazz.getPackage().getName(), enclosingNames, clazz.getSimpleName());
182   }
183 
getPackage(Element type)184   private static PackageElement getPackage(Element type) {
185     while (type.getKind() != ElementKind.PACKAGE) {
186       type = type.getEnclosingElement();
187     }
188     return (PackageElement) type;
189   }
190 
191   /**
192    * Returns a new {@link ClassName} instance for the given fully-qualified class name string. This
193    * method assumes that the input is ASCII and follows typical Java style (lower-case package
194    * names, upper-camel-case class names) and may produce incorrect results or throw
195    * {@link IllegalArgumentException} otherwise. For that reason, {@link #fromClass(Class)} and
196    * {@link #fromClass(Class)} should be preferred as they can correctly create {@link ClassName}
197    * instances without such restrictions.
198    */
bestGuessFromString(String classNameString)199   public static ClassName bestGuessFromString(String classNameString) {
200     checkNotNull(classNameString);
201     List<String> parts = Splitter.on('.').splitToList(classNameString);
202     int firstClassPartIndex = -1;
203     for (int i = 0; i < parts.size(); i++) {
204       String part = parts.get(i);
205       checkArgument(SourceVersion.isIdentifier(part));
206       char firstChar = part.charAt(0);
207       if (Ascii.isLowerCase(firstChar)) {
208         // looks like a package part
209         if (firstClassPartIndex >= 0) {
210           throw new IllegalArgumentException("couldn't make a guess for " + classNameString);
211         }
212       } else if (Ascii.isUpperCase(firstChar)) {
213         // looks like a class part
214         if (firstClassPartIndex < 0) {
215           firstClassPartIndex = i;
216         }
217       } else {
218         throw new IllegalArgumentException("couldn't make a guess for " + classNameString);
219       }
220     }
221     int lastIndex = parts.size() - 1;
222     return new ClassName(
223         Joiner.on('.').join(parts.subList(0, firstClassPartIndex)),
224         firstClassPartIndex == lastIndex
225             ? ImmutableList.<String>of()
226             : ImmutableList.copyOf(parts.subList(firstClassPartIndex, lastIndex)),
227         parts.get(lastIndex));
228   }
229 
create( String packageName, List<String> enclosingSimpleNames, String simpleName)230   public static ClassName create(
231       String packageName, List<String> enclosingSimpleNames, String simpleName) {
232     return new ClassName(packageName, ImmutableList.copyOf(enclosingSimpleNames),
233         simpleName);
234   }
235 
create(String packageName, String simpleName)236   public static ClassName create(String packageName, String simpleName) {
237     return new ClassName(packageName, ImmutableList.<String>of(), simpleName);
238   }
239 
240   @Override
toString()241   public String toString() {
242     return canonicalName();
243   }
244 
245   @Override
write(Appendable appendable, Context context)246   public Appendable write(Appendable appendable, Context context) throws IOException {
247     appendable.append(context.sourceReferenceForClassName(this));
248     return appendable;
249   }
250 
251   @Override
equals(Object obj)252   public boolean equals(Object obj) {
253     if (obj == this) {
254       return true;
255     } else if (obj instanceof ClassName) {
256       ClassName that = (ClassName) obj;
257       return this.packageName.equals(that.packageName)
258           && this.enclosingSimpleNames.equals(that.enclosingSimpleNames)
259           && this.simpleName.equals(that.simpleName);
260     } else {
261       return false;
262     }
263   }
264 
265   @Override
hashCode()266   public int hashCode() {
267     return Objects.hashCode(packageName, enclosingSimpleNames, simpleName);
268   }
269 
270   @Override
compareTo(ClassName o)271   public int compareTo(ClassName o) {
272     return canonicalName().compareTo(o.canonicalName());
273   }
274 
275   @Override
referencedClasses()276   public Set<ClassName> referencedClasses() {
277     return ImmutableSet.of(this);
278   }
279 }
280