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