001/* 002 * Copyright 2015 DuraSpace, Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.fcrepo.migration; 017 018import io.micrometer.core.instrument.Metrics; 019import io.micrometer.core.instrument.Timer; 020import org.fcrepo.migration.pidlist.ResumePidListManager; 021import org.fcrepo.migration.pidlist.UserProvidedPidListManager; 022import org.slf4j.Logger; 023import org.springframework.context.ConfigurableApplicationContext; 024import org.springframework.context.support.FileSystemXmlApplicationContext; 025import org.springframework.core.io.ClassPathResource; 026 027import javax.xml.stream.XMLStreamException; 028import java.io.BufferedReader; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.InputStreamReader; 032 033import static org.slf4j.LoggerFactory.getLogger; 034 035/** 036 * A class that represents a command-line program to migrate a fedora 3 037 * repository to fedora 4. 038 * 039 * There are two main configuration options: the source and the handler. 040 * 041 * The source is responsible for exposing objects from a fedora repository, 042 * while the handler is responsible for processing each one. 043 * @author mdurbin 044 */ 045public class Migrator { 046 047 private static final Logger LOGGER = getLogger(Migrator.class); 048 049 private static final Timer nextTimer = Metrics.timer("fcrepo.storage.foxml.object", "operation", "findNext"); 050 051 /** 052 * the main method. 053 * @param args the arguments 054 * @throws IOException IO exception 055 * @throws XMLStreamException xml stream exception 056 */ 057 public static void main(final String [] args) throws IOException, XMLStreamException { 058 // Single arg with path to properties file is required 059 if (args.length != 1) { 060 printHelp(); 061 return; 062 } 063 064 final ConfigurableApplicationContext context = new FileSystemXmlApplicationContext(args[0]); 065 final Migrator m = context.getBean("migrator", Migrator.class); 066 try { 067 m.run(); 068 } finally { 069 context.close(); 070 } 071 } 072 073 private ObjectSource source; 074 075 private StreamingFedoraObjectHandler handler; 076 077 private int limit; 078 079 private ResumePidListManager resumePidListManager; 080 private UserProvidedPidListManager userProvidedPidListManager; 081 082 private boolean continueOnError; 083 084 /** 085 * the migrator. set limit to -1. 086 */ 087 public Migrator() { 088 limit = -1; 089 } 090 091 /** 092 * set the limit. 093 * @param limit the limit 094 */ 095 public void setLimit(final int limit) { 096 this.limit = limit; 097 } 098 099 /** 100 * set the source. 101 * @param source the object source 102 */ 103 public void setSource(final ObjectSource source) { 104 this.source = source; 105 } 106 107 108 /** 109 * set the handler. 110 * @param handler the handler 111 */ 112 public void setHandler(final StreamingFedoraObjectHandler handler) { 113 this.handler = handler; 114 } 115 116 /** 117 * set UserProvidedPidListManager 118 * 119 * @param manager the list 120 */ 121 public void setUserProvidedPidListManager(final UserProvidedPidListManager manager) { 122 this.userProvidedPidListManager = manager; 123 } 124 125 public void setResumePidListManager(final ResumePidListManager manager) { 126 this.resumePidListManager = manager; 127 } 128 129 /** 130 * set the continue on error flag 131 * 132 * @param flag flag indicating whether or not to continue on error. 133 */ 134 public void setContinueOnError(final boolean flag) { 135 this.continueOnError = flag; 136 } 137 138 /** 139 * The constructor for migrator. 140 * @param source the source 141 * @param handler the handler 142 */ 143 public Migrator(final ObjectSource source, final StreamingFedoraObjectHandler handler) { 144 this(); 145 this.source = source; 146 this.handler = handler; 147 } 148 149 /** 150 * the run method for migrator. 151 * 152 * @throws XMLStreamException xml stream exception 153 */ 154 public void run() throws XMLStreamException { 155 int index = 0; 156 157 for (final var iterator = source.iterator(); iterator.hasNext();) { 158 try (final var o = nextTimer.record(iterator::next)) { 159 final String pid = o.getObjectInfo().getPid(); 160 if (pid != null) { 161 // Process if limit is '-1', or we have not hit the non-negative 'limit'... 162 if (!(limit < 0 || index++ < limit)) { 163 LOGGER.info("Reached processing limit {}", limit); 164 break; 165 } 166 167 if (acceptPid(pid)) { 168 LOGGER.info("Processing \"" + pid + "\"..."); 169 try { 170 o.processObject(handler); 171 } catch (Exception ex) { 172 final var message = String.format("MIGRATION_FAILURE: pid=\"%s\", message=\"%s\"", 173 pid, ex.getMessage()); 174 175 if (this.continueOnError) { 176 LOGGER.error(message, ex); 177 } else { 178 throw new RuntimeException(message, ex); 179 } 180 } 181 } 182 if (userProvidedPidListManager != null && 183 userProvidedPidListManager.finishedProcessingAllPids()) { 184 LOGGER.info("finished processing everything in pidlist - exiting."); 185 return; 186 } 187 } 188 } catch (Exception ex) { 189 final var message = String.format("MIGRATION_FAILURE: UNREADABLE_OBJECT: message=\"%s\"", 190 ex.getMessage()); 191 192 if (this.continueOnError) { 193 LOGGER.error(message, ex); 194 } else { 195 throw new RuntimeException(message, ex); 196 } 197 } 198 } 199 } 200 201 private boolean acceptPid(final String pid) { 202 // If any manager DOES NOT accept the PID, return false 203 // check user pid list first, so it gets registered in the UserProvidedPidListManager as an accepted pid 204 // even if the resume pid list manager rejects it. This way the UserProvidedPidListManager can still see 205 // when all the items in the list have been processed, even if we're resuming in the middle. 206 if (userProvidedPidListManager != null && !userProvidedPidListManager.accept(pid)) { 207 return false; 208 } 209 if (resumePidListManager != null && !resumePidListManager.accept(pid)) { 210 return false; 211 } 212 213 return true; 214 } 215 216 private static void printHelp() throws IOException { 217 final StringBuilder sb = new StringBuilder(); 218 sb.append("============================\n"); 219 sb.append("Please provide the directory path to a configuration file!"); 220 sb.append("\n"); 221 sb.append("See: https://github.com/fcrepo-exts/migration-utils/blob/master/"); 222 sb.append("src/main/resources/spring/migration-bean.xml"); 223 sb.append("\n\n"); 224 sb.append("The configuration file should contain the following (with appropriate values):"); 225 sb.append("\n"); 226 sb.append("~~~~~~~~~~~~~~\n"); 227 228 final ClassPathResource resource = new ClassPathResource("spring/migration-bean.xml"); 229 try (final InputStream example = resource.getInputStream(); 230 final BufferedReader reader = new BufferedReader(new InputStreamReader(example))) { 231 String line = reader.readLine(); 232 while (null != line) { 233 sb.append(line); 234 sb.append("\n"); 235 line = reader.readLine(); 236 } 237 238 sb.append("~~~~~~~~~~~~~~\n\n"); 239 sb.append("See top of this output for details.\n"); 240 sb.append("============================\n"); 241 System.out.println(sb.toString()); 242 } 243 } 244}