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.lazy(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    registerModule(MetaschemaModelModule.class);
102  }
103
104  @Override
105  @NonNull
106  public final IModuleLoaderStrategy getModuleLoaderStrategy() {
107    return moduleLoaderStrategy;
108  }
109
110  @Override
111  public IBindingModuleLoader newModuleLoader() {
112    return new ModuleLoader(this, getModuleLoaderStrategy());
113  }
114
115  @Override
116  @NonNull
117  public final IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz) {
118    IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
119    IBoundModule module = strategy.loadModule(clazz, this);
120    registerImportedModules(module);
121    return strategy.registerModule(module, this);
122  }
123
124  private void registerImportedModules(@NonNull IBoundModule module) {
125    IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
126    module.getImportedModules().stream()
127        .forEachOrdered(parentModule -> {
128          registerImportedModules(ObjectUtils.notNull(parentModule));
129          strategy.registerModule(ObjectUtils.notNull(parentModule), this);
130        });
131  }
132
133  /**
134   * Get the binding matchers that are associated with this class.
135   *
136   * @return the list of matchers
137   */
138  @NonNull
139  protected Collection<IBindingMatcher> getBindingMatchers() {
140    return getModuleLoaderStrategy().getBindingMatchers();
141  }
142
143  @Override
144  public final IBoundDefinitionModelComplex registerClassBinding(IBoundDefinitionModelComplex definition) {
145    Class<?> clazz = definition.getBoundClass();
146    return boundClassToStrategyMap.computeIfAbsent(clazz, k -> definition);
147  }
148
149  @Override
150  public final IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz) {
151    return moduleLoaderStrategy.getBoundDefinitionForClass(clazz, this);
152  }
153
154  /**
155   * {@inheritDoc}
156   * <p>
157   * A serializer returned by this method is thread-safe.
158   */
159  @Override
160  public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
161      @NonNull Format format,
162      @NonNull Class<CLASS> clazz) {
163    Objects.requireNonNull(format, "format");
164    IBoundDefinitionModelAssembly definition;
165    try {
166      definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
167    } catch (ClassCastException ex) {
168      throw new IllegalStateException(
169          String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
170    }
171    if (definition == null) {
172      throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
173    }
174    ISerializer<CLASS> retval;
175    switch (format) {
176    case JSON:
177      retval = new DefaultJsonSerializer<>(definition);
178      break;
179    case XML:
180      retval = new DefaultXmlSerializer<>(definition);
181      break;
182    case YAML:
183      retval = new DefaultYamlSerializer<>(definition);
184      break;
185    default:
186      throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
187    }
188    return retval;
189  }
190
191  /**
192   * {@inheritDoc}
193   * <p>
194   * A deserializer returned by this method is thread-safe.
195   */
196  @Override
197  public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
198      @NonNull Format format,
199      @NonNull Class<CLASS> clazz) {
200    IBoundDefinitionModelAssembly definition;
201    try {
202      definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
203    } catch (ClassCastException ex) {
204      throw new IllegalStateException(
205          String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
206          ex);
207    }
208    if (definition == null) {
209      throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
210    }
211    IDeserializer<CLASS> retval;
212    switch (format) {
213    case JSON:
214      retval = new DefaultJsonDeserializer<>(definition);
215      break;
216    case XML:
217      retval = new DefaultXmlDeserializer<>(definition);
218      break;
219    case YAML:
220      retval = new DefaultYamlDeserializer<>(definition);
221      break;
222    default:
223      throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
224    }
225
226    return retval;
227  }
228
229  @Override
230  public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
231    Class<? extends IBoundObject> retval = null;
232    for (IBindingMatcher matcher : getBindingMatchers()) {
233      retval = matcher.getBoundClassForXmlQName(rootQName);
234      if (retval != null) {
235        break;
236      }
237    }
238    return retval;
239  }
240
241  @Override
242  public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
243    Class<? extends IBoundObject> retval = null;
244    for (IBindingMatcher matcher : getBindingMatchers()) {
245      retval = matcher.getBoundClassForJsonName(rootName);
246      if (retval != null) {
247        break;
248      }
249    }
250    return retval;
251  }
252
253  @Override
254  public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
255      throws BindingException {
256    IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
257    if (definition == null) {
258      throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
259    }
260    return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
261  }
262
263  private static class ModuleLoader
264      extends BindingModuleLoader {
265
266    public ModuleLoader(
267        @NonNull IBindingContext bindingContext,
268        @NonNull ModuleLoadingPostProcessor postProcessor) {
269      super(bindingContext, postProcessor);
270    }
271
272    @Override
273    public IBindingMetaschemaModule load(URI resource) throws MetaschemaException, IOException {
274      IBindingMetaschemaModule module = super.load(resource);
275      getBindingContext().registerModule(module);
276      return module;
277    }
278
279    @Override
280    public IBindingMetaschemaModule load(Path path) throws MetaschemaException, IOException {
281      IBindingMetaschemaModule module = super.load(path);
282      getBindingContext().registerModule(module);
283      return module;
284    }
285
286    @Override
287    public IBindingMetaschemaModule load(URL url) throws MetaschemaException, IOException {
288      IBindingMetaschemaModule module = super.load(url);
289      getBindingContext().registerModule(module);
290      return module;
291    }
292
293    @Override
294    protected IBindingMetaschemaModule newModule(
295        URI resource,
296        METASCHEMA binding,
297        List<? extends IBindingMetaschemaModule> importedModules) throws MetaschemaException {
298      return super.newModule(resource, binding, importedModules);
299    }
300
301  }
302}