View Javadoc
1   package fr.ifremer.quadrige3.core.service.persistence;
2   
3   /*-
4    * #%L
5    * Quadrige3 Core :: Quadrige3 Client Core
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import fr.ifremer.quadrige3.core.ProgressionCoreModel;
27  import fr.ifremer.quadrige3.core.config.QuadrigeConfiguration;
28  import fr.ifremer.quadrige3.core.config.QuadrigeCoreConfiguration;
29  import fr.ifremer.quadrige3.core.dao.technical.Assert;
30  import fr.ifremer.quadrige3.core.dao.technical.Daos;
31  import fr.ifremer.quadrige3.core.dao.technical.Files;
32  import fr.ifremer.quadrige3.core.dao.technical.ZipUtils;
33  import fr.ifremer.quadrige3.core.exception.QuadrigeBusinessException;
34  import fr.ifremer.quadrige3.core.exception.QuadrigeTechnicalException;
35  import fr.ifremer.quadrige3.core.service.ClientServiceLocator;
36  import org.apache.commons.lang3.StringUtils;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.nuiton.jaxx.application.ApplicationIOUtil;
40  import org.nuiton.jaxx.application.ApplicationTechnicalException;
41  
42  import javax.sql.DataSource;
43  import java.io.File;
44  import java.io.IOException;
45  import java.nio.file.FileVisitResult;
46  import java.nio.file.Path;
47  import java.nio.file.Paths;
48  import java.nio.file.SimpleFileVisitor;
49  import java.nio.file.attribute.BasicFileAttributes;
50  import java.time.LocalDate;
51  import java.time.format.DateTimeFormatter;
52  import java.util.ArrayList;
53  import java.util.List;
54  import java.util.stream.Collectors;
55  import java.util.zip.ZipEntry;
56  
57  import static org.nuiton.i18n.I18n.t;
58  
59  /**
60   * <p>PersistenceServiceHelper class.</p>
61   *
62   * @author Ludovic Pecquot <ludovic.pecquot@e-is.pro>
63   */
64  public class PersistenceServiceHelper {
65  
66      private static final Log LOG = LogFactory.getLog(PersistenceServiceHelper.class);
67  
68      private static final DateTimeFormatter BACKUP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
69  
70      private static final String BACKUP_DIRECTORY_FORMAT = "%s-%s-%s";
71  
72      /**
73       * backup the database using hsqldb backup feature
74       * backup also:
75       * - version.appup of the db
76       * - import & export properties for all users
77       * - attachment files
78       * - additional db related directories like measurementGridPresets and extractionConfig (used by Dali)
79       *
80       * @param file             the target file
81       * @param progressionModel the progression model
82       */
83      public static void backupDatabase(Path file, ProgressionCoreModel progressionModel) {
84          Assert.notNull(file);
85          DataSource dataSource = ClientServiceLocator.instance().getService("dataSource", DataSource.class);
86          Assert.notNull(dataSource);
87          QuadrigeCoreConfiguration config = QuadrigeCoreConfiguration.getInstance();
88          Assert.notNull(config);
89  
90          // create zip structure
91          String tempDirName = String.format(
92                  BACKUP_DIRECTORY_FORMAT,
93                  config.getApplicationName(),
94                  config.getVersion(),
95                  BACKUP_DATE_FORMAT.format(LocalDate.now()));
96  
97          File structureDirectory = new File(config.newTempFile("backupDb"), tempDirName);
98  
99          try {
100             ApplicationIOUtil.forceMkdir(structureDirectory,
101                     t("quadrige3.error.create.directory", structureDirectory));
102 
103             if (LOG.isDebugEnabled()) {
104                 LOG.debug("Backup directory: " + structureDirectory);
105             }
106 
107             // backup database in a temp dir
108             String dbPath = structureDirectory.getAbsolutePath();
109             dbPath = StringUtils.appendIfMissing(dbPath, File.separator) + Daos.DB_DIRECTORY + File.separator;
110             String sql = "BACKUP DATABASE TO '" + dbPath + "' BLOCKING AS FILES";
111             try {
112                 Daos.sqlUpdate(dataSource, sql);
113             } catch (Exception e) {
114                 throw new ApplicationTechnicalException(t("quadrige3.service.persistence.copyDirectory.db.error"), e);
115             }
116 
117             // backup version.appup
118             try {
119                 progressionModel.setTotal(0);
120                 Files.copyFile(
121                         config.getDbDirectory().toPath().resolve(Daos.DB_VERSION_FILE),
122                         Paths.get(dbPath).resolve(Daos.DB_VERSION_FILE),
123                         progressionModel
124                 );
125             } catch (IOException e) {
126                 throw new QuadrigeTechnicalException(t("quadrige3.service.persistence.copyFile.error", Daos.DB_VERSION_FILE), e);
127             }
128 
129             // backup attachments
130             backupDirectory(config.getDbAttachmentDirectory().toPath(), structureDirectory.toPath(), progressionModel);
131             // backup photos
132             backupDirectory(config.getDbPhotoDirectory().toPath(), structureDirectory.toPath(), progressionModel);
133 
134             // other directories
135             config.getDbOtherDirectories().forEach(directory -> backupDirectory(directory.toPath(), structureDirectory.toPath(), progressionModel));
136 
137             // backup synchro files for all users
138             if (config.getSynchronizationDirectory() != null && config.getSynchronizationDirectory().isDirectory()) {
139                 String synchroDirectoryName = config.getSynchronizationDirectory().getName();
140                 final String[] currentFile = new String[1];
141                 try {
142                     progressionModel.setTotal(0);
143                     java.nio.file.Files.walkFileTree(config.getSynchronizationDirectory().toPath(), new SimpleFileVisitor<Path>() {
144                         @Override
145                         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
146                             if (file.getFileName().toString().equalsIgnoreCase(Daos.IMPORT_PROPERTIES_FILE)
147                                     || file.getFileName().toString().equalsIgnoreCase(Daos.EXPORT_PROPERTIES_FILE)) {
148                                 currentFile[0] = file.toAbsolutePath().toString();
149                                 Path target = structureDirectory.toPath().resolve(synchroDirectoryName).resolve(config.getSynchronizationDirectory().toPath().relativize(file));
150                                 Files.copyFile(file, target, progressionModel);
151                             }
152                             return FileVisitResult.CONTINUE;
153                         }
154                     });
155                 } catch (IOException e) {
156                     throw new ApplicationTechnicalException(t("quadrige3.service.persistence.copyFile.error", currentFile[0]), e);
157                 }
158             }
159 
160             // create zip
161             try {
162                 progressionModel.setTotal(0);
163                 ZipUtils.compressFilesInPath(structureDirectory.toPath(), file, progressionModel, false, true);
164             } catch (IOException e) {
165                 throw new QuadrigeTechnicalException(t("quadrige3.service.persistence.backupDb.zip.error", file), e);
166             }
167 
168         } finally {
169 
170             // delete temp files
171             ApplicationIOUtil.forceDeleteOnExit(
172                     structureDirectory,
173                     t("quadrige3.service.persistence.backupDb.deleteTempDir.error", structureDirectory));
174         }
175 
176     }
177 
178     private static void backupDirectory(Path sourceDirectory, Path targetDirectory, ProgressionCoreModel progressionModel) {
179         if (sourceDirectory != null && java.nio.file.Files.isDirectory(sourceDirectory)) {
180             try {
181                 if (progressionModel != null)
182                     progressionModel.setTotal(0);
183                 Files.copyDirectory(
184                         sourceDirectory,
185                         targetDirectory.resolve(sourceDirectory.getFileName().toString()),
186                         progressionModel);
187             } catch (IOException e) {
188                 throw new QuadrigeTechnicalException(t("quadrige3.service.persistence.copyDirectory.error", sourceDirectory), e);
189             }
190         }
191     }
192 
193     public static void restoreDatabase(Path zipFile, ProgressionCoreModel progressionModel) {
194 
195         QuadrigeConfiguration config = QuadrigeConfiguration.getInstance();
196         Assert.notNull(config);
197 
198         // need a file to restore
199         Assert.notNull(zipFile);
200 
201         // get the root directory of archive
202         String rootDirName = getRootDirectoryOfArchive(zipFile);
203 
204         Path target = config.getDataDirectory().toPath();
205 
206         if (LOG.isInfoEnabled()) {
207             LOG.info("Import db to " + target);
208         }
209 
210         try {
211             progressionModel.setTotal(0);
212             ZipUtils.uncompressFileToPath(zipFile, target, rootDirName, progressionModel, false);
213         } catch (IOException e) {
214             throw new QuadrigeTechnicalException(t("quadrige3.service.persistence.restoreDb.extractFailed", zipFile), e);
215         }
216 
217         // Migrate imported DB (Mantis #40873)
218         migrateDbName();
219     }
220 
221     /**
222      * Rename old db name to new name (Mantis #40873)
223      */
224     public static void migrateDbName() {
225 
226         QuadrigeConfiguration config = QuadrigeConfiguration.getInstance();
227         Assert.notNull(config);
228 
229         Path dbDir = Paths.get(config.getDbDirectory().getAbsolutePath());
230         if (java.nio.file.Files.isDirectory(dbDir)) {
231             try {
232 
233                 // find old db name
234                 for (Path oldDbFile : java.nio.file.Files.list(dbDir).filter(path -> path.getFileName().toString().startsWith("quadrige2")).collect(Collectors.toList())) {
235                     String fileName = oldDbFile.getFileName().toString();
236                     int extensionIndex = fileName.lastIndexOf(".");
237                     String extension = extensionIndex != -1 ? fileName.substring(extensionIndex) : "";
238                     // Rename file
239                     java.nio.file.Files.move(oldDbFile, oldDbFile.resolveSibling(config.getDbName() + extension));
240                 }
241             } catch (IOException e) {
242                 throw new QuadrigeTechnicalException(t("quadrige3.service.persistence.migrateDbName.error"), e);
243             }
244         }
245 
246     }
247 
248     public static String getRootDirectoryOfArchive(Path file) {
249 
250         QuadrigeConfiguration config = QuadrigeConfiguration.getInstance();
251         Assert.notNull(config);
252         Assert.notNull(file);
253 
254         if (!java.nio.file.Files.exists(file)) {
255             throw new QuadrigeBusinessException(t("quadrige3.service.persistence.restoreDb.fileNotExist", file));
256         }
257 
258         String rootDirectory = null;
259         boolean dbDirectoryFound = false;
260         boolean dbScriptFileFound = false;
261 
262         try {
263             for (ZipEntry zipEntry : ZipUtils.getEntries(file)) {
264                 if (zipEntry.isDirectory()) {
265                     // search for root directory
266                     if (rootDirectory == null) {
267                         rootDirectory = zipEntry.getName().substring(0, zipEntry.getName().indexOf("/"));
268                     } else if (!zipEntry.getName().startsWith(rootDirectory)) {
269                         throw new QuadrigeBusinessException(t("quadrige3.service.persistence.restoreDb.tooManyChildren"));
270                     }
271                     // search for db directory
272                     if (String.format("%s/%s/", rootDirectory, Daos.DB_DIRECTORY).equalsIgnoreCase(zipEntry.getName())) {
273                         dbDirectoryFound = true;
274                     }
275                 } else {
276                     // search for db script file
277                     if (String.format("%s/%s/%s.%s", rootDirectory, Daos.DB_DIRECTORY, config.getDbName(), "script").equalsIgnoreCase(zipEntry.getName())) {
278                         dbScriptFileFound = true;
279                     }
280                 }
281             }
282         } catch (IOException e) {
283             throw new QuadrigeTechnicalException(t("quadrige3.service.persistence.restoreDb.readFailed", file), e);
284         }
285 
286         if (!dbDirectoryFound)
287             throw new QuadrigeBusinessException(t("quadrige3.service.persistence.restoreDb.itemNotFound", file, Daos.DB_DIRECTORY));
288         if (!dbScriptFileFound)
289             throw new QuadrigeBusinessException(t("quadrige3.service.persistence.restoreDb.itemNotFound", file, String.format("%s.%s", config.getDbName(), "script")));
290 
291         return rootDirectory;
292     }
293 
294     /**
295      * Delete database and related directories
296      *
297      * @param progressionModel the progression model
298      */
299     public static void deleteDatabaseDirectory(ProgressionCoreModel progressionModel) {
300 
301         QuadrigeCoreConfiguration config = QuadrigeCoreConfiguration.getInstance();
302         Assert.notNull(config);
303 
304         List<Runnable> runnables = new ArrayList<>();
305         runnables.add(() -> {
306             // delete db directory
307             File dbDirectory = config.getDbDirectory();
308             if (dbDirectory.isDirectory()) {
309                 if (LOG.isInfoEnabled()) {
310                     LOG.info("Delete previous database directory: " + dbDirectory);
311                 }
312                 Files.cleanDirectory(dbDirectory.toPath(), "Could not delete old db directory", progressionModel);
313             }
314         });
315 
316         runnables.add(() -> {
317             // delete db cache directory
318             File cacheDirectory = config.getCacheDirectory();
319             if (cacheDirectory.isDirectory()) {
320                 if (LOG.isInfoEnabled()) {
321                     LOG.info("Delete previous database cache directory: " + cacheDirectory);
322                 }
323                 Files.cleanDirectory(cacheDirectory.toPath(), "Could not delete old db cache directory", progressionModel);
324             }
325         });
326 
327         runnables.add(() -> {
328             // delete synchro directory
329             File synchroDirectory = config.getSynchronizationDirectory();
330             if (synchroDirectory.isDirectory()) {
331                 if (LOG.isInfoEnabled()) {
332                     LOG.info("Delete previous database data synchro directory: " + synchroDirectory);
333                 }
334                 // Use a clean directory (see Mantis#0029005)
335                 Files.cleanDirectory(synchroDirectory.toPath(), "Could not delete old synchro directory", progressionModel);
336             }
337         });
338 
339         runnables.add(() -> {
340             // delete measurement files directory
341             File measFileDirectory = config.getDbAttachmentDirectory();
342             if (measFileDirectory.isDirectory()) {
343                 if (LOG.isInfoEnabled()) {
344                     LOG.info("Delete previous database measurement files directory: " + measFileDirectory);
345                 }
346                 // Use a clean directory (see Mantis#0029005)
347                 Files.cleanDirectory(measFileDirectory.toPath(), "Could not delete old measurement files directory", progressionModel);
348             }
349         });
350 
351         runnables.add(() -> {
352             // delete measurement files directory
353             File photoDirectory = config.getDbPhotoDirectory();
354             if (photoDirectory.isDirectory()) {
355                 if (LOG.isInfoEnabled()) {
356                     LOG.info("Delete previous database photos directory: " + photoDirectory);
357                 }
358                 // Use a clean directory (see Mantis#0029005)
359                 Files.cleanDirectory(photoDirectory.toPath(), "Could not delete old photos directory", progressionModel);
360             }
361         });
362 
363         runnables.add(() -> {
364             // delete other db directories
365             config.getDbOtherDirectories().forEach(otherDirectory -> {
366                 if (otherDirectory.isDirectory()) {
367                     if (LOG.isInfoEnabled()) {
368                         LOG.info("Delete previous database additional directory: " + otherDirectory);
369                     }
370                     Files.cleanDirectory(otherDirectory.toPath(), "Could not delete old additional directory: " + otherDirectory.getName(), progressionModel);
371                 }
372             });
373         });
374 
375         runnables.forEach(Runnable::run);
376 
377     }
378 
379 }