content: /*****************************************************************
   *   Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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
   *
   *    https://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 org.apache.cayenne.unit.di.runtime;
  
  import org.apache.cayenne.access.DataDomain;
  import org.apache.cayenne.access.DataNode;
  import org.apache.cayenne.access.DbGenerator;
  import org.apache.cayenne.access.dbsync.SkipSchemaUpdateStrategy;
  import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory;
  import org.apache.cayenne.access.translator.batch.DefaultBatchTranslatorFactory;
  import org.apache.cayenne.access.translator.select.DefaultSelectTranslatorFactory;
  import org.apache.cayenne.ashwood.AshwoodEntitySorter;
  import org.apache.cayenne.cache.MapQueryCache;
  import org.apache.cayenne.configuration.DataMapLoader;
  import org.apache.cayenne.dba.DbAdapter;
  import org.apache.cayenne.di.Inject;
  import org.apache.cayenne.event.DefaultEventManager;
  import org.apache.cayenne.log.JdbcEventLogger;
  import org.apache.cayenne.map.DataMap;
  import org.apache.cayenne.map.DbAttribute;
  import org.apache.cayenne.map.DbEntity;
  import org.apache.cayenne.map.Procedure;
  import org.apache.cayenne.resource.URLResource;
  import org.apache.cayenne.testdo.extended_type.StringET1ExtendedType;
  import org.apache.cayenne.unit.UnitDbAdapter;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.net.URL;
  import java.sql.Connection;
  import java.sql.DatabaseMetaData;
  import java.sql.ResultSet;
  import java.sql.SQLException;
  import java.sql.Statement;
  import java.sql.Types;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  
  /**
   * Default implementation of the AccessStack that has a single DataNode per DataMap.
   */
  public class SchemaBuilder {
  
  	private static final Logger logger = LoggerFactory.getLogger(SchemaBuilder.class);
  
  	public static final String SKIP_SCHEMA_KEY = "cayenneTestSkipSchemaCreation";
  
  	private static final String[] MAPS_REQUIRING_SCHEMA_SETUP = { "testmap.map.xml", "compound.map.xml",
  			"misc-types.map.xml", "things.map.xml", "numeric-types.map.xml", "binary-pk.map.xml", "no-pk.map.xml",
  			"lob.map.xml", "date-time.map.xml", "enum.map.xml", "json.map.xml", "extended-type.map.xml",
  			"generated.map.xml", "mixed-persistence-strategy.map.xml", "people.map.xml", "primitive.map.xml",
  			"inheritance.map.xml", "locking.map.xml", "soft-delete.map.xml", "empty.map.xml", "relationships.map.xml",
  			"relationships-activity.map.xml", "relationships-delete-rules.map.xml",
  			"relationships-collection-to-many.map.xml", "relationships-child-master.map.xml",
  			"relationships-clob.map.xml", "relationships-flattened.map.xml", "relationships-many-to-many-join.map.xml",
  			"relationships-set-to-many.map.xml", "relationships-to-many-fk.map.xml", "relationships-to-one-fk.map.xml",
  			"return-types.map.xml", "uuid.map.xml", "multi-tier.map.xml", "reflexive.map.xml", "delete-rules.map.xml",
              "lifecycle-callbacks-order.map.xml", "lifecycles.map.xml", "map-to-many.map.xml", "toone.map.xml",
              "meaningful-pk.map.xml", "table-primitives.map.xml", "generic.map.xml", "map-db1.map.xml",
              "map-db2.map.xml", "embeddable.map.xml", "qualified.map.xml", "quoted-identifiers.map.xml",
              "inheritance-single-table1.map.xml", "inheritance-vertical.map.xml", "oneway-rels.map.xml",
              "unsupported-distinct-types.map.xml", "array-type.map.xml", "cay-2032.map.xml",
              "weighted-sort.map.xml", "hybrid-data-object.map.xml", "legacy-date-time.map.xml", "inheritance-with-enum.map.xml",
              "lazy-attributes.map.xml", "cay2666/datamap.map.xml", "cay2641/datamapLazy.map.xml",
              "annotation/datamapAnnotation.map.xml" };
  
  	// hardcoded dependent entities that should be excluded
  	// if LOBs are not supported
  	private static final Set<String> EXTRA_EXCLUDED_FOR_NO_LOB = Set.of("CLOB_DETAIL");
  
  	private static final Set<String> EXTRA_EXCLUDED_FOR_NO_NATIVE_JSON = Set.of("JSON_OTHER");
  
  	private RuntimeCaseDataSourceFactory dataSourceFactory;
  	private UnitDbAdapter unitDbAdapter;
  	private DbAdapter dbAdapter;
  	private DataDomain domain;
  	private JdbcEventLogger jdbcEventLogger;
  
  	@Inject
  	DataMapLoader loader;
  
  	public SchemaBuilder(@Inject RuntimeCaseDataSourceFactory dataSourceFactory, @Inject UnitDbAdapter unitDbAdapter,
                           @Inject DbAdapter dbAdapter, @Inject JdbcEventLogger jdbcEventLogger) {
  		this.dataSourceFactory = dataSourceFactory;
  		this.unitDbAdapter = unitDbAdapter;
  		this.dbAdapter = dbAdapter;
  		this.jdbcEventLogger = jdbcEventLogger;
  	}
  
  	/**
  	 * Completely rebuilds test schema.
  	 */
  	// TODO - this method changes the internal state of the object ... refactor
  	public void rebuildSchema() {
  
  		// generate schema combining all DataMaps that require schema support.
  		// Schema generation is done like that instead of per DataMap on demand
  		// to avoid conflicts when dropping and generating PK objects.
  
  		DataMap[] maps = new DataMap[MAPS_REQUIRING_SCHEMA_SETUP.length];
  
  		for (int i = 0; i < maps.length; i++) {
  			URL mapURL = getClass().getClassLoader().getResource(MAPS_REQUIRING_SCHEMA_SETUP[i]);
  			maps[i] = loader.load(new URLResource(mapURL));
  		}
  
  		this.domain = new DataDomain("temp");
  		domain.setEventManager(new DefaultEventManager(2));
  		domain.setEntitySorter(new AshwoodEntitySorter());
  		domain.setQueryCache(new MapQueryCache(50));
  
  		try {
  			for (DataMap map : maps) {
  				initNode(map);
  			}
  
  			if ("true".equalsIgnoreCase(System.getProperty(SKIP_SCHEMA_KEY))) {
  				logger.info("skipping schema generation... ");
  			} else {
  				dropSchema();
  				dropPKSupport();
  				createSchema();
  				createPKSupport();
  			}
  		} catch (Exception e) {
  			throw new RuntimeException("Error rebuilding schema", e);
  		}
  	}
  
  	private void initNode(DataMap map) {
  
  		DataNode node = new DataNode(map.getName());
  		node.setJdbcEventLogger(jdbcEventLogger);
  		node.setAdapter(dbAdapter);
  		node.setDataSource(dataSourceFactory.getSharedDataSource());
  
  		// setup test extended types
  		node.getAdapter().getExtendedTypes().registerType(new StringET1ExtendedType());
  
  		// tweak mapping with a delegate
  		for (Procedure proc : map.getProcedures()) {
  			unitDbAdapter.tweakProcedure(proc);
  		}
  		filterDataMap(map);
  
  		node.addDataMap(map);
  
  		node.setSchemaUpdateStrategy(new SkipSchemaUpdateStrategy());
  		node.setRowReaderFactory(new DefaultRowReaderFactory());
  		node.setBatchTranslatorFactory(new DefaultBatchTranslatorFactory());
  		node.setSelectTranslatorFactory(new DefaultSelectTranslatorFactory());
  		domain.addNode(node);
  	}
  
  	/**
  	 * Remote binary pk {@link DbEntity} for {@link DbAdapter} not supporting
  	 * that and so on.
  	 */
  	protected void filterDataMap(DataMap map) {
  		boolean supportsBinaryPK = unitDbAdapter.supportsBinaryPK();
  
  		if (supportsBinaryPK) {
  			return;
  		}
  
  		List<DbEntity> entitiesToRemove = new ArrayList<>();
  
  		for (DbEntity ent : map.getDbEntities()) {
  			for (DbAttribute attr : ent.getAttributes()) {
  				// check for BIN PK or FK to BIN Pk
  				if (attr.getType() == Types.BINARY || attr.getType() == Types.VARBINARY
  						|| attr.getType() == Types.LONGVARBINARY) {
  					if (attr.isPrimaryKey() || attr.isForeignKey()) {
  						entitiesToRemove.add(ent);
  						break;
  					}
  				}
  			}
  		}
  
  		for (DbEntity e : entitiesToRemove) {
  			map.removeDbEntity(e.getName(), true);
  		}
  	}
  
  	/** Drops all test tables. */
  	private void dropSchema() throws Exception {
  		for (DataNode node : domain.getDataNodes()) {
  			dropSchema(node, node.getDataMaps().iterator().next());
  		}
  	}
  
  	/**
  	 * Creates all test tables in the database.
  	 */
  	private void createSchema() throws Exception {
  		for (DataNode node : domain.getDataNodes()) {
  			createSchema(node, node.getDataMaps().iterator().next());
  		}
  	}
  
  	public void dropPKSupport() throws Exception {
  		for (DataNode node : domain.getDataNodes()) {
  			dropPKSupport(node, node.getDataMaps().iterator().next());
  		}
  	}
  
  	/**
  	 * Creates primary key support for all node DbEntities. Will use its
  	 * facilities provided by DbAdapter to generate any necessary database
  	 * objects and data for primary key support.
  	 */
  	public void createPKSupport() throws Exception {
  		for (DataNode node : domain.getDataNodes()) {
  			createPKSupport(node, node.getDataMaps().iterator().next());
  		}
  	}
  
  	/**
  	 * Helper method that orders DbEntities to satisfy referential constraints
  	 * and returns an ordered list.
  	 */
  	private List<DbEntity> dbEntitiesInInsertOrder(DataMap map) {
  		TreeMap<String, DbEntity> dbEntityMap = new TreeMap<>(map.getDbEntityMap());
  		List<DbEntity> entities = new ArrayList<>(dbEntityMap.values());
  		List<DbEntity> excludedEntities = excludeEntities(entities);
  		entities.removeAll(excludedEntities);
  
  		domain.getEntitySorter().sortDbEntities(entities, false);
  		return entities;
  	}
  
  	protected List<DbEntity> dbEntitiesInDeleteOrder(DataMap dataMap) {
  		DataMap map = domain.getDataMap(dataMap.getName());
  		Map<String, DbEntity> dbEntityMap = new TreeMap<>(map.getDbEntityMap());
  		List<DbEntity> entities = new ArrayList<>(dbEntityMap.values());
  		List<DbEntity> excludedEntities = excludeEntities(entities);
  		entities.removeAll(excludedEntities);
  
  		domain.getEntitySorter().sortDbEntities(entities, true);
  		return entities;
  	}
  
  	private List<DbEntity> excludeEntities(Collection<DbEntity> entities) {
  		// exclude various unsupported tests...
  
  		boolean excludeLOB = !unitDbAdapter.supportsLobs();
  		boolean excludeNativeJson = !unitDbAdapter.supportsJsonType();
  		boolean excludeBinaryPK = !unitDbAdapter.supportsBinaryPK();
  		if (!excludeLOB && !excludeNativeJson && !excludeBinaryPK) {
  			return Collections.emptyList();
  		}
  
  		List<DbEntity> excludedEntities = new ArrayList<>();
  		for (DbEntity entity : entities) {
  
  			// check for LOB attributes
  			if (excludeLOB) {
  				if (EXTRA_EXCLUDED_FOR_NO_LOB.contains(entity.getName())) {
  					excludedEntities.add(entity);
  					continue;
  				}
  				Set<Integer> lobTypes = Set.of(Types.BLOB, Types.CLOB, Types.NCLOB);
  				boolean hasLob = entity.getAttributes().stream()
  						.map(DbAttribute::getType)
  						.anyMatch(lobTypes::contains);
  				if (hasLob) {
  					excludedEntities.add(entity);
  					continue;
  				}
  			}
  
  			// check for native json type
  			if (excludeNativeJson) {
  				if (EXTRA_EXCLUDED_FOR_NO_NATIVE_JSON.contains(entity.getName())) {
  					excludedEntities.add(entity);
  					continue;
  				}
  			}
  
  			// check for BIN PK
  			if (excludeBinaryPK) {
  				Set<Integer> binaryTypes = Set.of(Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY);
  				boolean hasBinaryPK = entity.getAttributes().stream()
  						.filter(attribute -> attribute.isPrimaryKey() || attribute.isForeignKey())
  						.map(DbAttribute::getType)
  						.anyMatch(binaryTypes::contains);
  				if (hasBinaryPK) {
  					excludedEntities.add(entity);
  				}
  			}
  		}
  		return excludedEntities;
  	}
  
  	private void dropSchema(DataNode node, DataMap map) throws Exception {
  
  		List<DbEntity> list = dbEntitiesInInsertOrder(map);
  
  		try (Connection conn = dataSourceFactory.getSharedDataSource().getConnection()) {
  
  			DatabaseMetaData md = conn.getMetaData();
  			List<String> allTables = new ArrayList<>();
  
  			try (ResultSet tables = md.getTables(null, null, "%", null)) {
  				while (tables.next()) {
  					// 'toUpperCase' is needed since most databases are case insensitive,
  					// and some will convert names to lower case (e.g. PostgreSQL)
  					String name = tables.getString("TABLE_NAME");
  					if (name != null) {
  						allTables.add(name.toUpperCase());
  					}
  				}
  			}
  
  			unitDbAdapter.willDropTables(conn, map, allTables);
  
  			// drop all tables in the map
  			try (Statement stmt = conn.createStatement()) {
  
  				ListIterator<DbEntity> it = list.listIterator(list.size());
  				while (it.hasPrevious()) {
  					DbEntity ent = it.previous();
  					if (!allTables.contains(ent.getName().toUpperCase())) {
  						continue;
  					}
  
  					for (String dropSql : node.getAdapter().dropTableStatements(ent)) {
  						try {
  							logger.info(dropSql);
  							stmt.execute(dropSql);
  						} catch (SQLException sqe) {
  							logger.warn("Can't drop table " + ent.getName() + ", ignoring...", sqe);
  						}
  					}
  				}
  			}
  
  			unitDbAdapter.droppedTables(conn, map);
  		}
  	}
  
  	private void dropPKSupport(DataNode node, DataMap map) throws Exception {
  		List<DbEntity> filteredEntities = dbEntitiesInInsertOrder(map);
  		node.getAdapter().getPkGenerator().dropAutoPk(node, filteredEntities);
  	}
  
  	private void createPKSupport(DataNode node, DataMap map) throws Exception {
  		List<DbEntity> filteredEntities = dbEntitiesInInsertOrder(map);
  		node.getAdapter().getPkGenerator().createAutoPk(node, filteredEntities);
  	}
  
  	private void createSchema(DataNode node, DataMap map) throws Exception {
  
  		try (Connection conn = dataSourceFactory.getSharedDataSource().getConnection()) {
  			unitDbAdapter.willCreateTables(conn, map);
  			try (Statement stmt = conn.createStatement()) {
  
  				for (String query : tableCreateQueries(node, map)) {
  					logger.info(query);
  					stmt.execute(query);
  				}
  			}
  			unitDbAdapter.createdTables(conn, map);
  		}
  	}
  
  	/**
  	 * Returns iterator of preprocessed table create queries.
  	 */
  	private Collection<String> tableCreateQueries(DataNode node, DataMap map) {
  		DbAdapter adapter = node.getAdapter();
  
  		List<DbEntity> orderedEntities = dbEntitiesInInsertOrder(map);
  		List<DbEntity> excludedEntities = excludeEntities(map.getDbEntities());
  		DbGenerator gen = new DbGenerator(adapter, map, excludedEntities, domain, jdbcEventLogger);
  		List<String> queries = new ArrayList<>();
  
  		// table definitions
  		for (DbEntity ent : orderedEntities) {
  			queries.add(adapter.createTable(ent));
  		}
  
  		// FK constraints
  		for (DbEntity ent : orderedEntities) {
  			if (!unitDbAdapter.supportsFKConstraints(ent)) {
  				continue;
  			}
  
  			List<String> qs = gen.createConstraintsQueries(ent);
  			queries.addAll(qs);
  		}
  
  		return queries;
  	}
  }
  