Wednesday, 5 September 2018

Zipping is easy in Java/Spring/SOASuite

Why using Spring in SOASuite?

This post is actually about Spring in SOASuite. Last few weeks I've come around several questions and implementations using Embedded Java in BPEL. The Java activity in Oracle BPEL is ideal for very small java snippets, to do a simple Base64 encode/decode for example. However, in the situations I encountered these weeks, the code samples were more complex. And my very first recommendation in these situations is to use the SpringContext component, a technique introduced in SOASuite 11g, that I wrote about in 2012 already. I called it 'Forget about WSIF: welcome Spring', but really: forget about Embedded Java in most cases too! So, with this article, I want to showcase the SpringContext component in SOASuite.

A few disadvantages of the Embedded Java activity:
  • To get to, and set or manipulate, your BPEL data, you'll need to compose getVariableData() and setVariableData() functions with xpath-references. You can use temporary Assign activities with copy rules, from where you can extract the actual variable and xpath references, to copy and paste into the getVariableData() functions. But best workaround, to me is to declare a few xsd:string based variables and use Assigns before and after to exchange the data with the java snippet.
  • You can't test/debug the java snippets properly.
  • Catching and handling exceptions may not behave as you might want.
  • You can introspect variables that are referenced in your java snippet from the flow trace. But you must rely on the visual code compare between bpel and java snippet.
The advantages of using a SpringContext component:
  • You can test your java code standalone, and just reuse the methods as is.
  • Wiring your Spring Component to your BPEL will result in a WSDL and a PartnerLink. From BPEL you just invoke it like any other service. 
  • In the flowtrace you will see the input and output variables as is (provided that you set the audit level on Development). And you can just assign the data to and fro the variables based on the wsdl message types.
  • It's all declarative.
A disadvantage might be that it is a bit more complex. It might feel like a hazzle, so you wouldn't use it for 2 lines of Java code, probably. At the other hand, try it out, I guess you might experience it as so much easier and more reliable.

The case at hand

In the case at hand, as referred to in the title,  we need to read a zip file. We get files from Oracle B2B, that are saved on the filesystem with a system generated filename. That is different from the file as send, and provided using the context-disposition MIME header. The client wants to have the file moved and saved using the originating filename. We couldn't get it from B2B in an acceptable way. B2B saves the file using a filename like 'M1234987309891.2341921@myTPR1234_te2309098033.dat'. But it turns out to be a zip file that contains the file, with the name as we want it. So, we could just introspect the file to determine the name as which we want to move it. This means of course that we need to read the file, from Java. And to me it does not feel right to do that from a Java activity in BPEL. It's something more or less functional, so I want to abstract that in a service, based on a java class.

Unzip in Java

I found an unzip method in Java that does just about what I want here. It comes down to:
package nl.darwinit.ziputils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import nl.darwinit.ziputils.beans.ListZipEntriesRequest;
import nl.darwinit.ziputils.beans.ListZipEntriesResponse;
import nl.darwinit.ziputils.beans.ZipEntryFile;

public class Unzip implements IUnzip {

    public static final String className = "nl.darwinit.ziputils.Unzip";

    public Unzip() {
        super();
    }
...

...
    private static void unzip(String zipFilePath, String destDir) {
        File dir = new File(destDir);
        // create output directory if it doesn't exist
        if (!dir.exists())
            dir.mkdirs();
        FileInputStream fis;
        //buffer for read and write data to file
        byte[] buffer = new byte[1024];
        try {
            fis = new FileInputStream(zipFilePath);
            ZipInputStream zis = new ZipInputStream(fis);
            ZipEntry ze = zis.getNextEntry();
            while (ze != null) {
                String fileName = ze.getName();
                File newFile = new File(destDir + File.separator + fileName);
                System.out.println("Unzipping to " + newFile.getAbsolutePath());
                //create directories for sub directories in zip
                new File(newFile.getParent()).mkdirs();
                FileOutputStream fos = new FileOutputStream(newFile);
                int len;
                while ((len = zis.read(buffer)) > 0) {
                    fos.write(buffer, 0, len);
                }
                fos.close();
                //close this ZipEntry
                zis.closeEntry();
                ze = zis.getNextEntry();
            }
            //close last ZipEntry
            zis.closeEntry();
            zis.close();
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Unzip unzip = new Unzip();
        int i = 0;
        for (String arg : args) {
            log("main", "arg[" + i++ + "]: " + arg);
        }
        ListZipEntriesRequest listZipEntriesRequest = new ListZipEntriesRequest();
        listZipEntriesRequest.setZipFileFolder(args[0]);
        listZipEntriesRequest.setZipFileName(args[1]);
        ListZipEntriesResponse listZipEntriesResponse = unzip.listZipEntries(listZipEntriesRequest);
        log("main", listZipEntriesResponse.toString());
    }
}


Besides reading all the ZipEntries in the zip file one by one, this does also save it using a FileOutputStream. But I do not want to output the files, but just get the filename. Another thing: this example shows a private static function. But for the SpringContext I need an instantiatable java class, so with a constructor, and a public method. So I created an Unzip class (with default constructor) and the following method:
    public ListZipEntriesResponse listZipEntries(ListZipEntriesRequest listZipEntriesRequest) {
        final String methodName = "ListZipEntriesResponse";
        logStart(methodName);
        ListZipEntriesResponse listZipEntriesResponse = new ListZipEntriesResponse();
        FileInputStream fileInputStream;
        //buffer for read and write data to file
        log(methodName, "List entries of " + listZipEntriesRequest.getZipFilePath());
        try {

            fileInputStream = new FileInputStream(listZipEntriesRequest.getZipFilePath());
            ZipInputStream zipInputStream = new ZipInputStream(fileInputStream);
            ZipEntry zipEntry = zipInputStream.getNextEntry();
            while (zipEntry != null) {
                String fileName = zipEntry.getName();
                log(methodName, "Entry: " + fileName);
                ZipEntryFile zipEntryFile = new ZipEntryFile();
                zipEntryFile.setFileName(fileName);
                listZipEntriesResponse.addZipEntryFile(zipEntryFile);
                //close this ZipEntry
                zipInputStream.closeEntry();
                zipEntry = zipInputStream.getNextEntry();
            }
            //close last ZipEntry
            zipInputStream.closeEntry();
            zipInputStream.close();
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        logEnd(methodName);
        return listZipEntriesResponse;
    }

The logStart, logEnd and log methods are just wrappers around System.out.println(). As an input I have a bean with:
package nl.darwinit.ziputils.beans;

import java.io.File;


public class ListZipEntriesRequest implements IListZipEntriesRequest {
  
  private String zipFileFolder;
    private String zipFileName;

    public void setZipFileFolder(String zipFileFolder) {
        this.zipFileFolder = zipFileFolder;
    }

    public String getZipFileFolder() {
        return zipFileFolder;
    }

    public void setZipFileName(String zipFileName) {
        this.zipFileName = zipFileName;
    }

    public String getZipFileName() {
        return zipFileName;
    }

    public String getZipFilePath() {
        return getZipFileFolder() +File.separator+ getZipFileName();
    }    
}

Then as a response a bean with:
package nl.darwinit.ziputils.beans;

import java.util.ArrayList;
import java.util.List;

public class ListZipEntriesResponse implements IListZipEntriesResponse {
    private List zipEntryFiles;

    public void setZipEntryFiles(List zipEntryFiles) {
        this.zipEntryFiles = zipEntryFiles;
    }

    public List getZipEntryFiles() {
        return zipEntryFiles;
    }

    public void addZipEntryFile(ZipEntryFile zipEntryFile) {
        if (zipEntryFiles == null) {
            zipEntryFiles = new ArrayList();
        }
        zipEntryFiles.add(zipEntryFile);
    }
    
    public String toString(){
        StringBuffer strBuf = new StringBuffer("ListZipEntriesResponse\n");
        for (ZipEntryFile zipEntryFile : zipEntryFiles){
            strBuf.append("ZipEntryFile: "+zipEntryFile.toString()+"\n");
        }        
        return strBuf.toString();
    }
}

That uses a ZipEntryFile bean, as a List:
package nl.darwinit.ziputils.beans;


public class ZipEntryFile implements IZipEntryFile {
   private String fileName;

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileName() {
        return fileName;
    }
    
    public String toString(){
        return "fileName: "+getFileName()+"\n";
    }
}

Place your the source of your classes in the src of your SOA project. The classes are compiled in the the SOA/SCA-INF/classes folder (in 11g, you won't have the SOA subfolder). In 12c the sources can also be placed in the SOA/SCA-INF/src folder. But I found that this will cause a misterious nullpointer exception.

The spring context

My article 'Forget about WSIF: welcome Spring' neatly describes how to create a spring context in 11g. In 12c it does not differ much. You don't need to define separate Spring components per bean, though, but you actually can as shown in the article.  You do need to extract an Interface out of the main class. With only those methods you need in the interface. I actually extracted interfaces from all the beans, but you shouldn't have to.

In this case the Service xml would look like:
<?xml version="1.0" encoding="windows-1252" ?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util"
       xmlns:jee="http://www.springframework.org/schema/jee" xmlns:lang="http://www.springframework.org/schema/lang"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:sca="http://xmlns.oracle.com/weblogic/weblogic-sca" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://xmlns.oracle.com/weblogic/weblogic-sca META-INF/weblogic-sca.xsd">
  <!--Spring Bean definitions go here-->
  <sca:service name="Unzip" target="UnzipBean"
               type="nl.minvenj.ind.indigo.ziputils.IUnzip"/>
  <bean id="UnzipBean" class="nl.minvenj.ind.indigo.ziputils.Unzip"/>
  <bean id="ListZipEntriesRequestBean" class="nl.darwinit.ziputils.beans.ListZipEntriesRequest"/>
  <bean id="ListZipEntriesResponseBean" class="nl.darwinit.ziputils.beans.ListZipEntriesResponse"/>
  <bean id="ZipEntryFileBean" class="nl.darwinit.ziputils.beans.ZipEntryFile"/>
</beans>

Wire your Spring component to the BPEL component, and then you should have a dialog telling you that the WSDL is created, probably with an accompanying wrapper wsdl for the partnerlink roles.

And then in BPEL just invoke the code.

Happy Spring in SOA (better that than the otherway around if you speak Dutch...).


2 comments:

  1. Can you please email me this Sample spring in SOA to zip unzip ?

    ReplyDelete
  2. I'm sorry: I've extracted and anonymized this from sources from my current customer.

    But you should be able to reconstruct it based on my instructions. Feel free to ask if you get stuck on a specific step. But preferably via community.oracle.com.

    ReplyDelete