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.formats;
020
021import org.apache.reef.tang.Configuration;
022import org.apache.reef.tang.annotations.Name;
023import org.apache.reef.tang.exceptions.BindException;
024import org.apache.reef.tang.exceptions.ClassHierarchyException;
025import org.apache.reef.tang.exceptions.NameResolutionException;
026import org.apache.reef.tang.implementation.ConfigurationBuilderImpl;
027import org.apache.reef.tang.implementation.ConfigurationImpl;
028import org.apache.reef.tang.types.ClassNode;
029import org.apache.reef.tang.types.ConstructorArg;
030import org.apache.reef.tang.types.NamedParameterNode;
031import org.apache.reef.tang.types.Node;
032import org.apache.reef.tang.util.*;
033
034import java.lang.reflect.Field;
035import java.util.ArrayList;
036import java.util.List;
037import java.util.Map;
038import java.util.Map.Entry;
039import java.util.Set;
040import java.util.logging.Level;
041import java.util.logging.Logger;
042
043/**
044 * Allows applications to bundle sets of configuration options together into
045 * discrete packages.  Unlike more conventional approaches,
046 * ConfigurationModules store such information in static data structures that
047 * can be statically discovered and sanity-checked.
048 *
049 * See org.apache.reef.tang.formats.TestConfigurationModule for more information and examples.
050 */
051public class ConfigurationModule {
052
053  private static final Logger LOG = Logger.getLogger(ConfigurationModule.class.getName());
054
055  private final ConfigurationModuleBuilder builder;
056  // Set of required unset parameters. Must be empty before build.
057  private final Set<Field> reqSet = new MonotonicHashSet<>();
058  private final Map<Impl<?>, Class<?>> setImpls = new MonotonicHashMap<>();
059  private final MonotonicMultiHashMap<Impl<?>, Class<?>> setImplSets = new MonotonicMultiHashMap<>();
060  private final MonotonicMultiHashMap<Impl<?>, String> setLateImplSets = new MonotonicMultiHashMap<>();
061  private final MonotonicMultiHashMap<Param<?>, String> setParamSets = new MonotonicMultiHashMap<>();
062  private final Map<Impl<?>, String> setLateImpls = new MonotonicHashMap<>();
063  private final Map<Param<?>, String> setParams = new MonotonicHashMap<>();
064  private final Map<Impl<List>, List<?>> setImplLists = new MonotonicHashMap<>();
065  private final Map<Param<List>, List<?>> setParamLists = new MonotonicHashMap<>();
066
067  protected ConfigurationModule(final ConfigurationModuleBuilder builder) {
068    this.builder = builder.deepCopy();
069  }
070
071  private ConfigurationModule deepCopy() {
072    final ConfigurationModule cm = new ConfigurationModule(builder.deepCopy());
073    cm.setImpls.putAll(setImpls);
074    cm.setImplSets.addAll(setImplSets);
075    cm.setLateImplSets.addAll(setLateImplSets);
076    cm.setParamSets.addAll(setParamSets);
077    cm.setLateImpls.putAll(setLateImpls);
078    cm.setParams.putAll(setParams);
079    cm.reqSet.addAll(reqSet);
080    cm.setImplLists.putAll(setImplLists);
081    cm.setParamLists.putAll(setParamLists);
082    return cm;
083  }
084
085  private <T> void processSet(final Object impl) {
086    final Field f = builder.map.get(impl);
087    if (f == null) { /* throw */
088      throw new ClassHierarchyException("Unknown Impl/Param when setting " +
089          ReflectionUtilities.getSimpleName(impl.getClass()) + ".  Did you pass in a field from some other module?");
090    }
091    if (!reqSet.contains(f)) {
092      reqSet.add(f);
093    }
094  }
095
096  public final <T> ConfigurationModule set(final Impl<T> opt, final Class<? extends T> impl) {
097    final ConfigurationModule c = deepCopy();
098    c.processSet(opt);
099    if (c.builder.setOpts.contains(opt)) {
100      c.setImplSets.put(opt, impl);
101    } else {
102      c.setImpls.put(opt, impl);
103    }
104    return c;
105  }
106
107  public final <T> ConfigurationModule set(final Impl<T> opt, final String impl) {
108    final ConfigurationModule c = deepCopy();
109    c.processSet(opt);
110    if (c.builder.setOpts.contains(opt)) {
111      c.setLateImplSets.put(opt, impl);
112    } else {
113      c.setLateImpls.put(opt, impl);
114    }
115    return c;
116  }
117
118  /**
119   * Binds a list to a specific optional/required Impl using ConfigurationModule.
120   *
121   * @param opt      Target optional/required Impl
122   * @param implList List object to be injected
123   * @param <T> a type
124   * @return the configuration module
125   */
126  public final <T> ConfigurationModule set(final Impl<List> opt, final List implList) {
127    final ConfigurationModule c = deepCopy();
128    c.processSet(opt);
129    c.setImplLists.put(opt, implList);
130    return c;
131  }
132
133  public final <T> ConfigurationModule set(final Param<T> opt, final Class<? extends T> val) {
134    return set(opt, ReflectionUtilities.getFullName(val));
135  }
136
137  public final ConfigurationModule set(final Param<Boolean> opt, final boolean val) {
138    return set(opt, "" + val);
139  }
140
141  public final ConfigurationModule set(final Param<? extends Number> opt, final Number val) {
142    return set(opt, "" + val);
143  }
144
145  public final <T> ConfigurationModule set(final Param<T> opt, final String val) {
146    final ConfigurationModule c = deepCopy();
147    c.processSet(opt);
148    if (c.builder.setOpts.contains(opt)) {
149      c.setParamSets.put(opt, val);
150    } else {
151      c.setParams.put(opt, val);
152    }
153    return c;
154  }
155
156  /**
157   * Binds a set of values to a Param using ConfigurationModule.
158   *
159   * @param opt    Target Param
160   * @param values Values to bind to the Param
161   * @param <T> type
162   * @return the Configuration module
163   */
164  public final <T> ConfigurationModule setMultiple(final Param<T> opt, final Iterable<String> values) {
165    ConfigurationModule c = deepCopy();
166    for (final String val : values) {
167      c = c.set(opt, val);
168    }
169    return c;
170  }
171
172  /**
173   * Binds a set of values to a Param using ConfigurationModule.
174   *
175   * @param opt    Target Param
176   * @param values Values to bind to the Param
177   * @param <T> type
178   * @return the Configuration module
179   */
180  public final <T> ConfigurationModule setMultiple(final Param<T> opt, final String... values) {
181    ConfigurationModule c = deepCopy();
182    for (final String val : values) {
183      c = c.set(opt, val);
184    }
185    return c;
186  }
187
188  /**
189   * Binds a list to a specific optional/required Param using ConfigurationModule.
190   *
191   * @param opt      target optional/required Param
192   * @param implList List object to be injected
193   * @param <T>      type
194   * @return the Configuration module
195   */
196  public final <T> ConfigurationModule set(final Param<List> opt, final List implList) {
197    final ConfigurationModule c = deepCopy();
198    c.processSet(opt);
199    c.setParamLists.put(opt, implList);
200    return c;
201  }
202
203  @SuppressWarnings({"unchecked", "rawtypes"})
204  public Configuration build() throws BindException {
205    final ConfigurationModule c = deepCopy();
206
207    //TODO[REEF-968] check that required parameters have not been set to null
208
209    if (!c.reqSet.containsAll(c.builder.reqDecl)) {
210      final Set<Field> missingSet = new MonotonicHashSet<>();
211      for (final Field f : c.builder.reqDecl) {
212        if (!c.reqSet.contains(f)) {
213          missingSet.add(f);
214        }
215      }
216      throw new BindException(
217          "Attempt to build configuration before setting required option(s): "
218              + builder.toString(missingSet));
219    }
220
221    for (final Class<?> clazz : c.builder.freeImpls.keySet()) {
222      final Impl<?> i = c.builder.freeImpls.get(clazz);
223      boolean foundOne = false;
224      if (c.setImpls.containsKey(i)) {
225        if (c.setImpls.get(i) != null) {
226          c.builder.b.bind(clazz, c.setImpls.get(i));
227          foundOne = true;
228        }
229      } else if (c.setLateImpls.containsKey(i)) {
230        if (c.setLateImpls.get(i) != null) {
231          c.builder.b.bind(ReflectionUtilities.getFullName(clazz), c.setLateImpls.get(i));
232          foundOne = true;
233        }
234      } else if (c.setImplSets.containsKey(i) || c.setLateImplSets.containsKey(i)) {
235        if (c.setImplSets.getValuesForKey(i) != null) {
236          for (final Class<?> clz : c.setImplSets.getValuesForKey(i)) {
237            c.builder.b.bindSetEntry((Class) clazz, (Class) clz);
238          }
239          foundOne = true;
240        }
241        if (c.setLateImplSets.getValuesForKey(i) != null) {
242          for (final String s : c.setLateImplSets.getValuesForKey(i)) {
243            c.builder.b.bindSetEntry((Class) clazz, s);
244          }
245          foundOne = true;
246        }
247      } else if (c.setImplLists.containsKey(i)) {
248        if (c.setImplLists.get(i) != null) {
249          c.builder.b.bindList((Class) clazz, c.setImplLists.get(i));
250          foundOne = true;
251        }
252      }
253      if(!foundOne && !(i instanceof  OptionalImpl)) {
254        final IllegalStateException e =
255            new IllegalStateException("Cannot find the value for the RequiredImplementation of the " + clazz
256            + ". Check that you don't pass null as an implementation value.");
257        LOG.log(Level.SEVERE, "Failed to build configuration", e);
258        throw e;
259      }
260    }
261    for (final Class<? extends Name<?>> clazz : c.builder.freeParams.keySet()) {
262      final Param<?> p = c.builder.freeParams.get(clazz);
263      final String s = c.setParams.get(p);
264      boolean foundOne = false;
265      if (s != null) {
266        c.builder.b.bindNamedParameter(clazz, s);
267        foundOne = true;
268      }
269      // Find the bound list for the NamedParameter
270      final List list = c.setParamLists.get(p);
271      if (list != null) {
272        c.builder.b.bindList((Class) clazz, list);
273        foundOne = true;
274      }
275      for (final String paramStr : c.setParamSets.getValuesForKey(p)) {
276        c.builder.b.bindSetEntry((Class) clazz, paramStr);
277        foundOne = true;
278      }
279
280      if (!foundOne && !(p instanceof OptionalParameter)) {
281        final IllegalStateException e =
282            new IllegalStateException("Cannot find the value for the RequiredParameter of the " + clazz
283                    + ". Check that you don't pass null as the parameter value.");
284        LOG.log(Level.SEVERE, "Failed to build configuration", e);
285        throw e;
286      }
287
288    }
289
290    return c.builder.b.build();
291
292  }
293
294
295  public Set<NamedParameterNode<?>> getBoundNamedParameters() {
296    final Configuration c = this.builder.b.build();
297    final Set<NamedParameterNode<?>> nps = new MonotonicSet<>();
298    nps.addAll(c.getNamedParameters());
299    for (final Class<?> np : this.builder.freeParams.keySet()) {
300      try {
301        nps.add((NamedParameterNode<?>) builder.b.getClassHierarchy().getNode(ReflectionUtilities.getFullName(np)));
302      } catch (final NameResolutionException e) {
303        throw new IllegalStateException(e);
304      }
305    }
306    return nps;
307  }
308
309  /**
310   * Replace any \'s in the input string with \\. and any "'s with \".
311   *
312   * @param in
313   * @return
314   */
315  private static String escape(final String in) {
316    // After regexp escaping \\\\ = 1 slash, \\\\\\\\ = 2 slashes.
317
318    // Also, the second args of replaceAll are neither strings nor regexps, and
319    // are instead a special DSL used by Matcher. Therefore, we need to double
320    // escape slashes (4 slashes) and quotes (3 slashes + ") in those strings.
321    // Since we need to write \\ and \", we end up with 8 and 7 slashes,
322    // respectively.
323    return in.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\\\"");
324  }
325
326  private static StringBuilder join(final StringBuilder sb, final String sep, final ConstructorArg[] types) {
327    if (types.length > 0) {
328      sb.append(types[0].getType());
329      for (int i = 1; i < types.length; i++) {
330        sb.append(sep).append(types[i].getType());
331      }
332    }
333    return sb;
334  }
335
336  /**
337   * Convert Configuration to a list of strings formatted as "param=value".
338   *
339   * @param c
340   * @return
341   */
342  private static List<String> toConfigurationStringList(final Configuration c) {
343    final ConfigurationImpl conf = (ConfigurationImpl) c;
344    final List<String> l = new ArrayList<>();
345    for (final ClassNode<?> opt : conf.getBoundImplementations()) {
346      l.add(opt.getFullName()
347          + '='
348          + escape(conf.getBoundImplementation(opt).getFullName()));
349    }
350    for (final ClassNode<?> opt : conf.getBoundConstructors()) {
351      l.add(opt.getFullName()
352          + '='
353          + escape(conf.getBoundConstructor(opt).getFullName()));
354    }
355    for (final NamedParameterNode<?> opt : conf.getNamedParameters()) {
356      l.add(opt.getFullName()
357          + '='
358          + escape(conf.getNamedParameter(opt)));
359    }
360    for (final ClassNode<?> cn : conf.getLegacyConstructors()) {
361      final StringBuilder sb = new StringBuilder();
362      join(sb, "-", conf.getLegacyConstructor(cn).getArgs());
363      l.add(cn.getFullName()
364          + escape('='
365              + ConfigurationBuilderImpl.INIT
366              + '('
367              + sb.toString()
368              + ')'
369      ));
370    }
371    for (final NamedParameterNode<Set<?>> key : conf.getBoundSets()) {
372      for (final Object value : conf.getBoundSet(key)) {
373        final String val;
374        if (value instanceof String) {
375          val = (String) value;
376        } else if (value instanceof Node) {
377          val = ((Node) value).getFullName();
378        } else {
379          throw new IllegalStateException("The value bound to a given NamedParameterNode "
380                  + key + " is neither the set of class hierarchy nodes nor strings.");
381        }
382        l.add(key.getFullName() + '=' + escape(val));
383      }
384    }
385    return l;
386  }
387
388  public List<Entry<String, String>> toStringPairs() {
389    final List<Entry<String, String>> ret = new ArrayList<>();
390    class MyEntry implements Entry<String, String> {
391      private final String k;
392      private final String v;
393
394      MyEntry(final String k, final String v) {
395        this.k = k;
396        this.v = v;
397      }
398
399      @Override
400      public String getKey() {
401        return k;
402      }
403
404      @Override
405      public String getValue() {
406        return v;
407      }
408
409      @Override
410      public String setValue(final String value) {
411        throw new UnsupportedOperationException();
412      }
413
414    }
415    for (final Class<?> c : this.builder.freeParams.keySet()) {
416      ret.add(new MyEntry(ReflectionUtilities.getFullName(c),
417          this.builder.map.get(this.builder.freeParams.get(c)).getName()));
418    }
419    for (final Class<?> c : this.builder.freeImpls.keySet()) {
420      ret.add(new MyEntry(ReflectionUtilities.getFullName(c),
421          this.builder.map.get(this.builder.freeImpls.get(c)).getName()));
422    }
423    for (final String s : toConfigurationStringList(builder.b.build())) {
424      final String[] tok = s.split("=", 2);
425      ret.add(new MyEntry(tok[0], tok[1]));
426    }
427
428    return ret;
429  }
430
431  public String toPrettyString() {
432    final StringBuilder sb = new StringBuilder();
433
434    for (final Entry<String, String> l : toStringPairs()) {
435      sb.append(l.getKey() + "=" + l.getValue() + "\n");
436    }
437    return sb.toString();
438  }
439
440  public void assertStaticClean() throws ClassHierarchyException {
441    if (!(
442        setImpls.isEmpty() &&
443            setImplSets.isEmpty() &&
444            setLateImplSets.isEmpty() &&
445            setParamSets.isEmpty() &&
446            setLateImpls.isEmpty() &&
447            setParams.isEmpty() &&
448            setImplLists.isEmpty() &&
449            setParamLists.isEmpty()
450      )) {
451      throw new ClassHierarchyException("Detected statically set ConfigurationModule Parameter / Implementation.  " +
452          "set() should only be used dynamically.  Use bind...() instead.");
453    }
454  }
455
456  public ConfigurationModuleBuilder getBuilder() {
457    return builder;
458  }
459}