View Javadoc
1   package net.sumaris.core.dao.technical.schema;
2   
3   /*-
4    * #%L
5    * SUMARiS:: Core
6    * %%
7    * Copyright (C) 2018 SUMARiS Consortium
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   * 
14   * This program 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
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
23   */
24  
25  
26  import com.google.common.base.Preconditions;
27  import com.google.common.collect.Maps;
28  import com.google.common.collect.Sets;
29  import net.sumaris.core.config.SumarisConfiguration;
30  import net.sumaris.core.dao.cache.CacheNames;
31  import org.apache.commons.lang3.StringUtils;
32  import org.hibernate.HibernateException;
33  import org.hibernate.boot.Metadata;
34  import org.hibernate.boot.model.naming.Identifier;
35  import org.hibernate.boot.model.relational.QualifiedTableName;
36  import org.hibernate.dialect.Dialect;
37  import org.hibernate.mapping.Column;
38  import org.hibernate.mapping.PersistentClass;
39  import org.hibernate.mapping.Property;
40  import org.hibernate.mapping.Table;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  import org.springframework.beans.factory.BeanInitializationException;
44  import org.springframework.beans.factory.annotation.Autowired;
45  import org.springframework.cache.annotation.Cacheable;
46  import org.springframework.context.annotation.Lazy;
47  import org.springframework.jdbc.datasource.DataSourceUtils;
48  import org.springframework.stereotype.Component;
49  
50  import javax.annotation.PostConstruct;
51  import javax.sql.DataSource;
52  import java.sql.*;
53  import java.util.HashSet;
54  import java.util.Iterator;
55  import java.util.Map;
56  import java.util.Set;
57  /**
58   * Sumaris database metadatas.
59   * 
60   * @author Benoit Lavenier <benoit.lavenier@e-is.pro>
61   * @since 1.0
62   */
63  @Lazy
64  @Component(value = "sumarisDatabaseMetadata")
65  public class SumarisDatabaseMetadata {
66  
67  	/** Logger. */
68  	private static final Logger log =
69  			LoggerFactory.getLogger(SumarisDatabaseMetadata.class);
70  
71  	@Autowired
72  	protected SumarisDatabaseMetadata databaseMetadata;
73  
74  	protected final Map<String, SumarisTableMetadata> tables;
75  	protected final Map<String, PersistentClass> entities;
76  
77  	protected String defaultSchemaName = null;
78  	protected String defaultCatalogName = null;
79  	protected Dialect dialect = null;
80  
81  	protected DataSource dataSource = null;
82  
83  	protected final Metadata metadata;
84  
85  	protected Set<String> sequences;
86  
87  	protected String sequenceSuffix;
88  
89  	protected String defaultUpdateDateColumnName;
90  
91  	@Autowired
92  	public SumarisDatabaseMetadata(DataSource dataSource, SumarisConfiguration configuration) {
93  		super();
94  		Preconditions.checkNotNull(dataSource);
95  		Preconditions.checkNotNull(configuration);
96  
97  		this.dataSource = dataSource;
98  		this.metadata = MetadataExtractorIntegrator.INSTANCE.getMetadata();
99  		this.dialect = metadata.getDatabase().getDialect();
100 
101 		this.defaultSchemaName = configuration.getJdbcSchema();
102 		this.defaultCatalogName = configuration.getJdbcCatalog();
103 		this.sequenceSuffix = configuration.getSequenceSuffix();
104 
105 		this.defaultUpdateDateColumnName = "update_date"; // TODO: load it from configuration
106 
107 		tables = Maps.newTreeMap();
108 		entities = Maps.newHashMap();
109 
110 		//loadAllTables();
111 	}
112 
113 	@Cacheable(cacheNames = CacheNames.TABLE_META_BY_NAME, key = "#name.toLowerCase()", unless = "#result == null")
114 	public SumarisTableMetadata getTable(String name) throws HibernateException {
115 		return getTable(name, defaultSchemaName, defaultCatalogName);
116 	}
117 
118 	@Cacheable(cacheNames = CacheNames.TABLE_META_BY_NAME, key = "#name.toLowerCase()", unless = "#result == null")
119 	public SumarisHibernateTableMetadata getHibernateTable(String name) throws HibernateException {
120 		return (SumarisHibernateTableMetadata) getTable(name);
121 	}
122 
123 	public int getTableCount() {
124 		return tables.size();
125 	}
126 
127 	public Set<String> getTableNames() {
128 		HashSet<String> result = Sets.newHashSet();
129 		for (SumarisTableMetadata tableMetadata : tables.values()) {
130 			result.add(tableMetadata.getName());
131 		}
132 		return result;
133 	}
134 
135 	public Set<String> getSequences() {
136 		return sequences;
137 	}
138 
139 	public String getSequenceSuffix() {
140 		return sequenceSuffix;
141 	}
142 
143 	public Dialect getDialect() {
144 		return dialect;
145 	}
146 
147 	public QualifiedTableName getQualifiedTableName(String catalog, String schema, String tableName) {
148 		return new QualifiedTableName(
149 				Identifier.toIdentifier(catalog),
150 				Identifier.toIdentifier(schema),
151 				Identifier.toIdentifier(tableName));
152 	}
153 
154 	public String getDefaultUpdateDateColumnName() {
155 		return defaultUpdateDateColumnName;
156 	}
157 
158 	/* -- protected methods -- */
159 
160 	@PostConstruct
161 	protected void init() {
162 
163 		Connection conn = null;
164 		try {
165 			conn = DataSourceUtils.getConnection(dataSource);
166 
167 			// Init sequences
168 			this.sequences = initSequences(conn, dialect);
169 
170 			// Init tables
171 			initTables(conn);
172 
173 		}
174 		catch(SQLException e) {
175 			throw new BeanInitializationException("Could not init SumarisDatabaseMetadata", e);
176 		}
177 		finally {
178 			DataSourceUtils.releaseConnection(conn, dataSource);
179 		}
180 	}
181 
182 	/**
183 	 * <p>initSequences.</p>
184 	 *
185 	 * @param connection a {@link java.sql.Connection} object.
186 	 * @param dialect a {@link org.hibernate.dialect.Dialect} object.
187 	 * @return a {@link java.util.Set} object.
188 	 * @throws java.sql.SQLException if any.
189 	 */
190 	protected Set<String> initSequences(Connection connection, Dialect dialect)
191 			throws SQLException {
192 		Set<String> sequences = Sets.newHashSet();
193 		if (dialect.supportsSequences()) {
194 			String sql = dialect.getQuerySequencesString();
195 			if (sql != null) {
196 
197 				Statement statement = null;
198 				ResultSet rs = null;
199 				try {
200 					statement = connection.createStatement();
201 					rs = statement.executeQuery(sql);
202 
203 					while (rs.next()) {
204 						sequences.add(StringUtils.lowerCase(rs.getString(1))
205 								.trim());
206 					}
207 				} finally {
208 					rs.close();
209 					statement.close();
210 				}
211 
212 			}
213 		}
214 		return sequences;
215 	}
216 
217 	protected void initTables(Connection conn) {
218 		Map<String, PersistentClass> persistentClassMap = Maps.newHashMap();
219 		for (PersistentClass persistentClass: metadata.getEntityBindings()) {
220 
221 			Table table = persistentClass.getTable();
222 
223 			log.debug( String.format("Entity: %s is mapped to table: %s",
224 					persistentClass.getClassName(),
225 					table.getName()));
226 
227 			String catalog = StringUtils.isBlank(table.getCatalog()) ? defaultCatalogName : table.getCatalog();
228 			String schema = StringUtils.isBlank(table.getSchema()) ? defaultSchemaName : table.getSchema();
229 			String qualifiedTableName = getQualifiedTableName(catalog, schema, table.getName()).render().toLowerCase();
230 			persistentClassMap.put(qualifiedTableName, persistentClass);
231 
232 			if (log.isDebugEnabled()) {
233 				for (Iterator propertyIterator = persistentClass.getPropertyIterator();
234 					 propertyIterator.hasNext(); ) {
235 					Property property = (Property) propertyIterator.next();
236 
237 					for (Iterator columnIterator = property.getColumnIterator();
238 						 columnIterator.hasNext(); ) {
239 						Column column = (Column) columnIterator.next();
240 
241 						log.debug(String.format("Property: %s is mapped on table column: %s of type: %s",
242 								property.getName(),
243 								column.getName(),
244 								column.getSqlType())
245 						);
246 					}
247 				}
248 			}
249 		}
250 
251 		try {
252 			DatabaseMetaData jdbcMeta = conn.getMetaData();
253 
254 			// Init tables
255 			for (DatabaseTableEnum table : DatabaseTableEnum.values()) {
256 				String tableName = table.name().toLowerCase();
257 				if (log.isDebugEnabled()) {
258 					log.debug("Load metas of table: " + tableName);
259 				}
260 				String qualifiedTableName = getQualifiedTableName(defaultCatalogName, defaultSchemaName, tableName).render().toLowerCase();
261 				PersistentClass persistentClass = persistentClassMap.get(qualifiedTableName);
262 				entities.put(qualifiedTableName, persistentClass);
263 
264 				getTable(tableName, defaultSchemaName, defaultCatalogName, jdbcMeta, persistentClass);
265 			}
266 		}
267 		catch (SQLException e) {
268 			throw new BeanInitializationException(
269 					"Could not init database meta on connection " + conn, e);
270 		}
271 		finally {
272 			DataSourceUtils.releaseConnection(conn, dataSource);
273 		}
274 	}
275 
276 	protected SumarisTableMetadata getTable(QualifiedTableName qualifiedTableName,
277 											DatabaseMetaData jdbcMeta,
278 											PersistentClass persistentClass) throws HibernateException, SQLException {
279 		Preconditions.checkNotNull(qualifiedTableName);
280 		Preconditions.checkNotNull(jdbcMeta);
281 
282 		String fullTableName = qualifiedTableName.render().toLowerCase();
283 		SumarisTableMetadata sumarisTableMetadata = tables.get(fullTableName);
284 		if (sumarisTableMetadata == null) {
285 			// Try to retrieve the persistence class
286 			if (persistentClass == null) {
287 				persistentClass = entities.get(fullTableName);
288 			}
289 
290 			// If persistence class exists
291 			if (persistentClass != null) {
292 				// Get the table mapping
293 				Table table = persistentClass.getTable();
294 				table.setCatalog(qualifiedTableName.getCatalogName() != null ? qualifiedTableName.getCatalogName().getText() : null);
295 				table.setSchema(qualifiedTableName.getSchemaName() != null ? qualifiedTableName.getSchemaName().getText() : null);
296 				sumarisTableMetadata = new SumarisHibernateTableMetadata(table, this, jdbcMeta, persistentClass);
297 			}
298 
299 			// No persistence class: load as JDBC table
300 			else {
301 				sumarisTableMetadata = new SumarisTableMetadata(qualifiedTableName, this, jdbcMeta);
302 			}
303 
304 			// Add to cached (if not extraction)
305 			// TODO: use include/exclude pattern, by configuration
306 			String tableName = qualifiedTableName.getTableName().getText().toLowerCase();
307 			if (!tableName.startsWith("ext_") && !tableName.startsWith("agg_"))  {
308 				tables.put(fullTableName, sumarisTableMetadata);
309 			}
310 		}
311 		return sumarisTableMetadata;
312 	}
313 
314 	protected SumarisTableMetadata getTable(String name,
315 											String schema,
316 											String catalog,
317 											DatabaseMetaData jdbcMeta,
318 											PersistentClass persistentClass) throws HibernateException, SQLException {
319 		return getTable(getQualifiedTableName(catalog, schema, name), jdbcMeta, persistentClass);
320 	}
321 
322 	public SumarisTableMetadata getTable(String name,
323 											String schema,
324 											String catalog) throws HibernateException {
325 		QualifiedTableName qualifiedTableName = getQualifiedTableName(catalog, schema, name);
326 		SumarisTableMetadata sumarisTableMetadata = tables.get(qualifiedTableName.render().toLowerCase());
327 		if (sumarisTableMetadata == null) {
328 			// Create a new connection then retrieve the metadata :
329 			Connection conn = null;
330 			try {
331 				conn = DataSourceUtils.getConnection(dataSource);
332 				DatabaseMetaData jdbcMeta = conn.getMetaData();
333 				return getTable(qualifiedTableName, jdbcMeta, null);
334 			} catch (SQLException e) {
335 				throw new RuntimeException(
336 						"Could not init database meta on connection " + conn, e);
337 			} finally {
338 				DataSourceUtils.releaseConnection(conn, dataSource);
339 			}
340 
341 		}
342 		return sumarisTableMetadata;
343 	}
344 }