001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind;
007
008import gov.nist.secauto.metaschema.core.datatype.DataTypeService;
009import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
010import gov.nist.secauto.metaschema.core.model.IBoundObject;
011import gov.nist.secauto.metaschema.core.model.IModule;
012import gov.nist.secauto.metaschema.core.model.IModuleLoader;
013import gov.nist.secauto.metaschema.core.util.ObjectUtils;
014import gov.nist.secauto.metaschema.databind.codegen.IProduction;
015import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
016import gov.nist.secauto.metaschema.databind.io.BindingException;
017import gov.nist.secauto.metaschema.databind.io.DefaultBoundLoader;
018import gov.nist.secauto.metaschema.databind.io.Format;
019import gov.nist.secauto.metaschema.databind.io.IDeserializer;
020import gov.nist.secauto.metaschema.databind.io.ISerializer;
021import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonDeserializer;
022import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonSerializer;
023import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlDeserializer;
024import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlSerializer;
025import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlDeserializer;
026import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlSerializer;
027import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
028import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
029import gov.nist.secauto.metaschema.databind.model.IBoundModule;
030import gov.nist.secauto.metaschema.databind.model.binding.metaschema.METASCHEMA;
031
032import java.io.IOException;
033import java.nio.file.Files;
034import java.nio.file.Path;
035import java.util.Collection;
036import java.util.List;
037import java.util.Map;
038import java.util.Objects;
039import java.util.concurrent.ConcurrentHashMap;
040
041import javax.xml.namespace.QName;
042
043import edu.umd.cs.findbugs.annotations.NonNull;
044import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
045
046/**
047 * The implementation of a {@link IBindingContext} provided by this library.
048 * <p>
049 * This implementation caches Module information, which can dramatically improve
050 * read and write performance at the cost of some memory use. Thus, using the
051 * same singleton of this class across multiple I/O operations will improve
052 * overall read and write performance when processing the same types of data.
053 * <p>
054 * Serializers and deserializers provided by this class using the
055 * {@link #newSerializer(Format, Class)} and
056 * {@link #newDeserializer(Format, Class)} methods will
057 * <p>
058 * This class is synchronized and is thread-safe.
059 */
060public class DefaultBindingContext implements IBindingContext {
061  private static DefaultBindingContext singleton;
062  @NonNull
063  private final IModuleLoaderStrategy moduleLoaderStrategy;
064  @NonNull
065  private final Map<Class<?>, IBoundDefinitionModelComplex> boundClassToStrategyMap = new ConcurrentHashMap<>();
066  @NonNull
067  private final Map<IBoundDefinitionModelAssembly, IBindingMatcher> bindingMatchers = new ConcurrentHashMap<>();
068
069  /**
070   * Get the singleton instance of this binding context.
071   *
072   * @return the binding context
073   */
074  @NonNull
075  public static DefaultBindingContext instance() {
076    synchronized (DefaultBindingContext.class) {
077      if (singleton == null) {
078        singleton = new DefaultBindingContext();
079      }
080    }
081    return ObjectUtils.notNull(singleton);
082  }
083
084  /**
085   * Construct a new binding context.
086   *
087   * @param modulePostProcessors
088   *          a list of module post processors to call after loading a module
089   */
090  @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
091  public DefaultBindingContext(@NonNull List<IModuleLoader.IModulePostProcessor> modulePostProcessors) {
092    // only allow extended classes
093    moduleLoaderStrategy = new PostProcessingModuleLoaderStrategy(this, modulePostProcessors);
094    registerBindingMatcher(METASCHEMA.class);
095  }
096
097  /**
098   * Construct a new binding context.
099   */
100  @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
101  public DefaultBindingContext() {
102    // only allow extended classes
103    moduleLoaderStrategy = new SimpleModuleLoaderStrategy(this);
104    registerBindingMatcher(METASCHEMA.class);
105  }
106
107  @NonNull
108  protected IModuleLoaderStrategy getModuleLoaderStrategy() {
109    return moduleLoaderStrategy;
110  }
111
112  /**
113   * Get the binding matchers that are associated with this class.
114   *
115   * @return the list of matchers
116   * @see #registerBindingMatcher(Class)
117   * @see #registerBindingMatcher(IBoundDefinitionModelAssembly)
118   */
119  @NonNull
120  protected Collection<IBindingMatcher> getBindingMatchers() {
121    return ObjectUtils.notNull(bindingMatchers.values());
122  }
123
124  @Override
125  @NonNull
126  public final IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) {
127    return ObjectUtils.notNull(bindingMatchers.computeIfAbsent(definition, key -> IBindingMatcher.of(definition)));
128  }
129
130  @Override
131  public final IBindingMatcher registerBindingMatcher(@NonNull Class<? extends IBoundObject> clazz) {
132    IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(clazz);
133    if (definition == null) {
134      throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
135          clazz.getName()));
136    }
137
138    try {
139      IBoundDefinitionModelAssembly assemblyDefinition = IBoundDefinitionModelAssembly.class.cast(definition);
140      return registerBindingMatcher(ObjectUtils.notNull(assemblyDefinition));
141    } catch (ClassCastException ex) {
142      throw new IllegalArgumentException(
143          String.format("The provided class '%s' is not a root assembly.", clazz.getName()), ex);
144    }
145  }
146
147  @Override
148  public final IBoundDefinitionModelComplex registerClassBinding(IBoundDefinitionModelComplex definition) {
149    Class<?> clazz = definition.getBoundClass();
150    return boundClassToStrategyMap.computeIfAbsent(clazz, k -> definition);
151  }
152
153  @Override
154  public final IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz) {
155    return moduleLoaderStrategy.getBoundDefinitionForClass(clazz);
156  }
157
158  @Override
159  public <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz) {
160    return DataTypeService.getInstance().getJavaTypeAdapterByClass(clazz);
161  }
162
163  /**
164   * {@inheritDoc}
165   * <p>
166   * A serializer returned by this method is thread-safe.
167   */
168  @Override
169  public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
170      @NonNull Format format,
171      @NonNull Class<CLASS> clazz) {
172    Objects.requireNonNull(format, "format");
173    IBoundDefinitionModelAssembly definition;
174    try {
175      definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
176    } catch (ClassCastException ex) {
177      throw new IllegalStateException(
178          String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
179    }
180    if (definition == null) {
181      throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
182    }
183    ISerializer<CLASS> retval;
184    switch (format) {
185    case JSON:
186      retval = new DefaultJsonSerializer<>(definition);
187      break;
188    case XML:
189      retval = new DefaultXmlSerializer<>(definition);
190      break;
191    case YAML:
192      retval = new DefaultYamlSerializer<>(definition);
193      break;
194    default:
195      throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
196    }
197    return retval;
198  }
199
200  /**
201   * {@inheritDoc}
202   * <p>
203   * A deserializer returned by this method is thread-safe.
204   */
205  @Override
206  public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
207      @NonNull Format format,
208      @NonNull Class<CLASS> clazz) {
209    IBoundDefinitionModelAssembly definition;
210    try {
211      definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
212    } catch (ClassCastException ex) {
213      throw new IllegalStateException(
214          String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
215          ex);
216    }
217    if (definition == null) {
218      throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
219    }
220    IDeserializer<CLASS> retval;
221    switch (format) {
222    case JSON:
223      retval = new DefaultJsonDeserializer<>(definition);
224      break;
225    case XML:
226      retval = new DefaultXmlDeserializer<>(definition);
227      break;
228    case YAML:
229      retval = new DefaultYamlDeserializer<>(definition);
230      break;
231    default:
232      throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
233    }
234
235    return retval;
236  }
237
238  @Override
239  @SuppressWarnings({ "PMD.UseProperClassLoader", "unchecked" }) // false positive
240  @NonNull
241  public IBindingContext registerModule(
242      @NonNull IModule module,
243      @NonNull Path compilePath) throws IOException {
244    if (!(module instanceof IBoundModule)) {
245      Files.createDirectories(compilePath);
246
247      ClassLoader classLoader = ModuleCompilerHelper.newClassLoader(
248          compilePath,
249          ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));
250
251      IProduction production = ModuleCompilerHelper.compileMetaschema(module, compilePath);
252      production.getModuleProductions().stream()
253          .map(item -> {
254            try {
255              return (Class<? extends IBoundModule>) classLoader.loadClass(item.getClassName().reflectionName());
256            } catch (ClassNotFoundException ex) {
257              throw new IllegalStateException(ex);
258            }
259          })
260          .forEachOrdered(clazz -> {
261            IBoundModule boundModule = registerModule(ObjectUtils.notNull(clazz));
262            // force the binding matchers to load
263            boundModule.getRootAssemblyDefinitions();
264          });
265    }
266    return this;
267  }
268
269  @Override
270  public IBoundModule registerModule(Class<? extends IBoundModule> clazz) {
271    return getModuleLoaderStrategy().loadModule(clazz);
272    // retval.getExportedAssemblyDefinitions().stream()
273    // .map(def -> (IBoundDefinitionModelAssembly) def)
274    // .filter(def -> def.isRoot())
275    // .forEachOrdered(def -> registerBindingMatcher(ObjectUtils.notNull(def)));
276    // return retval;
277  }
278
279  @Override
280  public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
281    Class<? extends IBoundObject> retval = null;
282    for (IBindingMatcher matcher : getBindingMatchers()) {
283      retval = matcher.getBoundClassForXmlQName(rootQName);
284      if (retval != null) {
285        break;
286      }
287    }
288    return retval;
289  }
290
291  @Override
292  public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
293    Class<? extends IBoundObject> retval = null;
294    for (IBindingMatcher matcher : getBindingMatchers()) {
295      retval = matcher.getBoundClassForJsonName(rootName);
296      if (retval != null) {
297        break;
298      }
299    }
300    return retval;
301  }
302
303  @Override
304  public DefaultBoundLoader newBoundLoader() {
305    return new DefaultBoundLoader(this);
306  }
307
308  @Override
309  public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
310      throws BindingException {
311    IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
312    if (definition == null) {
313      throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
314    }
315    return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
316  }
317}