All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cp.elements.data.conversion.provider.SimpleConversionService Maven / Gradle / Ivy

/*
 * Copyright 2011-Present 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.cp.elements.data.conversion.provider;

import static org.cp.elements.lang.RuntimeExceptionsFactory.newRuntimeException;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.cp.elements.data.conversion.AbstractConversionService;
import org.cp.elements.data.conversion.ConversionException;
import org.cp.elements.data.conversion.Converter;
import org.cp.elements.data.conversion.converters.BigDecimalConverter;
import org.cp.elements.data.conversion.converters.BigIntegerConverter;
import org.cp.elements.data.conversion.converters.BooleanConverter;
import org.cp.elements.data.conversion.converters.ByteConverter;
import org.cp.elements.data.conversion.converters.CharacterConverter;
import org.cp.elements.data.conversion.converters.DateConverter;
import org.cp.elements.data.conversion.converters.DoubleConverter;
import org.cp.elements.data.conversion.converters.EnumConverter;
import org.cp.elements.data.conversion.converters.FloatConverter;
import org.cp.elements.data.conversion.converters.IdentifiableConverter;
import org.cp.elements.data.conversion.converters.IntegerConverter;
import org.cp.elements.data.conversion.converters.LongConverter;
import org.cp.elements.data.conversion.converters.NumberConverter;
import org.cp.elements.data.conversion.converters.ShortConverter;
import org.cp.elements.data.conversion.converters.StringConverter;
import org.cp.elements.data.conversion.converters.URIConverter;
import org.cp.elements.data.conversion.converters.URLConverter;
import org.cp.elements.io.FileExtensionFilter;
import org.cp.elements.io.FileUtils;
import org.cp.elements.lang.Assert;
import org.cp.elements.lang.ClassUtils;
import org.cp.elements.lang.Identifiable;
import org.cp.elements.lang.ObjectUtils;
import org.cp.elements.lang.StringUtils;
import org.cp.elements.service.annotation.Service;
import org.cp.elements.util.ArrayUtils;
import org.cp.elements.util.CollectionUtils;

/**
 * {@link SimpleConversionService} is an application {@link Service} class that performs {@link Class type} conversions
 * using pre-canned {@link Class type} {@link Converter Converters} defined in
 * {@link org.cp.elements.data.conversion.converters}.
 *
 * @author John J. Blum
 * @see org.cp.elements.data.conversion.AbstractConversionService
 * @see org.cp.elements.data.conversion.AbstractConverterRegistry
 * @see org.cp.elements.data.conversion.Converter
 * @see org.cp.elements.data.conversion.converters.StringConverter
 * @see org.cp.elements.service.annotation.Service
 * @see How can I enumerate all classes in a package and add them to a List?
 * @since 1.0.0
 */
@Service
@SuppressWarnings("unused")
public class SimpleConversionService extends AbstractConversionService {

  protected static final Class CONVERTER_CLASS = StringConverter.class;

  protected static final Package CONVERTERS_PACKAGE = CONVERTER_CLASS.getPackage();

  private volatile boolean defaultsEnabled;

  private final Map, Object> defaultValues =
    Collections.synchronizedMap(new HashMap<>(13, 0.95f));

  /**
   * Constructs a new instance of {@link SimpleConversionService} initialized with all the {@link Converter Converters}
   * defined in the {@link org.cp.elements.data.conversion.converters} package.
   *
   * @throws IllegalArgumentException if the resource {@link URL} of the chosen {@link Converter} {@link Class}
   * cannot be resolved.
   * @see #registerConverters()
   * @see #initDefaultValues()
   */
  public SimpleConversionService() {

    registerConverters();
    initDefaultValues();
  }

  /**
   * Registers all the {@link Converter Converters} provided by {@literal cp-elements}
   * in {@link org.cp.elements.data.conversion.converters}.
   *
   * @throws IllegalArgumentException if the resource {@link URL} of the chosen {@link Converter} {@link Class}
   * cannot be resolved.
   */
  private void registerConverters() {

    String converterClassResourceName = toResourceName(CONVERTER_CLASS);

    URL converterClassResourceLocation = resolveResourceLocation(converterClassResourceName);

    Assert.notNull(converterClassResourceLocation,
        "Could not resolve URL for Converter class [%1$s] having resource name [%2$s]",
          CONVERTER_CLASS.getName(), converterClassResourceName);

    try {
      if (isJarFile(converterClassResourceLocation)) {
        registerConvertersFromJarFile(converterClassResourceName, converterClassResourceLocation);
      }
      else {
        registerConvertersFromFileSystem(converterClassResourceName, converterClassResourceLocation);
      }
    }
    catch (Exception ignore) {
      registerConvertersFromPackage();
    }
  }

  private String toResourceName(Class type) {
    return ObjectUtils.getResourceName(type);
  }

  private URL resolveResourceLocation(String resourceName) {
    return Thread.currentThread().getContextClassLoader().getResource(resourceName);
  }

  private boolean isJarFile(URL resourceLocation) {

    String resourceLocationString = resourceLocation.toExternalForm();

    return resourceLocationString.startsWith("jar:file:") || resourceLocationString.contains(".jar!");
  }

  private void registerConvertersFromPackage() {

    register(new BigDecimalConverter());
    register(new BigIntegerConverter());
    register(new BooleanConverter());
    register(new ByteConverter());
    register(new DateConverter());
    register(new CharacterConverter());
    register(new DoubleConverter());
    register(new EnumConverter());
    register(new FloatConverter());
    register(new IdentifiableConverter());
    register(new IntegerConverter());
    register(new LongConverter());
    register(new NumberConverter());
    register(new ShortConverter());
    register(new StringConverter());
    register(new URIConverter());
    register(new URLConverter());
  }

  private void registerConvertersFromJarFile(String converterClassResourceName, URL converterClassResourceLocation) {

    String converterClassResourceLocationString = converterClassResourceLocation.toExternalForm();

    String convertersPackageResourceName = CONVERTERS_PACKAGE.getName().replaceAll("\\.", "/");

    String jarFilePathname = converterClassResourceLocationString.contains(".jar!/")
      ? converterClassResourceLocationString.substring(0, converterClassResourceLocationString.indexOf(".jar!/"))
        .concat(".jar")
      : converterClassResourceLocationString;

    jarFilePathname = jarFilePathname.startsWith("jar:file:")
      ? jarFilePathname.substring("jar:file:".length())
      : jarFilePathname;

    try (JarFile jarFile = new JarFile(new File(jarFilePathname))) {

      for (JarEntry jarEntry : CollectionUtils.asIterable(jarFile.entries())) {

        String jarEntryName = jarEntry.getName();

        if (jarEntryName.startsWith(convertersPackageResourceName)
          && jarEntryName.endsWith(ClassUtils.CLASS_FILE_EXTENSION)) {

          String converterClassName = jarEntryName.replaceAll("/", ".");

          converterClassName = converterClassName.endsWith(ClassUtils.CLASS_FILE_EXTENSION)
            ? converterClassName.substring(0, converterClassName.indexOf(ClassUtils.CLASS_FILE_EXTENSION))
            : converterClassName;

          register(converterClassName);
        }
      }
    }
    catch (IOException cause) {
      throw newRuntimeException(cause, "Failed to access JAR file [%s]", jarFilePathname);
    }
  }

  @SuppressWarnings("all")
  private void registerConvertersFromFileSystem(String converterClassResourceName, URL converterClassResourceLocation) {

    try {

      File convertersPackagePath = new File(converterClassResourceLocation.toURI()).getParentFile();

      Assert.isTrue(convertersPackagePath.isDirectory(),
          "Directory for Converters package [%s] does not exist", convertersPackagePath.getAbsolutePath());

      File[] classFiles = ArrayUtils.nullSafeArray(convertersPackagePath
        .listFiles(new FileExtensionFilter(ClassUtils.CLASS_FILE_EXTENSION)), File.class);

      for (File classFile : classFiles) {

        String converterClassName = CONVERTERS_PACKAGE.getName()
            .concat(StringUtils.DOT_SEPARATOR).concat(FileUtils.getName(classFile));

        register(converterClassName);
      }
    }
    catch (URISyntaxException cause) {
      throw newRuntimeException(cause, "Failed to create File reference to directory [%s] of Converters package",
          converterClassResourceName.substring(0, converterClassResourceName.lastIndexOf(File.separator) + 1));
    }
  }

  @SuppressWarnings("rawtypes")
  private void register(String converterClassName) {

    Class converterClass = ObjectUtils.loadClass(converterClassName);

    if (ObjectUtils.assignableTo(converterClass, Converter.class)) {
      try {
        Converter converter = (Converter) converterClass.newInstance();
        register(converter);
      }
      catch (Exception ignore) { }
    }
  }

  /**
   * Initializes default values to use for a specific {@link Class} type
   * when the {@link Object value} to convert is {@literal null}.
   */
  private void initDefaultValues() {

    this.defaultValues.put(BigDecimal.class, new BigDecimal("0.0"));
    this.defaultValues.put(BigInteger.class, new BigInteger("0"));
    this.defaultValues.put(Boolean.class, false);
    this.defaultValues.put(Byte.class, (byte) 0);
    this.defaultValues.put(Calendar.class, CalendarValueSupplier.INSTANCE);
    this.defaultValues.put(Character.class, '\0');
    this.defaultValues.put(Double.class, 0.0d);
    this.defaultValues.put(Enum.class, null);
    this.defaultValues.put(Float.class, 0.0f);
    this.defaultValues.put(Integer.class, 0);
    this.defaultValues.put(Identifiable.class, null);
    this.defaultValues.put(Long.class, 0L);
    this.defaultValues.put(Short.class, (short) 0);
    this.defaultValues.put(String.class, null);
  }

  /**
   * Unsets the {@link Object default value} for the specified {@link Class type}.
   *
   * @param  {@link Class type} of the {@link Object default value}.
   * @param type {@link Class type} to remove the {@link Object default value} for.
   * @return the {@link Object default value} currently mapped to the given {@link Class type}.
   * @see #setDefaultValue(Class, Supplier)
   * @see #setDefaultValue(Class, Object)
   * @see java.lang.Class
   */
  @SuppressWarnings("unchecked")
  public  T unsetDefaultValue(Class type) {
    return (T) this.defaultValues.remove(type);
  }

  /**
   * Sets the {@link Object default value} for the specified {@link Class type}.
   *
   * @param  {@link Class classification/type} of objects which may have {@link Object default values}.
   * @param type {@link Class} type to define the {@link Object default value} for.
   * @param defaultValue {@link Object default value} to use for the specified {@link Class} type.
   * @throws IllegalArgumentException if {@link Class type} is {@literal null}.
   * @see #setDefaultValue(Class, Supplier)
   * @see java.lang.Class
   */
  public  void setDefaultValue(Class type, T defaultValue) {
    Assert.notNull(type, "Class type is required");
    this.defaultValues.put(type, defaultValue);
  }

  /**
   * Sets the {@link Supplier} used to supply a {@link Object default value} for the specified {@link Class type}.
   *
   * This overloaded method is useful for dynamically generating new {@link Object values} at runtime,
   * such as date/time values
   *
   * @param  {@link Class classification/type} of objects which may have {@link Object default values}.
   * @param type {@link Class} type to define the {@link Object default value} for.
   * @param defaultValueSupplier {@link Supplier} used to supply a {@link Object default value}
   * for the specified {@link Class} type.
   * @throws IllegalArgumentException if {@link Class type} is {@literal null}.
   * @see #setDefaultValue(Class, Object)
   * @see java.util.function.Supplier
   * @see java.lang.Class
   */
  public  void setDefaultValue(Class type, Supplier defaultValueSupplier) {
    Assert.notNull(type, "Class type is required");
    this.defaultValues.put(type, defaultValueSupplier);
  }

  /**
   * Gets the {@link Object default value} for the specified {@link Class type}.
   *
   * @param  {@link Class classification/type} of objects which may have {@link Object default values}.
   * @param type {@link Class} type to get the {@link Object default value} for.
   * @return the {@link Object default value} for the specified {@link Class type}.
   * @see java.lang.Class
   */
  @SuppressWarnings("unchecked")
  public  T getDefaultValue(Class type) {

    Object value = this.defaultValues.get(type);

    if (value instanceof Supplier) {
      value = ((Supplier) value).get();
    }

    return type.cast(value);
  }

  /**
   * Sets whether {@link Object default values} will be used during conversion
   * if the {@link Object value} to convert is {@literal null}.
   *
   * @param defaultsEnabled a boolean value to indicate whether to use {@link Object default values}
   * during conversion when the {@link Object value} to convert is {@literal null}.
   */
  public void setDefaultValuesEnabled(boolean defaultsEnabled) {
    this.defaultsEnabled = defaultsEnabled;
  }

  /**
   * Determines whether {@link Object default values} will be used during conversion
   * if the {@link Object value} to convert is {@literal null}.
   *
   * @return a boolean value indicating whether to use {@link Object default values}
   * during conversion when the {@link Object value} to convert is {@literal null}.
   */
  public boolean isDefaultValuesEnabled() {
    return this.defaultsEnabled;
  }

  /**
   * Converts the {@link Object value} into an {@link Object} of the speicfied target {@link Class type}.
   *
   * If {@link Object value} is {@literal null} and {@link Object default values} have been
   * {@link #isDefaultValuesEnabled()} enabled}, then the {@link Object default value} will be based on
   * the {@link Class type} to convert to and the {@link Class type} has been initialized with
   * a {@link Object default value}.
   *
   * @param  {@link Class target type} of the conversion.
   * @param value {@link Object value} to convert.
   * @param toType {@link Class type} to convert the {@link Object value} into.
   * @return the {@link Object value} converted into an instance of the desired {@link Class target type},
   * or a {@link Object default value} if the {@link Object converted value} is {@literal null},
   * {@link Object default values} are {@link #isDefaultValuesEnabled()} enabled} and the {@link Class type}
   * has been set with a {@link Object default value}.
   * @throws ConversionException if converting the {@link Object value} into an instance of
   * the {@link Class target type} results in error.
   * @see org.cp.elements.data.conversion.AbstractConversionService#convert(Object, Class)
   * @see org.cp.elements.data.conversion.Converter#convert(Object)
   * @see #useDefault(Object, Class)
   * @see #getDefaultValue(Class)
   * @see java.lang.Class
   */
  @Override
  public  T convert(Object value, Class toType) {
    return useDefault(value, toType) ? getDefaultValue(toType) : super.convert(value, toType);
  }

  /**
   * Determines whether the {@link Object default value} for the specified {@link Class type}
   * should be used as the converted {@link Object value} when the converted {@link Object value}
   * is {@literal null}.
   *
   * @param value {@link Object value} to convert.
   * @param toType {@link Class type} to convert the {@link Object value} into.
   * @return a boolean value indicating whether the {@link Object default value} for the specified {@link Class type}
   * should be used as the converted {@link Object value} when the converted {@link Object value}
   * is {@literal null}.
   * @see #isDefaultValuesEnabled()
   */
  protected boolean useDefault(Object value, Class toType) {
    return value == null && isDefaultValuesEnabled() && this.defaultValues.containsKey(toType);
  }

  /**
   * The {@link CalendarValueSupplier} class is a {@link Supplier} that creates a new instance of {@link Calendar}
   * with the current date/time for every invocaton of {@link Supplier#get()}.
   *
   * @see java.util.function.Supplier
   * @see java.util.Calendar
   */
  public static class CalendarValueSupplier implements Supplier {

    public static final CalendarValueSupplier INSTANCE = new CalendarValueSupplier();

    @Override
    public Calendar get() {
      return Calendar.getInstance();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy