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.HashMap;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import com.healthmarketscience.jackcess.TableMetaData;
28  import com.healthmarketscience.jackcess.query.Query;
29  
30  /**
31   * The name map of a query, which ms access uses to repair the sql of the query
32   * when a table or column it uses is renamed.  Its records cannot be changed,
33   * for the reason {@link com.healthmarketscience.jackcess.NameMap} gives.
34   *
35   * @author James Ahlborn
36   * @usage _advanced_class_
37   */
38  public class QueryNameMapImpl extends NameMapImpl
39  {
40    /** the database which holds the query */
41    private final DatabaseImpl _database;
42  
43    private QueryNameMapImpl(byte[] bytes, DatabaseImpl database)
44      throws IOException
45    {
46      super(bytes);
47      _database = database;
48    }
49  
50    /**
51     * @param bytes the value of the name map property of a query
52     * @param database the database which holds the query
53     * @return the decoded name map
54     * @throws IOException if the bytes are not a valid name map
55     */
56    public static QueryNameMapImpl read(byte[] bytes, DatabaseImpl database)
57      throws IOException
58    {
59      return new QueryNameMapImpl(bytes, database);
60    }
61  
62    @Override
63    protected boolean isWritable() {
64      return false;
65    }
66  
67    /**
68     * Compares each record with the table or column its guid names, which is
69     * how ms access finds the objects a query uses.  A record whose guid names
70     * nothing is found by its name instead.  A record for a source query is
71     * compared with that query, and the column records under it are not
72     * checked, because they carry the guids of the table columns the source
73     * query reads.  The records for a linked table are not checked, because the
74     * guid of a link is on the link and not on the table it opens.
75     */
76    @Override
77    public List<Mismatch> findMismatches() throws IOException {
78      DatabaseImpl db = _database;
79      List<Mismatch> mismatches = new ArrayList<>();
80      Map<String,TableImpl> tablesByGuid = null;
81      // the table found for each table record, keyed by the record guid, so a
82      // column record can find its table through its parent guid
83      Map<String,TableImpl> recordTables = new HashMap<>();
84      // the guids of the table records for a link or a source query, whose
85      // column records are not checked
86      Set<String> skippedGuids = new HashSet<>();
87      SourceQueries queries = null;
88  
89      for(RecordImpl rec : _records) {
90        if(rec._type == TABLE_TYPE) {
91          if(tablesByGuid == null) {
92            tablesByGuid = readTablesByGuid(db);
93          }
94          String guid = rec.getGuid();
95          TableImpl table = ((guid != null) ?
96                             tablesByGuid.get(toGuidKey(guid)) : null);
97          if(table != null) {
98            recordTables.put(toGuidKey(guid), table);
99            if(!rec._name.equals(table.getName())) {
100             mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, table,
101                                             null, rec));
102           }
103           continue;
104         }
105 
106         if(queries == null) {
107           queries = new SourceQueries(db);
108         }
109         Query query = ((guid != null) ? queries.getByGuid(guid) : null);
110         if(query != null) {
111           skippedGuids.add(toGuidKey(guid));
112           if(!rec._name.equals(query.getName())) {
113             mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, query,
114                                             rec));
115           }
116           continue;
117         }
118 
119         TableMetaData metaData = db.getTableMetaData(rec._name);
120         if(metaData == null) {
121           query = queries.getByName(rec._name);
122           if(query == null) {
123             mismatches.add(new MismatchImpl(MismatchType.MISSING_OBJECT, null,
124                                             null, rec));
125             continue;
126           }
127           if(getGuid(query.getProperties()) != null) {
128             mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, query,
129                                             rec));
130           }
131         }
132         if((metaData == null) || metaData.isLinked()) {
133           if(guid != null) {
134             skippedGuids.add(toGuidKey(guid));
135           }
136           continue;
137         }
138         // getTableMetaData finds a system table, which a query can read, so
139         // open it the same way
140         table = db.getTable(rec._name, true);
141         if(guid != null) {
142           recordTables.put(toGuidKey(guid), table);
143         }
144         if(getGuid(table.getProperties()) != null) {
145           mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, table,
146                                           null, rec));
147         }
148 
149       } else if(rec._type == COLUMN_TYPE) {
150         String parentGuid = rec.getParentGuid();
151         String parentKey = ((parentGuid != null) ? toGuidKey(parentGuid) :
152                             null);
153         if(skippedGuids.contains(parentKey)) {
154           continue;
155         }
156         TableImpl table = recordTables.get(parentKey);
157         if(table == null) {
158           mismatches.add(new MismatchImpl(MismatchType.MISSING_OBJECT, null,
159                                           null, rec));
160           continue;
161         }
162 
163         ColumnImpl col = findColumn(table, rec.getGuid());
164         if(col != null) {
165           if(!rec._name.equals(col.getName())) {
166             mismatches.add(new MismatchImpl(MismatchType.NAME_DIFFERS, table,
167                                             col, rec));
168           }
169           continue;
170         }
171 
172         col = (table.hasColumn(rec._name) ? table.getColumn(rec._name) :
173                null);
174         if(col == null) {
175           mismatches.add(new MismatchImpl(MismatchType.MISSING_OBJECT, table,
176                                           null, rec));
177         } else if(getGuid(col.getProperties()) != null) {
178           mismatches.add(new MismatchImpl(MismatchType.GUID_DIFFERS, table,
179                                           col, rec));
180         }
181       }
182     }
183 
184     return mismatches;
185   }
186 
187   /**
188    * @return the local tables of the database, keyed by their guid
189    */
190   private static Map<String,TableImpl> readTablesByGuid(DatabaseImpl db)
191     throws IOException
192   {
193     Map<String,TableImpl> tables = new HashMap<>();
194     for(String name : db.getTableNames()) {
195       if(db.getTableMetaData(name).isLinked()) {
196         continue;
197       }
198       TableImpl table = db.getTable(name);
199       String guid = getGuid(table.getProperties());
200       if(guid != null) {
201         tables.put(toGuidKey(guid), table);
202       }
203     }
204     return tables;
205   }
206 
207   /**
208    * The queries of the database, by guid and by name, which a query can read
209    * like a table.
210    */
211   private static final class SourceQueries
212   {
213     private final Map<String,Query> _byGuid = new HashMap<>();
214     private final Map<String,Query> _byName = new HashMap<>();
215 
216     private SourceQueries(DatabaseImpl db) throws IOException {
217       for(Query query : db.getQueries()) {
218         _byName.put(DatabaseImpl.toLookupName(query.getName()), query);
219         String guid = getGuid(query.getProperties());
220         if(guid != null) {
221           _byGuid.put(toGuidKey(guid), query);
222         }
223       }
224     }
225 
226     private Query getByGuid(String guid) {
227       return _byGuid.get(toGuidKey(guid));
228     }
229 
230     private Query getByName(String name) {
231       return _byName.get(DatabaseImpl.toLookupName(name));
232     }
233   }
234 
235   private static ColumnImpl findColumn(TableImpl table, String guid)
236     throws IOException
237   {
238     for(ColumnImpl col : table.getColumns()) {
239       String colGuid = getGuid(col.getProperties());
240       if((colGuid != null) && colGuid.equalsIgnoreCase(guid)) {
241         return col;
242       }
243     }
244     return null;
245   }
246 }