Java tutorial
/******************************************************************************* * 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); } } }