001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io;
007
008import java.io.IOException;
009import java.io.Reader;
010import java.net.URI;
011import java.util.concurrent.locks.Lock;
012import java.util.concurrent.locks.ReentrantLock;
013
014import dev.metaschema.core.configuration.IConfiguration;
015import dev.metaschema.core.configuration.IMutableConfiguration;
016import dev.metaschema.core.metapath.DynamicContext;
017import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
018import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
019import dev.metaschema.core.metapath.item.node.INodeItem;
020import dev.metaschema.core.model.IBoundObject;
021import dev.metaschema.core.model.constraint.ConstraintValidationException;
022import dev.metaschema.core.model.constraint.DefaultConstraintValidator;
023import dev.metaschema.core.model.constraint.IConstraintValidationHandler;
024import dev.metaschema.core.model.constraint.LoggingConstraintValidationHandler;
025import dev.metaschema.core.util.ObjectUtils;
026import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
027import edu.umd.cs.findbugs.annotations.NonNull;
028
029/**
030 * The base class of all format-specific deserializers.
031 *
032 * @param <CLASS>
033 *          the bound class to deserialize to
034 */
035public abstract class AbstractDeserializer<CLASS extends IBoundObject>
036    extends AbstractSerializationBase<DeserializationFeature<?>>
037    implements IDeserializer<CLASS> {
038
039  private IConstraintValidationHandler constraintValidationHandler;
040  private final Lock handlerLock = new ReentrantLock();
041
042  /**
043   * Construct a new deserializer.
044   *
045   * @param definition
046   *          the bound class information for the Java type this deserializer is
047   *          operating on
048   */
049  protected AbstractDeserializer(@NonNull IBoundDefinitionModelAssembly definition) {
050    super(definition);
051  }
052
053  /**
054   * Get the constraint validation handler configured for this deserializer, which
055   * will be used to validate loaded data.
056   *
057   * @return the deserializer
058   */
059  @Override
060  @NonNull
061  public IConstraintValidationHandler getConstraintValidationHandler() {
062    handlerLock.lock();
063    try {
064      if (constraintValidationHandler == null) {
065        constraintValidationHandler = new LoggingConstraintValidationHandler();
066      }
067      return ObjectUtils.notNull(constraintValidationHandler);
068    } finally {
069      handlerLock.unlock();
070    }
071  }
072
073  @Override
074  public void setConstraintValidationHandler(@NonNull IConstraintValidationHandler constraintValidationHandler) {
075    handlerLock.lock();
076    try {
077      this.constraintValidationHandler = constraintValidationHandler;
078    } finally {
079      handlerLock.unlock();
080    }
081  }
082
083  @Override
084  public INodeItem deserializeToNodeItem(Reader reader, URI documentUri) throws IOException {
085
086    INodeItem nodeItem;
087    try {
088      nodeItem = deserializeToNodeItemInternal(reader, documentUri);
089    } catch (Exception ex) { // NOPMD - this is intentional
090      throw new IOException(ex);
091    }
092
093    if (isValidating()) {
094      try {
095        validate(nodeItem);
096      } catch (ConstraintValidationException ex) {
097        throw new IOException(ex);
098      }
099    }
100    return nodeItem;
101  }
102
103  /**
104   * This abstract method delegates parsing to the concrete implementation.
105   *
106   * @param reader
107   *          the reader instance to read data from
108   * @param documentUri
109   *          the URI of the document that is being read
110   * @return a new node item containing the read contents
111   * @throws IOException
112   *           if an error occurred while reading data from the stream
113   */
114  @NonNull
115  protected abstract INodeItem deserializeToNodeItemInternal(@NonNull Reader reader, @NonNull URI documentUri)
116      throws IOException;
117
118  @Override
119  public final CLASS deserializeToValue(Reader reader, URI documentUri) throws IOException {
120    CLASS retval;
121
122    if (isValidating()) {
123      INodeItem nodeItem = deserializeToNodeItemInternal(reader, documentUri);
124      try {
125        validate(nodeItem);
126      } catch (ConstraintValidationException ex) {
127        throw new IOException(ex);
128      }
129      retval = ObjectUtils.asType(ObjectUtils.requireNonNull(nodeItem.getValue()));
130    } else {
131      retval = deserializeToValueInternal(reader, documentUri);
132    }
133    return retval;
134  }
135
136  private void validate(@NonNull INodeItem nodeItem) throws ConstraintValidationException {
137    IDefinitionNodeItem<?, ?> definitionNodeItem;
138    if (nodeItem instanceof IDocumentNodeItem) {
139      definitionNodeItem = ((IDocumentNodeItem) nodeItem).getRootAssemblyNodeItem();
140    } else if (nodeItem instanceof IDefinitionNodeItem) {
141      definitionNodeItem = (IDefinitionNodeItem<?, ?>) nodeItem;
142    } else {
143      throw new UnsupportedOperationException(String.format(
144          "The node item type '%s' is not supported for validation.",
145          nodeItem.getClass().getName()));
146    }
147
148    DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext());
149    // Use permissive loader for documents referenced in constraint expressions
150    dynamicContext.setDocumentLoader(getBindingContext().newPermissiveBoundLoader());
151    try (DefaultConstraintValidator validator = new DefaultConstraintValidator(getConstraintValidationHandler())) {
152      validator.validate(definitionNodeItem, dynamicContext);
153      validator.finalizeValidation(dynamicContext);
154    }
155  }
156
157  /**
158   * This abstract method delegates parsing to the concrete implementation,
159   * returning the deserialized value directly.
160   *
161   * @param reader
162   *          the reader instance to read data from
163   * @param documentUri
164   *          the URI of the document that is being read
165   * @return the deserialized object
166   * @throws IOException
167   *           if an error occurred while reading data from the stream
168   */
169  @NonNull
170  protected abstract CLASS deserializeToValueInternal(@NonNull Reader reader, @NonNull URI documentUri)
171      throws IOException;
172
173  @Override
174  public IDeserializer<CLASS> enableFeature(DeserializationFeature<?> feature) {
175    return set(feature, true);
176  }
177
178  @Override
179  public IDeserializer<CLASS> disableFeature(DeserializationFeature<?> feature) {
180    return set(feature, false);
181  }
182
183  @Override
184  public IDeserializer<CLASS> applyConfiguration(
185      @NonNull IConfiguration<DeserializationFeature<?>> other) {
186    IMutableConfiguration<DeserializationFeature<?>> config = getConfiguration();
187    config.applyConfiguration(other);
188    configurationChanged(config);
189    return this;
190  }
191
192  @Override
193  public IDeserializer<CLASS> set(DeserializationFeature<?> feature, Object value) {
194    IMutableConfiguration<DeserializationFeature<?>> config = getConfiguration();
195    config.set(feature, value);
196    configurationChanged(config);
197    return this;
198  }
199}