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.File;
22  import java.io.IOException;
23  import java.net.URI;
24  import java.net.URISyntaxException;
25  import java.nio.file.Path;
26  import java.util.Iterator;
27  
28  import org.apache.logging.log4j.LogManager;
29  import org.apache.logging.log4j.Logger;
30  import org.jdom2.JDOMException;
31  import org.mycore.common.MCRPersistenceException;
32  import org.mycore.common.MCRSessionMgr;
33  import org.mycore.common.config.MCRConfiguration2;
34  import org.mycore.common.config.MCRConfigurationException;
35  import org.mycore.common.content.MCRContent;
36  import org.tmatesoft.svn.core.SVNCancelException;
37  import org.tmatesoft.svn.core.SVNCommitInfo;
38  import org.tmatesoft.svn.core.SVNException;
39  import org.tmatesoft.svn.core.SVNURL;
40  import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
41  import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
42  import org.tmatesoft.svn.core.auth.SVNAuthentication;
43  import org.tmatesoft.svn.core.auth.SVNUserNameAuthentication;
44  import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
45  import org.tmatesoft.svn.core.io.ISVNEditor;
46  import org.tmatesoft.svn.core.io.SVNRepository;
47  import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
48  import org.tmatesoft.svn.core.wc.SVNEvent;
49  import org.tmatesoft.svn.core.wc.admin.ISVNAdminEventHandler;
50  import org.tmatesoft.svn.core.wc.admin.SVNAdminClient;
51  import org.tmatesoft.svn.core.wc.admin.SVNAdminEvent;
52  
53  /**
54   * Stores metadata objects both in a local filesystem structure and in a
55   * Subversion repository. Changes can be tracked and restored. To enable
56   * versioning, configure the repository URL, for example
57   *
58   * MCR.IFS2.Store.DocPortal_document.SVNRepositoryURL=file:///foo/svnroot/
59   *
60   * @author Frank Lützenkirchen
61   */
62  public class MCRVersioningMetadataStore extends MCRMetadataStore {
63  
64      protected static final Logger LOGGER = LogManager.getLogger(MCRVersioningMetadataStore.class);
65  
66      protected SVNURL repURL;
67  
68      protected static final boolean SYNC_LAST_MODIFIED_ON_SVN_COMMIT = MCRConfiguration2
69          .getBoolean("MCR.IFS2.SyncLastModifiedOnSVNCommit").orElse(true);
70  
71      static {
72          FSRepositoryFactory.setup();
73      }
74  
75      @Override
76      protected void init(String type) {
77          super.init(type);
78          setupSVN(type);
79      }
80  
81      @Override
82      protected void init(MCRStoreConfig config) {
83          super.init(config);
84          setupSVN(config.getID());
85      }
86  
87      private void setupSVN(String type) {
88          URI repositoryURI;
89          String repositoryURIString = MCRConfiguration2.getStringOrThrow("MCR.IFS2.Store." + type + ".SVNRepositoryURL");
90          try {
91              repositoryURI = new URI(repositoryURIString);
92          } catch (URISyntaxException e) {
93              String msg = "Syntax error in MCR.IFS2.Store." + type + ".SVNRepositoryURL property: "
94                  + repositoryURIString;
95              throw new MCRConfigurationException(msg, e);
96          }
97          try {
98              LOGGER.info("Versioning metadata store {} repository URL: {}", type, repositoryURI);
99              repURL = SVNURL.create(repositoryURI.getScheme(), repositoryURI.getUserInfo(), repositoryURI.getHost(),
100                 repositoryURI.getPort(), repositoryURI.getPath(), true);
101             LOGGER.info("repURL: {}", repURL);
102             File dir = new File(repURL.getPath());
103             if (!dir.exists() || (dir.isDirectory() && dir.list().length == 0)) {
104                 LOGGER.info("Repository does not exist, creating new SVN repository at {}", repositoryURI);
105                 repURL = SVNRepositoryFactory.createLocalRepository(dir, true, false);
106             }
107         } catch (SVNException ex) {
108             String msg = "Error initializing SVN repository at URL " + repositoryURI;
109             throw new MCRConfigurationException(msg, ex);
110         }
111     }
112 
113     /**
114      * When metadata is saved, this results in SVN commit. If the property
115      * MCR.IFS2.SyncLastModifiedOnSVNCommit=true (which is default), the
116      * last modified date of the metadata file in the store will be set to the exactly
117      * same timestamp as the SVN commit. Due to permission restrictions on Linux systems,
118      * this may fail, so you can disable that behaviour.
119      *
120      * @return true, if last modified of file should be same as timestamp of SVN commit
121      */
122     public static boolean shouldSyncLastModifiedOnSVNCommit() {
123         return SYNC_LAST_MODIFIED_ON_SVN_COMMIT;
124     }
125 
126     /**
127      * Returns the SVN repository used to manage metadata versions in this
128      * store.
129      *
130      * @return the SVN repository used to manage metadata versions in this
131      *         store.
132      */
133     SVNRepository getRepository() throws SVNException {
134         SVNRepository repository = SVNRepositoryFactory.create(repURL);
135         String user = MCRSessionMgr.getCurrentSession().getUserInformation().getUserID();
136         SVNAuthentication[] auth = {
137             SVNUserNameAuthentication.newInstance(user, false, repURL, false) };
138         BasicAuthenticationManager authManager = new BasicAuthenticationManager(auth);
139         repository.setAuthenticationManager(authManager);
140         return repository;
141     }
142 
143     /**
144      * Returns the URL of the SVN repository used to manage metadata versions in
145      * this store.
146      *
147      * @return the URL of the SVN repository used to manage metadata versions in
148      *         this store.
149      */
150     SVNURL getRepositoryURL() {
151         return repURL;
152     }
153 
154     /**
155      * Checks to local SVN repository for errors
156      * @throws MCRPersistenceException if 'svn verify' fails
157      */
158     public void verify() throws MCRPersistenceException {
159         String replURLStr = repURL.toString();
160         if (!repURL.getProtocol().equals("file")) {
161             LOGGER.warn("Cannot verify non local SVN repository '{}'.", replURLStr);
162             return;
163         }
164         try {
165             SVNRepository repository = getRepository();
166             long latestRevision = repository.getLatestRevision();
167             if (latestRevision == 0) {
168                 LOGGER.warn("Cannot verify SVN repository '{}' with no revisions.", replURLStr);
169             }
170             ISVNAuthenticationManager authenticationManager = repository.getAuthenticationManager();
171             SVNAdminClient adminClient = new SVNAdminClient(authenticationManager, null);
172             File repositoryRoot = new File(URI.create(replURLStr));
173             adminClient.setEventHandler(new ISVNAdminEventHandler() {
174                 //if more than batchSize revisions print progress
175                 int batchSize = 100;
176 
177                 @Override
178                 public void checkCancelled() throws SVNCancelException {
179                 }
180 
181                 @Override
182                 public void handleEvent(SVNEvent event, double progress) throws SVNException {
183                 }
184 
185                 @Override
186                 public void handleAdminEvent(SVNAdminEvent event, double progress) throws SVNException {
187                     if (event.getMessage() != null) {
188                         if (event.getRevision() % batchSize != 0 || event.getRevision() == 0) {
189                             LOGGER.debug(event::getMessage);
190                         } else {
191                             LOGGER.info("{} ({}% done)", event.getMessage(),
192                                 (int) (event.getRevision() * 100.0 / latestRevision));
193                         }
194                     }
195                 }
196             });
197             adminClient.doVerify(repositoryRoot);
198             LOGGER.info("Verified SVN repository '{}'.", replURLStr);
199         } catch (Exception e) {
200             throw new MCRPersistenceException("SVN repository contains errors and could not be verified: " + replURLStr,
201                 e);
202         }
203     }
204 
205     @Override
206     public MCRVersionedMetadata create(MCRContent xml, int id) throws IOException, JDOMException {
207         return (MCRVersionedMetadata) super.create(xml, id);
208     }
209 
210     @Override
211     public MCRVersionedMetadata create(MCRContent xml) throws IOException, JDOMException {
212         return (MCRVersionedMetadata) super.create(xml);
213     }
214 
215     /**
216      * Returns the metadata stored under the given ID, or null. Note that this
217      * metadata may not exist currently in the store, it may be a deleted
218      * version, which can be restored then.
219      *
220      * @param id
221      *            the ID of the XML document
222      * @return the metadata stored under that ID, or null when there is no such
223      *         metadata object
224      */
225     @Override
226     public MCRVersionedMetadata retrieve(int id) throws IOException {
227         MCRVersionedMetadata metadata = (MCRVersionedMetadata) super.retrieve(id);
228         if (metadata != null) {
229             return metadata;
230         }
231         return new MCRVersionedMetadata(this, getSlot(id), id, super.forceDocType, true);
232     }
233 
234     /**
235      * Updates all stored metadata to the latest revision in SVN
236      */
237     public void updateAll() throws Exception {
238         for (Iterator<Integer> ids = listIDs(true); ids.hasNext();) {
239             retrieve(ids.next()).update();
240         }
241     }
242 
243     @Override
244     public void delete(int id) throws IOException {
245         String commitMsg = "Deleted metadata object " + getID() + "_" + id + " in store";
246         // Commit to SVN
247         SVNCommitInfo info;
248         try {
249             SVNRepository repository = getRepository();
250             ISVNEditor editor = repository.getCommitEditor(commitMsg, null);
251             editor.openRoot(-1);
252             editor.deleteEntry("/" + getSlotPath(id), -1);
253             editor.closeDir();
254 
255             info = editor.closeEdit();
256             LOGGER.info("SVN commit of delete finished, new revision {}", info.getNewRevision());
257         } catch (SVNException e) {
258             LOGGER.error("Error while deleting {} in SVN ", id, e);
259         } finally {
260             super.delete(id);
261         }
262     }
263 
264     @Override
265     protected MCRVersionedMetadata buildMetadataObject(Path fo, int id) {
266         return new MCRVersionedMetadata(this, fo, id, super.forceDocType, false);
267     }
268 
269 }