GpxJaxbContextHelper.java

/*
 * Copyright 2019-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bremersee.gpx;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Unmarshaller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.bremersee.gpx.model.ExtensionsType;
import org.w3c.dom.Element;

/**
 * GPX XML context helper.
 *
 * @author Christian Bremer
 */
public abstract class GpxJaxbContextHelper {

  private GpxJaxbContextHelper() {
  }

  /**
   * Parses the elements of the given GPX extensions ({@link ExtensionsType#getAnies()}) with the
   * given {@link JAXBContext}.
   *
   * <p>If the {@link JAXBContext} can unmarshall the {@link Element} to a concrete object, a map
   * entry will be created with the class of the object as key and a list with all unmarshalled
   * objects as value.
   *
   * <p>If the {@link JAXBContext} cannot unmarshall the {@link Element} to a concrete object, a
   * map entry will be created with the class of the element as key and a list with all elements as
   * value.
   *
   * @param extensions the GPX extension
   * @param jaxbContext the {@link JAXBContext} to parse the elements
   * @return an unmodifiable map with the unmarshalled objects (key is the class of the objects,
   *     value is a list with all unmarshalled objects of this class)
   */
  public static Map<Class<?>, List<Object>> parseExtensions(
      final ExtensionsType extensions,
      final JAXBContext jaxbContext) {

    try {
      return parseExtensions(extensions, jaxbContext.createUnmarshaller());
    } catch (final Exception ignored) {
      return parseExtensions(extensions, (Unmarshaller) null);
    }
  }

  /**
   * Parses the elements of the given GPX extensions ({@link ExtensionsType#getAnies()}) with the
   * given {@link Unmarshaller}.
   *
   * <p>If the {@link Unmarshaller} can unmarshall the {@link Element} to a concrete object, a map
   * entry will be created with the class of the object as key and a list with all unmarshalled
   * objects as value.
   *
   * <p>If the {@link Unmarshaller} cannot unmarshall the {@link Element} to a concrete object, a
   * map entry will be created with the class of the element as key and a list with all elements as
   * value.
   *
   * @param extensions the GPX extension
   * @param unmarshaller the {@link Unmarshaller} to parse the elements
   * @return an unmodifiable map with the unmarshalled objects (key is the class of the objects,
   *     value is a list with all unmarshalled objects of this class)
   */
  public static Map<Class<?>, List<Object>> parseExtensions(
      final ExtensionsType extensions,
      final Unmarshaller unmarshaller) {

    final Map<Class<?>, List<Object>> map = new HashMap<>();
    if (extensions == null || extensions.getAnies() == null) {
      return map;
    }
    for (final Element element : extensions.getAnies()) {
      if (element != null) {
        final Object strictElement = parseElement(element, unmarshaller);
        final List<Object> values = map.computeIfAbsent(
            strictElement.getClass(), k -> new ArrayList<>());
        values.add(strictElement);
      }
    }
    return Collections.unmodifiableMap(map);
  }

  private static Object parseElement(final Element element, final Unmarshaller unmarshaller) {
    try {
      return unmarshaller.unmarshal(element);

    } catch (final Exception ignored) {
      return element;
    }
  }

  /**
   * Find all extensions of the given type.
   *
   * @param cls the type
   * @param instancesOf if {@code true} instanceof will be used, otherwise
   *     {@link Class#equals(Object)} will be used
   * @param parsedExtensions the parsed extensions (see
   *     {@link GpxJaxbContextHelper#parseExtensions(ExtensionsType, JAXBContext)})
   * @param <T> the type
   * @return an unmodifiable list of all extensions of the given type
   */
  @SuppressWarnings("unchecked")
  public static <T> List<T> findExtensions(
      final Class<T> cls,
      final boolean instancesOf,
      final Map<Class<?>, List<Object>> parsedExtensions) {

    if (cls == null || parsedExtensions == null || parsedExtensions.isEmpty()) {
      return Collections.emptyList();
    }
    final List<T> list = new ArrayList<>();
    for (Map.Entry<Class<?>, List<Object>> entry : parsedExtensions.entrySet()) {
      final Class<?> c = entry.getKey();
      if (cls.equals(c) || (instancesOf && cls.isAssignableFrom(c))) {
        final List<Object> values = entry.getValue();
        if (values != null) {
          for (final Object value : values) {
            if (value != null
                && (cls.equals(value.getClass())
                || (instancesOf && cls.isAssignableFrom(value.getClass())))) {
              list.add((T) value);
            }
          }
        }
      }
    }
    return Collections.unmodifiableList(list);
  }

  /**
   * Find all extensions of the given type.
   *
   * @param cls the type
   * @param instancesOf if {@code true} instanceof will be used, otherwise
   *     {@link Class#equals(Object)} will be used
   * @param extensions the GPX extensions
   * @param jaxbContext the {@link JAXBContext} to parse the elements
   * @param <T> the type
   * @return an unmodifiable list of all extensions of the given type
   */
  public static <T> List<T> findExtensions(
      final Class<T> cls,
      final boolean instancesOf,
      final ExtensionsType extensions,
      final JAXBContext jaxbContext) {

    return findExtensions(cls, instancesOf, parseExtensions(extensions, jaxbContext));
  }

  /**
   * Find all extensions of the given type.
   *
   * @param cls the type
   * @param instancesOf if {@code true} instanceof will be used, otherwise
   *     {@link Class#equals(Object)} will be used
   * @param extensions the GPX extensions
   * @param unmarshaller the {@link Unmarshaller} to parse the elements
   * @param <T> the type
   * @return an unmodifiable list of all extensions of the given type
   */
  public static <T> List<T> findExtensions(
      final Class<T> cls,
      final boolean instancesOf,
      final ExtensionsType extensions,
      final Unmarshaller unmarshaller) {

    return findExtensions(cls, instancesOf, parseExtensions(extensions, unmarshaller));
  }

  /**
   * Find the first extension of the given type.
   *
   * @param cls the type
   * @param instancesOf if {@code true} instanceof will be used, otherwise
   *     {@link Class#equals(Object)} will be used
   * @param parsedExtensions the parsed extensions (see
   *     {@link GpxJaxbContextHelper#parseExtensions(ExtensionsType, JAXBContext)})
   * @param <T> the type
   * @return {@link Optional#empty()} if there is no such element, otherwise an optional with the
   *     parsed element
   */
  public static <T> Optional<T> findFirstExtension(
      final Class<T> cls,
      final boolean instancesOf,
      final Map<Class<?>, List<Object>> parsedExtensions) {

    final List<T> list = findExtensions(cls, instancesOf, parsedExtensions);
    return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
  }

  /**
   * Find the first extension of the given type.
   *
   * @param cls the type
   * @param instancesOf if {@code true} instanceof will be used, otherwise
   *     {@link Class#equals(Object)} will be used
   * @param extensions the GPX extensions
   * @param jaxbContext the {@link JAXBContext} to parse the elements
   * @param <T> the type
   * @return {@link Optional#empty()} if there is no such element, otherwise an optional with the
   *     parsed element
   */
  public static <T> Optional<T> findFirstExtension(
      final Class<T> cls,
      final boolean instancesOf,
      final ExtensionsType extensions,
      final JAXBContext jaxbContext) {

    final List<T> list = findExtensions(cls, instancesOf, extensions, jaxbContext);
    return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
  }

  /**
   * Find the first extension of the given type.
   *
   * @param cls the type
   * @param instancesOf if {@code true} instanceof will be used, otherwise
   *     {@link Class#equals(Object)} will be used
   * @param extensions the GPX extensions
   * @param unmarshaller the {@link Unmarshaller} to parse the elements
   * @param <T> the type
   * @return {@link Optional#empty()} if there is no such element, otherwise an optional with the
   *     parsed element
   */
  public static <T> Optional<T> findFirstExtension(
      final Class<T> cls,
      final boolean instancesOf,
      final ExtensionsType extensions,
      final Unmarshaller unmarshaller) {

    final List<T> list = findExtensions(cls, instancesOf, extensions, unmarshaller);
    return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
  }

}