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 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   // <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 IllegalStateException("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(BeanStoreEntry::properties).collect(toList()))
705         .append("outer", ObjectUtils.identity(outer.orElse(null)))
706         .append("parent", parent.map(BeanStore::properties).orElse(null))
707         .appendIf(nf, "readOnly", readOnly)
708         .appendIf(nf, "threadSafe", threadSafe)
709      ;
710   }
711}