View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.datamodel.ifs2;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.function.Supplier;
30  
31  import org.apache.logging.log4j.LogManager;
32  import org.apache.logging.log4j.Logger;
33  import org.jdom2.JDOMException;
34  import org.mycore.common.MCRUsageException;
35  import org.mycore.common.content.MCRByteContent;
36  import org.mycore.common.content.MCRContent;
37  import org.mycore.common.content.streams.MCRByteArrayOutputStream;
38  import org.tmatesoft.svn.core.ISVNLogEntryHandler;
39  import org.tmatesoft.svn.core.SVNCommitInfo;
40  import org.tmatesoft.svn.core.SVNDirEntry;
41  import org.tmatesoft.svn.core.SVNException;
42  import org.tmatesoft.svn.core.SVNLogEntry;
43  import org.tmatesoft.svn.core.SVNLogEntryPath;
44  import org.tmatesoft.svn.core.SVNNodeKind;
45  import org.tmatesoft.svn.core.SVNProperty;
46  import org.tmatesoft.svn.core.SVNPropertyValue;
47  import org.tmatesoft.svn.core.io.ISVNEditor;
48  import org.tmatesoft.svn.core.io.SVNRepository;
49  import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
50  
51  /**
52   * Represents an XML metadata document that is stored in a local filesystem
53   * store and in parallel in a Subversion repository to track and restore
54   * changes.
55   *
56   * @author Frank Lützenkirchen
57   */
58  public class MCRVersionedMetadata extends MCRStoredMetadata {
59  
60      /**
61       * The logger
62       */
63      protected static final Logger LOGGER = LogManager.getLogger();
64  
65      /**
66       * The revision number of the metadata version that is currently in the
67       * local filesystem store.
68       */
69      protected Supplier<Optional<Long>> revision;
70  
71      /**
72       * Creates a new metadata object both in the local store and in the SVN
73       * repository
74       *
75       * @param store
76       *            the store this object is stored in
77       * @param fo
78       *            the file storing the data in the local filesystem
79       * @param id
80       *            the id of the metadata object
81       */
82      MCRVersionedMetadata(MCRMetadataStore store, Path fo, int id, String docType, boolean deleted) {
83          super(store, fo, id, docType);
84          super.deleted = deleted;
85          revision = () -> {
86              try {
87                  // 1. current revision, 2. deleted revision, empty()
88                  return Optional.ofNullable(Optional.ofNullable(getStore().getRepository().info(getFilePath(), -1))
89                      .map(SVNDirEntry::getRevision).orElseGet(this::getLastRevision));
90              } catch (SVNException e) {
91                  LOGGER.error("Could not get last revision of {}_{}", getStore().getID(), id, e);
92                  return Optional.empty();
93              }
94          };
95      }
96  
97      @Override
98      public MCRVersioningMetadataStore getStore() {
99          return (MCRVersioningMetadataStore) store;
100     }
101 
102     /**
103      * Stores a new metadata object first in the SVN repository, then
104      * additionally in the local store.
105      *
106      * @param xml
107      *            the metadata document to store
108      * @throws JDOMException thrown by {@link MCRStoredMetadata#create(MCRContent)}
109      */
110     @Override
111     void create(MCRContent xml) throws IOException, JDOMException {
112         super.create(xml);
113         commit("create");
114     }
115 
116     /**
117      * Updates this metadata object, first in the SVN repository and then in the
118      * local store
119      *
120      * @param xml
121      *            the new version of the document metadata
122      * @throws JDOMException thrown by {@link MCRStoredMetadata#create(MCRContent)}
123      */
124     @Override
125     public void update(MCRContent xml) throws IOException, JDOMException {
126         if (isDeleted() || isDeletedInRepository()) {
127             create(xml);
128         } else {
129             super.update(xml);
130             commit("update");
131         }
132     }
133 
134     void commit(String mode) throws IOException {
135         // Commit to SVN
136         SVNCommitInfo info;
137         try {
138             SVNRepository repository = getStore().getRepository();
139 
140             // Check which paths already exist in SVN
141             String[] paths = store.getSlotPaths(id);
142             int existing = paths.length - 1;
143             for (; existing >= 0; existing--) {
144                 if (!repository.checkPath(paths[existing], -1).equals(SVNNodeKind.NONE)) {
145                     break;
146                 }
147             }
148 
149             existing += 1;
150 
151             // Start commit editor
152             String commitMsg = mode + "d metadata object " + store.getID() + "_" + id + " in store";
153             ISVNEditor editor = repository.getCommitEditor(commitMsg, null);
154             editor.openRoot(-1);
155 
156             // Create directories in SVN that do not exist yet
157             for (int i = existing; i < paths.length - 1; i++) {
158                 LOGGER.debug("SVN create directory {}", paths[i]);
159                 editor.addDir(paths[i], null, -1);
160                 editor.closeDir();
161             }
162 
163             // Commit file changes
164             String filePath = paths[paths.length - 1];
165             if (existing < paths.length) {
166                 editor.addFile(filePath, null, -1);
167             } else {
168                 editor.openFile(filePath, -1);
169             }
170 
171             editor.applyTextDelta(filePath, null);
172             SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
173 
174             String checksum;
175             try (InputStream in = Files.newInputStream(path)) {
176                 checksum = deltaGenerator.sendDelta(filePath, in, editor, true);
177             }
178 
179             if (store.shouldForceXML()) {
180                 editor.changeFileProperty(filePath, SVNProperty.MIME_TYPE, SVNPropertyValue.create("text/xml"));
181             }
182 
183             editor.closeFile(filePath, checksum);
184             editor.closeDir(); // root
185 
186             info = editor.closeEdit();
187         } catch (SVNException e) {
188             throw new IOException(e);
189         }
190         revision = () -> Optional.of(info.getNewRevision());
191         LOGGER.info("SVN commit of {} finished, new revision {}", mode, getRevision());
192 
193         if (MCRVersioningMetadataStore.shouldSyncLastModifiedOnSVNCommit()) {
194             setLastModified(info.getDate());
195         }
196     }
197 
198     /**
199      * Deletes this metadata object in the SVN repository, and in the local
200      * store.
201      */
202     @Override
203     public void delete() throws IOException {
204         if (isDeleted()) {
205             String msg = "You can not delete already deleted data: " + id;
206             throw new MCRUsageException(msg);
207         }
208         if (!store.exists(id)) {
209             throw new IOException("Does not exist: " + store.getID() + "_" + id);
210         }
211         getStore().delete(getID());
212         deleted = true;
213     }
214 
215     public boolean isDeletedInRepository() throws IOException {
216         long rev = getRevision();
217         return rev >= 0 && getRevision(rev).getType() == MCRMetadataVersion.DELETED;
218     }
219 
220     /**
221      * Updates the version stored in the local filesystem to the latest version
222      * from Subversion repository HEAD.
223      */
224     public void update() throws Exception {
225         SVNRepository repository = getStore().getRepository();
226         MCRByteArrayOutputStream baos = new MCRByteArrayOutputStream();
227         long rev = repository.getFile(getFilePath(), -1, null, baos);
228         revision = () -> Optional.of(rev);
229         baos.close();
230         new MCRByteContent(baos.getBuffer(), 0, baos.size(), this.getLastModified().getTime()).sendTo(path);
231     }
232 
233     /**
234      * Lists all versions of this metadata object available in the subversion
235      * repository
236      *
237      * @return all stored versions of this metadata object
238      */
239     @SuppressWarnings("unchecked")
240     public List<MCRMetadataVersion> listVersions() throws IOException {
241         try {
242             List<MCRMetadataVersion> versions = new ArrayList<>();
243             SVNRepository repository = getStore().getRepository();
244             String path = getFilePath();
245             String dir = getDirectory();
246 
247             Collection<SVNLogEntry> entries = null;
248             try {
249                 entries = repository.log(new String[] { dir }, null, 0, repository.getLatestRevision(), true, true);
250             } catch (Exception ioex) {
251                 LOGGER.error("Could not get versions", ioex);
252                 return versions;
253             }
254 
255             for (SVNLogEntry entry : entries) {
256                 SVNLogEntryPath svnLogEntryPath = entry.getChangedPaths().get(path);
257                 if (svnLogEntryPath != null) {
258                     char type = svnLogEntryPath.getType();
259                     versions.add(new MCRMetadataVersion(this, Long.toString(entry.getRevision()), entry.getAuthor(),
260                         entry.getDate(), type));
261                 }
262             }
263             return versions;
264         } catch (SVNException svnExc) {
265             throw new IOException(svnExc);
266         }
267     }
268 
269     private String getFilePath() {
270         return "/" + store.getSlotPath(id);
271     }
272 
273     private String getDirectory() {
274         String path = getFilePath();
275         return path.substring(0, path.lastIndexOf('/'));
276     }
277 
278     public MCRMetadataVersion getRevision(long revision) throws IOException {
279         try {
280             if (revision < 0) {
281                 revision = getLastPresentRevision();
282                 if (revision < 0) {
283                     LOGGER.warn("Metadata object {} in store {} has no last revision!", getID(), getStore().getID());
284                     return null;
285                 }
286             }
287             SVNRepository repository = getStore().getRepository();
288             String path = getFilePath();
289             String dir = getDirectory();
290             @SuppressWarnings("unchecked")
291             Collection<SVNLogEntry> log = repository.log(new String[] { dir }, null, revision, revision, true, true);
292             for (SVNLogEntry logEntry : log) {
293                 SVNLogEntryPath svnLogEntryPath = logEntry.getChangedPaths().get(path);
294                 if (svnLogEntryPath != null) {
295                     char type = svnLogEntryPath.getType();
296                     return new MCRMetadataVersion(this, Long.toString(logEntry.getRevision()), logEntry.getAuthor(),
297                         logEntry.getDate(), type);
298                 }
299             }
300             LOGGER.warn("Metadata object {} in store {} has no revision ''{}''!", getID(), getStore().getID(),
301                 getRevision());
302             return null;
303         } catch (SVNException svnExc) {
304             throw new IOException(svnExc);
305         }
306     }
307 
308     public long getLastPresentRevision() throws SVNException {
309         return getLastRevision(false);
310     }
311 
312     private long getLastRevision(boolean deleted) throws SVNException {
313         SVNRepository repository = getStore().getRepository();
314         if (repository.getLatestRevision() == 0) {
315             //new repository cannot hold a revision yet (MCR-1196)
316             return -1;
317         }
318         final String path = getFilePath();
319         String dir = getDirectory();
320         LastRevisionLogHandler lastRevisionLogHandler = new LastRevisionLogHandler(path, deleted);
321         int limit = 0; //we stop through LastRevisionFoundException
322         try {
323             repository.log(new String[] { dir }, repository.getLatestRevision(), 0, true, true, limit, false, null,
324                 lastRevisionLogHandler);
325         } catch (LastRevisionFoundException ignored) {
326         }
327         return lastRevisionLogHandler.getLastRevision();
328     }
329 
330     private Long getLastRevision() {
331         try {
332             long lastRevision = getLastRevision(true);
333             return lastRevision < 0 ? null : lastRevision;
334         } catch (SVNException e) {
335             LOGGER.warn("Could not get last revision of: {}_{}", getStore(), id, e);
336             return null;
337         }
338     }
339 
340     /**
341      * Returns the revision number of the version currently stored in the local
342      * filesystem store.
343      *
344      * @return the revision number of the local version
345      */
346     public long getRevision() {
347         return revision.get().orElse(-1L);
348     }
349 
350     /**
351      * Checks if the version in the local store is up to date with the latest
352      * version in SVN repository
353      *
354      * @return true, if the local version in store is the latest version
355      */
356     public boolean isUpToDate() throws IOException {
357         SVNDirEntry entry;
358         try {
359             SVNRepository repository = getStore().getRepository();
360             entry = repository.info(getFilePath(), -1);
361         } catch (SVNException e) {
362             throw new IOException(e);
363         }
364         return entry.getRevision() <= getRevision();
365     }
366 
367     private static final class LastRevisionFoundException extends RuntimeException {
368         private static final long serialVersionUID = 1L;
369     }
370 
371     private static final class LastRevisionLogHandler implements ISVNLogEntryHandler {
372         private final String path;
373 
374         long lastRevision = -1;
375 
376         private boolean deleted;
377 
378         private LastRevisionLogHandler(String path, boolean deleted) {
379             this.path = path;
380             this.deleted = deleted;
381         }
382 
383         @Override
384         public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
385             SVNLogEntryPath svnLogEntryPath = logEntry.getChangedPaths().get(path);
386             if (svnLogEntryPath != null) {
387                 char type = svnLogEntryPath.getType();
388                 if (deleted || type != SVNLogEntryPath.TYPE_DELETED) {
389                     lastRevision = logEntry.getRevision();
390                     //no other way to stop svnkit from logging
391                     throw new LastRevisionFoundException();
392                 }
393             }
394         }
395 
396         long getLastRevision() {
397             return lastRevision;
398         }
399     }
400 
401 }