/**
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.geowebcache.storage;
import static org.geowebcache.storage.blobstore.file.FilePathUtils.appendFiltered;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.filter.parameters.ParametersUtils;
import org.geowebcache.mime.MimeException;
import org.geowebcache.mime.MimeType;
import org.geowebcache.storage.blobstore.file.FilePathGenerator;
import org.geowebcache.storage.blobstore.file.FilePathUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
/**
* Upgrades a 1.3.x GWC cache directory to the 1.4.x metastore-less style.
*
*
* @author Andrea Aime - GeoSolutions
*/
public class MetastoreRemover {
private static Log log = LogFactory.getLog(org.geowebcache.storage.MetastoreRemover.class);
private DefaultStorageFinder storageFinder;
private boolean defaultLocation;
public MetastoreRemover(DefaultStorageFinder finder) throws Exception {
this.storageFinder = finder;
File root = new File(storageFinder.getDefaultPath());
Connection conn = null;
try {
conn = getMetaStoreConnection(root);
if (conn != null) {
log.info("Migrating the old metastore to filesystem storage");
SingleConnectionDataSource ds = new SingleConnectionDataSource(conn, false);
JdbcTemplate template = new JdbcTemplate(ds);
// maybe we should make this optional?
boolean migrateCreationDates = Boolean.getBoolean("MIGRATE_CREATION_DATES");
if(migrateCreationDates) {
migrateTileDates(template, new FilePathGenerator(root.getPath()));
}
migrateParameters(template, root);
// remove all the tiles from storage to avoid further migration attempts
// in the future, but only if the old metastore was external to the data dir
if (!defaultLocation) {
removeTiles(template);
}
}
} finally {
if (conn != null) {
conn.close();
}
}
// wipe out the entire database if the db location is the default one
if (defaultLocation) {
File dbFile = getDefaultH2Path(root).getParentFile();
if(dbFile.exists()) {
log.info("Cleaning up the old H2 database");
FileUtils.deleteDirectory(dbFile);
}
}
// remove disk quota if necessary (this we have to do regardless as we changed the
// structure of the params from int to string)
String path = root.getPath() + File.separator + "diskquota_page_store";
File quotaRoot = new File(path);
if(quotaRoot.exists()) {
File version = new File(quotaRoot, "version.txt");
if(!version.exists()) {
log.warn("Old style DiskQuota database found, removing it.");
FileUtils.deleteDirectory(quotaRoot);
}
}
}
/**
* Drop all the tiles to prevent a future migration
*
* @param template
*/
private void removeTiles(JdbcTemplate template) {
template.execute("delete from tiles");
}
private void migrateParameters(JdbcTemplate template, final File root) {
// find all possible combinations of layer, zoom level, gridset and parameter id
String query = "select layers.value as layer, gridsets.value as gridset, tiles.z, parameters.value as parameters, parameters_id\n"
+ "from tiles join layers on layers.id = tiles.layer_id \n"
+ " join gridsets on gridsets.id = tiles.gridset_id\n"
+ " join parameters on parameters.id = tiles.parameters_id\n"
+ "group by layer, gridset, z, parameters, parameters_id";
final long total = template.queryForObject("select count(*) from (" + query + ")", Long.class);
log.info("Migrating " + total + " parameters from the metastore to the file system");
template.query(query, new RowCallbackHandler() {
long count = 0;
public void processRow(ResultSet rs) throws SQLException {
String layer = rs.getString(1);
String gridset = rs.getString(2);
int z = rs.getInt(3);
String paramsKvp = rs.getString(4);
String paramsId = rs.getString(5);
String sha = getParamsSha1(paramsKvp);
// move the folders containing params
File origin = new File(buildFolderPath(root, layer, gridset, z, paramsId));
File destination = new File(buildFolderPath(root, layer, gridset, z, sha));
org.geowebcache.util.FileUtils.renameFile(origin, destination);
count++;
if(count % 1000 == 0 || count >= total) {
log.info("Migrated " + count + "/" + total + " parameters from the metastore to the file system");
}
}
private String buildFolderPath(final File root, String layer, String gridset, int z,
String paramsId) {
// build the old path
StringBuilder path = new StringBuilder();
path.append(root.getPath());
path.append(File.separatorChar);
appendFiltered(layer, path);
path.append(File.separatorChar);
FilePathUtils.appendGridsetZoomLevelDir(gridset, z, path);
path.append('_');
path.append(paramsId);
path.append(File.separatorChar);
return path.toString();
}
private String getParamsSha1(String paramsKvp) {
Map<String, String> params = toMap(paramsKvp);
return ParametersUtils.getId(params);
}
/**
* Parses the param list stored in the db to a parameter list (since this is coming from
* the database the assumption is that the contents are sane)
*
* @param paramsKvp
* @return
*/
private Map<String, String> toMap(String paramsKvp) {
// TODO: wondering, shall we URL decode the values??
Map<String, String> result = new HashMap<String, String>();
String[] kvps = paramsKvp.split("&");
for (String kvp : kvps) {
if (kvp != null && !"".equals(kvp)) {
String[] kv = kvp.split("=");
result.put(kv[0], kv[1]);
}
}
return result;
}
});
}
private void migrateTileDates(JdbcTemplate template, final FilePathGenerator generator) {
String query = "select layers.value as layer, gridsets.value as gridset, " +
"tiles.parameters_id, tiles.z, tiles.x, tiles.y, created, formats.value as format \n" +
"from tiles join layers on layers.id = tiles.layer_id \n" +
"join gridsets on gridsets.id = tiles.gridset_id \n" +
"join formats on formats.id = tiles.format_id \n" +
"order by layer_id, parameters_id, gridset, z, x, y";
final long total = template.queryForObject("select count(*) from (" + query + ")", Long.class);
log.info("Migrating " + total + " tile creation dates from the metastore to the file system");
template.query(query, new RowCallbackHandler() {
int count = 0;
public void processRow(ResultSet rs) throws SQLException {
// read the result set
String layer = rs.getString(1);
String gridset = rs.getString(2);
String paramsId = rs.getString(3);
long z = rs.getLong(4);
long x = rs.getLong(5);
long y = rs.getLong(6);
long created = rs.getLong(7);
String format = rs.getString(8);
// create the tile and thus the tile path
TileObject tile = TileObject.createCompleteTileObject(layer, new long[] {x,y,z}, gridset, format, null, null);
tile.setParametersId(paramsId);
try {
File file = generator.tilePath(tile, MimeType.createFromFormat(format));
// update the last modified according to the date
if(file.exists()) {
file.setLastModified(created);
}
} catch(MimeException e) {
log.error("Failed to locate mime type for format '" + format + "', this should never happen!");
}
count++;
if(count % 10000 == 0 || count >= total) {
log.info("Migrated " + count + "/" + total + " tile creation dates from the metastore to the file system");
}
}
});
}
private String getVariable(String variable, String defaultValue) {
String value = storageFinder.findEnvVar(DefaultStorageFinder.GWC_METASTORE_USERNAME);
if (value != null) {
return value;
} else {
return defaultValue;
}
}
private Connection getMetaStoreConnection(File root) throws ClassNotFoundException,
SQLException {
try {
String username = getVariable(DefaultStorageFinder.GWC_METASTORE_USERNAME, "sa");
String password = getVariable(DefaultStorageFinder.GWC_METASTORE_PASSWORD, "");
String defaultJDBCURL = getDefaultJDBCURL(root);
String jdbcString = getVariable(DefaultStorageFinder.GWC_METASTORE_JDBC_URL,
defaultJDBCURL);
String driver = getVariable(DefaultStorageFinder.GWC_METASTORE_DRIVER_CLASS,
"org.h2.Driver");
if (defaultJDBCURL.equals(jdbcString)) {
defaultLocation = true;
}
// load the driver
Class.forName(driver);
// if we are going against a H2 metastore open it only if it exists,
// otherwise the only way to know if the metastore is still there
// is to actually try to open it
if ("org.h2.Driver".equals(driver) && jdbcString.equals(defaultJDBCURL)) {
File dbPath = getDefaultH2Path(root);
if (!dbPath.exists()) {
return null;
}
}
// grab the connection
return DriverManager.getConnection(jdbcString, username, password);
} catch (ClassNotFoundException e) {
log.warn("Could not find the metastore driver, skipping migration", e);
return null;
} catch (SQLException e) {
log.warn("Failed to connect to the legacy metastore, skipping migration", e);
return null;
}
}
private File getDefaultH2Path(File root) {
String path = root.getPath() + File.separator + "meta_jdbc_h2";
File dbPath = new File(path + File.separator + "gwc_metastore.data.db");
return dbPath;
}
private String getDefaultJDBCURL(File root) {
String path = root.getPath() + File.separator + "meta_jdbc_h2";
String jdbcString = "jdbc:h2:file:" + path + File.separator + "gwc_metastore"
+ ";TRACE_LEVEL_FILE=0;AUTO_SERVER=TRUE";
return jdbcString;
}
}