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 if (c.setImpls.containsKey(i)) { 224 c.builder.b.bind(clazz, c.setImpls.get(i)); 225 } else if (c.setLateImpls.containsKey(i)) { 226 c.builder.b.bind(ReflectionUtilities.getFullName(clazz), c.setLateImpls.get(i)); 227 } else if (c.setImplSets.containsKey(i) || c.setLateImplSets.containsKey(i)) { 228 for (final Class<?> clz : c.setImplSets.getValuesForKey(i)) { 229 c.builder.b.bindSetEntry((Class) clazz, (Class) clz); 230 } 231 for (final String s : c.setLateImplSets.getValuesForKey(i)) { 232 c.builder.b.bindSetEntry((Class) clazz, s); 233 } 234 } else if (c.setImplLists.containsKey(i)) { 235 c.builder.b.bindList((Class) clazz, c.setImplLists.get(i)); 236 } 237 } 238 for (final Class<? extends Name<?>> clazz : c.builder.freeParams.keySet()) { 239 final Param<?> p = c.builder.freeParams.get(clazz); 240 final String s = c.setParams.get(p); 241 boolean foundOne = false; 242 if (s != null) { 243 c.builder.b.bindNamedParameter(clazz, s); 244 foundOne = true; 245 } 246 // Find the bound list for the NamedParameter 247 final List list = c.setParamLists.get(p); 248 if (list != null) { 249 c.builder.b.bindList((Class) clazz, list); 250 foundOne = true; 251 } 252 for (final String paramStr : c.setParamSets.getValuesForKey(p)) { 253 c.builder.b.bindSetEntry((Class) clazz, paramStr); 254 foundOne = true; 255 } 256 257 if (!foundOne && !(p instanceof OptionalParameter)) { 258 final IllegalStateException e = 259 new IllegalStateException("Cannot find the value for the RequiredParameter of the " + clazz 260 + ". Check that you don't pass null as the parameter value."); 261 LOG.log(Level.SEVERE, "Failed to build configuration", e); 262 throw e; 263 } 264 265 } 266 267 return c.builder.b.build(); 268 269 } 270 271 272 public Set<NamedParameterNode<?>> getBoundNamedParameters() { 273 final Configuration c = this.builder.b.build(); 274 final Set<NamedParameterNode<?>> nps = new MonotonicSet<>(); 275 nps.addAll(c.getNamedParameters()); 276 for (final Class<?> np : this.builder.freeParams.keySet()) { 277 try { 278 nps.add((NamedParameterNode<?>) builder.b.getClassHierarchy().getNode(ReflectionUtilities.getFullName(np))); 279 } catch (final NameResolutionException e) { 280 throw new IllegalStateException(e); 281 } 282 } 283 return nps; 284 } 285 286 /** 287 * Replace any \'s in the input string with \\. and any "'s with \". 288 * 289 * @param in 290 * @return 291 */ 292 private static String escape(final String in) { 293 // After regexp escaping \\\\ = 1 slash, \\\\\\\\ = 2 slashes. 294 295 // Also, the second args of replaceAll are neither strings nor regexps, and 296 // are instead a special DSL used by Matcher. Therefore, we need to double 297 // escape slashes (4 slashes) and quotes (3 slashes + ") in those strings. 298 // Since we need to write \\ and \", we end up with 8 and 7 slashes, 299 // respectively. 300 return in.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\\\""); 301 } 302 303 private static StringBuilder join(final StringBuilder sb, final String sep, final ConstructorArg[] types) { 304 if (types.length > 0) { 305 sb.append(types[0].getType()); 306 for (int i = 1; i < types.length; i++) { 307 sb.append(sep).append(types[i].getType()); 308 } 309 } 310 return sb; 311 } 312 313 /** 314 * Convert Configuration to a list of strings formatted as "param=value". 315 * 316 * @param c 317 * @return 318 */ 319 private static List<String> toConfigurationStringList(final Configuration c) { 320 final ConfigurationImpl conf = (ConfigurationImpl) c; 321 final List<String> l = new ArrayList<>(); 322 for (final ClassNode<?> opt : conf.getBoundImplementations()) { 323 l.add(opt.getFullName() 324 + '=' 325 + escape(conf.getBoundImplementation(opt).getFullName())); 326 } 327 for (final ClassNode<?> opt : conf.getBoundConstructors()) { 328 l.add(opt.getFullName() 329 + '=' 330 + escape(conf.getBoundConstructor(opt).getFullName())); 331 } 332 for (final NamedParameterNode<?> opt : conf.getNamedParameters()) { 333 l.add(opt.getFullName() 334 + '=' 335 + escape(conf.getNamedParameter(opt))); 336 } 337 for (final ClassNode<?> cn : conf.getLegacyConstructors()) { 338 final StringBuilder sb = new StringBuilder(); 339 join(sb, "-", conf.getLegacyConstructor(cn).getArgs()); 340 l.add(cn.getFullName() 341 + escape('=' 342 + ConfigurationBuilderImpl.INIT 343 + '(' 344 + sb.toString() 345 + ')' 346 )); 347 } 348 for (final NamedParameterNode<Set<?>> key : conf.getBoundSets()) { 349 for (final Object value : conf.getBoundSet(key)) { 350 final String val; 351 if (value instanceof String) { 352 val = (String) value; 353 } else if (value instanceof Node) { 354 val = ((Node) value).getFullName(); 355 } else { 356 throw new IllegalStateException(); 357 } 358 l.add(key.getFullName() + '=' + escape(val)); 359 } 360 } 361 return l; 362 } 363 364 public List<Entry<String, String>> toStringPairs() { 365 final List<Entry<String, String>> ret = new ArrayList<>(); 366 class MyEntry implements Entry<String, String> { 367 private final String k; 368 private final String v; 369 370 MyEntry(final String k, final String v) { 371 this.k = k; 372 this.v = v; 373 } 374 375 @Override 376 public String getKey() { 377 return k; 378 } 379 380 @Override 381 public String getValue() { 382 return v; 383 } 384 385 @Override 386 public String setValue(final String value) { 387 throw new UnsupportedOperationException(); 388 } 389 390 } 391 for (final Class<?> c : this.builder.freeParams.keySet()) { 392 ret.add(new MyEntry(ReflectionUtilities.getFullName(c), 393 this.builder.map.get(this.builder.freeParams.get(c)).getName())); 394 } 395 for (final Class<?> c : this.builder.freeImpls.keySet()) { 396 ret.add(new MyEntry(ReflectionUtilities.getFullName(c), 397 this.builder.map.get(this.builder.freeImpls.get(c)).getName())); 398 } 399 for (final String s : toConfigurationStringList(builder.b.build())) { 400 final String[] tok = s.split("=", 2); 401 ret.add(new MyEntry(tok[0], tok[1])); 402 } 403 404 return ret; 405 } 406 407 public String toPrettyString() { 408 final StringBuilder sb = new StringBuilder(); 409 410 for (final Entry<String, String> l : toStringPairs()) { 411 sb.append(l.getKey() + "=" + l.getValue() + "\n"); 412 } 413 return sb.toString(); 414 } 415 416 public void assertStaticClean() throws ClassHierarchyException { 417 if (!( 418 setImpls.isEmpty() && 419 setImplSets.isEmpty() && 420 setLateImplSets.isEmpty() && 421 setParamSets.isEmpty() && 422 setLateImpls.isEmpty() && 423 setParams.isEmpty() && 424 setImplLists.isEmpty() && 425 setParamLists.isEmpty() 426 )) { 427 throw new ClassHierarchyException("Detected statically set ConfigurationModule Parameter / Implementation. " + 428 "set() should only be used dynamically. Use bind...() instead."); 429 } 430 } 431 432 public ConfigurationModuleBuilder getBuilder() { 433 return builder; 434 } 435}