/** * 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. * <p> * 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. * <p> * 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/>. * * @author Nuno Oliveira, GeoSolutions S.A.S., Copyright 2016 */ package org.geowebcache.sqlite; import org.apache.commons.io.FileUtils; import org.geowebcache.config.BlobStoreConfig; import org.geowebcache.config.XMLConfiguration; import org.geowebcache.sqlite.Utils.Tuple; import org.geowebcache.storage.BlobStore; import org.geowebcache.storage.TileObject; import org.junit.After; import org.junit.AfterClass; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import static org.geowebcache.sqlite.Utils.Tuple.tuple; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebAppConfiguration @ContextConfiguration(classes = OperationsRestWebConfig.class) @RunWith(SpringJUnit4ClassRunner.class) @ActiveProfiles("test") public class OperationsRestTest extends TestSupport { @Autowired private WebApplicationContext webApplicationContext; @After public void afterClass() throws Exception { closeSqliteStoresConnections(); super.afterTest(); FileUtils.deleteQuietly(OperationsRestWebConfig.ROOT_DIRECTORY); } @Test public void testMultipleFilesUploadReplace() throws Exception { // creates some database files for the tests Tuple<File, Tuple<String, String>> testFiles = createTestFiles(); File rootDirectory = testFiles.first; try (FileInputStream fileA = new FileInputStream(new File(rootDirectory, testFiles.second.first)); FileInputStream fileB = new FileInputStream(new File(rootDirectory, testFiles.second.second))) { // perform the rest request MockMultipartFile fileUploadA = new MockMultipartFile("file", fileA); MockMultipartFile fileUploadB = new MockMultipartFile("file", fileB); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); // first request mockMvc.perform(MockMvcRequestBuilders.fileUpload("/sqlite/replace") .file(fileUploadA) .param("layer", "europe") .param("destination", testFiles.second.first)) .andExpect(status().is(200)); // second request mockMvc.perform(MockMvcRequestBuilders.fileUpload("/sqlite/replace") .file(fileUploadB) .param("layer", "europe") .param("destination", testFiles.second.second)) .andExpect(status().is(200)); // check that files were replaced checkThatStoreContainsReplacedTiles(testFiles.second.first, testFiles.second.second); } } @Test public void testZipFileUploadReplace() throws Exception { // creates some database files for the tests Tuple<File, Tuple<String, String>> testFiles = createTestFiles(); File rootDirectory = testFiles.first; // zip store files File tempDirectory = Files.createTempDirectory("gwc-").toFile(); addFilesToDelete(tempDirectory); File zipFile = new File(tempDirectory, "replace.zip"); zipDirectory(Paths.get(rootDirectory.getPath()), zipFile); try (FileInputStream zipFileInputStream = new FileInputStream(zipFile)) { // perform the rest request MockMultipartFile zipUpload = new MockMultipartFile("file", zipFileInputStream); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); // execute request mockMvc.perform(MockMvcRequestBuilders.fileUpload("/sqlite/replace") .file(zipUpload) .param("layer", "europe")) .andExpect(status().is(200)); // check that files were replaced checkThatStoreContainsReplacedTiles(testFiles.second.first, testFiles.second.second); } } @Test public void testLocalDirectoryReplace() throws Exception { // creates some database files for the tests Tuple<File, Tuple<String, String>> testFiles = createTestFiles(); File rootDirectory = testFiles.first; // perform the rest request MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); // execute request mockMvc.perform(MockMvcRequestBuilders.fileUpload("/sqlite/replace") .param("layer", "europe") .param("source", rootDirectory.getPath())) .andExpect(status().is(200)); // check that files were replaced checkThatStoreContainsReplacedTiles(testFiles.second.first, testFiles.second.second); } private Tuple<File, Tuple<String, String>> createTestFiles() throws Exception { // instantiating the stores File rootDirectoryA = OperationsRestWebConfig.ROOT_DIRECTORY; addFilesToDelete(rootDirectoryA); FileUtils.deleteQuietly(OperationsRestWebConfig.ROOT_DIRECTORY); OperationsRestWebConfig.ROOT_DIRECTORY.mkdirs(); File rootDirectoryB = Files.createTempDirectory("gwc-").toFile(); addFilesToDelete(rootDirectoryB); MbtilesConfiguration configurationA = new MbtilesConfiguration(); configurationA.setRootDirectory(rootDirectoryA.getPath()); configurationA.setTemplatePath(Utils.buildPath("{grid}", "{layer}", "{format}", "{z}", "tiles-{x}-{y}.sqlite")); MbtilesConfiguration configurationB = new MbtilesConfiguration(); configurationB.setRootDirectory(rootDirectoryB.getPath()); configurationB.setTemplatePath(Utils.buildPath("{grid}", "{layer}", "{format}", "{z}", "tiles-{x}-{y}.sqlite")); MbtilesBlobStore storeA = new MbtilesBlobStore(configurationA); MbtilesBlobStore storeB = new MbtilesBlobStore(configurationB); addStoresToClean(storeA, storeB); // create the tiles that will be stored TileObject putTileA = TileObject.createCompleteTileObject("africa", new long[]{10, 50, 5}, "EPSG:4326", "image/png", null, TestSupport.stringToResource("IMAGE-10-50-5-A")); TileObject putTileB = TileObject.createCompleteTileObject("africa", new long[]{10, 50, 5}, "EPSG:4326", "image/png", null, TestSupport.stringToResource("IMAGE-10-50-5-B")); TileObject putTileC = TileObject.createCompleteTileObject("africa", new long[]{10, 5050, 15}, "EPSG:4326", "image/png", null, TestSupport.stringToResource("IMAGE-15-5050-5-B")); // storing the tile storeA.put(putTileA); storeB.put(putTileB); storeB.put(putTileC); // make sure no sqlite file is in use to allow the move operation storeA.clear(); storeB.clear(); // return relative paths String relativePathA = Utils.buildPath("EPSG_4326", "africa", "image_png", "5", "tiles-0-0.sqlite"); String relativePathB = Utils.buildPath("EPSG_4326", "africa", "image_png", "15", "tiles-0-5000.sqlite"); return tuple(rootDirectoryB, tuple(relativePathA, relativePathB)); } private void checkThatStoreContainsReplacedTiles(String relativePathA, String relativePathB) throws Exception { // instantiating the store File rootDirectory = OperationsRestWebConfig.ROOT_DIRECTORY; MbtilesConfiguration configuration = new MbtilesConfiguration(); configuration.setRootDirectory(rootDirectory.getPath()); configuration.setTemplatePath(Utils.buildPath("{grid}", "{layer}", "{format}", "{z}", "tiles-{x}-{y}.sqlite")); MbtilesBlobStore store = new MbtilesBlobStore(configuration); addStoresToClean(store); // checking that all the files are present File fileA = new File(OperationsRestWebConfig.ROOT_DIRECTORY, relativePathA); File fileB = new File(OperationsRestWebConfig.ROOT_DIRECTORY, relativePathB); assertThat(fileA.exists(), is(true)); assertThat(fileB.exists(), is(true)); // let's query the store to see if we get the replaced tiles TileObject getTile = TileObject.createQueryTileObject("africa", new long[]{10, 50, 5}, "EPSG:4326", "image/png", null); assertThat(store.get(getTile), is(true)); assertThat(getTile.getBlob(), notNullValue()); assertThat(TestSupport.resourceToString(getTile.getBlob()), is("IMAGE-10-50-5-B")); // let's query the second tile getTile = TileObject.createQueryTileObject("africa", new long[]{10, 5050, 15}, "EPSG:4326", "image/png", null); assertThat(store.get(getTile), is(true)); assertThat(getTile.getBlob(), notNullValue()); assertThat(TestSupport.resourceToString(getTile.getBlob()), is("IMAGE-15-5050-5-B")); } private void zipDirectory(Path directoryToZip, File outputZipFile) throws IOException { try (FileOutputStream fileOutputStream = new FileOutputStream(outputZipFile); ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream)) { Files.walkFileTree(directoryToZip, new SimpleFileVisitor<Path>() { public FileVisitResult visitFile(Path file, BasicFileAttributes fileAttributes) throws IOException { zipOutputStream.putNextEntry(new ZipEntry(directoryToZip.relativize(file).toString())); Files.copy(file, zipOutputStream); zipOutputStream.closeEntry(); return FileVisitResult.CONTINUE; } public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException { if (directory.equals(directoryToZip)) { return FileVisitResult.CONTINUE; } // the zip structure is not tied the OS file separator zipOutputStream.putNextEntry(new ZipEntry(directoryToZip.relativize(directory).toString() + "/")); zipOutputStream.closeEntry(); return FileVisitResult.CONTINUE; } }); } } /** * Helper method that simply makes sure that all the connections to the sqlite databases * are closed allowing file operations to be performed on the databases files. */ private void closeSqliteStoresConnections() { XMLConfiguration configuration = webApplicationContext.getBean(XMLConfiguration.class); assertThat(configuration, notNullValue()); List<BlobStoreConfig> blobStoresConfig = configuration.getBlobStores(); assertThat(blobStoresConfig, notNullValue()); // let's iterate over all the available blob stores configurations for (BlobStoreConfig blobStoreConfig : blobStoresConfig) { if (blobStoreConfig instanceof SqliteConfiguration) { // we have a sqlite based blob store, let's close all the connections ((MbtilesConfiguration) blobStoreConfig).getConnectionManager().reapAllConnections(); } } } }