001/*
002 * Copyright c 2018 Rusi Popov, MDA Tools.net All rights reserved.
003 *
004 * This program and the accompanying materials are made available under the terms of the
005 * Eclipse Public License v2.0 which accompanies this distribution, and is available at
006 * http://www.eclipse.org/legal/epl-v20.html
007 */
008package net.mdatools.modelant.core.wrap;
009
010import java.lang.reflect.Constructor;
011import java.util.ArrayList;
012import java.util.Collection;
013import java.util.IdentityHashMap;
014import java.util.List;
015import java.util.Map;
016
017import net.mdatools.modelant.core.api.wrap.Wrapper;
018import net.mdatools.modelant.core.api.wrap.WrapperFactory;
019
020/**
021 * This class is the root class for all factories of wrapper classes.
022 * CONVENTION:<ul>
023 * <li> each Factory subclass must have all its constructors <b>protected</b>, where one of them must be without parameters.
024 *      This prevents accidental instantiation of the factory itself this way violating its Singleton property.
025 * <li> each Factory instance caches internally all the wrapper objects it creates, in order to guarantee: <pre>
026 *      (for each X, Y)( factory.wrap(X) == factory.wrap(X) &lt;=&gt; X == Y)
027 *      </pre>
028 * <li> The subclasses must provide the mapping from wrapped to wrapper classes.
029 * </ul>
030 * @author Rusi Popov (popovr@mdatools.net)
031 */
032public abstract class BaseWrapperFactory implements WrapperFactory {
033
034  /**
035   * Maps wrapped to wrapper objects, so that whenever a new wrapper is requested
036   * for an object, its already existing wrapper will be re-used.
037   */
038  private final Map<Object, Wrapper<? extends Object>> wrappedToWrapperMap = new IdentityHashMap<>(1023);
039
040
041  /**
042   * @param toWrap could be null
043   * @return a non-null instance unique for each non-null wrapped object or null, when wrapped is null
044   * @throws IllegalArgumentException when there is no wrapper class corresponding to the class of toWrap
045   */
046  public final <A> Wrapper<A> wrap(A toWrap) throws IllegalArgumentException {
047    Wrapper<A> result;
048
049    if ( toWrap == null ) {
050      result = null;
051    } else {
052      result = (Wrapper<A>) wrappedToWrapperMap.get( toWrap );
053      if ( result == null ) {
054        result = constructWrapper( toWrap );
055        wrappedToWrapperMap.put(toWrap, result );
056      }
057    }
058    return result;
059  }
060
061
062  /**
063   * @param toWrap is a non-null collection of (any) objects to wrap
064   * @return a non-null ArrayList of wrapped objects. Note that the type is important, because
065   *         it is used in any further wrapping
066   * @throws IllegalArgumentException when there is no wrapper class for an object to wrap
067   * @see #wrap(Object)
068   */
069  public final <A> List<Wrapper<A>> wrap(Collection<A> toWrap) throws IllegalArgumentException {
070    List<Wrapper<A>> result;
071
072    if ( toWrap == null ) {
073      result = null;
074    } else {
075      result = new ArrayList<>( toWrap.size() );
076      for (A obj : toWrap ) {
077        result.add( wrap( obj ) );
078      }
079    }
080    return result;
081  }
082
083  /**
084   * Make sure this method is the last method called on a factory instance.
085   */
086  public final void destroy() {
087    wrappedToWrapperMap.clear();
088  }
089
090  /**
091   * This method binds an already built wrapper for a wrapped object that <b>has not been used</b> in this factory.
092   * Thus, any further call to wrap( wrapper.getWrapped() ) would return the wrapper object.
093   * @param wrapper is a non-null wrapper object
094   * @throws IllegalArgumentException when there is already a wrapper registered for wrapper.getWrapped()
095   */
096  public final <A> void bind(Wrapper<A> wrapper) throws IllegalArgumentException {
097    Wrapper<A> existing;
098
099    existing = (Wrapper<A>) wrappedToWrapperMap.put(wrapper.getWrapped(), wrapper );
100    if ( existing != null ) {
101      throw new IllegalArgumentException( "Registering a wrapper for an object that was already wrapped" );
102    }
103  }
104
105  /**
106   * @return a non null initialized wrapper instance or an exception is thrown.
107   *         It is Wrapper by default
108   */
109  private <A> Wrapper<A> constructWrapper(A toWrap) throws IllegalArgumentException {
110    Wrapper<A> result;
111    Class resultClass;
112    Constructor constructor;
113
114    // because the wrapper class could be mapped by class or interface, try finding it both ways
115    resultClass = getMappedClass( toWrap );
116    try {
117      constructor = resultClass.getConstructor(Object.class, WrapperFactory.class);
118      result = (Wrapper<A>) constructor.newInstance( toWrap, this );
119    } catch (Exception ex) {
120      throw new IllegalArgumentException( ex );
121    }
122    return result;
123  }
124
125
126  /**
127   * Recursively find the root wrapped object and use its  class (or interface) to map it to the actual wrapper class
128   * @param toWrap not null object to wrap, which by itself might be a wrapper
129   * @return the wrapper class
130   * @throws IllegalArgumentException when no wrapper class found
131   */
132  private <A> Class getMappedClass(A toWrap) throws IllegalArgumentException {
133    Class sourceClass;
134    Class result;
135
136    if ( toWrap instanceof Wrapper ) {
137      result = getMappedClass(((Wrapper) toWrap).getWrapped());
138
139    } else {
140      sourceClass = toWrap.getClass();
141      result = getMappedClass( sourceClass );
142
143      if ( result == null &&  sourceClass.getInterfaces().length > 0) {
144        result = getMappedClass( sourceClass.getInterfaces()[0] );
145
146        if (result == null) {
147          throw new IllegalArgumentException( "Could not identify the wrapper class for: "+toWrap );
148        }
149      }
150    }
151    return result;
152  }
153
154  /**
155   * This method maps a class to wrap to the corresponding wrapper class
156   * @param original is a non-null class to wrap
157   * @return a possibly null wrapper class for that original class. Null is returned
158   *         when there is no class to map to.
159   */
160  protected abstract Class<? extends Wrapper> getMappedClass(Class<?> original);
161}