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