View Javadoc
1   /*
2   Copyright (c) 2026 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.IdentityHashMap;
23  import java.util.List;
24  import java.util.Objects;
25  import java.util.Set;
26  
27  import com.healthmarketscience.jackcess.PropertyMap;
28  import com.healthmarketscience.jackcess.TableNameMap;
29  
30  /**
31   * The name map of a table, which can be changed and saved back into the
32   * property of the table.
33   *
34   * @author James Ahlborn
35   * @usage _advanced_class_
36   */
37  public class TableNameMapImpl extends NameMapImpl implements TableNameMap
38  {
39    /** the table which holds this name map */
40    private final TableImpl _table;
41  
42    private TableNameMapImpl(byte[] bytes, TableImpl table) throws IOException {
43      super(bytes);
44      _table = table;
45    }
46  
47    /**
48     * @param bytes the value of the name map property of a table
49     * @param table the table which holds the property
50     * @return the decoded name map
51     * @throws IOException if the bytes are not a valid name map
52     */
53    public static TableNameMapImpl read(byte[] bytes, TableImpl table)
54      throws IOException
55    {
56      return new TableNameMapImpl(bytes, table);
57    }
58  
59    @Override
60    protected boolean isWritable() {
61      return true;
62    }
63  
64    @Override
65    public Record getTableRecord() {
66      return findTableRecord();
67    }
68  
69    private RecordImpl findTableRecord() {
70      for(RecordImpl rec : _records) {
71        if(rec._type == TABLE_TYPE) {
72          return rec;
73        }
74      }
75      return null;
76    }
77  
78    @Override
79    public Record getColumnRecord(String name) {
80      return findColumnRecord(name);
81    }
82  
83    private RecordImpl findColumnRecord(String name) {
84      for(RecordImpl rec : _records) {
85        if((rec._type == COLUMN_TYPE) && rec._name.equalsIgnoreCase(name)) {
86          return rec;
87        }
88      }
89      return null;
90    }
91  
92    @Override
93    public List<Record> getReferences() {
94      return getRecords(REFERENCE_TYPE);
95    }
96  
97    @Override
98    public Record addColumn(String name, String guid) {
99      Objects.requireNonNull(name, "name");
100     Objects.requireNonNull(guid, "guid");
101     RecordImpl tableRec = findTableRecord();
102     if(tableRec == null) {
103       throw new IllegalStateException("Name map has no table record");
104     }
105 
106     RecordImpl rec = new RecordImpl(COLUMN_TYPE, toGuidBytes(guid),
107                                     tableRec._guid.clone(), name);
108     int idx = _records.size();
109     for(int i = 0; i < _records.size(); ++i) {
110       if(_records.get(i)._type == END_TYPE) {
111         idx = i;
112         break;
113       }
114     }
115     _records.add(idx, rec);
116     return rec;
117   }
118 
119   @Override
120   public Record removeColumn(String name) {
121     RecordImpl rec = findColumnRecord(name);
122     if(rec != null) {
123       _records.remove(rec);
124     }
125     return rec;
126   }
127 
128   @Override
129   public List<Mismatch> findMismatches() throws IOException {
130     TableImpl table = _table;
131     List<Mismatch> mismatches = new ArrayList<>();
132 
133     RecordImpl tableRec = findTableRecord();
134     if(tableRec != null) {
135       String guid = getGuid(table.getProperties());
136       if((guid != null) && !guid.equalsIgnoreCase(tableRec.getGuid())) {
137         mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, table,
138                                         null, tableRec));
139       }
140       if(!tableRec._name.equals(table.getName())) {
141         mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, table,
142                                         null, tableRec));
143       }
144     }
145 
146     // every guid is matched before any name, so a record which a column
147     // claims by guid is not given to another column of the same name
148     Set<RecordImpl> matched = Collections.newSetFromMap(
149         new IdentityHashMap<>());
150     List<ColumnImpl> unmatchedCols = new ArrayList<>();
151     for(ColumnImpl col : table.getColumns()) {
152       String guid = getGuid(col.getProperties());
153       RecordImpl rec = ((guid != null) ? findRecord(guid) : null);
154       if((rec == null) || (rec._type != COLUMN_TYPE)) {
155         unmatchedCols.add(col);
156         continue;
157       }
158       matched.add(rec);
159       if(!rec._name.equals(col.getName())) {
160         mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, table,
161                                         col, rec));
162       }
163     }
164 
165     for(ColumnImpl col : unmatchedCols) {
166       RecordImpl rec = findUnmatchedColumnRecord(col.getName(), matched);
167       if(rec == null) {
168         mismatches.add(new MismatchImpl(MismatchType.MISSING_RECORD, table,
169                                         col, null));
170         continue;
171       }
172       matched.add(rec);
173       if(getGuid(col.getProperties()) != null) {
174         mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, table,
175                                         col, rec));
176       }
177     }
178 
179     for(RecordImpl rec : _records) {
180       if((rec._type == COLUMN_TYPE) && !matched.contains(rec)) {
181         mismatches.add(new MismatchImpl(MismatchType.MISSING_OBJECT, table,
182                                         null, rec));
183       }
184     }
185 
186     return mismatches;
187   }
188 
189   private RecordImpl findUnmatchedColumnRecord(String name,
190                                                Set<RecordImpl> matched) {
191     for(RecordImpl rec : _records) {
192       if((rec._type == COLUMN_TYPE) && rec._name.equalsIgnoreCase(name) &&
193          !matched.contains(rec)) {
194         return rec;
195       }
196     }
197     return null;
198   }
199 
200   @Override
201   public void save() throws IOException {
202     PropertyMap props = _table.getProperties();
203     props.put(PropertyMap.NAME_MAP_PROP, toBytes());
204     props.save();
205   }
206 }