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