/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, 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/agpl.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 se.streamsource.streamflow.web.application.pdf; import org.apache.commons.lang.StringUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.qi4j.api.common.Optional; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.Uses; import org.qi4j.api.io.Input; import org.qi4j.api.io.Output; import org.qi4j.api.io.Outputs; import org.qi4j.api.io.Receiver; import org.qi4j.api.io.Sender; import org.qi4j.api.io.Transforms; import org.qi4j.api.structure.Module; import org.qi4j.api.unitofwork.UnitOfWork; import org.qi4j.api.util.DateFunctions; import se.streamsource.streamflow.api.administration.form.AttachmentFieldValue; import se.streamsource.streamflow.api.administration.form.DateFieldValue; import se.streamsource.streamflow.api.administration.form.FieldValue; import se.streamsource.streamflow.api.administration.form.GeoLocationFieldValue; import se.streamsource.streamflow.api.administration.form.LocationDTO; import se.streamsource.streamflow.api.workspace.cases.CaseOutputConfigDTO; import se.streamsource.streamflow.api.workspace.cases.contact.ContactAddressDTO; import se.streamsource.streamflow.api.workspace.cases.contact.ContactDTO; import se.streamsource.streamflow.api.workspace.cases.conversation.MessageType; import se.streamsource.streamflow.api.workspace.cases.form.AttachmentFieldSubmission; import se.streamsource.streamflow.api.workspace.cases.general.FormSignatureDTO; import se.streamsource.streamflow.util.Translator; import se.streamsource.streamflow.web.context.workspace.cases.conversation.MessagesContext; import se.streamsource.streamflow.web.domain.Describable; import se.streamsource.streamflow.web.domain.entity.caze.CaseDescriptor; import se.streamsource.streamflow.web.domain.entity.caze.CaseOutput; import se.streamsource.streamflow.web.domain.entity.form.FieldEntity; import se.streamsource.streamflow.web.domain.interaction.gtd.Assignable; import se.streamsource.streamflow.web.domain.interaction.gtd.Assignee; import se.streamsource.streamflow.web.domain.interaction.gtd.CaseId; import se.streamsource.streamflow.web.domain.interaction.gtd.DueOn; import se.streamsource.streamflow.web.domain.interaction.gtd.Ownable; import se.streamsource.streamflow.web.domain.interaction.gtd.Owner; import se.streamsource.streamflow.web.domain.structure.SubmittedFieldValue; import se.streamsource.streamflow.web.domain.structure.attachment.AttachedFile; import se.streamsource.streamflow.web.domain.structure.attachment.Attachment; import se.streamsource.streamflow.web.domain.structure.caselog.CaseLogEntryValue; import se.streamsource.streamflow.web.domain.structure.casetype.CaseType; import se.streamsource.streamflow.web.domain.structure.casetype.Resolution; import se.streamsource.streamflow.web.domain.structure.casetype.Resolvable; import se.streamsource.streamflow.web.domain.structure.casetype.TypedCase; import se.streamsource.streamflow.web.domain.structure.caze.Case; import se.streamsource.streamflow.web.domain.structure.caze.CasePriority; import se.streamsource.streamflow.web.domain.structure.caze.Notes; import se.streamsource.streamflow.web.domain.structure.conversation.Conversation; import se.streamsource.streamflow.web.domain.structure.conversation.Message; import se.streamsource.streamflow.web.domain.structure.conversation.Messages; import se.streamsource.streamflow.web.domain.structure.created.Creator; import se.streamsource.streamflow.web.domain.structure.form.SubmittedFormValue; import se.streamsource.streamflow.web.domain.structure.form.SubmittedPageValue; import se.streamsource.streamflow.web.domain.structure.label.Label; import se.streamsource.streamflow.web.domain.structure.label.Labelable; import se.streamsource.streamflow.web.infrastructure.attachment.AttachmentStore; import java.awt.Color; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import static se.streamsource.streamflow.util.Strings.*; /** * A specialisation of CaseOutput that is responsible for exporting a case in * PDF format; The provided configuration tells what parts of the case are * included in the export. */ public class CasePdfGenerator implements CaseOutput { @Structure Module module; @Service AttachmentStore store; private PdfDocument document; private ResourceBundle bundle; private final CaseOutputConfigDTO config; private Locale locale; private String templateUri; private PdfFont h1Font = new PdfFont( PDType1Font.HELVETICA_BOLD, 16 ); private PdfFont valueFont = new PdfFont( PDType1Font.HELVETICA, 12 ); private PdfFont valueFontBold = new PdfFont( PDType1Font.HELVETICA_BOLD, 12 ); private PdfFont valueFontBoldItalic = new PdfFont( PDType1Font.HELVETICA_BOLD_OBLIQUE, 12 ); private PdfFont headerFont = new PdfFont( PDType1Font.HELVETICA, 10 ); private String caseId = ""; private String printedOn = ""; private Color headingColor = new Color(0x4b,0x89,0xd0); public CasePdfGenerator(@Uses CaseOutputConfigDTO config, @Optional @Uses String templateUri, @Uses Locale locale, @Uses PdfDocument document) { this.config = config; this.locale = locale; this.templateUri = templateUri; bundle = ResourceBundle.getBundle( CasePdfGenerator.class.getName(), locale ); this.document = document; document.init(); } public void outputCase( CaseDescriptor cazeDescriptor ) throws Throwable { Case caze = cazeDescriptor.getCase(); caseId = ((CaseId.Data) caze).caseId().get(); printedOn = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, locale ).format( new Date() ); document.print( "", valueFont ).changeColor( headingColor ) .println( bundle.getString( "caseSummary" ) + " - " + caseId, h1Font ).line(headingColor).changeColor( Color.BLACK ) .print( "", valueFont ); float tabStop = document.calculateTabStop( valueFontBold, bundle.getString( "title" ), bundle.getString( "createdOn" ), bundle.getString( "createdBy" ), bundle.getString( "owner" ), bundle.getString( "assignedTo" ), bundle.getString( "caseType" ), bundle.getString( "labels" ), bundle.getString( "resolution" ), bundle.getString( "dueOn" ), bundle.getString( "priority" ) ); if (StringUtils.isNotEmpty(caze.getDescription())) { document.printLabelAndTextWithTabStop(bundle.getString("title") + ": ", valueFontBold, caze.getDescription(), valueFont, tabStop); } if (((CasePriority.Data)caze).casepriority().get() != null) { document.printLabelAndTextWithTabStop( bundle.getString( "priority" ) + ": ", valueFontBold, ((CasePriority.Data)caze).casepriority().get().getDescription(), valueFont, tabStop ); } if (caze.createdOn().get() != null) { document.printLabelAndTextWithTabStop(bundle.getString("createdOn") + ": ", valueFontBold, DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale).format(caze.createdOn().get()), valueFont, tabStop); } if (((DueOn.Data) caze).dueOn().get() != null) { document.printLabelAndTextWithTabStop( bundle.getString( "dueOn" ) + ": ", valueFontBold, new SimpleDateFormat( bundle.getString( "date_format" ) ).format( ((DueOn.Data) caze).dueOn().get() ), valueFont, tabStop ); } Creator creator = caze.createdBy().get(); if (creator != null) { document.printLabelAndTextWithTabStop( bundle.getString( "createdBy" ) + ": ", valueFontBold, ((Describable) creator).getDescription(), valueFont, tabStop ); } Owner owner = ((Ownable.Data) caze).owner().get(); if (owner != null) { document.printLabelAndTextWithTabStop( bundle.getString( "owner" ) + ": ", valueFontBold, ((Describable) owner).getDescription(), valueFont, tabStop ); } Assignee assignee = ((Assignable.Data) caze).assignedTo().get(); if (assignee != null) { document.printLabelAndTextWithTabStop( bundle.getString( "assignedTo" ) + ": ", valueFontBold, ((Describable) assignee).getDescription(), valueFont, tabStop ); } CaseType caseType = ((TypedCase.Data) caze).caseType().get(); if (caseType != null) { document.printLabelAndTextWithTabStop( bundle.getString( "caseType" ) + ": ", valueFontBold, ((Describable) caseType).getDescription(), valueFont, tabStop ); } Resolution resolution = ((Resolvable.Data) caze).resolution().get(); if (resolution != null) { document.printLabelAndTextWithTabStop( bundle.getString( "resolution" ) + ":", valueFontBold, ((Describable) resolution).getDescription(), valueFont, tabStop ); } List<Label> labels = ((Labelable.Data) caze).labels().toList(); if (!labels.isEmpty()) { String allLabels = ""; int count = 0; for (Label label : labels) { count++; allLabels += label.getDescription() + (count == labels.size() ? "" : ", "); } document.printLabelAndTextWithTabStop( bundle.getString( "labels" ) + ": ", valueFontBold, allLabels, valueFont, tabStop ); } String note = ((Notes)caze).getLastNote() != null ? ((Notes)caze).getLastNote().note().get() : ""; if (!empty(note)) { if( Translator.HTML.equalsIgnoreCase( ((Notes) caze).getLastNote().contentType().get() )) { note = Translator.htmlToText( note ); } document.moveUpOneRow( valueFontBold ).print( bundle.getString( "note" ) + ":", valueFontBold ).print( note, valueFont ).print( "", valueFont ); } // traverse structure if (config.contacts().get()) { generateContacts( cazeDescriptor.contacts() ); } if (config.submittedForms().get()) { generateSubmittedForms( cazeDescriptor.submittedForms() ); } if (config.conversations().get()) { generateConversations( cazeDescriptor.conversations() ); } if (config.attachments().get()) { generateAttachments( cazeDescriptor.attachments() ); } if (config.caselog().get()) { generateCaselog(cazeDescriptor.caselog()); } } private void generateCaselog(Input<CaseLogEntryValue, RuntimeException> caselog) throws IOException { // TODO This needs to be cleaned up. Translations should be in a better place! ResourceBundle bnd = ResourceBundle.getBundle( MessagesContext.class.getName(), locale ); final Map<String, String> translations = new HashMap<String, String>(); for (String key : bnd.keySet()) { translations.put(key, bnd.getString(key)); } caselog.transferTo(new Output<CaseLogEntryValue, IOException>() { public <SenderThrowableType extends Throwable> void receiveFrom(Sender<? extends CaseLogEntryValue, SenderThrowableType> sender) throws IOException, SenderThrowableType { document.changeColor( headingColor ).println( bundle.getString( "caselog" ), valueFontBold ) .changeColor(Color.BLACK); sender.sendTo(new Receiver<CaseLogEntryValue, IOException>() { public void receive(CaseLogEntryValue entry) throws IOException { UnitOfWork uow = module.unitOfWorkFactory().currentUnitOfWork(); String label = uow.get( Describable.class, entry.createdBy().get().identity()).getDescription() + ", " + DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, locale ).format( entry.createdOn().get() ) + ": "; document.print( label, valueFontBold ).print( Translator.translate( entry.message().get(), translations ), valueFont ) .print("", valueFont); } }); } }); } private void generateContacts(Input<ContactDTO, RuntimeException> contacts) throws IOException { final Transforms.Counter<ContactDTO> counter = new Transforms.Counter<ContactDTO>(); contacts.transferTo(Transforms.map(counter, new Output<ContactDTO, IOException>() { public <SenderThrowableType extends Throwable> void receiveFrom(Sender<? extends ContactDTO, SenderThrowableType> sender) throws IOException, SenderThrowableType { sender.sendTo(new Receiver<ContactDTO, IOException>() { public void receive(ContactDTO value) throws IOException { Map<String, String> nameValuePairs = new LinkedHashMap<String, String>( 10 ); if (!empty(value.name().get())) nameValuePairs.put( bundle.getString( "name" ), value.name().get() ); if (!value.phoneNumbers().get().isEmpty() && !empty(value.phoneNumbers().get().get(0).phoneNumber().get())) nameValuePairs.put( bundle.getString( "phoneNumber" ), value.phoneNumbers().get().get( 0 ) .phoneNumber().get() ); if (!value.addresses().get().isEmpty()) { ContactAddressDTO address = value.addresses().get().get( 0 ); if (!empty(address.address().get())) nameValuePairs.put( bundle.getString( "address" ), address.address().get() ); if (!empty(address.zipCode().get())) nameValuePairs.put( bundle.getString( "zipCode" ), address.zipCode().get() ); if (!empty(address.city().get())) nameValuePairs.put( bundle.getString( "city" ), address.city().get() ); if (!empty(address.region().get())) nameValuePairs.put( bundle.getString( "region" ), address.region().get() ); if (!empty(address.country().get())) nameValuePairs.put( bundle.getString( "country" ), address.country().get() ); } if (!value.emailAddresses().get().isEmpty() && !empty(value.emailAddresses().get().get(0).emailAddress().get())) nameValuePairs.put( bundle.getString( "email" ), value.emailAddresses().get().get( 0 ).emailAddress() .get() ); if (!empty(value.contactId().get())) nameValuePairs.put( bundle.getString( "contactID" ), value.contactId().get() ); if (!empty(value.company().get())) nameValuePairs.put( bundle.getString( "company" ), value.company().get() ); if (!empty(value.note().get())) nameValuePairs.put( bundle.getString( "note" ), value.note().get() ); float tabStop = document.calculateTabStop( valueFontBold, nameValuePairs.keySet().toArray( new String[nameValuePairs.keySet().size()] ) ); if (!nameValuePairs.entrySet().isEmpty()) { document.changeColor( headingColor ); document.print( bundle.getString( "contact" ) + (counter.getCount() == 1 ? "" : " " + counter.getCount()), valueFontBold ); document.changeColor( Color.BLACK ).print( "", valueFont ); } for (Map.Entry<String, String> stringEntry : nameValuePairs.entrySet()) { document.printLabelAndTextWithTabStop( stringEntry.getKey() + ":", valueFontBold, stringEntry.getValue(), valueFont, tabStop ); } } } ); } } ) ); } public void generateConversations( Input<Conversation, RuntimeException> conversations ) throws IOException { final Transforms.Counter<Conversation> counter = new Transforms.Counter<Conversation>(); Output<Conversation, IOException> output = Transforms.map( counter, new Output<Conversation, IOException>() { public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends Conversation, SenderThrowableType> sender ) throws IOException, SenderThrowableType { sender.sendTo( new Receiver<Conversation, IOException>() { public void receive( Conversation conversation ) throws IOException { if (counter.getCount() == 1) { document.changeColor( headingColor ).print( bundle.getString( "conversations" ), valueFontBold ) .changeColor( Color.BLACK ); } List<Message> messages = ((Messages.Data) conversation).messages().toList(); if (!messages.isEmpty()) { document.print( conversation.getDescription(), valueFontBold ); for (Message message : messages) { Message.Data data = ((Message.Data) message); String label = data.sender().get().getDescription() + ", " + DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, locale ).format( data.createdOn().get() ) + ": "; String text = data.body().get(); if(MessageType.HTML.equals( data.messageType().get() )) { text = Translator.htmlToText( text ); } document.print( label, valueFontBold ).print( text, valueFont ) .print( "", valueFont ); } } } } ); } } ); conversations.transferTo( output ); } public void generateSubmittedForms( Input<SubmittedFormValue, RuntimeException> submittedForms ) throws IOException { ArrayList<SubmittedFormValue> formValues = new ArrayList<SubmittedFormValue>(); submittedForms.transferTo( Outputs.collection( formValues ) ); // Latest forms first please Collections.reverse( formValues ); Set<EntityReference> printedForms = new HashSet<EntityReference>(); boolean printedHeader = false; for (SubmittedFormValue formValue : formValues) { if (printedForms.contains( formValue.form().get() )) continue; // Skip this form - already printed Describable form = module.unitOfWorkFactory().currentUnitOfWork().get( Describable.class, formValue.form().get().identity() ); // Form PDF section header if (!printedHeader) { document.changeColor( headingColor ); document.print( bundle.getString( "submittedForms" ) + ":", valueFontBold ); document.changeColor( Color.BLACK ); printedHeader = true; } document.print( form.getDescription() + ":", valueFontBold ).print( "", valueFontBold ).print( "", valueFontBold ); float tabStop = document.calculateTabStop( valueFontBold, new String[]{bundle.getString( "lastSubmitted" ), bundle.getString( "lastSubmittedBy" )} ); // Submitted by whom and when document.printLabelAndTextWithTabStop( bundle.getString( "lastSubmitted" ) + ":", valueFontBold, DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, locale ).format( formValue.submissionDate().get() ), valueFont, tabStop ); document.printLabelAndTextWithTabStop( bundle.getString( "lastSubmittedBy" ) + ":", valueFontBold, module.unitOfWorkFactory().currentUnitOfWork().get( Describable.class, formValue.submitter().get().identity() ).getDescription(), valueFont, tabStop ); if (formValue.signatures().get() != null && !formValue.signatures().get().isEmpty()) { FormSignatureDTO signature = formValue.signatures().get().get( 0 ); document.printLabelAndTextWithTabStop( bundle.getString( "signedBy" ) + ":", valueFontBold, signature.signerName().get() + " (" + signature.signerId().get() + ")", valueFont, tabStop ) ; } for (SubmittedPageValue submittedPageValue : formValue.pages().get()) { if (!submittedPageValue.fields().get().isEmpty()) { Describable page = module.unitOfWorkFactory().currentUnitOfWork().get( Describable.class, submittedPageValue.page().get().identity() ); document.print( page.getDescription() + ( page.getDescription().trim().endsWith( ":" ) ? "" : ":" ), valueFontBoldItalic ); document.print( "", valueFont ); // Fetch and occasionally format field name and value for (SubmittedFieldValue submittedFieldValue : submittedPageValue.fields().get()) { FieldValue fieldValue = module.unitOfWorkFactory().currentUnitOfWork().get( FieldEntity.class, submittedFieldValue.field().get().identity() ) .fieldValue().get(); if (!empty(submittedFieldValue.value().get())) { String label = module.unitOfWorkFactory().currentUnitOfWork().get( Describable.class, submittedFieldValue.field().get().identity() ) .getDescription(); String value = ""; // convert JSON String if field type AttachmentFieldValue if (fieldValue instanceof AttachmentFieldValue) { AttachmentFieldSubmission attachment = module.valueBuilderFactory().newValueFromJSON(AttachmentFieldSubmission.class, submittedFieldValue .value().get()); value = attachment.name().get(); } else if (fieldValue instanceof DateFieldValue && !empty(submittedFieldValue.value().get())) { value = new SimpleDateFormat( bundle.getString( "date_format" ) ).format( DateFunctions .fromString( submittedFieldValue.value().get() ) ); } else if ( fieldValue instanceof GeoLocationFieldValue ) { LocationDTO locationDTO = module.valueBuilderFactory().newValueFromJSON( LocationDTO.class, submittedFieldValue.value().get() ); value = locationDTO.street().get() + ", " + locationDTO.zipcode().get() + ", " + locationDTO.city().get(); // String locationString = locationDTO.location().get(); // if (locationString != null) { // locationString = locationString.replace( ' ', '+' ); // if (locationString.contains( "(" )) { // String[] positions = locationString.split( "\\),\\("); // locationString = positions[0].substring( 1, positions[0].length() -1 ); // } // } // text += "<a href=\"http://maps.google.com/maps?z=13&t=m&q=" + locationString + "\" alt=\"Google Maps\">Klicka här för att visa karta</a>"; } else { value = submittedFieldValue.value().get(); } document.printLabelAndIndentedText( label + ( label.trim().endsWith( ":" ) ? "" : ":" ), valueFontBold, value, valueFont, 20.0f ); } } document.print( "", valueFont ); } } printedForms.add( formValue.form().get() ); } } public void generateAttachments( Input<Attachment, RuntimeException> attachments ) throws IOException { final Transforms.Counter<Attachment> counter = new Transforms.Counter<Attachment>(); attachments.transferTo( Transforms.map( counter, new Output<Attachment, IOException>() { public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends Attachment, SenderThrowableType> sender ) throws IOException, SenderThrowableType { sender.sendTo( new Receiver<Attachment, IOException>() { public void receive( Attachment attachment ) throws IOException { if (counter.getCount() == 1) { document.changeColor( headingColor ).print( bundle.getString( "attachments" ) + ":", valueFontBold ) .changeColor( Color.BLACK ); } document.print( ((AttachedFile.Data) attachment).name().get(), valueFont ); /* TODO Fix image insert. For some reason adding images to a PDF doesn't seem to work if (((AttachedFile.Data) attachment).mimeType().get().startsWith("image/")) { try { store.attachment(((AttachedFile.Data) attachment).uri().get(), new Visitor<InputStream, IOException>() { public boolean visit(InputStream visited) throws IOException { BufferedImage image = ImageIO.read(visited); document.print("Image insert", valueFont); document.insertImage(image); document.print("Image inserted", valueFont); return true; } }); } catch (IOException e) { LoggerFactory.getLogger(getClass()).warn("Could not insert image into generated PDF", e); } } */ } } ); } } ) ); } public PDDocument getPdf() throws IOException { document.closeAndReturn(); PDDocument generatedDoc = document.generateHeaderAndPageNumbers( headerFont, caseId, bundle.getString( "printDate" ) + ": " + printedOn ); generatedDoc.getDocumentInformation().setCreator( "Streamflow" ); Calendar calendar = Calendar.getInstance(); generatedDoc.getDocumentInformation().setCreationDate( calendar ); generatedDoc.getDocumentInformation().setTitle( caseId ); if (templateUri != null) { String attachmentId; try { attachmentId = new URI( templateUri ).getSchemeSpecificPart(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); store.attachment(attachmentId).transferTo(Outputs.byteBuffer(baos)); Underlay underlay = new Underlay(); generatedDoc = underlay.underlay(generatedDoc, new ByteArrayInputStream(baos.toByteArray())); } catch (Exception e) { e.printStackTrace(); } } return generatedDoc; } }