org.opentestsystem.authoring.testauth.publish.PublisherRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.opentestsystem.authoring.testauth.publish.PublisherRunner.java

Source

/*******************************************************************************
 * Educational Online Test Delivery System
 * Copyright (c) 2013 American Institutes for Research
 * 
 * Distributed under the AIR Open Source License, Version 1.0
 * See accompanying file AIR-License-1_0.txt or at
 * http://www.smarterapp.org/documents/American_Institutes_for_Research_Open_Source_Software_License.pdf
 ******************************************************************************/
package org.opentestsystem.authoring.testauth.publish;

import static org.opentestsystem.authoring.testauth.config.TestAuthUtil.paramArray;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.zip.Deflater;

import javax.xml.bind.JAXBException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.joda.time.DateTime;
import org.opentestsystem.authoring.testauth.domain.Approval;
import org.opentestsystem.authoring.testauth.domain.ApprovalStatus;
import org.opentestsystem.authoring.testauth.domain.Assessment;
import org.opentestsystem.authoring.testauth.domain.Permissions;
import org.opentestsystem.authoring.testauth.domain.PublishingRecord;
import org.opentestsystem.authoring.testauth.domain.PublishingStatus;
import org.opentestsystem.authoring.testauth.persistence.ApprovalRepository;
import org.opentestsystem.authoring.testauth.persistence.AssessmentRepository;
import org.opentestsystem.authoring.testauth.persistence.PublishingRecordRepository;
import org.opentestsystem.authoring.testauth.publish.domain.Purpose;
import org.opentestsystem.authoring.testauth.publish.domain.PurposeBaseContent;
import org.opentestsystem.authoring.testauth.publish.domain.TestSpecification;
import org.opentestsystem.authoring.testauth.service.ApprovalService;
import org.opentestsystem.authoring.testspecbank.client.TestSpecBankClientInterface;
import org.opentestsystem.authoring.testspecbank.client.domain.TestSpecBankClientObj;
import org.opentestsystem.shared.exception.LocalizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.xml.sax.SAXException;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

@Component
public class PublisherRunner {
    private static final Logger LOGGER = LoggerFactory.getLogger(PublisherRunner.class);
    public static final int BUFFER_SIZE = 4096;

    @Autowired
    @Qualifier("registrationPublisherHelper")
    private PublisherHelper registrationPublisherHelper;

    @Autowired
    @Qualifier("administrationPublisherHelper")
    private PublisherHelper administrationPublisherHelper;

    @Autowired
    @Qualifier("scoringPublisherHelper")
    private PublisherHelper scoringPublisherHelper;

    @Autowired
    @Qualifier("reportingPublisherHelper")
    private PublisherHelper reportingPublisherHelper;

    @Autowired
    @Qualifier("completePublisherHelper")
    private PublisherHelper completePublisherHelper;

    @Autowired
    private transient PublishingRecordRepository publishingRecordRepository;

    @Autowired
    private transient AssessmentRepository assessmentRepository;

    @Autowired
    private MessageSource messageSource;

    @Autowired
    private TestSpecBankClientInterface testSpecPublisher;

    @Autowired
    private transient ApprovalService approvalService;

    @Autowired
    private transient ApprovalRepository approvalRepository;

    @Value("${testauth.dtd.validation}")
    private String testSpecDtdValidation;

    @Value("${testauth.dtd.url}")
    private String testSpecDtdUrl;

    private static final Predicate<Approval> QA_LEAD_FILTER = new Predicate<Approval>() {
        @Override
        public boolean apply(final Approval approval) {
            return approval != null && approval.getPermission() != null
                    && Permissions.QA_LEAD.toSpringRoleName().equals(approval.getPermission());
        }
    };

    @Async
    public void publishAndSend(final Assessment assessment, final PublishingRecord publishingRecord) {
        final List<Purpose> specTypesToPublish = Lists.newArrayList(publishingRecord.getPurpose());
        try {
            TestSpecification<? extends PurposeBaseContent> seedingTestSpec = null;
            if (specTypesToPublish.contains(Purpose.COMPLETE)) {
                seedingTestSpec = createTestSpec(assessment, publishingRecord, Purpose.COMPLETE, false, null);
                final byte[] processedXml = generateAndValidateXml(seedingTestSpec);
                sendToTestSpecBank(seedingTestSpec, processedXml, assessment);
            }
            if (specTypesToPublish.contains(Purpose.ADMINISTRATION)) {
                final TestSpecification<? extends PurposeBaseContent> adminTestSpec = createTestSpec(assessment,
                        publishingRecord, Purpose.ADMINISTRATION, false, seedingTestSpec);
                final byte[] processedXml = generateAndValidateXml(adminTestSpec);
                sendToTestSpecBank(adminTestSpec, processedXml, assessment);
                if (!specTypesToPublish.contains(Purpose.COMPLETE)) {
                    seedingTestSpec = adminTestSpec;
                } else {
                    specTypesToPublish.remove(Purpose.COMPLETE);
                }
                specTypesToPublish.remove(Purpose.ADMINISTRATION);
            }
            for (final Purpose purpose : specTypesToPublish) {
                final TestSpecification<? extends PurposeBaseContent> testSpec = createTestSpec(assessment,
                        publishingRecord, purpose, false, seedingTestSpec);
                final byte[] processedXml = generateAndValidateXml(testSpec);
                sendToTestSpecBank(testSpec, processedXml, assessment);
            }

            publishingRecord.setPublishingStatus(PublishingStatus.PUBLISHED);
            publishingRecord.setErrorMessageText("");
            publishingRecord.setLastUpdatedDate(new DateTime());
            this.publishingRecordRepository.save(publishingRecord);

        } catch (final Exception e) {
            // publishing failed somewhere in creating elements for XML, marshalling, validating, or sending to TSB
            publishingRecord.setPublishingStatus(PublishingStatus.AWAITING_APPROVAL);

            // attempt to render the error code from within the publisher helpers/tsb/validation into a user-readable message
            String messageText = "Specification XML could not be published";
            if (e instanceof LocalizedException) {
                final LocalizedException le = (LocalizedException) e;
                messageText = this.messageSource.getMessage(le.getMessageCode(), le.getMessageArgs(), Locale.US);
            }
            publishingRecord.setErrorMessageText(messageText);
            publishingRecord.setLastUpdatedDate(new DateTime());
            this.publishingRecordRepository.save(publishingRecord);

            final List<Approval> approvalList = this.approvalService
                    .retrieveLatestApprovals(publishingRecord.getId());
            final Approval qaLeadApproval = Iterables.find(approvalList, QA_LEAD_FILTER);
            if (qaLeadApproval != null) {
                qaLeadApproval.setStatus(ApprovalStatus.PENDING);
                this.approvalRepository.save(qaLeadApproval);
            }

            LOGGER.error("Test Specification XML publishing failed; error: ["
                    + publishingRecord.getErrorMessageText() + "]", e);
        } finally {
            assessment.setStatus(publishingRecord.getPublishingStatus());
            this.assessmentRepository.save(assessment);
        }
    }

    public byte[] generateAndValidateXml(final TestSpecification<? extends PurposeBaseContent> testSpec) {
        final long startTime = System.currentTimeMillis();
        long marshalFinishTime = 0L;
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            PublisherSingletons.getInstance(this.testSpecDtdUrl).getJaxbMarshaller().marshal(testSpec, baos);
            marshalFinishTime = System.currentTimeMillis();
        } catch (final JAXBException e) {
            throw new LocalizedException("publishingRecord.testspec.xml.marshal", paramArray(e.getMessage()), e);
        }
        final byte[] generatedXml = baos.toByteArray();
        if (Boolean.valueOf(this.testSpecDtdValidation)) {
            validateXmlWithDtd(generatedXml);
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("testspecification XML marshalling duration: " + (marshalFinishTime - startTime) + "ms\n"
                    + "testspecification XML validation duration: "
                    + (System.currentTimeMillis() - marshalFinishTime) + "ms\n"
                    + "testspecification XML total duration: " + (System.currentTimeMillis() - startTime) + "ms");
        }
        return generatedXml;
    }

    public TestSpecification<? extends PurposeBaseContent> createTestSpec(final Assessment assessment,
            final PublishingRecord publishingRecord, final Purpose purpose, final boolean isSimulation,
            final TestSpecification<? extends PurposeBaseContent> seedingTestSpec) {
        final Purpose purposeToUse = isSimulation ? Purpose.SIMULATION : purpose;
        switch (purpose) {
        case REGISTRATION:
            return this.registrationPublisherHelper.createTestSpec(assessment,
                    publishingRecord.getLastUpdatedDate(), publishingRecord.getVersion(), purposeToUse,
                    seedingTestSpec);

        case ADMINISTRATION:
            return this.administrationPublisherHelper.createTestSpec(assessment,
                    publishingRecord.getLastUpdatedDate(), publishingRecord.getVersion(), purposeToUse,
                    seedingTestSpec);

        case SCORING:
            return this.scoringPublisherHelper.createTestSpec(assessment, publishingRecord.getLastUpdatedDate(),
                    publishingRecord.getVersion(), purposeToUse, seedingTestSpec);

        case REPORTING:
            return this.reportingPublisherHelper.createTestSpec(assessment, publishingRecord.getLastUpdatedDate(),
                    publishingRecord.getVersion(), purposeToUse, seedingTestSpec);

        case COMPLETE:
            return this.completePublisherHelper.createTestSpec(assessment, publishingRecord.getLastUpdatedDate(),
                    publishingRecord.getVersion(), purposeToUse, seedingTestSpec);

        default:
            throw new LocalizedException("publishingRecord.testspec.unsupported.spec.type",
                    new String[] { Arrays.toString(publishingRecord.getPurpose()) });
        }
    }

    private void validateXmlWithDtd(final byte[] testSpecificationXml) {
        try {
            // since the XML is created internally and doesn't contain a DOCTYPE, it is added here for validation
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PublisherSingletons.getInstance(this.testSpecDtdUrl).getDocumentTransformer().transform(
                    new StreamSource(new ByteArrayInputStream(testSpecificationXml)), new StreamResult(baos));
            // re-parse the transformed file, this time with DOCTYPE pointing at the DTD, to check validity
            PublisherSingletons.getInstance(this.testSpecDtdUrl).getDocumentBuilder()
                    .parse(new ByteArrayInputStream(baos.toByteArray()));
        } catch (SAXException | TransformerException | IOException e) {
            throw new LocalizedException("publishingRecord.testspec.xml.invalid", paramArray(e.getMessage()), e);
        }
    }

    private byte[] compress(final byte[] testSpecificationXml) {
        final Deflater deflater = new Deflater();
        deflater.setInput(testSpecificationXml);
        final ByteArrayOutputStream baos = new ByteArrayOutputStream(testSpecificationXml.length);

        deflater.finish();

        byte[] outBytes = null;

        try {
            final byte[] buffer = new byte[BUFFER_SIZE];
            while (!deflater.finished()) {
                final int count = deflater.deflate(buffer);
                baos.write(buffer, 0, count);
            }
            baos.close();
            outBytes = baos.toByteArray();

        } catch (final IOException e) {
            throw new LocalizedException("publishingRecord.testspec.xml.compress.error", e);
        }

        return outBytes;
    }

    private void sendToTestSpecBank(final TestSpecification<? extends PurposeBaseContent> testSpec,
            final byte[] validatedXml, final Assessment assessment) {
        // all fields in the TSB client object are required
        final TestSpecBankClientObj testSpecBankClientObj = new TestSpecBankClientObj();
        testSpecBankClientObj.setName(testSpec.getIdentifier().getName());
        testSpecBankClientObj.setVersion(testSpec.getIdentifier().getVersion());
        testSpecBankClientObj.setTenantId(assessment.getTenantId());
        testSpecBankClientObj.setSubjectAbbreviation(assessment.getSubject().getAbbreviation());
        testSpecBankClientObj.setSubjectName(assessment.getSubject().getName());
        testSpecBankClientObj.setLabel(assessment.getLabel());
        testSpecBankClientObj.setType(assessment.getType().getCode());
        testSpecBankClientObj.setCategory(assessment.getCategory());
        testSpecBankClientObj.setGrade(assessment.getGrade());
        testSpecBankClientObj
                .setPurpose(org.opentestsystem.authoring.testspecbank.client.domain.TestSpecBankClientObj.Purpose
                        .valueOf(testSpec.getPurpose()));
        // store XML to be saved, and validate (w/ injecting DTD) separately
        testSpecBankClientObj.setSpecificationXml(compress(validatedXml));

        try {
            this.testSpecPublisher.publishTestSpecification(testSpecBankClientObj);
        } catch (final Exception e) {
            String messageText = "Test Spec Bank is unavailable";
            if (e instanceof HttpClientErrorException) {
                final HttpClientErrorException hcee = (HttpClientErrorException) e;
                messageText = hcee.getStatusCode().equals(HttpStatus.NOT_FOUND) ? messageText
                        : hcee.getStatusText();
                messageText = messageText + " (" + hcee.getStatusCode() + ")";
            }
            throw new LocalizedException("publishingRecord.testspec.unexpected.response", paramArray(messageText),
                    e);
        }
    }
}