001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.cp; 018 019import static java.util.stream.Collectors.*; 020import static java.util.stream.Collectors.toList; 021import static org.apache.juneau.collections.JsonMap.*; 022import static org.apache.juneau.common.utils.StringUtils.*; 023import static org.apache.juneau.common.utils.Utils.*; 024import static org.apache.juneau.internal.CollectionUtils.map; 025 026import java.lang.annotation.*; 027import java.util.*; 028import java.util.concurrent.*; 029import java.util.function.*; 030import java.util.stream.*; 031 032import org.apache.juneau.*; 033import org.apache.juneau.collections.*; 034import org.apache.juneau.common.utils.*; 035import org.apache.juneau.internal.*; 036import org.apache.juneau.marshaller.*; 037import org.apache.juneau.reflect.*; 038 039/** 040 * Java bean store. 041 * 042 * <p> 043 * A simple storage database for beans keyed by type and name. 044 * Used to retrieve and instantiate beans using an injection-like API. 045 * It's similar in concept to the injection framework of Spring but greatly simplified in function and not intended to implement a full-fledged injection framework. 046 * 047 * <p> 048 * Beans can be stored with or without names. Named beans are typically resolved using 049 * the <ja>@Named</ja> or <ja>@Qualified</ja> annotations on constructor or method parameters. 050 * 051 * <p> 052 * Beans are added through the following methods: 053 * <ul class='javatreec'> 054 * <li class='jm'>{@link #add(Class,Object) add(Class,Object)} 055 * <li class='jm'>{@link #add(Class,Object,String) add(Class,Object,String)} 056 * <li class='jm'>{@link #addBean(Class,Object) addBean(Class,Object)} 057 * <li class='jm'>{@link #addBean(Class,Object,String) addBean(Class,Object,String)} 058 * <li class='jm'>{@link #addSupplier(Class,Supplier) addSupplier(Class,Supplier)} 059 * <li class='jm'>{@link #addSupplier(Class,Supplier,String) addSupplier(Class,Supplier,String)} 060 * </ul> 061 * 062 * <p> 063 * Beans are retrieved through the following methods: 064 * <ul class='javatreec'> 065 * <li class='jm'>{@link #getBean(Class) getBean(Class)} 066 * <li class='jm'>{@link #getBean(Class,String) getBean(Class,String)} 067 * <li class='jm'>{@link #stream(Class) stream(Class)} 068 * </ul> 069 * 070 * <p> 071 * Beans are created through the following methods: 072 * <ul class='javatreec'> 073 * <li class='jm'>{@link #createBean(Class) createBean(Class)} 074 * <li class='jm'>{@link #createMethodFinder(Class) createMethodFinder(Class)} 075 * <li class='jm'>{@link #createMethodFinder(Class,Class) createMethodFinder(Class,Class)} 076 * <li class='jm'>{@link #createMethodFinder(Class,Object) createMethodFinder(Class,Object)} 077 * </ul> 078 * 079 * <h5 class='section'>Notes:</h5><ul> 080 * <li class='note'>Bean stores can be nested using {@link Builder#parent(BeanStore)}. 081 * <li class='note'>Bean stores can be made read-only using {@link Builder#readOnly()}. 082 * <li class='note'>Bean stores can be made thread-safe using {@link Builder#threadSafe()}. 083 * </ul> 084 * 085 * <h5 class='section'>See Also:</h5><ul> 086 * </ul> 087 */ 088public class BeanStore { 089 090 //----------------------------------------------------------------------------------------------------------------- 091 // Static 092 //----------------------------------------------------------------------------------------------------------------- 093 094 /** 095 * Non-existent bean store. 096 */ 097 public static final class Void extends BeanStore {} 098 099 /** 100 * Static read-only reusable instance. 101 */ 102 public static final BeanStore INSTANCE = create().readOnly().build(); 103 104 /** 105 * Static creator. 106 * 107 * @return A new {@link Builder} object. 108 */ 109 public static Builder create() { 110 return new Builder(); 111 } 112 113 /** 114 * Static creator. 115 * 116 * @param parent Parent bean store. Can be <jk>null</jk> if this is the root resource. 117 * @return A new {@link BeanStore} object. 118 */ 119 public static BeanStore of(BeanStore parent) { 120 return create().parent(parent).build(); 121 } 122 123 /** 124 * Static creator. 125 * 126 * @param parent Parent bean store. Can be <jk>null</jk> if this is the root resource. 127 * @param outer The outer bean used when instantiating inner classes. Can be <jk>null</jk>. 128 * @return A new {@link BeanStore} object. 129 */ 130 public static BeanStore of(BeanStore parent, Object outer) { 131 return create().parent(parent).outer(outer).build(); 132 } 133 134 //----------------------------------------------------------------------------------------------------------------- 135 // Builder 136 //----------------------------------------------------------------------------------------------------------------- 137 138 /** 139 * Builder class. 140 */ 141 public static class Builder { 142 143 BeanStore parent; 144 boolean readOnly, threadSafe; 145 Object outer; 146 Class<? extends BeanStore> type; 147 BeanStore impl; 148 149 /** 150 * Constructor. 151 */ 152 protected Builder() {} 153 154 /** 155 * Instantiates this bean store. 156 * 157 * @return A new bean store. 158 */ 159 public BeanStore build() { 160 if (impl != null) 161 return impl; 162 if (type == null || type == BeanStore.class) 163 return new BeanStore(this); 164 165 ClassInfo c = ClassInfo.of(type); 166 167 MethodInfo m = c.getDeclaredMethod( 168 x -> x.isPublic() 169 && x.hasNoParams() 170 && x.isStatic() 171 && x.hasName("getInstance") 172 ); 173 if (m != null) 174 return m.invoke(null); 175 176 ConstructorInfo ci = c.getPublicConstructor(x -> x.canAccept(this)); 177 if (ci != null) 178 return ci.invoke(this); 179 180 ci = c.getDeclaredConstructor(x -> x.isProtected() && x.canAccept(this)); 181 if (ci != null) 182 return ci.accessible().invoke(this); 183 184 throw new BasicRuntimeException("Could not find a way to instantiate class {0}", type); 185 } 186 187 //------------------------------------------------------------------------------------------------------------- 188 // Properties 189 //------------------------------------------------------------------------------------------------------------- 190 191 /** 192 * Specifies the parent bean store. 193 * 194 * <p> 195 * Bean searches are performed recursively up this parent chain. 196 * 197 * @param value The setting value. 198 * @return This object. 199 */ 200 public Builder parent(BeanStore value) { 201 parent = value; 202 return this; 203 } 204 205 /** 206 * Specifies that the bean store is read-only. 207 * 208 * <p> 209 * This means methods such as {@link BeanStore#addBean(Class, Object)} cannot be used. 210 * 211 * @return This object. 212 */ 213 public Builder readOnly() { 214 readOnly = true; 215 return this; 216 } 217 218 /** 219 * Specifies that the bean store being created should be thread-safe. 220 * 221 * @return This object. 222 */ 223 public Builder threadSafe() { 224 threadSafe = true; 225 return this; 226 } 227 228 /** 229 * Specifies the outer bean context. 230 * 231 * <p> 232 * The outer context bean to use when calling constructors on inner classes. 233 * 234 * @param value The outer bean context. Can be <jk>null</jk>. 235 * @return This object. 236 */ 237 public Builder outer(Object value) { 238 this.outer = value; 239 return this; 240 } 241 242 /** 243 * Overrides the bean to return from the {@link #build()} method. 244 * 245 * @param value The bean to return from the {@link #build()} method. 246 * @return This object. 247 */ 248 public Builder impl(BeanStore value) { 249 this.impl = value; 250 return this; 251 } 252 253 /** 254 * Overrides the bean store type. 255 * 256 * <p> 257 * The specified type must have one of the following: 258 * <ul> 259 * <li>A static <c>getInstance()</c> method. 260 * <li>A public constructor that takes in this builder. 261 * <li>A protected constructor that takes in this builder. 262 * </ul> 263 * 264 * @param value The bean store type. 265 * @return This object. 266 */ 267 public Builder type(Class<? extends BeanStore> value) { 268 this.type = value; 269 return this; 270 } 271 } 272 273 //----------------------------------------------------------------------------------------------------------------- 274 // Instance 275 //----------------------------------------------------------------------------------------------------------------- 276 277 private final Deque<BeanStoreEntry<?>> entries; 278 private final Map<Class<?>,BeanStoreEntry<?>> unnamedEntries; 279 280 final Optional<BeanStore> parent; 281 final Optional<Object> outer; 282 final boolean readOnly, threadSafe; 283 final SimpleReadWriteLock lock; 284 285 BeanStore() { 286 this(create()); 287 } 288 289 /** 290 * Constructor. 291 * 292 * @param builder The builder containing the settings for this bean. 293 */ 294 protected BeanStore(Builder builder) { 295 parent = Utils.opt(builder.parent); 296 outer = Utils.opt(builder.outer); 297 readOnly = builder.readOnly; 298 threadSafe = builder.threadSafe; 299 lock = threadSafe ? new SimpleReadWriteLock() : SimpleReadWriteLock.NO_OP; 300 entries = threadSafe ? new ConcurrentLinkedDeque<>() : new LinkedList<>(); 301 unnamedEntries = threadSafe ? new ConcurrentHashMap<>() : map(); 302 } 303 304 /** 305 * Adds an unnamed bean of the specified type to this factory. 306 * 307 * @param <T> The class to associate this bean with. 308 * @param beanType The class to associate this bean with. 309 * @param bean The bean. Can be <jk>null</jk>. 310 * @return This object. 311 */ 312 public <T> BeanStore addBean(Class<T> beanType, T bean) { 313 return addBean(beanType, bean, null); 314 } 315 316 /** 317 * Adds a named bean of the specified type to this factory. 318 * 319 * @param <T> The class to associate this bean with. 320 * @param beanType The class to associate this bean with. 321 * @param bean The bean. Can be <jk>null</jk>. 322 * @param name The bean name if this is a named bean. Can be <jk>null</jk>. 323 * @return This object. 324 */ 325 public <T> BeanStore addBean(Class<T> beanType, T bean, String name) { 326 return addSupplier(beanType, ()->bean, name); 327 } 328 329 /** 330 * Adds a supplier for an unnamed bean of the specified type to this factory. 331 * 332 * @param <T> The class to associate this bean with. 333 * @param beanType The class to associate this bean with. 334 * @param bean The bean supplier. 335 * @return This object. 336 */ 337 public <T> BeanStore addSupplier(Class<T> beanType, Supplier<T> bean) { 338 return addSupplier(beanType, bean, null); 339 } 340 341 /** 342 * Adds a supplier for a named bean of the specified type to this factory. 343 * 344 * @param <T> The class to associate this bean with. 345 * @param beanType The class to associate this bean with. 346 * @param bean The bean supplier. 347 * @param name The bean name if this is a named bean. Can be <jk>null</jk>. 348 * @return This object. 349 */ 350 public <T> BeanStore addSupplier(Class<T> beanType, Supplier<T> bean, String name) { 351 assertCanWrite(); 352 BeanStoreEntry<T> e = createEntry(beanType, bean, name); 353 try (SimpleLock x = lock.write()) { 354 entries.addFirst(e); 355 if (Utils.isEmpty(name)) 356 unnamedEntries.put(beanType, e); 357 } 358 return this; 359 } 360 361 /** 362 * Same as {@link #addBean(Class,Object)} but returns the bean instead of this object for fluent calls. 363 * 364 * @param <T> The class to associate this bean with. 365 * @param beanType The class to associate this bean with. 366 * @param bean The bean. Can be <jk>null</jk>. 367 * @return The bean. 368 */ 369 public <T> T add(Class<T> beanType, T bean) { 370 add(beanType, bean, null); 371 return bean; 372 } 373 374 /** 375 * Same as {@link #addBean(Class,Object,String)} but returns the bean instead of this object for fluent calls. 376 * 377 * @param <T> The class to associate this bean with. 378 * @param beanType The class to associate this bean with. 379 * @param bean The bean. Can be <jk>null</jk>. 380 * @param name The bean name if this is a named bean. Can be <jk>null</jk>. 381 * @return The bean. 382 */ 383 public <T> T add(Class<T> beanType, T bean, String name) { 384 addBean(beanType, bean, name); 385 return bean; 386 } 387 388 /** 389 * Clears out all bean in this bean store. 390 * 391 * <p> 392 * Does not affect the parent bean store. 393 * 394 * @return This object. 395 */ 396 public BeanStore clear() { 397 assertCanWrite(); 398 try (SimpleLock x = lock.write()) { 399 unnamedEntries.clear(); 400 entries.clear(); 401 } 402 return this; 403 } 404 405 /** 406 * Returns the unnamed bean of the specified type. 407 * 408 * @param <T> The type of bean to return. 409 * @param beanType The type of bean to return. 410 * @return The bean. 411 */ 412 @SuppressWarnings("unchecked") 413 public <T> Optional<T> getBean(Class<T> beanType) { 414 try (SimpleLock x = lock.read()) { 415 BeanStoreEntry<T> e = (BeanStoreEntry<T>) unnamedEntries.get(beanType); 416 if (e != null) 417 return Utils.opt(e.get()); 418 if (parent.isPresent()) 419 return parent.get().getBean(beanType); 420 return Utils.opte(); 421 } 422 } 423 424 /** 425 * Returns the named bean of the specified type. 426 * 427 * @param <T> The type of bean to return. 428 * @param beanType The type of bean to return. 429 * @param name The bean name. Can be <jk>null</jk>. 430 * @return The bean. 431 */ 432 @SuppressWarnings("unchecked") 433 public <T> Optional<T> getBean(Class<T> beanType, String name) { 434 try (SimpleLock x = lock.read()) { 435 BeanStoreEntry<T> e = (BeanStoreEntry<T>)entries.stream().filter(x2 -> x2.matches(beanType, name)).findFirst().orElse(null); 436 if (e != null) 437 return Utils.opt(e.get()); 438 if (parent.isPresent()) 439 return parent.get().getBean(beanType, name); 440 return Utils.opte(); 441 } 442 } 443 444 /** 445 * Returns all the beans in this store of the specified type. 446 * 447 * <p> 448 * Returns both named and unnamed beans. 449 * 450 * <p> 451 * The results from the parent bean store are appended to the list of beans from this beans store. 452 * 453 * @param <T> The bean type to return. 454 * @param beanType The bean type to return. 455 * @return The bean entries. Never <jk>null</jk>. 456 */ 457 public <T> Stream<BeanStoreEntry<T>> stream(Class<T> beanType) { 458 @SuppressWarnings("unchecked") 459 Stream<BeanStoreEntry<T>> s = entries.stream().filter(x -> x.matches(beanType)).map(x -> (BeanStoreEntry<T>)x); 460 if (parent.isPresent()) 461 s = Stream.concat(s, parent.get().stream(beanType)); 462 return s; 463 } 464 465 /** 466 * Removes an unnamed bean from this store. 467 * 468 * @param beanType The bean type being removed. 469 * @return This object. 470 */ 471 public BeanStore removeBean(Class<?> beanType) { 472 return removeBean(beanType, null); 473 } 474 475 /** 476 * Removes a named bean from this store. 477 * 478 * @param beanType The bean type being removed. 479 * @param name The bean name to remove. 480 * @return This object. 481 */ 482 public BeanStore removeBean(Class<?> beanType, String name) { 483 assertCanWrite(); 484 try (SimpleLock x = lock.write()) { 485 if (name == null) 486 unnamedEntries.remove(beanType); 487 entries.removeIf(y -> y.matches(beanType, name)); 488 } 489 return this; 490 } 491 492 /** 493 * Returns <jk>true</jk> if this store contains the specified unnamed bean type. 494 * 495 * @param beanType The bean type to check. 496 * @return <jk>true</jk> if this store contains the specified unnamed bean type. 497 */ 498 public boolean hasBean(Class<?> beanType) { 499 return unnamedEntries.containsKey(beanType) || parent.map(x -> x.hasBean(beanType)).orElse(false); 500 } 501 502 /** 503 * Returns <jk>true</jk> if this store contains the specified named bean type. 504 * 505 * @param beanType The bean type to check. 506 * @param name The bean name. 507 * @return <jk>true</jk> if this store contains the specified named bean type. 508 */ 509 public boolean hasBean(Class<?> beanType, String name) { 510 return entries.stream().anyMatch(x -> x.matches(beanType, name)) || parent.map(x -> x.hasBean(beanType, name)).orElse(false); 511 } 512 513 /** 514 * Instantiates a bean creator. 515 * 516 * <h5 class='section'>See Also:</h5><ul> 517 * <li class='jc'>{@link BeanCreator} for usage. 518 * </ul> 519 * 520 * @param <T> The bean type to create. 521 * @param beanType The bean type to create. 522 * @return A new bean creator. 523 */ 524 public <T> BeanCreator<T> createBean(Class<T> beanType) { 525 return new BeanCreator<>(beanType, this); 526 } 527 528 /** 529 * Create a method finder for finding bean creation methods. 530 * 531 * <h5 class='section'>See Also:</h5><ul> 532 * <li class='jc'>{@link BeanCreateMethodFinder} for usage. 533 * </ul> 534 * 535 * @param <T> The bean type to create. 536 * @param beanType The bean type to create. 537 * @param resource The class containing the bean creator method. 538 * @return The method finder. Never <jk>null</jk>. 539 */ 540 public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType, Object resource) { 541 return new BeanCreateMethodFinder<>(beanType, resource, this); 542 } 543 544 /** 545 * Create a method finder for finding bean creation methods. 546 * 547 * <p> 548 * Same as {@link #createMethodFinder(Class,Class)} but looks for only static methods on the specified resource class 549 * and not also instance methods within the context of a bean. 550 * 551 * <h5 class='section'>See Also:</h5><ul> 552 * <li class='jc'>{@link BeanCreateMethodFinder} for usage. 553 * </ul> 554 * 555 * @param <T> The bean type to create. 556 * @param beanType The bean type to create. 557 * @param resourceClass The class containing the bean creator method. 558 * @return The method finder. Never <jk>null</jk>. 559 */ 560 public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType, Class<?> resourceClass) { 561 return new BeanCreateMethodFinder<>(beanType, resourceClass , this); 562 } 563 564 /** 565 * Create a method finder for finding bean creation methods. 566 * 567 * <p> 568 * Same as {@link #createMethodFinder(Class,Object)} but uses {@link Builder#outer(Object)} as the resource bean. 569 * 570 * <h5 class='section'>See Also:</h5><ul> 571 * <li class='jc'>{@link BeanCreateMethodFinder} for usage. 572 * </ul> 573 * 574 * @param <T> The bean type to create. 575 * @param beanType The bean type to create. 576 * @return The method finder. Never <jk>null</jk>. 577 */ 578 public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType) { 579 return new BeanCreateMethodFinder<>(beanType, outer.orElseThrow(() -> new IllegalArgumentException("Method cannot be used without outer bean definition.")), this); 580 } 581 582 /** 583 * Given an executable, returns a list of types that are missing from this factory. 584 * 585 * @param executable The constructor or method to get the params for. 586 * @return A comma-delimited list of types that are missing from this factory, or <jk>null</jk> if none are missing. 587 */ 588 public String getMissingParams(ExecutableInfo executable) { 589 List<ParamInfo> params = executable.getParams(); 590 List<String> l = list(); 591 loop: for (int i = 0; i < params.size(); i++) { 592 ParamInfo pi = params.get(i); 593 ClassInfo pt = pi.getParameterType(); 594 if (i == 0 && outer.isPresent() && pt.isInstance(outer.get())) 595 continue loop; 596 if (pt.is(Optional.class) || pt.is(BeanStore.class)) 597 continue loop; 598 String beanName = findBeanName(pi); 599 Class<?> ptc = pt.inner(); 600 if (beanName == null && !hasBean(ptc)) 601 l.add(pt.getSimpleName()); 602 if (beanName != null && !hasBean(ptc, beanName)) 603 l.add(pt.getSimpleName() + '@' + beanName); 604 } 605 return l.isEmpty() ? null : l.stream().sorted().collect(joining(",")); 606 } 607 608 /** 609 * Given the list of param types, returns <jk>true</jk> if this factory has all the parameters for the specified executable. 610 * 611 * @param executable The constructor or method to get the params for. 612 * @return A comma-delimited list of types that are missing from this factory. 613 */ 614 public boolean hasAllParams(ExecutableInfo executable) { 615 loop: for (int i = 0; i < executable.getParamCount(); i++) { 616 ParamInfo pi = executable.getParam(i); 617 ClassInfo pt = pi.getParameterType(); 618 if (i == 0 && outer.isPresent() && pt.isInstance(outer.get())) 619 continue loop; 620 if (pt.is(Optional.class) || pt.is(BeanStore.class)) 621 continue loop; 622 String beanName = findBeanName(pi); 623 Class<?> ptc = pt.inner(); 624 if ((beanName == null && !hasBean(ptc)) || (beanName != null && !hasBean(ptc, beanName))) 625 return false; 626 } 627 return true; 628 } 629 630 631 /** 632 * Returns the corresponding beans in this factory for the specified param types. 633 * 634 * @param executable The constructor or method to get the params for. 635 * @return The corresponding beans in this factory for the specified param types. 636 */ 637 public Object[] getParams(ExecutableInfo executable) { 638 Object[] o = new Object[executable.getParamCount()]; 639 for (int i = 0; i < executable.getParamCount(); i++) { 640 ParamInfo pi = executable.getParam(i); 641 ClassInfo pt = pi.getParameterType(); 642 if (i == 0 && outer.isPresent() && pt.isInstance(outer.get())) { 643 o[i] = outer.get(); 644 } else if (pt.is(BeanStore.class)) { 645 o[i] = this; 646 } else { 647 String beanName = findBeanName(pi); 648 Class<?> ptc = pt.unwrap(Optional.class).inner(); 649 Optional<?> o2 = beanName == null ? getBean(ptc) : getBean(ptc, beanName); 650 o[i] = pt.is(Optional.class) ? o2 : o2.orElse(null); 651 } 652 } 653 return o; 654 } 655 656 @Override /* Object */ 657 public String toString() { 658 return Json5.of(properties()); 659 } 660 661 //----------------------------------------------------------------------------------------------------------------- 662 // Extension methods 663 //----------------------------------------------------------------------------------------------------------------- 664 665 /** 666 * Creates an entry in this store for the specified bean. 667 * 668 * <p> 669 * Subclasses can override this method to create their own entry subtypes. 670 * 671 * @param <T> The class type to associate with the bean. 672 * @param type The class type to associate with the bean. 673 * @param bean The bean supplier. 674 * @param name Optional name to associate with the bean. Can be <jk>null</jk>. 675 * @return A new bean store entry. 676 */ 677 protected <T> BeanStoreEntry<T> createEntry(Class<T> type, Supplier<T> bean, String name) { 678 return BeanStoreEntry.create(type, bean, name); 679 } 680 //----------------------------------------------------------------------------------------------------------------- 681 // Helper methods 682 //----------------------------------------------------------------------------------------------------------------- 683 private String findBeanName(ParamInfo pi) { 684 Annotation n = pi.getAnnotation(Annotation.class, x -> x.annotationType().getSimpleName().equals("Named")); 685 if (n != null) 686 return AnnotationInfo.of((ClassInfo)null, n).getValue(String.class, "value", NOT_EMPTY).orElse(null); 687 return null; 688 } 689 690 private void assertCanWrite() { 691 if (readOnly) 692 throw new IllegalStateException("Method cannot be used because BeanStore is read-only."); 693 } 694 695 private JsonMap properties() { 696 Predicate<Boolean> nf = Utils::isTrue; 697 return filteredMap() 698 .append("identity", Utils2.identity(this)) 699 .append("entries", entries.stream().map(BeanStoreEntry::properties).collect(toList())) 700 .append("outer", Utils2.identity(outer.orElse(null))) 701 .append("parent", parent.map(BeanStore::properties).orElse(null)) 702 .appendIf(nf, "readOnly", readOnly) 703 .appendIf(nf, "threadSafe", threadSafe) 704 ; 705 } 706}