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.model.IBoundObject;
009import gov.nist.secauto.metaschema.core.model.MetaschemaException;
010import gov.nist.secauto.metaschema.core.util.ObjectUtils;
011import gov.nist.secauto.metaschema.databind.io.BindingException;
012import gov.nist.secauto.metaschema.databind.io.Format;
013import gov.nist.secauto.metaschema.databind.io.IDeserializer;
014import gov.nist.secauto.metaschema.databind.io.ISerializer;
015import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonDeserializer;
016import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonSerializer;
017import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlDeserializer;
018import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlSerializer;
019import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlDeserializer;
020import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlSerializer;
021import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
022import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
023import gov.nist.secauto.metaschema.databind.model.IBoundModule;
024import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
025import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
026import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
027import gov.nist.secauto.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
028import gov.nist.secauto.metaschema.databind.model.metaschema.binding.METASCHEMA;
029import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
030
031import java.io.IOException;
032import java.net.URI;
033import java.net.URL;
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;
045import nl.talsmasoftware.lazy4j.Lazy;
046
047/**
048 * The implementation of a {@link IBindingContext} provided by this library.
049 * <p>
050 * This implementation caches Module information, which can dramatically improve
051 * read and write performance at the cost of some memory use. Thus, using the
052 * same singleton of this class across multiple I/O operations will improve
053 * overall read and write performance when processing the same types of data.
054 * <p>
055 * Serializers and deserializers provided by this class using the
056 * {@link #newSerializer(Format, Class)} and
057 * {@link #newDeserializer(Format, Class)} methods will
058 * <p>
059 * This class is synchronized and is thread-safe.
060 */
061@SuppressWarnings("PMD.CouplingBetweenObjects")
062public class DefaultBindingContext implements IBindingContext {
063  private static Lazy<DefaultBindingContext> singleton = Lazy.of(DefaultBindingContext::new);
064  @NonNull
065  private final IModuleLoaderStrategy moduleLoaderStrategy;
066  @NonNull
067  private final Map<Class<?>, IBoundDefinitionModelComplex> boundClassToStrategyMap = new ConcurrentHashMap<>();
068
069  /**
070   * Get the singleton instance of this binding context.
071   * <p>
072   * Note: It is general a better practice to use a new {@link IBindingContext}
073   * and reuse that instance instead of this global instance.
074   *
075   * @return the binding context
076   * @see IBindingContext#newInstance()
077   */
078  @NonNull
079  static DefaultBindingContext instance() {
080    return ObjectUtils.notNull(singleton.get());
081  }
082
083  /**
084   * Construct a new binding context.
085   */
086  @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
087  public DefaultBindingContext() {
088    this(new SimpleModuleLoaderStrategy());
089  }
090
091  /**
092   * Construct a new binding context.
093   *
094   * @param strategy
095   *          the behavior class to use for loading Metaschema modules
096   * @since 2.0.0
097   */
098  public DefaultBindingContext(@NonNull IBindingContext.IModuleLoaderStrategy strategy) {
099    // only allow extended classes
100    moduleLoaderStrategy = strategy;
101    try {
102      registerModule(MetaschemaModelModule.class);
103    } catch (MetaschemaException ex) {
104      throw new IllegalStateException("Unable to register the builtin Metaschema module.", ex);
105    }
106  }
107
108  @Override
109  @NonNull
110  public final IModuleLoaderStrategy getModuleLoaderStrategy() {
111    return moduleLoaderStrategy;
112  }
113
114  @Override
115  public IBindingModuleLoader newModuleLoader() {
116    return new ModuleLoader(this, getModuleLoaderStrategy());
117  }
118
119  @Override
120  @NonNull
121  public final IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz) throws MetaschemaException {
122    IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
123    IBoundModule module = strategy.loadModule(clazz, this);
124    registerImportedModules(module);
125    return strategy.registerModule(module, this);
126  }
127
128  private void registerImportedModules(@NonNull IBoundModule module) throws MetaschemaException {
129    IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
130    for (IBoundModule parentModule : module.getImportedModules()) {
131      assert parentModule != null;
132      registerImportedModules(parentModule);
133      strategy.registerModule(parentModule, this);
134    }
135  }
136
137  /**
138   * Get the binding matchers that are associated with this class.
139   *
140   * @return the list of matchers
141   */
142  @NonNull
143  protected Collection<IBindingMatcher> getBindingMatchers() {
144    return getModuleLoaderStrategy().getBindingMatchers();
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, this);
156  }
157
158  /**
159   * {@inheritDoc}
160   * <p>
161   * A serializer returned by this method is thread-safe.
162   */
163  @Override
164  public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
165      @NonNull Format format,
166      @NonNull Class<CLASS> clazz) {
167    Objects.requireNonNull(format, "format");
168    IBoundDefinitionModelAssembly definition;
169    try {
170      definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
171    } catch (ClassCastException ex) {
172      throw new IllegalStateException(
173          String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
174    }
175    if (definition == null) {
176      throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
177    }
178    ISerializer<CLASS> retval;
179    switch (format) {
180    case JSON:
181      retval = new DefaultJsonSerializer<>(definition);
182      break;
183    case XML:
184      retval = new DefaultXmlSerializer<>(definition);
185      break;
186    case YAML:
187      retval = new DefaultYamlSerializer<>(definition);
188      break;
189    default:
190      throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
191    }
192    return retval;
193  }
194
195  /**
196   * {@inheritDoc}
197   * <p>
198   * A deserializer returned by this method is thread-safe.
199   */
200  @Override
201  public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
202      @NonNull Format format,
203      @NonNull Class<CLASS> clazz) {
204    IBoundDefinitionModelAssembly definition;
205    try {
206      definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
207    } catch (ClassCastException ex) {
208      throw new IllegalStateException(
209          String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
210          ex);
211    }
212    if (definition == null) {
213      throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
214    }
215    IDeserializer<CLASS> retval;
216    switch (format) {
217    case JSON:
218      retval = new DefaultJsonDeserializer<>(definition);
219      break;
220    case XML:
221      retval = new DefaultXmlDeserializer<>(definition);
222      break;
223    case YAML:
224      retval = new DefaultYamlDeserializer<>(definition);
225      break;
226    default:
227      throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
228    }
229
230    return retval;
231  }
232
233  @Override
234  public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
235    Class<? extends IBoundObject> retval = null;
236    for (IBindingMatcher matcher : getBindingMatchers()) {
237      retval = matcher.getBoundClassForXmlQName(rootQName);
238      if (retval != null) {
239        break;
240      }
241    }
242    return retval;
243  }
244
245  @Override
246  public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
247    Class<? extends IBoundObject> retval = null;
248    for (IBindingMatcher matcher : getBindingMatchers()) {
249      retval = matcher.getBoundClassForJsonName(rootName);
250      if (retval != null) {
251        break;
252      }
253    }
254    return retval;
255  }
256
257  @Override
258  public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
259      throws BindingException {
260    IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
261    if (definition == null) {
262      throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
263    }
264    return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
265  }
266
267  /**
268   * Used to prevent finalizer attacks as recommended in SEI CERT Rule OBJ-11.
269   * This is needed because the class is non-final and the constructor can throw.
270   */
271  @Override
272  @SuppressWarnings({
273      "PMD.EmptyFinalizer",
274      "checkstyle:NoFinalizer" })
275  protected final void finalize() {
276    // Do nothing
277  }
278
279  private static class ModuleLoader
280      extends BindingModuleLoader {
281
282    public ModuleLoader(
283        @NonNull IBindingContext bindingContext,
284        @NonNull ModuleLoadingPostProcessor postProcessor) {
285      super(bindingContext, postProcessor);
286    }
287
288    @Override
289    public IBindingMetaschemaModule load(URI resource) throws MetaschemaException, IOException {
290      IBindingMetaschemaModule module = super.load(resource);
291      getBindingContext().registerModule(module);
292      return module;
293    }
294
295    @Override
296    public IBindingMetaschemaModule load(Path path) throws MetaschemaException, IOException {
297      IBindingMetaschemaModule module = super.load(path);
298      getBindingContext().registerModule(module);
299      return module;
300    }
301
302    @Override
303    public IBindingMetaschemaModule load(URL url) throws MetaschemaException, IOException {
304      IBindingMetaschemaModule module = super.load(url);
305      getBindingContext().registerModule(module);
306      return module;
307    }
308
309    @Override
310    protected IBindingMetaschemaModule newModule(
311        URI resource,
312        METASCHEMA binding,
313        List<? extends IBindingMetaschemaModule> importedModules) throws MetaschemaException {
314      return super.newModule(resource, binding, importedModules);
315    }
316  }
317}