ComplexColumnDesc.java

/*
Copyright (c) 2026 James Ahlborn

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 com.healthmarketscience.jackcess.impl;

import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;

import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.PropertyMap;
import com.healthmarketscience.jackcess.complex.ComplexColumnInfo;
import com.healthmarketscience.jackcess.complex.ComplexDataType;
import com.healthmarketscience.jackcess.impl.complex.MultiValueColumnInfoImpl;

/**
 * The complex column which a {@link com.healthmarketscience.jackcess.ColumnBuilder}
 * declares.  A complex column is a column plus a table of values and a row
 * which ties the two together, and this carries what the caller asked for
 * until {@link ComplexColumnCreator} builds them.
 *
 * @author James Ahlborn
 * @usage _advanced_class_
 */
public class ComplexColumnDesc
{
  /** the value types which have a shipped type table */
  private static final Set<DataType> VALUE_TYPES = EnumSet.of(
      DataType.BYTE, DataType.INT, DataType.LONG, DataType.FLOAT,
      DataType.DOUBLE, DataType.GUID, DataType.NUMERIC, DataType.TEXT);

  /** the kind of complex column */
  private final ComplexDataType _type;
  /** the type of a multi-value column's values */
  private final DataType _valueType;
  /** the name of the memo column a version history column tracks */
  private final String _memoColumnName;
  /** the RowSourceType property of a multi-value column */
  private String _rowSourceType;
  /** the RowSource property of a multi-value column */
  private String _rowSource;
  /** the number of columns the row source gives */
  private int _columnCount = 1;

  private ComplexColumnDesc(ComplexDataType type, DataType valueType,
                            String memoColumnName) {
    _type = type;
    _valueType = valueType;
    _memoColumnName = memoColumnName;
  }

  public static ComplexColumnDesc multiValue(DataType valueType) {
    return new ComplexColumnDesc(ComplexDataType.MULTI_VALUE, valueType, null);
  }

  public static ComplexColumnDesc attachment() {
    return new ComplexColumnDesc(ComplexDataType.ATTACHMENT, null, null);
  }

  public static ComplexColumnDesc versionHistory(String memoColumnName) {
    return new ComplexColumnDesc(ComplexDataType.VERSION_HISTORY, null,
                                 memoColumnName);
  }

  /**
   * Reads the declaration back off a complex column which exists, so that a
   * column can be copied with {@link
   * com.healthmarketscience.jackcess.ColumnBuilder#setFromColumn}.
   *
   * @return the declaration, or {@code null} if the column is not a complex one
   */
  public static ComplexColumnDesc fromColumn(Column template)
    throws IOException
  {
    ComplexColumnInfo<?> info = template.getComplexInfo();
    if(info == null) {
      return null;
    }

    switch(info.getType()) {
    case ATTACHMENT:
      return attachment();
    case MULTI_VALUE:
      Column valueCol = ((MultiValueColumnInfoImpl)info).getValueColumn();
      ComplexColumnDesc desc = multiValue(valueCol.getType());
      PropertyMap props = valueCol.getProperties();
      desc.setRowSource(
          (String)props.getValue(PropertyMap.ROW_SOURCE_TYPE_PROP),
          (String)props.getValue(PropertyMap.ROW_SOURCE_PROP),
          toColumnCount(props.getValue(PropertyMap.COLUMN_COUNT_PROP)));
      return desc;
    case VERSION_HISTORY:
      throw noVersionHistoryCopy("Column=" + template.getName());
    default:
      throw new IllegalArgumentException(
          "Cannot copy a complex column of a kind jackcess cannot read " +
          "(Column=" + template.getName() + ")");
    }
  }

  private static int toColumnCount(Object value) {
    return ((value instanceof Number) ? ((Number)value).intValue() : 1);
  }

  /**
   * @return a declaration of the same complex column
   */
  public ComplexColumnDesc copy() {
    if(_type == ComplexDataType.VERSION_HISTORY) {
      // a copy would follow the memo column of the original, under whatever
      // name the copy is given, which is a column ms access will not open
      throw noVersionHistoryCopy("MemoColumn=" + _memoColumnName);
    }
    ComplexColumnDesc desc = new ComplexColumnDesc(_type, _valueType,
                                                   _memoColumnName);
    desc.setRowSource(_rowSourceType, _rowSource, _columnCount);
    return desc;
  }

  private static IllegalArgumentException noVersionHistoryCopy(String what) {
    return new IllegalArgumentException(
        "A version history column follows the memo column it holds the " +
        "versions of, so it is copied with setAppendOnly on that column " +
        "rather than on its own (" + what + ")");
  }

  public ComplexDataType getType() {
    return _type;
  }

  public DataType getValueType() {
    return _valueType;
  }

  public String getMemoColumnName() {
    return _memoColumnName;
  }

  public String getRowSourceType() {
    return _rowSourceType;
  }

  public String getRowSource() {
    return _rowSource;
  }

  public int getColumnCount() {
    return _columnCount;
  }

  public void setRowSource(String rowSourceType, String rowSource,
                           int columnCount) {
    _rowSourceType = rowSourceType;
    _rowSource = rowSource;
    _columnCount = columnCount;
  }

  /**
   * Checks that this declaration is complete enough to build.
   *
   * @throws IllegalArgumentException if it is not
   */
  public void validate(String colName) {
    if(_type != ComplexDataType.MULTI_VALUE) {
      return;
    }
    if(!VALUE_TYPES.contains(_valueType)) {
      throw new IllegalArgumentException(
          "A multi-value column holds values of type " + VALUE_TYPES +
          ", not " + _valueType + " (Column=" + colName + ")");
    }
    // ms access shows the values a column holds whatever its row source is,
    // but offers no way to add one without a row source which names at least
    // one value
    if((_rowSource == null) || _rowSource.isEmpty()) {
      throw new IllegalArgumentException(
          "A multi-value column needs a row source, from either setValueList " +
          "or setRowSource (Column=" + colName + ")");
    }
  }

  @Override
  public String toString() {
    return ToStringBuilder.builder(this)
      .append("type", _type)
      .appendIfNotNull("valueType", _valueType)
      .appendIfNotNull("memoColumn", _memoColumnName)
      .appendIfNotNull("rowSourceType", _rowSourceType)
      .appendIfNotNull("rowSource", _rowSource)
      .toString();
  }
}