/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
// https://issues.jboss.org/browse/TEIID-2347
// https://issues.jboss.org/browse/TEIID-5427
// https://issues.redhat.com/browse/TEIID-2195
// https://issues.redhat.com/browse/TEIID-2746
// https://issues.redhat.com/browse/TEIID-2353
// https://issues.redhat.com/browse/TEIID-2656
package org.teiid.query.metadata;

import org.teiid.adminapi.impl.ModelMetaData;
import org.teiid.adminapi.impl.ModelMetaData.Message.Severity;
import org.teiid.adminapi.impl.VDBMetaData;
import org.teiid.api.exception.query.QueryResolverException;
import org.teiid.core.CoreConstants;
import org.teiid.core.TeiidException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.util.StringUtil;
import org.teiid.language.SQLConstants;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.logging.MessageLevel;
import org.teiid.metadata.*;
import org.teiid.metadata.FunctionMethod;
import org.teiid.metadata.FunctionMethod.Determinism;
import org.teiid.query.QueryPlugin;
import org.teiid.query.function.FunctionLibrary;
import org.teiid.query.function.metadata.FunctionMetadataValidator;
import org.teiid.query.mapping.relational.QueryNode;
import org.teiid.query.parser.QueryParser;
import org.teiid.query.report.ActivityReport;
import org.teiid.query.report.ReportItem;
import org.teiid.query.resolver.QueryResolver;
import org.teiid.query.resolver.util.ResolverUtil;
import org.teiid.query.resolver.util.ResolverVisitor;
import org.teiid.query.sql.lang.CacheHint;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.QueryCommand;
import org.teiid.query.sql.navigator.PreOrPostOrderNavigator;
import org.teiid.query.sql.symbol.*;
import org.teiid.query.sql.visitor.EvaluatableVisitor;
import org.teiid.query.sql.visitor.ValueIteratorProviderCollectorVisitor;
import org.teiid.query.validator.Validator;
import org.teiid.query.validator.ValidatorFailure;
import org.teiid.query.validator.ValidatorReport;
import org.teiid.translator.TranslatorException;

import java.util.*;

public class MetadataValidator {
	
	interface MetadataRule {
		default void execute(VDBMetaData vdb, MetadataStore store, ValidatorReport report, MetadataValidator metadataValidator) {
			QueryMetadataInterface metadata = wrapWithTempAdapter(getQueryMetadataInterface(vdb));
			for (Schema schema : store.getSchemaList()) {
				ModelMetaData model = vdb.getModel(schema.getName());
				executeDetached(metadata, store, report, metadataValidator, schema, null, model);
			}
		}

		void executeDetached(QueryMetadataInterface metadata, MetadataStore vdbStore, ValidatorReport report, MetadataValidator metadataValidator,
							 Schema schema, MetadataFactory mf, ModelMetaData model);
	}

	private static QueryMetadataInterface getQueryMetadataInterface(VDBMetaData vdb) {
		return vdb.getAttachment(QueryMetadataInterface.class);
	}

	private static QueryMetadataInterface wrapWithTempAdapter(QueryMetadataInterface metadata) {
		return new TempMetadataAdapter(metadata, new TempMetadataStore());
	}

	public ValidatorReport validate(VDBMetaData vdb, MetadataStore store) {
		ValidatorReport report = new ValidatorReport();
		if (store != null && !store.getSchemaList().isEmpty()) {
			new SourceModelArtifacts().execute(vdb, store, report, this);
			new CrossSchemaResolver().execute(vdb, store, report, this);
			new ResolveQueryPlans().execute(vdb, store, report, this);
			// EXTENSION
			// ISSUE: DVCORE-14
			// FIXED BY: Salvatore Raunich
			// DATE: 20.11.2012
			// skip checking if the schema has no tables/columns/functions
//			new MinimalMetadata().execute(vdb, store, report, this);			
			// END EXTENSION
		}
		return report;
	}
	
	public ValidatorReport validateModel(VDBMetaData vdb, MetadataStore store, Schema schema, MetadataFactory mf, ModelMetaData model) {
		return validateModel(getQueryMetadataInterface(vdb), store, schema, mf, model);
	}

	public ValidatorReport validateModel(QueryMetadataInterface metadata, MetadataStore store, Schema schema, MetadataFactory mf, ModelMetaData model) {
		metadata = wrapWithTempAdapter(metadata);
		ValidatorReport report = new ValidatorReport();
		if (store != null && !store.getSchemaList().isEmpty()) {
			new SourceModelArtifacts().executeDetached(metadata, store, report, this, schema, mf, model);
			new ResolveQueryPlans().executeDetached(metadata, store, report, this, schema, mf, model);
			new CrossSchemaResolver().executeDetached(metadata, store, report, this, schema, mf, model);
		}
		return report;
	}

	// At minimum the model must have table/view, procedure or function 
	static class MinimalMetadata implements MetadataRule {
		@Override
		public void executeDetached(QueryMetadataInterface metadata, MetadataStore vdbStore, ValidatorReport report,
									MetadataValidator metadataValidator, Schema schema, MetadataFactory mf, ModelMetaData model) {
			if (schema.getTables().isEmpty()
					&& schema.getProcedures().isEmpty()
					&& schema.getFunctions().isEmpty()) {
				metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31070, model.getName()));
			}

			for (Table t : schema.getTables().values()) {
				if (t.getColumns() == null || t.getColumns().size() == 0) {
					metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31071, t.getName()));
				}
			}

			// procedure validation is handled in parsing routines.

			if (!schema.getFunctions().isEmpty()) {
				ActivityReport<ReportItem> funcReport = new ActivityReport<ReportItem>("Translator metadata load " + model.getName()); //$NON-NLS-1$
				FunctionMetadataValidator.validateFunctionMethods(schema.getFunctions().values(), report);
				if (report.hasItems()) {
					metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31073, funcReport));
				}
			}
		}
	}
	
	// do not allow foreign tables, source functions in view model and vice versa 
	static class SourceModelArtifacts implements MetadataRule {
		@Override
		public void executeDetached(QueryMetadataInterface metadata, MetadataStore vdbStore, ValidatorReport report, MetadataValidator metadataValidator,
									Schema schema, MetadataFactory mf, ModelMetaData model) {
			if (isSystemSchema(model, schema))
				return;
			for (Table t : schema.getTables().values()) {
				if (t.isPhysical() && !model.isSource() && t.getTableType() != Table.Type.View) {
					metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31075, t.getName(), model.getName()));
				}
			}

			Set<String> names = new HashSet<String>();
			for (Procedure p : schema.getProcedures().values()) {
				boolean hasReturn = false;
				names.clear();
				for (ProcedureParameter param : p.getParameters()) {
					if (param.isVarArg() && param != p.getParameters().get(p.getParameters().size() - 1)) {
						metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31112, p.getFullName()));
					}
					if (param.getType() == ProcedureParameter.Type.ReturnValue) {
						if (hasReturn) {
							metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31107, p.getFullName()));
						}
						hasReturn = true;
					}
					if (!names.add(param.getName())) {
						metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31106, p.getFullName(), param.getName()));
					}
				}
				if (!p.isVirtual() && !model.isSource()) {
					metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31077, p.getName(), model.getName()));
				}
			}

			for (FunctionMethod func : schema.getFunctions().values()) {
				for (FunctionParameter param : func.getInputParameters()) {
					if (param.isVarArg() && param != func.getInputParameters().get(func.getInputParameterCount() - 1)) {
						metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31112, func.getFullName()));
					}
				}
				if (func.getPushdown().equals(FunctionMethod.PushDown.MUST_PUSHDOWN) && !model.isSource()) {
					metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31078, func.getName(), model.getName()));
				}
			}
		}
	}
	
	// Resolves metadata query plans to make sure they are accurate
	static class ResolveQueryPlans implements MetadataRule {
		public void executeDetached(QueryMetadataInterface metadata, MetadataStore store, ValidatorReport report, MetadataValidator metadataValidator,
									Schema schema, MetadataFactory mf, ModelMetaData model) {
			if (isSystemSchema(model, schema))
				return;
			if (model == null)
				return;
			for (AbstractMetadataRecord record : schema.getResolvingOrder()) {
				if (record instanceof Table) {
					Table t = (Table) record;
					// no need to verify the transformation of the xml mapping document,
					// as this is very specific and designer already validates it.
					if (t.getTableType() == Table.Type.Document
							|| t.getTableType() == Table.Type.XmlMappingClass
							|| t.getTableType() == Table.Type.XmlStagingTable) {
						continue;
					}
					if (t.isVirtual()) {
						if (t.getSelectTransformation() == null) {
							metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31079, t.getName(), model.getName()));
						} else {
							metadataValidator.validate(model, t, store, report, mf, metadata);
						}
					}
				} else if (record instanceof Procedure) {
					Procedure p = (Procedure) record;
					if (p.isVirtual()) {
						if (p.getQueryPlan() == null) {
							metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31081, p.getName(), model.getName()));
						} else {
							metadataValidator.validate(model, p, store, report, mf, metadata);
						}
					}
				}
			}
		}
	}	

	public void log(ValidatorReport report, ModelMetaData model, String msg) {
		log(report, model, Severity.ERROR, msg);
	}
	
	public void log(ValidatorReport report, ModelMetaData model, Severity severity, String msg) {
		model.addRuntimeMessage(severity, msg);
		int messageLevel = MessageLevel.WARNING;
		if (severity == Severity.ERROR) {
			report.handleValidationError(msg);
		} else {
			messageLevel = MessageLevel.INFO;
			report.handleValidationWarning(msg);
		}
		LogManager.log(messageLevel, LogConstants.CTX_QUERY_RESOLVER, msg);
	}
	
    private void validate(ModelMetaData model, AbstractMetadataRecord record, MetadataStore store, ValidatorReport report, MetadataFactory mf, QueryMetadataInterface metadata) {
    	ValidatorReport resolverReport = null;
    	try {
    		if (record instanceof Procedure) {
    			Procedure p = (Procedure)record;
    			Command command = QueryParser.getQueryParser().parseProcedure(p.getQueryPlan(), false);
    			QueryResolver.resolveCommand(command, new GroupSymbol(p.getFullName()), Command.TYPE_EXECUTABLE, metadata, null);
    			resolverReport =  Validator.validate(command, metadata);
    		} else if (record instanceof Table) {
    			Table t = (Table)record;
				QueryNode node = null;

			    GroupSymbol symbol = new GroupSymbol(t.getFullName());
			    ResolverUtil.resolveGroup(symbol, metadata);
				if ((mf == null) || (t.getAttachment(MetadataFactory.class) != null)) {
					mf = t.removeAttachment(MetadataFactory.class);
				}
				if (t.isVirtual() && (t.getColumns() == null || t.getColumns().isEmpty())) {
    				QueryCommand command = (QueryCommand)QueryParser.getQueryParser().parseCommand(t.getSelectTransformation());
    				QueryResolver.resolveCommand(command, metadata, null);
    				resolverReport =  Validator.validate(command, metadata);
    				if(!resolverReport.hasItems()) {
    					List<Expression> symbols = command.getProjectedSymbols();
    					for (Expression column:symbols) {
    						try {
    							// BUG: DVCORE-122
    							// ISSUE: some column parameters are not correctly set for views defined in the VDB 
    							// SOLUTION: if the projected symbol is an ElementSymbol, copy properties values from the corresponding Column object
    							// FIXED by Salvatore Raunich
    							// date 18.10.2012
//								addColumn(Symbol.getShortName(column), column.getType(), t, mf);
								// EXTENSION
								// ISSUE: DVCORE-2441
								// FIXED BY: Roman Pedchenko
								// DATE: 2014.10.23
								// ADOPTED FROM: https://issues.jboss.org/browse/TEIID-247, 2166, 2151, 2234, 2581, 2571, 2369, 1386, 2226, 2233, 2224, 2225, 2257
								// addColumn(Symbol.getShortName(column), getDataType(store.getDatatypes().values(), column.getType()), t, column, mf);
							    addColumn(Symbol.getShortName(column), DataTypeHolder.buildDataTypeHolder(store.getDatatypes().values(), column.getType()), t, column, mf);
								// END EXTENSION
								// BUG END
							} catch (TranslatorException e) {
								log(report, model, e.getMessage());
							}
    					}
    				}
    			}

			    if (t.isMaterialized() && t.getMaterializedTable() == null) {
				    List<KeyRecord> fbis = t.getFunctionBasedIndexes();
				    List<GroupSymbol> groups = Arrays.asList(symbol);
				    if (fbis != null && !fbis.isEmpty()) {
					    for (KeyRecord fbi : fbis) {
						    for (int j = 0; j < fbi.getColumns().size(); j++) {
							    Column c = fbi.getColumns().get(j);
							    if (c.getParent() != fbi) {
								    continue;
							    }
							    String exprString = c.getNameInSource();
							    try {
								    Expression ex = QueryParser.getQueryParser().parseExpression(exprString);
								    ResolverVisitor.resolveLanguageObject(ex, groups, metadata);
								    if (!ValueIteratorProviderCollectorVisitor.getValueIteratorProviders(ex).isEmpty()) {
									    log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31114, exprString, fbi.getFullName()));
								    }
								    EvaluatableVisitor ev = new EvaluatableVisitor();
								    PreOrPostOrderNavigator.doVisit(ex, ev, PreOrPostOrderNavigator.PRE_ORDER);
								    if (ev.getDeterminismLevel().compareTo(Determinism.VDB_DETERMINISTIC) < 0) {
									    log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31115, exprString, fbi.getFullName()));
								    }
							    } catch (QueryResolverException e) {
								    log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31116, exprString, fbi.getFullName(), e.getMessage()));
							    }
						    }
					    }
				    }
			    }

    			// this seems to parse, resolve and validate.
    			node = QueryResolver.resolveView(symbol, new QueryNode(t.getSelectTransformation()), SQLConstants.Reserved.SELECT, metadata);

				if (node != null && t.isMaterialized()) {
					CacheHint cacheHint = node.getCommand().getCacheHint();
					if (cacheHint != null && cacheHint.getScope() != null
							&& cacheHint.getScope() != org.teiid.translator.CacheDirective.Scope.VDB) {
						log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31116, t.getFullName()));
					}
				}
    		}
			if(resolverReport != null && resolverReport.hasItems()) {
				for (ValidatorFailure v:resolverReport.getItems()) {
					log(report, model, v.getStatus() == ValidatorFailure.Status.ERROR?Severity.ERROR:Severity.WARNING, v.getMessage());
				}
			}
		} catch (TeiidException e) {
			log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31080, record.getFullName(), e.getFullMessage()));
		}
    }

	// BUG: DVCORE-122
	// ISSUE: some column parameters are not correctly set for views defined in the VDB 
	// SOLUTION: if the projected symbol is an ElementSymbol, copy properties values from the corresponding Column object
	// FIXED by Salvatore Raunich
	// date 18.10.2012
//     private Column addColumn(String name, Class<?> type, Table table, MetadataFactory mf) throws TranslatorException {
//     if (type == null) {
//    		throw new TranslatorException(QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31086, name, table.getFullName()));
//     }
//     Column column = mf.addColumn(name, DataTypeManager.getDataTypeName(type), table);
//     column.setUpdatable(table.supportsUpdate());
//	 return column;		
//     }

	// EXTENSION
	// ISSUE: DVCORE-2441
	// FIXED BY: Roman Pedchenko
	// DATE: 2014.10.23
	// ADOPTED FROM: https://issues.jboss.org/browse/TEIID-247, 2166, 2151, 2234, 2581, 2571, 2369, 1386, 2226, 2233, 2224, 2225, 2257
	// public static Column addColumn(String name, Datatype type, Table table, Expression originalColumnAsExpression, MetadataFactory mf) throws TranslatorException {
	public static Column addColumn(String name, DataTypeHolder dataTypeHolder, Table table, Expression originalColumnAsExpression, MetadataFactory mf) throws TranslatorException {
	// END EXTENSION
		Datatype datatype = dataTypeHolder.getDatatype();
		if (datatype == null) {
			throw new TranslatorException(QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31086, name, table.getFullName()));
		}
		// EXTENSION
		// ISSUE: DVCORE-2441
		// FIXED BY: Roman Pedchenko
		// DATE: 2014.10.23
		// ADOPTED FROM: https://issues.jboss.org/browse/TEIID-247, 2166, 2151, 2234, 2581, 2571, 2369, 1386, 2226, 2233, 2224, 2225, 2257
		// Column column = mf.addColumn(name, type, table);
		int arrayDimensions = dataTypeHolder.getArrayDimensions();
		Column column = mf.addColumn(name, datatype, arrayDimensions, table);
		// END EXTENSION
		Column originalColumn = null;
		if (originalColumnAsExpression instanceof AliasSymbol)
			originalColumnAsExpression = ((AliasSymbol) originalColumnAsExpression).getSymbol();
		if (originalColumnAsExpression instanceof ElementSymbol) {
			Object metadataId = ((ElementSymbol) originalColumnAsExpression).getMetadataID();
			if (metadataId instanceof Column) {
				originalColumn = (Column) ((ElementSymbol) originalColumnAsExpression).getMetadataID();
			}
			if (metadataId instanceof TempMetadataID) {
				TempMetadataID tempId = (TempMetadataID) metadataId;
				if (tempId.getOriginalMetadataID() instanceof Column) {
					originalColumn = (Column) tempId.getOriginalMetadataID();
				}
			}			
		}
		if (originalColumn != null) {
			column.setLength(originalColumn.getLength());
			column.setPrecision(originalColumn.getPrecision());
			column.setRadix(originalColumn.getRadix());
			column.setRuntimeType(originalColumn.getRuntimeType());
			column.setCaseSensitive(originalColumn.isCaseSensitive());
			column.setAutoIncremented(originalColumn.isAutoIncremented());
			column.setCharOctetLength(originalColumn.getCharOctetLength());
			column.setSigned(originalColumn.isSigned());  
			// EXTENSION
			// ISSUE: DVCORE-745
			// FIXED BY: Roman Pedchenko
			// DATE: 2013.10.28
			column.setScale(originalColumn.getScale());
			// END EXTENSION
		} else {
			column.setRuntimeType(datatype.getRuntimeTypeName());
		}
		column.setUUID("mmuuid:" + UUID.randomUUID()); //$NON-NLS-1$
		// EXTENSION
		// ISSUE: DVCORE-177
		// FIXED BY: Roman Pedchenko
		// DATE: 2012.12.11
		Function function = null;
		if (originalColumnAsExpression instanceof Function)
			function = (Function) originalColumnAsExpression;
		else if (originalColumnAsExpression instanceof ExpressionSymbol && ((ExpressionSymbol) originalColumnAsExpression).getExpression() instanceof Function)
			function = (Function) ((ExpressionSymbol) originalColumnAsExpression).getExpression();
		if (function != null) {
			if (FunctionLibrary.isConvert(function)) {
		        Constant constant = (Constant) function.getArg(1);
				String typeName = (String) constant.getValue();
				int colonIndex = typeName.indexOf('(');
				if (colonIndex >= 0)
					try {
						int length = Integer.parseInt(typeName.substring(colonIndex + 1, typeName.length() - 1));
						column.setPrecision(length);
						column.setLength(length);
					} catch (NumberFormatException e) { }
			}
		}
		// END EXTENSION
		column.setUpdatable(table.supportsUpdate());
		return column;		
	}
	// BUG END
	
	// this class resolves the artifacts that are dependent upon objects from other schemas
	// materialization sources, fk and data types (coming soon..)
	public static class CrossSchemaResolver implements MetadataRule {
		
		private boolean keyMatches(List<String> names, KeyRecord record) {
			if (names.size() != record.getColumns().size()) {
				return false;
			}
			for (int i = 0; i < names.size(); i++) {
				if (!names.get(i).equals(record.getColumns().get(i).getName())) {
					return false;
				}
			}
			return true;
		}
		
		@Override
		public void executeDetached(QueryMetadataInterface metadata, MetadataStore store, ValidatorReport report, MetadataValidator metadataValidator,
									Schema schema, MetadataFactory mf, ModelMetaData model) {
			if (isSystemSchema(model, schema))
				return;
			for (Table t : schema.getTables().values()) {
				if (t.isVirtual()) {
					if (t.isMaterialized() && t.getMaterializedTable() != null) {
						String matTableName = t.getMaterializedTable().getName();
						int index = matTableName.indexOf(Table.NAME_DELIM_CHAR);
						if (index == -1) {
							metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31088, matTableName, t.getName()));
						} else {
							String schemaName = matTableName.substring(0, index);
							Schema matSchema = store.getSchema(schemaName);
							if (matSchema == null) {
								metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31089, schemaName, matTableName, t.getName()));
							} else {
								Table matTable = matSchema.getTable(matTableName.substring(index + 1));
								if (matTable == null) {
									metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31090, matTableName.substring(index + 1), schemaName, t.getName()));
								} else {
									t.setMaterializedTable(matTable);
								}
							}
						}
					}
				}

				for (KeyRecord record : t.getAllKeys()) {
					if (record.getColumns() == null || record.getColumns().isEmpty()) {
						metadataValidator.log(report, model, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31149, t.getFullName(), record.getName()));
					}
				}

				List<ForeignKey> fks = t.getForeignKeys();
				if (fks == null || fks.isEmpty()) {
					continue;
				}

				setKeyReferences(fks, schema, t, store, metadataValidator, report, model);
			}
		}

		private void setKeyReferences(List<ForeignKey> fks, Schema schema, Table t, MetadataStore store,
		                                    MetadataValidator metadataValidator, ValidatorReport report, ModelMetaData model) {
			for (ForeignKey fk:fks) {
				if (fk.getReferenceKey() != null) {
					//ensure derived fields are set
					fk.setReferenceKey(fk.getReferenceKey());
					continue;
				}

				String referenceTableName = fk.getReferenceTableName();
				if (referenceTableName == null){
					String err = QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31091, t.getName());
					logError(metadataValidator, report, model, err);
					continue;
				}

				String referenceSchemaName = schema.getName();
				//TODO there is an ambiguity here because we don't properly track the name parts
				//so we have to first check for a table name that may contain .
				Table referenceTable = schema.getTable(referenceTableName);
				int index = referenceTableName.indexOf(Table.NAME_DELIM_CHAR);
				if (referenceTable == null) {
					if (index != -1) {
						referenceSchemaName = referenceTableName.substring(0, index);
						Schema referenceSchema = store.getSchema(referenceSchemaName);
						if (referenceSchema == null) {
							String err = QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31093, referenceSchemaName, t.getFullName());
							logError(metadataValidator, report, model, err);
							continue;
						}
						referenceTable = referenceSchema.getTable(referenceTableName.substring(index+1));
					}
					if (referenceTable == null) {
						String err = QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31092, t.getFullName(), referenceTableName.substring(index+1), referenceSchemaName);
						logError(metadataValidator, report, model, err);
						continue;
					}
				}

				KeyRecord uniqueKey = null;
				List<String> referenceColumns = fk.getReferenceColumns();
				if (referenceColumns == null || referenceColumns.isEmpty()) {
					if (referenceTable.getPrimaryKey() == null) {
						String err = QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31094, t.getName(), referenceTableName.substring(index+1), referenceSchemaName);
						logError(metadataValidator, report, model, err);
					}
					else {
						uniqueKey = referenceTable.getPrimaryKey();
					}

				} else {
					for (KeyRecord record : referenceTable.getUniqueKeys()) {
						if (keyMatches(referenceColumns, record)) {
							uniqueKey = record;
							break;
						}
					}
					if (uniqueKey == null && referenceTable.getPrimaryKey() != null && keyMatches(referenceColumns, referenceTable.getPrimaryKey())) {
						uniqueKey = referenceTable.getPrimaryKey();
					}
				}
				if (uniqueKey == null) {
					String err = QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31095, t.getName(), referenceTableName.substring(index+1), referenceSchemaName, referenceColumns);
					logError(metadataValidator, report, model, err);
				}
				else {
					fk.setReferenceKey(uniqueKey);
					fk.setUniqueKeyID(uniqueKey.getUUID());
				}
			}
		}

		private void logError(MetadataValidator metadataValidator, ValidatorReport report, ModelMetaData model, String err) {
			if (metadataValidator != null)
				metadataValidator.log(report, model, err);
			else
				throw new TeiidRuntimeException(err);
		}

		public static void setKeyReferences(List<ForeignKey> fks, Schema schema, Table t, MetadataStore store) {
			new CrossSchemaResolver().setKeyReferences(fks, schema, t, store, null, null, null);
		}
	}
	
	private static boolean isSystemSchema(ModelMetaData model, Schema schema) {
		return (model == null && (CoreConstants.SYSTEM_MODEL.equalsIgnoreCase(schema.getName()) 
				|| CoreConstants.SYSTEM_ADMIN_MODEL.equalsIgnoreCase(schema.getName()) 
				|| CoreConstants.ODBC_MODEL.equalsIgnoreCase(schema.getName())
				|| CoreConstants.SYSTEM_LOG_MODEL.equalsIgnoreCase(schema.getName())));
	}
	
	public static class DataTypeHolder {
		private Datatype datatype;
		private int arrayDimensions;

		public DataTypeHolder(Datatype datatype, int arrayDimensions) {
			this.datatype = datatype;
			this.arrayDimensions = arrayDimensions;
		}

		public static DataTypeHolder buildDataTypeHolder(Collection<Datatype> dataTypes, Class<?> clazz) {
			int arrayDimensions = 0;
			Class<?> clazz2 = clazz;
			while (clazz2.isArray()) {
				arrayDimensions++;
				clazz2 = clazz2.getComponentType();
			}
			for (Datatype type : dataTypes) {
				if (type.getJavaClassName().equals(clazz.getName())) {
					return new DataTypeHolder(type, arrayDimensions);
				}
			}
			if (arrayDimensions > 0) {
				Datatype foundType = null;
				for (Datatype type : dataTypes) {
					if (type.getJavaClassName().equals(clazz2.getName())) {
						foundType = type;
						break;
					}
				}
				if (foundType != null) {
					Datatype datatype = new Datatype();
					datatype.setRuntimeTypeName(foundType.getRuntimeTypeName() + StringUtil.join(Collections.nCopies(arrayDimensions, "[]"), ""));
					datatype.setName(datatype.getRuntimeTypeName());
					datatype.setJavaClassName(clazz.getCanonicalName());
					datatype.setVarietyType(Datatype.Variety.List);
					datatype.setSearchType(Column.SearchType.Unsearchable);
					return new DataTypeHolder(datatype, arrayDimensions);
				}
			}
			return null;
		}

		public Datatype getDatatype() {
			return datatype;
		}

		public int getArrayDimensions() {
			return arrayDimensions;
		}
	}

}
