001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.cp; 014 015import static org.apache.juneau.collections.JsonMap.*; 016import static org.apache.juneau.common.internal.StringUtils.*; 017import static org.apache.juneau.internal.CollectionUtils.*; 018import static java.util.stream.Collectors.*; 019 020import java.lang.annotation.*; 021import java.util.*; 022import java.util.concurrent.*; 023import java.util.function.*; 024import java.util.stream.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.collections.*; 028import org.apache.juneau.internal.*; 029import org.apache.juneau.marshaller.*; 030import org.apache.juneau.reflect.*; 031 032/** 033 * Java bean store. 034 * 035 * <p> 036 * A simple storage database for beans keyed by type and name. 037 * Used to retrieve and instantiate beans using an injection-like API. 038 * 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. 039 * 040 * <p> 041 * Beans can be stored with or without names. Named beans are typically resolved using 042 * the <ja>@Named</ja> or <ja>@Qualified</ja> annotations on constructor or method parameters. 043 * 044 * <p> 045 * Beans are added through the following methods: 046 * <ul class='javatreec'> 047 * <li class='jm'>{@link #add(Class,Object) add(Class,Object)} 048 * <li class='jm'>{@link #add(Class,Object,String) add(Class,Object,String)} 049 * <li class='jm'>{@link #addBean(Class,Object) addBean(Class,Object)} 050 * <li class='jm'>{@link #addBean(Class,Object,String) addBean(Class,Object,String)} 051 * <li class='jm'>{@link #addSupplier(Class,Supplier) addSupplier(Class,Supplier)} 052 * <li class='jm'>{@link #addSupplier(Class,Supplier,String) addSupplier(Class,Supplier,String)} 053 * </ul> 054 * 055 * <p> 056 * Beans are retrieved through the following methods: 057 * <ul class='javatreec'> 058 * <li class='jm'>{@link #getBean(Class) getBean(Class)} 059 * <li class='jm'>{@link #getBean(Class,String) getBean(Class,String)} 060 * <li class='jm'>{@link #stream(Class) stream(Class)} 061 * </ul> 062 * 063 * <p> 064 * Beans are created through the following methods: 065 * <ul class='javatreec'> 066 * <li class='jm'>{@link #createBean(Class) createBean(Class)} 067 * <li class='jm'>{@link #createMethodFinder(Class) createMethodFinder(Class)} 068 * <li class='jm'>{@link #createMethodFinder(Class,Class) createMethodFinder(Class,Class)} 069 * <li class='jm'>{@link #createMethodFinder(Class,Object) createMethodFinder(Class,Object)} 070 * </ul> 071 * 072 * <h5 class='section'>Notes:</h5><ul> 073 * <li class='note'>Bean stores can be nested using {@link Builder#parent(BeanStore)}. 074 * <li class='note'>Bean stores can be made read-only using {@link Builder#readOnly()}. 075 * <li class='note'>Bean stores can be made thread-safe using {@link Builder#threadSafe()}. 076 * </ul> 077 * 078 * <h5 class='section'>See Also:</h5><ul> 079 * </ul> 080 */ 081public class BeanStore { 082 083 //----------------------------------------------------------------------------------------------------------------- 084 // Static 085 //----------------------------------------------------------------------------------------------------------------- 086 087 /** 088 * Non-existent bean store. 089 */ 090 public static final class Void extends BeanStore {} 091 092 /** 093 * Static read-only reusable instance. 094 */ 095 public static final BeanStore INSTANCE = create().readOnly().build(); 096 097 /** 098 * Static creator. 099 * 100 * @return A new {@link Builder} object. 101 */ 102 public static Builder create() { 103 return new Builder(); 104 } 105 106 /** 107 * Static creator. 108 * 109 * @param parent Parent bean store. Can be <jk>null</jk> if this is the root resource. 110 * @return A new {@link BeanStore} object. 111 */ 112 public static BeanStore of(BeanStore parent) { 113 return create().parent(parent).build(); 114 } 115 116 /** 117 * Static creator. 118 * 119 * @param parent Parent bean store. Can be <jk>null</jk> if this is the root resource. 120 * @param outer The outer bean used when instantiating inner classes. Can be <jk>null</jk>. 121 * @return A new {@link BeanStore} object. 122 */ 123 public static BeanStore of(BeanStore parent, Object outer) { 124 return create().parent(parent).outer(outer).build(); 125 } 126 127 //----------------------------------------------------------------------------------------------------------------- 128 // Builder 129 //----------------------------------------------------------------------------------------------------------------- 130 131 /** 132 * Builder class. 133 */ 134 @FluentSetters 135 public static class Builder { 136 137 BeanStore parent; 138 boolean readOnly, threadSafe; 139 Object outer; 140 Class<? extends BeanStore> type; 141 BeanStore impl; 142 143 /** 144 * Constructor. 145 */ 146 protected Builder() {} 147 148 /** 149 * Instantiates this bean store. 150 * 151 * @return A new bean store. 152 */ 153 public BeanStore build() { 154 if (impl != null) 155 return impl; 156 if (type == null || type == BeanStore.class) 157 return new BeanStore(this); 158 159 ClassInfo c = ClassInfo.of(type); 160 161 MethodInfo m = c.getDeclaredMethod( 162 x -> x.isPublic() 163 && x.hasNoParams() 164 && x.isStatic() 165 && x.hasName("getInstance") 166 ); 167 if (m != null) 168 return m.invoke(null); 169 170 ConstructorInfo ci = c.getPublicConstructor(x -> x.canAccept(this)); 171 if (ci != null) 172 return ci.invoke(this); 173 174 ci = c.getDeclaredConstructor(x -> x.isProtected() && x.canAccept(this)); 175 if (ci != null) 176 return ci.accessible().invoke(this); 177 178 throw new BasicRuntimeException("Could not find a way to instantiate class {0}", type); 179 } 180 181 //------------------------------------------------------------------------------------------------------------- 182 // Properties 183 //------------------------------------------------------------------------------------------------------------- 184 185 /** 186 * Specifies the parent bean store. 187 * 188 * <p> 189 * Bean searches are performed recursively up this parent chain. 190 * 191 * @param value The setting value. 192 * @return This object. 193 */ 194 @FluentSetter 195 public Builder parent(BeanStore value) { 196 parent = value; 197 return this; 198 } 199 200 /** 201 * Specifies that the bean store is read-only. 202 * 203 * <p> 204 * This means methods such as {@link BeanStore#addBean(Class, Object)} cannot be used. 205 * 206 * @return This object. 207 */ 208 @FluentSetter 209 public Builder readOnly() { 210 readOnly = true; 211 return this; 212 } 213 214 /** 215 * Specifies that the bean store being created should be thread-safe. 216 * 217 * @return This object. 218 */ 219 @FluentSetter 220 public Builder threadSafe() { 221 threadSafe = true; 222 return this; 223 } 224 225 /** 226 * Specifies the outer bean context. 227 * 228 * <p> 229 * The outer context bean to use when calling constructors on inner classes. 230 * 231 * @param value The outer bean context. Can be <jk>null</jk>. 232 * @return This object. 233 */ 234 @FluentSetter 235 public Builder outer(Object value) { 236 this.outer = value; 237 return this; 238 } 239 240 /** 241 * Overrides the bean to return from the {@link #build()} method. 242 * 243 * @param value The bean to return from the {@link #build()} method. 244 * @return This object. 245 */ 246 @FluentSetter 247 public Builder impl(BeanStore value) { 248 this.impl = value; 249 return this; 250 } 251 252 /** 253 * Overrides the bean store type. 254 * 255 * <p> 256 * The specified type must have one of the following: 257 * <ul> 258 * <li>A static <c>getInstance()</c> method. 259 * <li>A public constructor that takes in this builder. 260 * <li>A protected constructor that takes in this builder. 261 * </ul> 262 * 263 * @param value The bean store type. 264 * @return This object. 265 */ 266 @FluentSetter 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 = optional(builder.parent); 296 outer = optional(builder.outer); 297 readOnly = builder.readOnly; 298 threadSafe = builder.threadSafe; 299 lock = threadSafe ? new SimpleReadWriteLock() : SimpleReadWriteLock.NO_OP; 300 entries = threadSafe ? new ConcurrentLinkedDeque<>() : 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 (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 optional(e.get()); 418 if (parent.isPresent()) 419 return parent.get().getBean(beanType); 420 return empty(); 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 optional(e.get()); 438 if (parent.isPresent()) 439 return parent.get().getBean(beanType, name); 440 return empty(); 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 RuntimeException("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 // <FluentSetters> 682 683 // </FluentSetters> 684 685 //----------------------------------------------------------------------------------------------------------------- 686 // Helper methods 687 //----------------------------------------------------------------------------------------------------------------- 688 private String findBeanName(ParamInfo pi) { 689 Annotation n = pi.getAnnotation(Annotation.class, x -> x.annotationType().getSimpleName().equals("Named")); 690 if (n != null) 691 return AnnotationInfo.of((ClassInfo)null, n).getValue(String.class, "value", NOT_EMPTY).orElse(null); 692 return null; 693 } 694 695 private void assertCanWrite() { 696 if (readOnly) 697 throw new RuntimeException("Method cannot be used because BeanStore is read-only."); 698 } 699 700 private JsonMap properties() { 701 Predicate<Boolean> nf = ObjectUtils::isTrue; 702 return filteredMap() 703 .append("identity", ObjectUtils.identity(this)) 704 .append("entries", entries.stream().map(x -> x.properties()).collect(toList())) 705 .append("outer", ObjectUtils.identity(outer.orElse(null))) 706 .append("parent", parent.map(x->x.properties()).orElse(null)) 707 .appendIf(nf, "readOnly", readOnly) 708 .appendIf(nf, "threadSafe", threadSafe) 709 ; 710 } 711}