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}