001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.reef.tang.implementation.java;
020
021import org.apache.reef.tang.ClassHierarchy;
022import org.apache.reef.tang.ExternalConstructor;
023import org.apache.reef.tang.JavaClassHierarchy;
024import org.apache.reef.tang.annotations.Name;
025import org.apache.reef.tang.annotations.NamedParameter;
026import org.apache.reef.tang.exceptions.BindException;
027import org.apache.reef.tang.exceptions.ClassHierarchyException;
028import org.apache.reef.tang.exceptions.NameResolutionException;
029import org.apache.reef.tang.exceptions.ParseException;
030import org.apache.reef.tang.formats.ParameterParser;
031import org.apache.reef.tang.types.*;
032import org.apache.reef.tang.util.MonotonicTreeMap;
033import org.apache.reef.tang.util.ReflectionUtilities;
034
035import java.lang.reflect.Type;
036import java.net.URL;
037import java.net.URLClassLoader;
038import java.util.*;
039
040public class ClassHierarchyImpl implements JavaClassHierarchy {
041  // TODO Want to add a "register namespace" method, but Java is not designed
042  // to support such things.
043  // There are third party libraries that would help, but they can fail if the
044  // relevant jar has not yet been loaded.  Tint works around this using such
045  // a library.
046
047  /**
048   * The ParameterParser that this ClassHierarchy uses to parse default values.
049   * Custom parameter parsers allow applications to extend the set of classes
050   * that Tang can parse.
051   */
052  private final ParameterParser parameterParser = new ParameterParser();
053  /**
054   * The classloader that was used to populate this class hierarchy.
055   */
056  private final URLClassLoader loader;
057  /**
058   * The jars that are reflected by that loader.  These are in addition to
059   * whatever jars are available by the default classloader (i.e., the one
060   * that loaded Tang.  We need this list so that we can merge class hierarchies
061   * that are backed by different classpaths.
062   */
063  private final List<URL> jars;
064  /**
065   * A reference to the root package which is a root of a tree of nodes.
066   * The children of each node in the tree are accessible by name, and the
067   * structure of the tree mirrors Java's package namespace.
068   */
069  private final PackageNode namespace;
070  /**
071   * A map from short name to named parameter node.  This is only used to
072   * sanity check short names so that name clashes get resolved.
073   */
074  private final Map<String, NamedParameterNode<?>> shortNames = new MonotonicTreeMap<>();
075
076  @SuppressWarnings("unchecked")
077  public ClassHierarchyImpl() {
078    this(new URL[0], new Class[0]);
079  }
080
081  @SuppressWarnings("unchecked")
082  public ClassHierarchyImpl(final URL... jars) {
083    this(jars, new Class[0]);
084  }
085
086  public ClassHierarchyImpl(final URL[] jars, final Class<? extends ExternalConstructor<?>>[] parameterParsers) {
087    this.namespace = JavaNodeFactory.createRootPackageNode();
088    this.jars = new ArrayList<>(Arrays.asList(jars));
089    this.loader = new URLClassLoader(jars, this.getClass().getClassLoader());
090    for (final Class<? extends ExternalConstructor<?>> p : parameterParsers) {
091      try {
092        parameterParser.addParser(p);
093      } catch (final BindException e) {
094        throw new IllegalArgumentException("Could not register parameter parsers", e);
095      }
096    }
097  }
098
099  /**
100   * A helper method that returns the parsed default value of a given
101   * NamedParameter.
102   *
103   * @return null or an empty set if there is no default value, the default value (or set of values) otherwise.
104   * @throws ClassHierarchyException if a default value was specified, but could not be parsed, or if a set of
105   *                                 values were specified for a non-set parameter.
106   */
107  @SuppressWarnings("unchecked")
108  @Override
109  public <T> T parseDefaultValue(final NamedParameterNode<T> name) {
110    final String[] vals = name.getDefaultInstanceAsStrings();
111    final T[] ret = (T[]) new Object[vals.length];
112    for (int i = 0; i < vals.length; i++) {
113      final String val = vals[i];
114      try {
115        ret[i] = parse(name, val);
116      } catch (final ParseException e) {
117        throw new ClassHierarchyException("Could not parse default value", e);
118      }
119    }
120    if (name.isSet()) {
121      return (T) new HashSet<T>(Arrays.asList(ret));
122    } else if (name.isList()) {
123      return (T) new ArrayList<T>(Arrays.asList(ret));
124    } else {
125      if (ret.length == 0) {
126        return null;
127      } else if (ret.length == 1) {
128        return ret[0];
129      } else {
130        throw new IllegalStateException("Multiple defaults for non-set named parameter! " + name.getFullName());
131      }
132    }
133  }
134
135  /**
136   * Parse a string, assuming that it is of the type expected by a given NamedParameter.
137   * <p>
138   * This method does not deal with sets; if the NamedParameter is set valued, then the provided
139   * string should correspond to a single member of the set.  It is up to the caller to call parse
140   * once for each value that should be parsed as a member of the set.
141   *
142   * @return a non-null reference to the parsed value.
143   */
144  @Override
145  @SuppressWarnings("unchecked")
146  public <T> T parse(final NamedParameterNode<T> np, final String value) throws ParseException {
147    final ClassNode<T> iface;
148    try {
149      iface = (ClassNode<T>) getNode(np.getFullArgName());
150    } catch (final NameResolutionException e) {
151      throw new IllegalStateException("Could not parse validated named parameter argument type.  NamedParameter is " +
152          np.getFullName() + " argument type is " + np.getFullArgName(), e);
153    }
154    Class<?> clazz;
155    String fullName;
156    try {
157      clazz = classForName(iface.getFullName());
158      fullName = null;
159    } catch (final ClassNotFoundException e) {
160      clazz = null;
161      fullName = iface.getFullName();
162    }
163    try {
164      if (clazz != null) {
165        return (T) parameterParser.parse(clazz, value);
166      } else {
167        return parameterParser.parse(fullName, value);
168      }
169    } catch (final UnsupportedOperationException e) {
170      try {
171        final Node impl = getNode(value);
172        if (impl instanceof ClassNode && isImplementation(iface, (ClassNode<?>) impl)) {
173          return (T) impl;
174        }
175        throw new ParseException("Name<" + iface.getFullName() + "> " + np.getFullName() +
176            " cannot take non-subclass " + impl.getFullName(), e);
177      } catch (final NameResolutionException e2) {
178        throw new ParseException("Name<" + iface.getFullName() + "> " + np.getFullName() +
179            " cannot take non-class " + value, e2);
180      }
181    }
182  }
183
184  /**
185   * Helper method that converts a String to a Class using this
186   * ClassHierarchy's classloader.
187   */
188  @Override
189  public Class<?> classForName(final String name) throws ClassNotFoundException {
190    return ReflectionUtilities.classForName(name, loader);
191  }
192
193  private <T, U> Node buildPathToNode(final Class<U> clazz)
194      throws ClassHierarchyException {
195    final String[] path = clazz.getName().split("\\$");
196
197    Node root = namespace;
198    for (int i = 0; i < path.length - 1; i++) {
199      root = root.get(path[i]);
200    }
201
202    if (root == null) {
203      throw new NullPointerException("The root of the path to node is null. "
204              + "The class name is likely to be malformed. The clazz.getName() returns " + clazz.getName());
205    }
206    final Node parent = root;
207
208    final Type argType = ReflectionUtilities.getNamedParameterTargetOrNull(clazz);
209
210    if (argType == null) {
211      return JavaNodeFactory.createClassNode(parent, clazz);
212    } else {
213
214      // checked inside of NamedParameterNode, using reflection.
215      @SuppressWarnings("unchecked") final NamedParameterNode<T> np = JavaNodeFactory.createNamedParameterNode(
216          parent, (Class<? extends Name<T>>) clazz, argType);
217
218      if (parameterParser.canParse(ReflectionUtilities.getFullName(argType)) &&
219          clazz.getAnnotation(NamedParameter.class).default_class() != Void.class) {
220        throw new ClassHierarchyException("Named parameter " + ReflectionUtilities.getFullName(clazz) +
221            " defines default implementation for parsable type " + ReflectionUtilities.getFullName(argType));
222      }
223
224      final String shortName = np.getShortName();
225      if (shortName != null) {
226        final NamedParameterNode<?> oldNode = shortNames.get(shortName);
227        if (oldNode != null) {
228          if (oldNode.getFullName().equals(np.getFullName())) {
229            throw new IllegalStateException("Tried to double bind "
230                + oldNode.getFullName() + " to short name " + shortName);
231          }
232          throw new ClassHierarchyException("Named parameters " + oldNode.getFullName()
233              + " and " + np.getFullName() + " have the same short name: "
234              + shortName);
235        }
236        shortNames.put(shortName, np);
237      }
238      return np;
239    }
240  }
241
242  private Node getAlreadyBoundNode(final Class<?> clazz) throws NameResolutionException {
243    return getAlreadyBoundNode(ReflectionUtilities.getFullName(clazz));
244  }
245
246  @Override
247  public Node getNode(final Class<?> clazz) {
248    try {
249      return getNode(ReflectionUtilities.getFullName(clazz));
250    } catch (final NameResolutionException e) {
251      throw new ClassHierarchyException("JavaClassHierarchy could not resolve " + clazz
252          + " which is definitely avalable at runtime", e);
253    }
254  }
255
256  @Override
257  public synchronized Node getNode(final String name) throws NameResolutionException {
258    final Node n = register(name);
259    if (n == null) {
260      // This will never succeed; it just generates a nice exception.
261      getAlreadyBoundNode(name);
262      throw new IllegalStateException("IMPLEMENTATION BUG: Register failed, "
263          + "but getAlreadyBoundNode succeeded!");
264    }
265    return n;
266  }
267
268  private Node getAlreadyBoundNode(final String name) throws NameResolutionException {
269    Node root = namespace;
270    final String[] toks = name.split("\\$");
271    final String outerClassName = toks[0];
272    root = root.get(outerClassName);
273    if (root == null) {
274      throw new NameResolutionException(name, outerClassName);
275    }
276    for (int i = 1; i < toks.length; i++) {
277      root = root.get(toks[i]);
278      if (root == null) {
279        final StringBuilder sb = new StringBuilder(outerClassName);
280        for (int j = 0; j < i; j++) {
281          sb.append(toks[j]);
282          if (j != i - 1) {
283            sb.append(".");
284          }
285        }
286        throw new NameResolutionException(name, sb.toString());
287      }
288    }
289    return root;
290  }
291
292  private Node register(final String s) {
293    final Class<?> c;
294    try {
295      c = classForName(s);
296    } catch (final ClassNotFoundException e1) {
297      return null;
298    }
299    try {
300      final Node n = getAlreadyBoundNode(c);
301      return n;
302    } catch (final NameResolutionException ignored) {
303      // node not bound yet
304    }
305    // First, walk up the class hierarchy, registering all out parents. This
306    // can't be loopy.
307    if (c.getSuperclass() != null) {
308      register(ReflectionUtilities.getFullName(c.getSuperclass()));
309    }
310    for (final Class<?> i : c.getInterfaces()) {
311      register(ReflectionUtilities.getFullName(i));
312    }
313    // Now, we'd like to register our enclosing classes. This turns out to be
314    // safe.
315    // Thankfully, Java doesn't allow:
316    // class A implements A.B { class B { } }
317
318    // It also doesn't allow cycles such as:
319    // class A implements B.BB { interface AA { } }
320    // class B implements A.AA { interface BB { } }
321
322    // So, even though grafting arbitrary DAGs together can give us cycles, Java
323    // seems
324    // to have our back on this one.
325    final Class<?> enclosing = c.getEnclosingClass();
326    if (enclosing != null) {
327      register(ReflectionUtilities.getFullName(enclosing));
328    }
329
330    // Now register the class. This has to be after the above so we know our
331    // parents (superclasses and enclosing packages) are already registered.
332    final Node n = registerClass(c);
333
334    // Finally, do things that might introduce cycles that invlove c.
335    // This has to be below registerClass, which ensures that any cycles
336    // this stuff introduces are broken.
337    for (final Class<?> innerClass : c.getDeclaredClasses()) {
338      register(ReflectionUtilities.getFullName(innerClass));
339    }
340    if (n instanceof ClassNode) {
341      final ClassNode<?> cls = (ClassNode<?>) n;
342      for (final ConstructorDef<?> def : cls.getInjectableConstructors()) {
343        for (final ConstructorArg arg : def.getArgs()) {
344          register(arg.getType());
345          if (arg.getNamedParameterName() != null) {
346            final NamedParameterNode<?> np = (NamedParameterNode<?>) register(arg
347                .getNamedParameterName());
348            try {
349              // TODO: When handling sets, need to track target of generic parameter, and check the type here!
350              if (!np.isSet() && !np.isList() &&
351                  !ReflectionUtilities.isCoercable(classForName(arg.getType()), classForName(np.getFullArgName()))) {
352                throw new ClassHierarchyException(
353                    "Named parameter type mismatch in " + cls.getFullName() + ".  Constructor expects a "
354                        + arg.getType() + " but " + np.getName() + " is a "
355                        + np.getFullArgName());
356              }
357            } catch (final ClassNotFoundException e) {
358              throw new ClassHierarchyException("Constructor refers to unknown class "
359                  + arg.getType(), e);
360            }
361          }
362        }
363      }
364    } else if (n instanceof NamedParameterNode) {
365      final NamedParameterNode<?> np = (NamedParameterNode<?>) n;
366      register(np.getFullArgName());
367    }
368    return n;
369  }
370
371  /**
372   * Assumes that all of the parents of c have been registered already.
373   *
374   * @param c
375   */
376  @SuppressWarnings("unchecked")
377  private <T> Node registerClass(final Class<T> c)
378      throws ClassHierarchyException {
379    if (c.isArray()) {
380      throw new UnsupportedOperationException("Can't register array types");
381    }
382    try {
383      return getAlreadyBoundNode(c);
384    } catch (final NameResolutionException ignored) {
385      // node not bound yet
386    }
387
388    final Node n = buildPathToNode(c);
389
390    if (n instanceof ClassNode) {
391      final ClassNode<T> cn = (ClassNode<T>) n;
392      final Class<T> superclass = (Class<T>) c.getSuperclass();
393      if (superclass != null) {
394        try {
395          ((ClassNode<T>) getAlreadyBoundNode(superclass)).putImpl(cn);
396        } catch (final NameResolutionException e) {
397          throw new IllegalStateException(e);
398        }
399      }
400      for (final Class<?> interf : c.getInterfaces()) {
401        try {
402          ((ClassNode<T>) getAlreadyBoundNode(interf)).putImpl(cn);
403        } catch (final NameResolutionException e) {
404          throw new IllegalStateException(e);
405        }
406      }
407    }
408    return n;
409  }
410
411  @Override
412  public PackageNode getNamespace() {
413    return namespace;
414  }
415
416  public ParameterParser getParameterParser() {
417    return parameterParser;
418  }
419
420  @Override
421  public synchronized boolean isImplementation(final ClassNode<?> inter, final ClassNode<?> impl) {
422    return impl.isImplementationOf(inter);
423  }
424
425  @Override
426  public synchronized ClassHierarchy merge(final ClassHierarchy ch) {
427    if (this == ch) {
428      return this;
429    }
430    if (!(ch instanceof ClassHierarchyImpl)) {
431      throw new UnsupportedOperationException("Can't merge java and non-java class hierarchies yet!");
432    }
433    if (this.jars.size() == 0) {
434      return ch;
435    }
436    final ClassHierarchyImpl chi = (ClassHierarchyImpl) ch;
437    final HashSet<URL> otherJars = new HashSet<>();
438    otherJars.addAll(chi.jars);
439    final HashSet<URL> myJars = new HashSet<>();
440    myJars.addAll(this.jars);
441    if (myJars.containsAll(otherJars)) {
442      return this;
443    } else if (otherJars.containsAll(myJars)) {
444      return ch;
445    } else {
446      myJars.addAll(otherJars);
447      return new ClassHierarchyImpl(myJars.toArray(new URL[0]));
448    }
449  }
450}