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}