Tuesday, September 23, 2014

Multiple Document Upload

Hi.

I faced one requirement where we need to allow the user to upload more than one document in one go. I am using 11.1.2.4 and in this release the following feature is not available:

http://andrejusb.blogspot.com/2013/04/multiple-file-upload-unlimited-file.html

So, if you are using a release earlier than PS6, or not using 12C yet, the only option left is to do it programatically. This blog explains how to do that in detail. The application that I created looks like this:


In the above figure:

1. BaseProject: Contains the interface UploadedFileDetails used in Model & ViewController project. This interface defines common method used to fetch uploaded file metadata.

2. Model Project: Contains the BC4J components, like DocumentEO, DocumentVO and AM used by the View layer to insert document to the DB.

3. ViewController Project: Contains the UI page, MultipleDocumentUpload.jspx, its managed bean MultipleDocumentUpload, and FileUpload class that stores the uploaded file before it is moved to the DB.

 When the MultipleDocumentUpload.jspx page is run, the UI looks like this:



The 'Add File' button allows you to add one more input file component, and 'Remove File' component removed the recently added input file component. Lets add another input file component to upload 2 files at once so click 'Add File' button once and select some files to upload in both the input file components, as shown below:



Now, click the 'insert' button to insert the records in the DB. The table gets populated with 2 records as shown below:



Now, since the documents are in the DB, you can use the download button to download each uploaded file.

This way, you can upload as many files as you want at run time.

Here is the code to implement this functionality:

The DOCUMENT table looks like this:

CREATE TABLE document (
  document_id   NUMBER        NOT NULL,
  document_name VARCHAR2(200) NULL,
  document_type VARCHAR2(200) NULL,
  document      BLOB          NULL
);

And here is DOCUMENT_SEQ used to generate unique document IDs:

CREATE SEQUENCE document_seq
  MINVALUE 1
  MAXVALUE 9999999999999999999999999999
  INCREMENT BY 1
  NOCYCLE
  NOORDER
  NOCACHE
/

Now, in the BaseProject I have defined an interface, UploadedFileDetails, that is used both in Model and ViewController project to fetch uploaded file details:

package base.interfaces;

import oracle.jbo.domain.BlobDomain;

public interface UploadedFileDetails {
   
    public String getFileName();
    
    public String getFileType();
    
    public Long getFileLength();
    
    public BlobDomain getFileContents();
    
}

In the Model project, I created DocumentEO over DOCUMENT table, DocumentVO based on DocumentEO, and AppModule application module. The following method is added to DocumentEOImpl to generate the document ID from sequence:

package model.entity;
...
public class DepartmentEOImpl extends EntityImpl {
...
    protected void create(oracle.jbo.AttributeList attributeList) { 
          super.create(attributeList);
          for (AttributeDef def : getEntityDef().getAttributeDefs()) {
              String sequenceName = (String)def.getProperty("SequenceName");
              if (sequenceName != null) {
                  SequenceImpl s =
                      new SequenceImpl(sequenceName, getDBTransaction());
                  populateAttributeAsChanged(def.getIndex(),
                                             s.getSequenceNumber());
              }
          }
    }
}

And on DocumentEO's DocumentID attribute, the following non-translatable property is added:


The AppModuleImpl looks like this:

package model.am;

import base.interfaces.UploadedFileDetails;

import java.util.ArrayList;

import model.am.common.AppModule;

import oracle.jbo.Row;
import oracle.jbo.domain.BlobDomain;
import oracle.jbo.server.ApplicationModuleImpl;
import oracle.jbo.server.ViewObjectImpl;
// ---------------------------------------------------------------------
// ---    File generated by Oracle ADF Business Components Design Time.
// ---    Sun Sep 21 21:44:29 PDT 2014
// ---    Custom code may be added to this class.
// ---    Warning: Do not modify method signatures of generated methods.
// ---------------------------------------------------------------------
public class AppModuleImpl extends ApplicationModuleImpl implements AppModule {
    /**
     * This is the default constructor (do not remove).
     */
    public AppModuleImpl() {
    }

    /**
     * Container's getter for DepartmentVO1.
     * @return DepartmentVO1
     */
    public ViewObjectImpl getDepartmentVO1() {
        return (ViewObjectImpl)findViewObject("DepartmentVO1");
    }
    
    public void insertDocumentDetails(ArrayList documentList){
        
        ViewObjectImpl departmentVO = getDepartmentVO1() ;
        
        for(int i = 0; i < documentList.size(); i++ ){
            UploadedFileDetails uploadedFile = ((UploadedFileDetails)documentList.get(i));
            if(uploadedFile != null){
                Long uploadedFileLength = uploadedFile.getFileLength();
                System.out.println("Lalit >> Uploaded File Index = " + i);
                if(uploadedFileLength > 0){
                    System.out.println("Lalit >> Uploaded File Index = " + i);
                    System.out.println("Lalit >> Uploaded File Length = " + uploadedFileLength);
                    System.out.println("LALIT: Uploaded File Name = " + uploadedFile.getFileName()  );
                    System.out.println("LALIT: Uploaded File Type = " + uploadedFile.getFileType()  );
                    BlobDomain uploadedFileContent = uploadedFile.getFileContents();
                    
                    Row row = departmentVO.createRow();
                    row.setAttribute("DocumentName", uploadedFile.getFileName());
                    row.setAttribute("DocumentType", uploadedFile.getFileType() );
                    row.setAttribute("Document", uploadedFileContent);
                    departmentVO.insertRow(row);
                    System.out.println("LKAPOOR:: INSERTING DOCUMENT ROW");
                }
            }else{
                    System.out.println("Lalit >> Uploaded File Index = " + i + " has no content.");            
            }
        
        }
        this.getDBTransaction().commit();
        
    }
    
}

The insertDocumentDetails method is used to insert document records in the DB.

In the ViewController project, the MultipleDocumentUpload.jspx looks like this:

<?xml version='1.0' encoding='UTF-8'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1" xmlns:f="http://java.sun.com/jsf/core"
          xmlns:af="http://xmlns.oracle.com/adf/faces/rich">
    <jsp:directive.page contentType="text/html;charset=UTF-8"/>
    <f:view>
        <af:document title="MultipleDocumentUpload.jspx" id="d1">
            <af:messages id="m1"/>
            <af:form id="f1" usesUpload="true">
                <af:panelStretchLayout id="psl1" binding="#{pageFlowScope.MultipleDocumentUpload.stretchLayoutBinding}"
                                       styleClass="AFStretchWidth">
                    <f:facet name="bottom">
                        <af:panelGroupLayout id="pgl3">
                            <af:commandButton text="Print File Metadata" id="cb1" partialSubmit="true"
                                              actionListener="#{pageFlowScope.MultipleDocumentUpload.handlePrintMetadata}"
                                              rendered="false"/>
                            <af:spacer width="10" height="10" id="s4"/>
                            <af:commandButton text="Insert " id="cb4"
                                              actionListener="#{pageFlowScope.MultipleDocumentUpload.handleInsertDocuments}"
                                              partialSubmit="true"/>
                        </af:panelGroupLayout>
                    </f:facet>
                    <f:facet name="center">
                        <af:panelStretchLayout id="psl2" topHeight="250px" startWidth="0px" endWidth="0px"
                                               bottomHeight="0px" styleClass="AFStretchWidth">
                            <f:facet name="bottom"/>
                            <f:facet name="center">
                                <af:panelCollection id="pc1">
                                    <f:facet name="menus"/>
                                    <f:facet name="toolbar"/>
                                    <f:facet name="statusbar"/>
                                    <af:table value="#{bindings.DepartmentVO1.collectionModel}" var="row"
                                              rows="#{bindings.DepartmentVO1.rangeSize}"
                                              emptyText="#{bindings.DepartmentVO1.viewable ? 'No data to display.' : 'Access Denied.'}"
                                              fetchSize="#{bindings.DepartmentVO1.rangeSize}" rowBandingInterval="0"
                                              filterModel="#{bindings.DepartmentVO1Query.queryDescriptor}"
                                              queryListener="#{bindings.DepartmentVO1Query.processQuery}"
                                              filterVisible="true" varStatus="vs"
                                              selectedRowKeys="#{bindings.DepartmentVO1.collectionModel.selectedRow}"
                                              selectionListener="#{bindings.DepartmentVO1.collectionModel.makeCurrent}"
                                              rowSelection="single" id="t1"
                                              binding="#{pageFlowScope.MultipleDocumentUpload.documentTable}"
                                              columnStretching="last">
                                        <af:column sortProperty="#{bindings.DepartmentVO1.hints.DocumentId.name}"
                                                   filterable="true" sortable="true"
                                                   headerText="#{bindings.DepartmentVO1.hints.DocumentId.label}"
                                                   id="c1">
                                            <af:outputText value="#{row.DocumentId}"
                                                           shortDesc="#{bindings.DepartmentVO1.hints.DocumentId.tooltip}"
                                                           id="ot2">
                                                <af:convertNumber groupingUsed="false"
                                                                  pattern="#{bindings.DepartmentVO1.hints.DocumentId.format}"/>
                                            </af:outputText>
                                        </af:column>
                                        <af:column sortProperty="#{bindings.DepartmentVO1.hints.DocumentName.name}"
                                                   filterable="true" sortable="true"
                                                   headerText="#{bindings.DepartmentVO1.hints.DocumentName.label}"
                                                   id="c2">
                                            <af:outputText value="#{row.DocumentName}"
                                                           shortDesc="#{bindings.DepartmentVO1.hints.DocumentName.tooltip}"
                                                           id="ot3"/>
                                        </af:column>
                                        <af:column sortProperty="#{bindings.DepartmentVO1.hints.DocumentType.name}"
                                                   filterable="true" sortable="true"
                                                   headerText="#{bindings.DepartmentVO1.hints.DocumentType.label}"
                                                   id="c3">
                                            <af:outputText value="#{row.DocumentType}"
                                                           shortDesc="#{bindings.DepartmentVO1.hints.DocumentType.tooltip}"
                                                           id="ot4"/>
                                        </af:column>
                                        <af:column sortProperty="#{bindings.DepartmentVO1.hints.Document.name}"
                                                   sortable="true"
                                                   headerText="#{bindings.DepartmentVO1.hints.Document.label}" id="c4"
                                                   rendered="false">
                                            <af:outputText value="#{row.Document}"
                                                           shortDesc="#{bindings.DepartmentVO1.hints.Document.tooltip}"
                                                           id="ot5"/>
                                        </af:column>
                                        <af:column id="c5">
                                            <af:commandLink text="Download" id="cl1" partialSubmit="true">
                                                <af:fileDownloadActionListener filename="#{row.DocumentName}"
                                                                               contentType="#{row.DocumentType}"
                                                                               method="#{pageFlowScope.MultipleDocumentUpload.handleDownload}"/>
                                            </af:commandLink>
                                        </af:column>
                                    </af:table>
                                </af:panelCollection>
                            </f:facet>
                            <f:facet name="start"/>
                            <f:facet name="end"/>
                            <f:facet name="top">
                                <af:panelGroupLayout id="pgl1" layout="vertical" partialTriggers="cb2 cb3">
                                    <af:forEach var="row" items="#{pageFlowScope.MultipleDocumentUpload.items}"
                                                varStatus="vStatus">
                                        <af:panelGroupLayout id="pgl4" layout="horizontal">
                                            <af:outputText value="#{vStatus.count}" id="ot1"/>
                                            <af:spacer width="10" height="10" id="s1"/>
                                            <af:inputFile label="Label #{vStatus.index}" id="if1"
                                                          value="#{pageFlowScope.MultipleDocumentUpload.items[vStatus.index].inputFile}"
                                                          autoSubmit="true"
                                                          valueChangeListener="#{pageFlowScope.MultipleDocumentUpload.inputFileValueChangeListener}">
                                                <f:attribute name="indexValue" value="#{vStatus.index}"/>
                                            </af:inputFile>
                                        </af:panelGroupLayout>
                                        <af:separator id="s2"/>
                                    </af:forEach>
                                </af:panelGroupLayout>
                            </f:facet>
                        </af:panelStretchLayout>
                    </f:facet>
                    <f:facet name="start"/>
                    <f:facet name="end"/>
                    <f:facet name="top">
                        <af:panelGroupLayout id="pgl2" layout="horizontal">
                            <af:commandButton text="Add File" id="cb2"
                                              actionListener="#{pageFlowScope.MultipleDocumentUpload.handleAddFileAction}"
                                              partialSubmit="true"/>
                            <af:spacer width="10" height="10" id="s3"/>
                            <af:commandButton text="Remove File" id="cb3"
                                              actionListener="#{pageFlowScope.MultipleDocumentUpload.handleRemoveFile}"
                                              partialSubmit="true"/>
                        </af:panelGroupLayout>
                    </f:facet>
                </af:panelStretchLayout>
            </af:form>
        </af:document>
    </f:view>
</jsp:root>

The FileUpload file is used to store the uploaded document details. It also implements the UploadedFileDetails interface and looks like this:

package view.bean;

import base.interfaces.UploadedFileDetails;

import java.io.Serializable;

import oracle.adf.view.rich.component.rich.input.RichInputFile;

import oracle.jbo.domain.BlobDomain;

import org.apache.myfaces.trinidad.model.UploadedFile;

public class FileUpload implements Serializable, UploadedFileDetails{

    @SuppressWarnings("compatibility:4466058592478245489")
    private static final long serialVersionUID = 1L;
    private UploadedFile inputFile;
    private BlobDomain inputFileContent;
    
    public FileUpload() {
        super();
    }

    public void setInputFile(UploadedFile inputFile) {
        this.inputFile = inputFile;
    }

    public UploadedFile getInputFile() {
        return inputFile;
    }

    public void setInputFileContent(BlobDomain inputFileContent) {
        this.inputFileContent = inputFileContent;
    }

    public BlobDomain getInputFileContent() {
        return inputFileContent;
    }
    
    public String getFileName(){
        return inputFile.getFilename();
    }
    
    public String getFileType(){
        return inputFile.getContentType();
    }
    
    public Long getFileLength(){
        return getInputFileContent().getLength();
    }
    
    public BlobDomain getFileContents(){
        return getInputFileContent();
    }
}

And MultipleDocumentUpload managed bean looks like this:

package view.bean;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.io.OutputStreamWriter;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.HashMap;
import java.util.List;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;

import oracle.adf.model.BindingContext;
import oracle.adf.model.binding.DCBindingContainer;
import oracle.adf.model.binding.DCIteratorBinding;
import oracle.adf.share.ADFContext;
import oracle.adf.share.security.SecurityContext;
import oracle.adf.view.rich.component.rich.data.RichTable;
import oracle.adf.view.rich.component.rich.input.RichInputFile;
import oracle.adf.view.rich.component.rich.layout.RichPanelStretchLayout;

import oracle.binding.BindingContainer;
import oracle.binding.OperationBinding;

import oracle.jbo.Row;
import oracle.jbo.domain.BlobDomain;

import oracle.jbo.uicli.binding.JUCtrlHierBinding;

import org.apache.myfaces.trinidad.model.CollectionModel;
import org.apache.myfaces.trinidad.model.UploadedFile;

public class MultipleDocumentUpload {
    private RichPanelStretchLayout stretchLayoutBinding;
    
    private ArrayList list = new ArrayList();
    private ArrayList<BlobDomain> blobList = new ArrayList<BlobDomain>();
    private RichTable documentTable;

    public MultipleDocumentUpload() {
        
        list.add(new FileUpload());
        System.out.println("Lalit >>size = " + list.size());

    }

    public void setStretchLayoutBinding(RichPanelStretchLayout stretchLayoutBinding) {
        this.stretchLayoutBinding = stretchLayoutBinding;
    }

    public RichPanelStretchLayout getStretchLayoutBinding() {
        return stretchLayoutBinding;
    }

    public List getItems() {
        return list;
    }


    public void handlePrintMetadata(ActionEvent actionEvent) {
        // Add event code here...
        for(int i = 0; i < list.size(); i++ ){
            FileUpload uploadedFile = ((FileUpload)list.get(i));
            if(uploadedFile != null){
                UploadedFile inputFile = uploadedFile.getInputFile();
                if(inputFile != null){
                    System.out.println("LALIT >> index = " + i);
                    System.out.println("LALIT: FILE NAME = " + inputFile.getFilename()  );
                    BlobDomain uploadedFileContent = ((FileUpload)list.get(i)).getInputFileContent();
                    if(uploadedFileContent != null)
                        System.out.println("LALIT: FILE SIZE = " + uploadedFileContent.getLength()   );
                    
                    System.out.println("LALIT: FILE CONTENT TYPE  = " + inputFile.getContentType()  );
                }
            }
        }
    }

    public void inputFileValueChangeListener(ValueChangeEvent valueChangeEvent) {
        // Add event code here...
        
        UploadedFile file;
        file = (UploadedFile)valueChangeEvent.getNewValue();
        
        Object source = valueChangeEvent.getSource();
        RichInputFile inputFile = (RichInputFile)source;
        Map attrObjMap = inputFile.getAttributes();
        Object indexObjAttr = attrObjMap.get("indexValue");
        System.out.println("Lalit:: attribute value = " + indexObjAttr);
                
        FileUpload toUpdateFile = (FileUpload)list.get(Integer.parseInt(indexObjAttr.toString()) );
        toUpdateFile.setInputFileContent(createBlobDomain(file));
        
        System.out.println("LKapoor>> updated file content>> " + toUpdateFile.getInputFileContent().getLength());
        
    }
    
    private BlobDomain createBlobDomain(UploadedFile file) {

        InputStream in = null;
        BlobDomain blobDomain = null;
        OutputStream out = null;

        try {
            in = file.getInputStream();

            blobDomain = new BlobDomain();
            out = blobDomain.getBinaryOutputStream();
            byte[] buffer = new byte[8192];
            int bytesRead = 0;

            while ((bytesRead = in.read(buffer, 0, 8192)) != -1) {
                out.write(buffer, 0, bytesRead);
            }

            in.close();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.fillInStackTrace();
        }

        return blobDomain;
    }

    public void handleAddFileAction(ActionEvent actionEvent) {
        // Add event code here...
        list.add(new FileUpload());
        
    }

    public void handleRemoveFile(ActionEvent actionEvent) {
        // Add event code here...
        list.remove(list.size() - 1);
        
    }

    public void handleInsertDocuments(ActionEvent actionEvent) {
        // Add event code here...
        BindingContext bindingctx = BindingContext.getCurrent();
        BindingContainer bindings = null;
        bindings = bindingctx.getCurrentBindingsEntry();
        DCBindingContainer bindingsImpl = (DCBindingContainer)bindings;
        OperationBinding insertDocsCall = null;
        insertDocsCall = bindingsImpl.getOperationBinding("insertDocumentDetails");
        
        insertDocsCall.getParamsMap().put("documentList", list);
        
        insertDocsCall.execute();
    }

    public void handleDownload(FacesContext facesContext, OutputStream outputStream) {
        // Add event code here...
        
        try {
            BufferedWriter w = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));

            CollectionModel collectionModel = (CollectionModel)getDocumentTable().getValue();
            JUCtrlHierBinding tableBinding = null;
            tableBinding = (JUCtrlHierBinding)collectionModel.getWrappedData();
            DCIteratorBinding iteratorBinding = tableBinding.getDCIteratorBinding();
            Row row = iteratorBinding.getCurrentRow();

            if (row != null) {
                BlobDomain payloadCanonical = (BlobDomain)row.getAttribute("Document");
                if (payloadCanonical != null) {

                    InputStream inStream = payloadCanonical.getBinaryStream();

                    int length = -1;
                    int size = payloadCanonical.getBufferSize();
                    byte[] buffer = new byte[size];

                    while ((length = inStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, length);
                        outputStream.flush();
                    }

                    inStream.close();
                    outputStream.close();

                } else
                    w.write("null");
            } else {
                w.write("null");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }  

    }

    public void setDocumentTable(RichTable documentTable) {
        this.documentTable = documentTable;
    }

    public RichTable getDocumentTable() {
        return documentTable;
    }
}

The MultipleDocumentUpload managed bean's scope is set as pageFlowScope.
That's all required to implement the multiple document upload functionality.