1 /*
2 * ModeShape (http://www.modeshape.org)
3 * See the COPYRIGHT.txt file distributed with this work for information
4 * regarding copyright ownership. Some portions may be licensed
5 * to Red Hat, Inc. under one or more contributor license agreements.
6 * See the AUTHORS.txt file in the distribution for a full listing of
7 * individual contributors.
8 *
9 * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10 * is licensed to you under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation; either version 2.1 of
12 * the License, or (at your option) any later version.
13 *
14 * ModeShape is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this software; if not, write to the Free
21 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23 */
24 package org.modeshape.graph.query.validate;
25
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import net.jcip.annotations.Immutable;
34 import net.jcip.annotations.NotThreadSafe;
35 import org.modeshape.common.text.ParsingException;
36 import org.modeshape.common.util.CheckArg;
37 import org.modeshape.graph.GraphI18n;
38 import org.modeshape.graph.query.QueryContext;
39 import org.modeshape.graph.query.model.QueryCommand;
40 import org.modeshape.graph.query.model.SelectorName;
41 import org.modeshape.graph.query.model.TypeSystem;
42 import org.modeshape.graph.query.model.Visitors;
43 import org.modeshape.graph.query.parse.InvalidQueryException;
44 import org.modeshape.graph.query.parse.SqlQueryParser;
45 import org.modeshape.graph.query.plan.CanonicalPlanner;
46 import org.modeshape.graph.query.plan.PlanHints;
47 import org.modeshape.graph.query.plan.PlanNode;
48 import org.modeshape.graph.query.plan.PlanNode.Property;
49 import org.modeshape.graph.query.plan.PlanNode.Type;
50
51 /**
52 * An immutable {@link Schemata} implementation.
53 */
54 @Immutable
55 public class ImmutableSchemata implements Schemata {
56
57 /**
58 * Obtain a new instance for building Schemata objects.
59 *
60 * @param typeSystem the type system that this schemata should use
61 * @return the new builder; never null
62 * @throws IllegalArgumentException if the context is null
63 */
64 public static Builder createBuilder( TypeSystem typeSystem ) {
65 CheckArg.isNotNull(typeSystem, "typeSystem");
66 return new Builder(typeSystem);
67 }
68
69 /**
70 * A builder of immutable {@link Schemata} objects.
71 */
72 @NotThreadSafe
73 public static class Builder {
74
75 private final TypeSystem typeSystem;
76 private final Map<SelectorName, ImmutableTable> tables = new HashMap<SelectorName, ImmutableTable>();
77 private final Map<SelectorName, QueryCommand> viewDefinitions = new HashMap<SelectorName, QueryCommand>();
78 private final Set<SelectorName> tablesOrViewsWithExtraColumns = new HashSet<SelectorName>();
79
80 protected Builder( TypeSystem typeSystem ) {
81 this.typeSystem = typeSystem;
82 }
83
84 /**
85 * Add a table with the supplied name and column names. Each column will be given a default type. The table will also
86 * overwrite any existing table definition with the same name.
87 *
88 * @param name the name of the new table
89 * @param columnNames the names of the columns.
90 * @return this builder, for convenience in method chaining; never null
91 * @throws IllegalArgumentException if the table name is null or empty, any column name is null or empty, or if no column
92 * names are given
93 */
94 public Builder addTable( String name,
95 String... columnNames ) {
96 CheckArg.isNotEmpty(name, "name");
97 CheckArg.isNotEmpty(columnNames, "columnNames");
98 List<Column> columns = new ArrayList<Column>();
99 int i = 0;
100 for (String columnName : columnNames) {
101 CheckArg.isNotEmpty(columnName, "columnName[" + (i++) + "]");
102 columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType()));
103 }
104 ImmutableTable table = new ImmutableTable(new SelectorName(name), columns, false);
105 tables.put(table.getName(), table);
106 return this;
107 }
108
109 /**
110 * Add a table with the supplied name and column names and types. The table will also overwrite any existing table
111 * definition with the same name.
112 *
113 * @param name the name of the new table
114 * @param columnNames the names of the columns
115 * @param types the types for the columns
116 * @return this builder, for convenience in method chaining; never null
117 * @throws IllegalArgumentException if the table name is null or empty, any column name is null or empty, if no column
118 * names are given, or if the number of types does not match the number of columns
119 */
120 public Builder addTable( String name,
121 String[] columnNames,
122 String[] types ) {
123 CheckArg.isNotEmpty(name, "name");
124 CheckArg.isNotEmpty(columnNames, "columnNames");
125 CheckArg.isNotEmpty(types, "types");
126 CheckArg.isEquals(columnNames.length, "columnNames.length", types.length, "types.length");
127 List<Column> columns = new ArrayList<Column>();
128 assert columnNames.length == types.length;
129 for (int i = 0; i != columnNames.length; ++i) {
130 String columnName = columnNames[i];
131 CheckArg.isNotEmpty(columnName, "columnName[" + i + "]");
132 columns.add(new ImmutableColumn(columnName, types[i]));
133 }
134 ImmutableTable table = new ImmutableTable(new SelectorName(name), columns, false);
135 tables.put(table.getName(), table);
136 return this;
137 }
138
139 /**
140 * Add a view with the supplied name and SQL string definition. The column names and types will be inferred from the
141 * source table(s) and views(s) used in the definition.
142 *
143 * @param name the name of the new view
144 * @param definition the SQL definition of the view
145 * @return this builder, for convenience in method chaining; never null
146 * @throws IllegalArgumentException if the view name is null or empty or the definition is null
147 * @throws ParsingException if the supplied definition is cannot be parsed as a SQL query
148 */
149 public Builder addView( String name,
150 String definition ) {
151 CheckArg.isNotEmpty(name, "name");
152 CheckArg.isNotEmpty(definition, "definition");
153 SqlQueryParser parser = new SqlQueryParser();
154 QueryCommand command = parser.parseQuery(definition, typeSystem);
155 this.viewDefinitions.put(new SelectorName(name), command);
156 return this;
157 }
158
159 /**
160 * Add a view with the supplied name and definition. The column names and types will be inferred from the source table(s)
161 * used in the definition.
162 *
163 * @param name the name of the new view
164 * @param definition the definition of the view
165 * @return this builder, for convenience in method chaining; never null
166 * @throws IllegalArgumentException if the view name is null or empty or the definition is null
167 */
168 public Builder addView( String name,
169 QueryCommand definition ) {
170 CheckArg.isNotEmpty(name, "name");
171 CheckArg.isNotNull(definition, "definition");
172 this.viewDefinitions.put(new SelectorName(name), definition);
173 return this;
174 }
175
176 /**
177 * Add a column with the supplied name and type to the named table. Any existing column with that name will be replaced
178 * with the new column. If the table does not yet exist, it will be added.
179 *
180 * @param tableName the name of the new table
181 * @param columnName the names of the column
182 * @param type the type for the column
183 * @return this builder, for convenience in method chaining; never null
184 * @throws IllegalArgumentException if the table name is null or empty, any column name is null or empty, if no column
185 * names are given, or if the number of types does not match the number of columns
186 */
187 public Builder addColumn( String tableName,
188 String columnName,
189 String type ) {
190 CheckArg.isNotEmpty(tableName, "tableName");
191 CheckArg.isNotEmpty(columnName, "columnName");
192 CheckArg.isNotNull(type, "type");
193 return addColumn(tableName, columnName, type, ImmutableColumn.DEFAULT_FULL_TEXT_SEARCHABLE);
194 }
195
196 /**
197 * Add a column with the supplied name and type to the named table. Any existing column with that name will be replaced
198 * with the new column. If the table does not yet exist, it will be added.
199 *
200 * @param tableName the name of the new table
201 * @param columnName the names of the column
202 * @param type the type for the column
203 * @param fullTextSearchable true if the column should be full-text searchable, or false if not
204 * @return this builder, for convenience in method chaining; never null
205 * @throws IllegalArgumentException if the table name is null or empty, the column name is null or empty, or if the
206 * property type is null
207 */
208 public Builder addColumn( String tableName,
209 String columnName,
210 String type,
211 boolean fullTextSearchable ) {
212 CheckArg.isNotEmpty(tableName, "tableName");
213 CheckArg.isNotEmpty(columnName, "columnName");
214 CheckArg.isNotNull(type, "type");
215 SelectorName selector = new SelectorName(tableName);
216 ImmutableTable existing = tables.get(selector);
217 ImmutableTable table = null;
218 if (existing == null) {
219 List<Column> columns = new ArrayList<Column>();
220 columns.add(new ImmutableColumn(columnName, type, fullTextSearchable));
221 table = new ImmutableTable(selector, columns, false);
222 } else {
223 table = existing.withColumn(columnName, type, fullTextSearchable);
224 }
225 tables.put(table.getName(), table);
226 return this;
227 }
228
229 /**
230 * Make sure the column on the named table is searchable.
231 *
232 * @param tableName the name of the new table
233 * @param columnName the names of the column
234 * @return this builder, for convenience in method chaining; never null
235 * @throws IllegalArgumentException if the table name is null or empty or if the column name is null or empty
236 */
237 public Builder makeSearchable( String tableName,
238 String columnName ) {
239 CheckArg.isNotEmpty(tableName, "tableName");
240 CheckArg.isNotEmpty(columnName, "columnName");
241 SelectorName selector = new SelectorName(tableName);
242 ImmutableTable existing = tables.get(selector);
243 ImmutableTable table = null;
244 if (existing == null) {
245 List<Column> columns = new ArrayList<Column>();
246 columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType(), true));
247 table = new ImmutableTable(selector, columns, false);
248 } else {
249 Column column = existing.getColumn(columnName);
250 String type = typeSystem.getDefaultType();
251 if (column != null) {
252 type = column.getPropertyType();
253 }
254 table = existing.withColumn(columnName, type, true);
255 }
256 tables.put(table.getName(), table);
257 return this;
258 }
259
260 /**
261 * Make sure the column on the named table has extra columns that can be used without validation error.
262 *
263 * @param tableName the name of the table
264 * @return this builder, for convenience in method chaining; never null
265 * @throws IllegalArgumentException if the table name does is null or empty
266 */
267 public Builder markExtraColumns( String tableName ) {
268 CheckArg.isNotEmpty(tableName, "tableName");
269 SelectorName selector = new SelectorName(tableName);
270 tablesOrViewsWithExtraColumns.add(selector);
271 return this;
272 }
273
274 /**
275 * Add to the specified table a key that references the existing named columns.
276 *
277 * @param tableName the name of the new table
278 * @param columnNames the names of the (existing) columns that make up the key
279 * @return this builder, for convenience in method chaining; never null
280 * @throws IllegalArgumentException if the table name is null or empty, the array of column names is null or empty, or if
281 * the column names do not reference existing columns in the table
282 */
283 public Builder addKey( String tableName,
284 String... columnNames ) {
285 CheckArg.isNotEmpty(tableName, "tableName");
286 CheckArg.isNotEmpty(columnNames, "columnNames");
287 ImmutableTable existing = tables.get(new SelectorName(tableName));
288 if (existing == null) {
289 throw new IllegalArgumentException(GraphI18n.tableDoesNotExist.text(tableName));
290 }
291 Set<Column> keyColumns = new HashSet<Column>();
292 for (String columnName : columnNames) {
293 Column existingColumn = existing.getColumnsByName().get(columnName);
294 if (existingColumn == null) {
295 String msg = GraphI18n.schemataKeyReferencesNonExistingColumn.text(tableName, columnName);
296 throw new IllegalArgumentException(msg);
297 }
298 keyColumns.add(existingColumn);
299 }
300 ImmutableTable table = existing.withKey(keyColumns);
301 tables.put(table.getName(), table);
302 return this;
303 }
304
305 /**
306 * Build the {@link Schemata} instance, using the current state of the builder. This method creates a snapshot of the
307 * tables (with their columns) as they exist at the moment this method is called.
308 *
309 * @return the new Schemata; never null
310 * @throws InvalidQueryException if any of the view definitions is invalid and cannot be resolved
311 */
312 public Schemata build() {
313 // Go through the tables and mark those that have extra columns ...
314 for (SelectorName tableName : tablesOrViewsWithExtraColumns) {
315 ImmutableTable table = tables.get(tableName);
316 if (table != null) {
317 tables.put(table.getName(), table.withExtraColumns());
318 }
319 }
320
321 ImmutableSchemata schemata = new ImmutableSchemata(new HashMap<SelectorName, Table>(tables));
322
323 // Make a copy of the view definitions, and create the views ...
324 Map<SelectorName, QueryCommand> definitions = new HashMap<SelectorName, QueryCommand>(viewDefinitions);
325 boolean added = false;
326 do {
327 added = false;
328 Set<SelectorName> viewNames = new HashSet<SelectorName>(definitions.keySet());
329 for (SelectorName name : viewNames) {
330 QueryCommand command = definitions.get(name);
331 // Create the canonical plan for the definition ...
332 PlanHints hints = new PlanHints();
333 hints.validateColumnExistance = false;
334 QueryContext queryContext = new QueryContext(schemata, typeSystem, hints);
335 CanonicalPlanner planner = new CanonicalPlanner();
336 PlanNode plan = planner.createPlan(queryContext, command);
337 if (queryContext.getProblems().hasErrors()) {
338 continue;
339 }
340
341 // Get the columns from the top-level PROJECT ...
342 PlanNode project = plan.findAtOrBelow(Type.PROJECT);
343 assert project != null;
344 List<org.modeshape.graph.query.model.Column> columns = project.getPropertyAsList(Property.PROJECT_COLUMNS,
345 org.modeshape.graph.query.model.Column.class);
346 assert !columns.isEmpty();
347
348 // Go through all the columns and look up the types ...
349 Map<SelectorName, SelectorName> tableNameByAlias = null;
350 List<Column> viewColumns = new ArrayList<Column>(columns.size());
351 for (org.modeshape.graph.query.model.Column column : columns) {
352 // Find the table that the column came from ...
353 Table source = schemata.getTable(column.selectorName());
354 if (source == null) {
355 // The column may be referring to the alias of the table ...
356 if (tableNameByAlias == null) {
357 tableNameByAlias = Visitors.getSelectorNamesByAlias(command);
358 }
359 SelectorName tableName = tableNameByAlias.get(column.selectorName());
360 if (tableName != null) source = schemata.getTable(tableName);
361 if (source == null) {
362 continue;
363 }
364 }
365 String viewColumnName = column.columnName();
366 String sourceColumnName = column.propertyName(); // getColumnName() returns alias
367 Column sourceColumn = source.getColumn(sourceColumnName);
368 if (sourceColumn == null) {
369 throw new InvalidQueryException(Visitors.readable(command),
370 "The view references a non-existant column '" + column.columnName()
371 + "' in '" + source.getName() + "'");
372 }
373 viewColumns.add(new ImmutableColumn(viewColumnName, sourceColumn.getPropertyType(),
374 sourceColumn.isFullTextSearchable()));
375 }
376 if (viewColumns.size() != columns.size()) {
377 // We weren't able to resolve all of the columns,
378 // so maybe the columns were referencing yet-to-be-built views ...
379 continue;
380 }
381
382 // If we could resolve the definition ...
383 boolean hasExtraColumns = tablesOrViewsWithExtraColumns.contains(name);
384 ImmutableView view = new ImmutableView(name, viewColumns, hasExtraColumns, command);
385 definitions.remove(name);
386 schemata = schemata.with(view);
387 added = true;
388 }
389 } while (added && !definitions.isEmpty());
390
391 if (!definitions.isEmpty()) {
392 QueryCommand command = definitions.values().iterator().next();
393 throw new InvalidQueryException(Visitors.readable(command), "The view definition cannot be resolved: "
394 + Visitors.readable(command));
395 }
396
397 return schemata;
398 }
399 }
400
401 private final Map<SelectorName, Table> tables;
402
403 protected ImmutableSchemata( Map<SelectorName, Table> tables ) {
404 this.tables = Collections.unmodifiableMap(tables);
405 }
406
407 /**
408 * {@inheritDoc}
409 *
410 * @see org.modeshape.graph.query.validate.Schemata#getTable(org.modeshape.graph.query.model.SelectorName)
411 */
412 public Table getTable( SelectorName name ) {
413 return tables.get(name);
414 }
415
416 public ImmutableSchemata with( Table table ) {
417 Map<SelectorName, Table> tables = new HashMap<SelectorName, Table>(this.tables);
418 tables.put(table.getName(), table);
419 return new ImmutableSchemata(tables);
420 }
421
422 /**
423 * {@inheritDoc}
424 *
425 * @see java.lang.Object#toString()
426 */
427 @Override
428 public String toString() {
429 StringBuilder sb = new StringBuilder();
430 boolean first = true;
431 for (Table table : tables.values()) {
432 if (first) first = false;
433 else sb.append('\n');
434 sb.append(table);
435 }
436 return sb.toString();
437 }
438
439 }