pl.psnc.synat.wrdz.zmkd.plan.MigrationPlanProcessorBean.java Source code

Java tutorial

Introduction

Here is the source code for pl.psnc.synat.wrdz.zmkd.plan.MigrationPlanProcessorBean.java

Source

/**
 * Copyright 2015 Pozna Supercomputing and Networking Center
 *
 * Licensed under the GNU General Public License, Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.gnu.org/licenses/gpl-3.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package pl.psnc.synat.wrdz.zmkd.plan;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Future;

import javax.annotation.Resource;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.EJB;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pl.psnc.synat.wrdz.common.config.WrdzModule;
import pl.psnc.synat.wrdz.common.dao.QueryFilter;
import pl.psnc.synat.wrdz.common.exception.WrdzRuntimeException;
import pl.psnc.synat.wrdz.common.https.HttpsClientHelper;
import pl.psnc.synat.wrdz.common.utility.UuidGenerator;
import pl.psnc.synat.wrdz.common.utility.ZipUtility;
import pl.psnc.synat.wrdz.zmd.entity.types.MigrationType;
import pl.psnc.synat.wrdz.zmd.object.IdentifierBrowser;
import pl.psnc.synat.wrdz.zmkd.config.ZmkdConfiguration;
import pl.psnc.synat.wrdz.zmkd.dao.plan.MigrationItemLogDao;
import pl.psnc.synat.wrdz.zmkd.dao.plan.MigrationItemLogFilterFactory;
import pl.psnc.synat.wrdz.zmkd.entity.plan.MigrationItemLog;
import pl.psnc.synat.wrdz.zmkd.entity.plan.MigrationItemStatus;
import pl.psnc.synat.wrdz.zmkd.entity.plan.MigrationPlan;
import pl.psnc.synat.wrdz.zmkd.object.DataFileInfo;
import pl.psnc.synat.wrdz.zmkd.object.DigitalObjectInfo;
import pl.psnc.synat.wrdz.zmkd.object.MetsReader;
import pl.psnc.synat.wrdz.zmkd.plan.execution.InconsistentServiceDescriptionException;
import pl.psnc.synat.wrdz.zmkd.plan.execution.PlanExecutionManager;
import pl.psnc.synat.wrdz.zmkd.plan.execution.PlanExecutionParser;
import pl.psnc.synat.wrdz.zmkd.plan.execution.TransformationException;
import pl.psnc.synat.wrdz.zmkd.plan.execution.TransformationInfo;
import pl.psnc.synat.wrdz.zu.dto.user.UserDto;
import pl.psnc.synat.wrdz.zu.permission.ObjectPermissionManager;
import pl.psnc.synat.wrdz.zu.types.ObjectPermissionType;
import pl.psnc.synat.wrdz.zu.user.UserBrowser;

/**
 * Default migration plan processor implementation.
 */
@Stateless
public class MigrationPlanProcessorBean implements MigrationPlanProcessor {

    /** Logger. */
    private static final Logger logger = LoggerFactory.getLogger(MigrationPlanProcessorBean.class);

    /** Mets metadata file path. */
    private static final String METS_PATH = "metadata" + File.separator + "extracted" + File.separator + "mets.xml";

    /** Digital object's content path prefix (in paths retrieved from METS file). */
    private static final String CONTENT_PREFIX = "content/";

    /** Http parameter for the origin object identifier. */
    private static final String ORIGIN_ID = "origin-id";

    /** Http parameter for the origin (migration) type. */
    private static final String ORIGIN_TYPE = "origin-type";

    /** Http parameter for the origin (migration) date. */
    private static final String ORIGIN_DATE = "origin-date";

    /** Format of the {@link ORIGIN_DATE} parameter value. */
    private static final String ORIGIN_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";

    /** Http parameter for the file content. */
    private static final String FILE_SRC = "file-%d-src";

    /** Http parameter for the file destination name. */
    private static final String FILE_DEST = "file-%d-dest";

    /** Http parameter for the file sequence value. */
    private static final String FILE_SEQ = "file-%d-seq";

    /** Character encoding. */
    private static final Charset ENCODING = Charset.forName("UTF-8");

    /** Injected session context. */
    @Resource
    private SessionContext ctx;

    /** Migration plan manager. */
    @EJB
    private MigrationPlanManager migrationPlanManager;

    /** Migration item manager. */
    @EJB
    private MigrationItemManager migrationItemManager;

    /** Migration item log bean. */
    @EJB
    private MigrationItemLogDao migrationItemLogDao;

    /** Migration path info retriever. */
    @EJB
    private MigrationPathRetriever migrationPathRetriever;

    /** Plan execution parser. */
    @EJB
    PlanExecutionParser planExecutionParser;

    /** Plan execution manager. */
    @EJB
    private PlanExecutionManager planExecutionManager;

    /** Mets file reader. */
    @EJB
    private MetsReader reader;

    /** Identifier browser. */
    @EJB(name = "IdentifierBrowser")
    private IdentifierBrowser identifierBrowser;

    /** Object permission manager. */
    @EJB(name = "ObjectPermissionManager")
    private ObjectPermissionManager objectPermissionManager;

    /** User browser. */
    @EJB(name = "UserBrowser")
    private UserBrowser userBrowser;

    /** HTTPS client helper. */
    @Inject
    private HttpsClientHelper httpsClientHelper;

    /** Generates cache identifiers. */
    @Inject
    private UuidGenerator uuidGenerator;

    /** Module configuration. */
    @Inject
    private ZmkdConfiguration zmkdConfiguration;

    @Override
    @Asynchronous
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public Future<Void> processAll(long planId) {
        MigrationPlanProcessor proxy = ctx.getBusinessObject(MigrationPlanProcessor.class);

        boolean finished = false;

        List<TransformationInfo> path = null;
        try {
            path = planExecutionParser.parseTransformationPath(migrationPathRetriever.retrieveActivePath(planId));
        } catch (InconsistentServiceDescriptionException e) {
            logger.error(e.getMessage(), e);
            finished = true;
        }

        while (!ctx.wasCancelCalled() && !finished) {
            try {
                MigrationProcessingResult migrationProcessingResult = proxy.processOne(planId, path);
                switch (migrationProcessingResult) {
                case PROCESSED:
                    finished = false;
                    break;
                case PAUSED:
                    finished = true;
                    break;
                case FINISHED:
                    finished = true;
                    migrationPlanManager.logFinished(planId);
                    break;
                default:
                    throw new WrdzRuntimeException("Unexpected result: " + migrationProcessingResult);
                }
            } catch (MigrationProcessingException e) {
                logger.error(e.getMessage(), e);
            } catch (MigrationPlanNotFoundException nfe) {
                throw new WrdzRuntimeException("Plan not found", nfe);
            }
        }

        return new AsyncResult<Void>(null);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public MigrationProcessingResult processOne(long planId, List<TransformationInfo> path)
            throws MigrationProcessingException, MigrationPlanNotFoundException {

        MigrationPlan plan = migrationPlanManager.getMigrationPlanById(planId);
        UserDto owner = userBrowser.getUser(plan.getOwnerId());
        if (owner == null) {
            throw new WrdzRuntimeException("Missing owner");
        }

        String objectIdentifier = getCurrentObjectIdentifier(planId);
        if (objectIdentifier == null) {
            return MigrationProcessingResult.FINISHED;
        }

        Long objectId = identifierBrowser.getObjectId(objectIdentifier);
        if (objectId == null) {
            migrationItemManager.logError(planId, objectIdentifier, null);
            throw new MigrationProcessingException(planId, "Object does not exist: " + objectIdentifier);
        }
        if (!objectPermissionManager.hasPermission(owner.getUsername(), objectId,
                ObjectPermissionType.METADATA_UPDATE)) {
            migrationItemManager.logPermissionError(planId, objectIdentifier, null);
            throw new MigrationProcessingException(planId, "Insufficient access rights: " + objectIdentifier);
        }

        migrationItemManager.logMigrationStarted(planId, objectIdentifier);

        HttpClient client = httpsClientHelper.getHttpsClient(WrdzModule.ZMKD);

        HttpGet get = new HttpGet(zmkdConfiguration.getZmdObjectUrl(objectIdentifier));
        HttpResponse response = null;

        File digitalObjectFile;
        File workDir;

        try {

            synchronized (this) {
                response = client.execute(get);
                if (response.getStatusLine().getStatusCode() == HttpStatus.SC_ACCEPTED) {
                    migrationPlanManager.logWaitingForObject(planId, objectIdentifier);
                    return MigrationProcessingResult.PAUSED;
                }
            }

            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                migrationItemManager.logFetchingError(planId, objectIdentifier,
                        response.getStatusLine().getStatusCode() + "");
                throw new MigrationProcessingException(planId,
                        "Could not fetch object from ZMD: " + response.getStatusLine());
            }

            workDir = new File(zmkdConfiguration.getWorkingDirectory(uuidGenerator.generateCacheFolderName()));
            workDir.mkdir();

            digitalObjectFile = httpsClientHelper.storeResponseEntity(workDir, response.getEntity(),
                    response.getFirstHeader("Content-Disposition"));

            ZipUtility.unzip(digitalObjectFile, workDir);

        } catch (IOException e) {
            migrationItemManager.logFetchingError(planId, objectIdentifier, null);
            throw new MigrationProcessingException(planId, "Could not fetch object from ZMD", e);
        } finally {
            if (response != null) {
                EntityUtils.consumeQuietly(response.getEntity());
            }
        }

        DigitalObjectInfo objectInfo = reader.parseMets(workDir, METS_PATH);
        try {
            planExecutionManager.transform(objectInfo, path);
        } catch (TransformationException e) {
            migrationItemManager.logServiceError(planId, objectIdentifier, e.getServiceIri());
            throw new MigrationProcessingException(planId, "Transformation failed", e);
        }

        Map<String, File> targetFiles = new HashMap<String, File>();
        Map<String, Integer> fileSequence = new HashMap<String, Integer>();
        for (DataFileInfo dataFileInfo : objectInfo.getFiles()) {
            String filename = dataFileInfo.getPath();
            if (filename.startsWith(CONTENT_PREFIX)) {
                filename = filename.substring(CONTENT_PREFIX.length());
            }
            targetFiles.put(filename, dataFileInfo.getFile());
            if (dataFileInfo.getSequence() != null) {
                fileSequence.put(filename, dataFileInfo.getSequence());
            }
        }

        MigrationType originType;
        switch (objectInfo.getType()) {
        case MASTER:
            originType = MigrationType.TRANSFORMATION;
            break;
        case OPTIMIZED:
            originType = MigrationType.OPTIMIZATION;
            break;
        case CONVERTED:
            originType = MigrationType.CONVERSION;
            break;
        default:
            throw new WrdzRuntimeException("Unexpected type: " + objectInfo.getType());
        }

        try {
            String requestId = saveObject(client, targetFiles, fileSequence, objectIdentifier, originType);
            migrationItemManager.logUploaded(planId, objectIdentifier, requestId);
        } catch (IOException e) {
            migrationItemManager.logCreationError(planId, objectIdentifier, null);
            throw new MigrationProcessingException(planId, "Upload failed", e);
        }

        return MigrationProcessingResult.PROCESSED;
    }

    /**
     * Creates a new object in ZMD.
     * 
     * @param client
     *            http client instance
     * @param files
     *            map of the new object's files; keys contain the file names/paths inside the new object's structure
     * @param fileSequence
     *            map of the new object's file sequence values; keys contain the file names/paths inside the new
     *            object's structure
     * @param originIdentifier
     *            identifier of the object the new object was migrated from
     * @param originType
     *            the type of migration performed
     * @return creation request identifier
     * @throws IOException
     *             if object upload fails
     */
    private String saveObject(HttpClient client, Map<String, File> files, Map<String, Integer> fileSequence,
            String originIdentifier, MigrationType originType) throws IOException {

        DateFormat format = new SimpleDateFormat(ORIGIN_DATE_FORMAT);

        HttpPost post = new HttpPost(zmkdConfiguration.getZmdObjectUrl());
        MultipartEntity entity = new MultipartEntity();

        try {

            entity.addPart(ORIGIN_ID, new StringBody(originIdentifier, ENCODING));
            entity.addPart(ORIGIN_TYPE, new StringBody(originType.name(), ENCODING));
            entity.addPart(ORIGIN_DATE, new StringBody(format.format(new Date()), ENCODING));

            int i = 0;
            for (Entry<String, File> dataFile : files.entrySet()) {
                FileBody file = new FileBody(dataFile.getValue());
                StringBody path = new StringBody(dataFile.getKey(), ENCODING);

                entity.addPart(String.format(FILE_SRC, i), file);
                entity.addPart(String.format(FILE_DEST, i), path);

                if (fileSequence.containsKey(dataFile.getKey())) {
                    StringBody seq = new StringBody(fileSequence.get(dataFile.getKey()).toString(), ENCODING);
                    entity.addPart(String.format(FILE_SEQ, i), seq);
                }

                i++;
            }
        } catch (UnsupportedEncodingException e) {
            throw new WrdzRuntimeException("The encoding " + ENCODING + " is not supported");
        }

        post.setEntity(entity);

        HttpResponse response = client.execute(post);
        EntityUtils.consumeQuietly(response.getEntity());
        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_ACCEPTED) {
            String location = response.getFirstHeader("location").getValue();
            return location.substring(location.lastIndexOf('/') + 1);
        } else {
            throw new IOException("Unexpected response: " + response.getStatusLine());
        }
    }

    /**
     * Returns the identifier of a digital object to be processed.
     * 
     * If that last object stored in the local has already been processed, this method retrieves the next object's
     * identifier and sets the new status for this identifier before returning it.
     * 
     * @param planId
     *            migration plan identifier
     * @return current the identifier of a digital object to be processed, or <code>null</code> if there are no more
     *         'next' objects
     */
    private String getCurrentObjectIdentifier(long planId) {
        MigrationItemLogFilterFactory filterFactory = migrationItemLogDao.createQueryModifier()
                .getQueryFilterFactory();

        QueryFilter<MigrationItemLog> filterInProgress = filterFactory.and(filterFactory.byMigrationPlan(planId),
                filterFactory.byStatus(MigrationItemStatus.IN_PROGRESS));
        MigrationItemLog migrationItemLogInProgress = migrationItemLogDao.findFirstResultBy(filterInProgress);

        if (migrationItemLogInProgress != null) {
            return migrationItemLogInProgress.getObjectIdentifier();
        }

        QueryFilter<MigrationItemLog> filterNotYetStarted = filterFactory.and(filterFactory.byMigrationPlan(planId),
                filterFactory.byStatus(MigrationItemStatus.NOT_YET_STARTED));

        MigrationItemLog migrationItemLogNotYetStarted = migrationItemLogDao.findFirstResultBy(filterNotYetStarted);
        if (migrationItemLogNotYetStarted != null) {
            return migrationItemLogNotYetStarted.getObjectIdentifier();
        }
        return null;
    }

}