TableNameMapImpl.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.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import com.healthmarketscience.jackcess.PropertyMap;
import com.healthmarketscience.jackcess.TableNameMap;

/**
 * The name map of a table, which can be changed and saved back into the
 * property of the table.
 *
 * @author James Ahlborn
 * @usage _advanced_class_
 */
public class TableNameMapImpl extends NameMapImpl implements TableNameMap
{
  /** the table which holds this name map */
  private final TableImpl _table;

  private TableNameMapImpl(byte[] bytes, TableImpl table) throws IOException {
    super(bytes);
    _table = table;
  }

  /**
   * @param bytes the value of the name map property of a table
   * @param table the table which holds the property
   * @return the decoded name map
   * @throws IOException if the bytes are not a valid name map
   */
  public static TableNameMapImpl read(byte[] bytes, TableImpl table)
    throws IOException
  {
    return new TableNameMapImpl(bytes, table);
  }

  @Override
  protected boolean isWritable() {
    return true;
  }

  @Override
  public Record getTableRecord() {
    return findTableRecord();
  }

  private RecordImpl findTableRecord() {
    for(RecordImpl rec : _records) {
      if(rec._type == TABLE_TYPE) {
        return rec;
      }
    }
    return null;
  }

  @Override
  public Record getColumnRecord(String name) {
    return findColumnRecord(name);
  }

  private RecordImpl findColumnRecord(String name) {
    for(RecordImpl rec : _records) {
      if((rec._type == COLUMN_TYPE) && rec._name.equalsIgnoreCase(name)) {
        return rec;
      }
    }
    return null;
  }

  @Override
  public List<Record> getReferences() {
    return getRecords(REFERENCE_TYPE);
  }

  @Override
  public Record addColumn(String name, String guid) {
    Objects.requireNonNull(name, "name");
    Objects.requireNonNull(guid, "guid");
    RecordImpl tableRec = findTableRecord();
    if(tableRec == null) {
      throw new IllegalStateException("Name map has no table record");
    }

    RecordImpl rec = new RecordImpl(COLUMN_TYPE, toGuidBytes(guid),
                                    tableRec._guid.clone(), name);
    int idx = _records.size();
    for(int i = 0; i < _records.size(); ++i) {
      if(_records.get(i)._type == END_TYPE) {
        idx = i;
        break;
      }
    }
    _records.add(idx, rec);
    return rec;
  }

  @Override
  public Record removeColumn(String name) {
    RecordImpl rec = findColumnRecord(name);
    if(rec != null) {
      _records.remove(rec);
    }
    return rec;
  }

  @Override
  public List<Mismatch> findMismatches() throws IOException {
    TableImpl table = _table;
    List<Mismatch> mismatches = new ArrayList<>();

    RecordImpl tableRec = findTableRecord();
    if(tableRec != null) {
      String guid = getGuid(table.getProperties());
      if((guid != null) && !guid.equalsIgnoreCase(tableRec.getGuid())) {
        mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, table,
                                        null, tableRec));
      }
      if(!tableRec._name.equals(table.getName())) {
        mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, table,
                                        null, tableRec));
      }
    }

    // every guid is matched before any name, so a record which a column
    // claims by guid is not given to another column of the same name
    Set<RecordImpl> matched = Collections.newSetFromMap(
        new IdentityHashMap<>());
    List<ColumnImpl> unmatchedCols = new ArrayList<>();
    for(ColumnImpl col : table.getColumns()) {
      String guid = getGuid(col.getProperties());
      RecordImpl rec = ((guid != null) ? findRecord(guid) : null);
      if((rec == null) || (rec._type != COLUMN_TYPE)) {
        unmatchedCols.add(col);
        continue;
      }
      matched.add(rec);
      if(!rec._name.equals(col.getName())) {
        mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, table,
                                        col, rec));
      }
    }

    for(ColumnImpl col : unmatchedCols) {
      RecordImpl rec = findUnmatchedColumnRecord(col.getName(), matched);
      if(rec == null) {
        mismatches.add(new MismatchImpl(MismatchType.MISSING_RECORD, table,
                                        col, null));
        continue;
      }
      matched.add(rec);
      if(getGuid(col.getProperties()) != null) {
        mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, table,
                                        col, rec));
      }
    }

    for(RecordImpl rec : _records) {
      if((rec._type == COLUMN_TYPE) && !matched.contains(rec)) {
        mismatches.add(new MismatchImpl(MismatchType.MISSING_OBJECT, table,
                                        null, rec));
      }
    }

    return mismatches;
  }

  private RecordImpl findUnmatchedColumnRecord(String name,
                                               Set<RecordImpl> matched) {
    for(RecordImpl rec : _records) {
      if((rec._type == COLUMN_TYPE) && rec._name.equalsIgnoreCase(name) &&
         !matched.contains(rec)) {
        return rec;
      }
    }
    return null;
  }

  @Override
  public void save() throws IOException {
    PropertyMap props = _table.getProperties();
    props.put(PropertyMap.NAME_MAP_PROP, toBytes());
    props.save();
  }
}