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