Projects‎ > ‎

Support Mime Data

Overview

Enhance the Framework / Server / Service to support "MIME" Data.

RESTful interface

A configured relationship will be used to provide the URI interface

Method / Syntax POST .../contexts/{id}/subjects/{id}/relationships/{id}
Description Store the media content. If the the content already exists in the Media Context, it will be replaced. The POST would contain the multi-media content. The Server would:
  1. extract the "content" multipart/form-data
  2. set attributes:
    • data (the "content")
    • context
    • subject
    • relationship
  3. determine attributes:
    • size
    • modified
    • type
    • digest
  4. store the "content" in the Media Context
    • The uniqueid attribute function will create the unique id by concatination of:
      • context-subject-relationship
Example POST .../contexts/LDAP/subjects/jwayne/relationships/photo
Method / Syntax PUT .../contexts/{id}/subjects/{id}/relationships/{id}
Description Store the media content. If the the content already exists in the Media Context, it will be replaced. The PUT would contain the multi-media "content". The Server would:
  1. extract the "content" byte[]
  2. set attributes:
    • data (the content)
    • context
    • subject
    • relationship
  3. determine attributes:
    • size
    • modified
    • type
    • digest
  4. store the "content" in the Media Context
    • The uniqueid attribute function will create the unique id by concatination of:
      • context-subject-relationship
Example PUT .../contexts/LDAP/subjects/jwayne/relationships/photo
Method / Syntax GET .../contexts/{id}/subjects/{id}/relationships/{id}
Description Get the media content.
  1. retrieve the "media" from the Media Context
    • The uniqueid attribute function will "assemble" the unique id by concatination of:
      • context-subject-relationship
  2. obtain attributes:
    • data
    • size
    • modified
    • type
    • digest
  3. calculate new digest from data and compare to stored digest
  4. create HTTP Response
    • set Context-Type, Size, Modified
    • include data
  5. return the "media" as the HTTP Response
Example GET .../contexts/LDAP/subjects/jwayne/relationships/photo
Method / Syntax DELETE .../contexts/{id}/subjects/{id}/relationships/{id}
Description The DELETE would remove the specified media items:
  1. delete the "media" in the Media Context
    • The uniqueid attribute function will "assemble" the unique id by concatination of:
      • context-subject-relationship
Example DELETE .../contexts/LDAP/subjects/jwayne/relationships/photo
Future
Method / Syntax GET .../contexts/{id}/subjects/{id}/relationships/{id}?view={id}
Description GETs the "thumbnail" for the named "media"
Example GET .../contexts/LDAP/subjects/jwayne/relationships/photo?view=thumbnail

OpenPTK Configuration Elements for Media

Context
      <Context id="Media-MySQL-JDBC" enabled="true" definition="Media" connection="MySQL" association="JDBC-MEDIA">
         <Properties>
            <Property name="context.description"  value="Media to MySQL using JDBC" />
            <Property name="operation.classname"  value="org.openptk.spi.operations.JdbcOperations" />
            <Property name="connection.table"     value="media" />
            <Property name="key"                  value="uniqueid" />
            <Property name="timeout"              value="%{timeout.write}" />
            <Property name="search.default.order" value="name,subject" />
            <Property name="search.operators"     value="AND,OR,CONTAINS,EQ" />
         </Properties>
         <Query type="NULL" />
         <Operations>
            <Operation id="create" attrgroup="media-create">
               <Actions>
                  <Action id="checkmimetype" mode="pre" >
                     <Properties>
                        <Property name="attribute.type" value="type"/>
                     </Properties>
                  </Action>
                  <Action id="ifexists" mode="pre" />
                  <Action id="thumbnail" mode="post" />
               </Actions>
            </Operation>
            <Operation id="read"   attrgroup="media-read" />
            <Operation id="update" attrgroup="media-update" >
               <Actions>
                  <Action id="checkmimetype" mode="pre" >
                     <Properties>
                        <Property name="attribute.type" value="type"/>
                     </Properties>
                  </Action>
                  <Action id="thumbnail" mode="post" />
               </Actions>
            </Operation>
            <Operation id="delete" attrgroup="media-delete" />
            <Operation id="search" attrgroup="media-search">
               <Properties>
                  <Property name="timeout"        value="%{timeout.read}" />
                  <Property name="sort"           value="name" />
               </Properties>
            </Operation>
         </Operations>
      </Context>
Definition
      <Definition id="Media">
         <Properties>
            <Property name="definition.classname"   value="org.openptk.definition.BasicSubject" />
            <Property name="definition.description" value="Media Repository" />
         </Properties>
         <Attributes>
            <Attribute id="uniqueid" required="true">
            </Attribute>
            <Attribute id="name" />
            <Attribute id="type" required="true">
               <Functions>
                  <Function id="MimeType" classname="org.openptk.definition.functions.DetectMimeType">
                     <Arguments>
                        <Argument name="data" type="attribute" value="data" />
                        <Argument name="plugin" type="literal" value="mimeutil"/>
                     </Arguments>
                     <Operations>
                        <Operation type="create"/>
                        <Operation type="update"/>
                     </Operations>
                  </Function>
               </Functions>
            </Attribute>
            <Attribute id="length" required="true" type="integer"/>
            <Attribute id="modified" required="true">
               <Functions>
                  <Function id="DateTime" classname="org.openptk.definition.functions.DateTimeStamp">
                     <Operations>
                        <Operation type="create"/>
                        <Operation type="update"/>
                     </Operations>
                  </Function>
               </Functions>
            </Attribute>
            <Attribute id="contextid" />
            <Attribute id="subjectid" />
            <Attribute id="relationshipid" />
            <Attribute id="digest" required="true" >
               <Functions>
                  <Function id="Digest" classname="org.openptk.definition.functions.CalculateDigest">
                     <Arguments>
                        <Argument name="data" type="attribute" value="data" />
                     </Arguments>
                     <Operations>
                        <Operation type="create"/>
                        <Operation type="update"/>
                     </Operations>
                  </Function>
               </Functions>
            </Attribute>
            <Attribute id="data" type="object" required="true" />
         </Attributes>
      </Definition>
Association
      <Association id="JDBC-MEDIA">
         <Attributes>
            <Attribute id="uniqueid" servicename="uuid" />
            <Attribute id="name" />
            <Attribute id="type" />
            <Attribute id="length"   servicename="size"/>
            <Attribute id="modified" />
            <Attribute id="contextid" servicename="context"/>
            <Attribute id="subjectid" servicename="subject"/>
            <Attribute id="relationshipid" servicename="relationship"/>
            <Attribute id="digest" />
            <Attribute id="data" />
         </Attributes>
      </Association>
AttrGroups
       <AttrGroup id="media-create">
         <Attributes>
            <Attribute id="name" />
            <Attribute id="type" />
            <Attribute id="length" />
            <Attribute id="modified" />
            <Attribute id="contextid" />
            <Attribute id="subjectid" />
            <Attribute id="relationshipid" />
            <Attribute id="digest" />
            <Attribute id="data" />
         </Attributes>
      </AttrGroup>
      <AttrGroup id="media-read">
         <Attributes>
            <Attribute id="name" />
            <Attribute id="type" />
            <Attribute id="length" />
            <Attribute id="modified" />
            <Attribute id="contextid" />
            <Attribute id="subjectid" />
            <Attribute id="relationshipid" />
            <Attribute id="digest" />
            <Attribute id="data" />
         </Attributes>
      </AttrGroup>
      <AttrGroup id="media-update">
         <Attributes>
            <Attribute id="name" />
            <Attribute id="type" />
            <Attribute id="length" />
            <Attribute id="modified" />
            <Attribute id="contextid" />
            <Attribute id="subjectid" />
            <Attribute id="relationshipid" />
            <Attribute id="digest" />
            <Attribute id="data" />
         </Attributes>
      </AttrGroup>
      <AttrGroup id="media-delete"/>
      <AttrGroup id="media-search">
         <Attributes>
            <Attribute id="name" />
            <Attribute id="contextid" />
            <Attribute id="subjectid" />
            <Attribute id="relationshipid" />
         </Attributes>
      </AttrGroup>

Media Database

First Generation Design

This is an initial DB design to support the relationship media. For the "My Photo" Use Case the uniqueid, in the Media Context, is derived by merging the contextId, SubjectId and relationshipId. The Relationship implementations would be able to derive the "primary key" (uniqueid) and directly check / create / read the related media.

This design can be used as a general purpose "media storage mechanism". The business logic will need to generate a uniqueid using the GUID facility. Note: the "syntax" of the GUID (primary key) will not match the Relationship syntax. There's no reason why this design should not be able to support both Relationship media and General-Purpose media.

Table MEDIA
Column Description Type Index Required Example Data
uuid
  • derived key from relationship mechanism
  • Global Unique Id
varchar(255) pri key +
  • "Media-MySQL-JDBC-ja1324-photo"
  • "add8f6f4-eb01-459d-827b-d1a7fcfc4117"
name original file name varchar(255)     "picture.png"
type mime-type (media/sub) varchar(255)   + "image/png"
size number of bytes int   + 2450
modified date/time stamp varchar(255)   + "Wed Dec 09 15:13:29 CST 2009"
digest data checksum varchar(255)   + "3527b14453a88c09cd8fb7ed1ef6cfb60f5ffcff"
data media / document mediumblob   + byte[]
context context id varchar(255) YES   "Media-MySQL-JDBC"
subject subject id varchar(255) YES   "ja1324"
relationship relationship id varchar(255) YES   "photo"
CREATE TABLE media (
    uuid            varchar(255) primary key not null,
    name            varchar(255),
    type            varchar(255),
    size            int,
    modified        varchar(255),
    digest          varchar(255),
    data            mediumblob not null,
    context         varchar(255),
    subject         varchar(255),
    relationship    varchar(255),
    INDEX INDEX1 (context),
    INDEX INDEX2 (subject),
    INDEX INDEX3 (relationship)
);

Second Generation Design

This is a proposed DB design to better support both relationship and general media requirements. The First Generation was focused on the supporting the "photo" use case requirements. Since the Media Context can be used as a general purpose storage mechanism (for images, documents, etc.) it should to be enahanced. The First Generation assumes that the uniqueid of the Media Context is derived from the Relationship process. This Second Generation design is focused on enhancing the Media Context to support business Use Cases for storing media items that may not have a "relationship".

Supporting Relationships:

The Relationship mechanisms will need to change:

  • Create/Update
    • The "checking" for existing "related media" will need to do a Search for the relid instead of a Read (for the uniqueid)
    • A GUID will be obtained for the uniqueid
    • relid will be derived and added as an attribute
  • Read
    • This will converted to a Search operation
    • The derived relid will be used to perform the search
    • The first result of the search (assume only one) will be returned
  • Delete
    • This will be a two step process
    • Search for the entry using the derived relid
    • Delete the first entry of the search (assume only one) that is returned
Table MEDIA
Column Description Type Index Required Example Data
id Global Unique Id (guid) varchar(255) pri key + "add8f6f4-eb01-459d-827b-d1a7fcfc4117"
name original file name varchar(255)     "picture.png"
type mime-type (media/sub) varchar(255)   + "image/png"
size number of bytes int   + 2450
modified date/time stamp varchar(255)   + "Wed Dec 09 15:13:29 CST 2009"
digest data checksum varchar(255)   + "3527b14453a88c09cd8fb7ed1ef6cfb60f5ffcff"
data media / document mediumblob   + byte[]
relid derived key from relationship mechanism varchar(255) alt key   "Media-MySQL-JDBC-ja1324-photo"
context context id varchar(255) YES   "Media-MySQL-JDBC"
subject subject id varchar(255) YES   "ja1324"
relationship relationship id varchar(255) YES   "photo"
CREATE TABLE media (
    id              varchar(255) primary key not null,
    name            varchar(255),
    type            varchar(255),
    size            int,
    modified        varchar(255),
    digest          varchar(255),
    data            mediumblob not null,
    relid           varchar(255),
    context         varchar(255),
    subject         varchar(255),
    relationship    varchar(255),
    INDEX INDEX1 (relid),
    INDEX INDEX2 (context),
    INDEX INDEX3 (subject),
    INDEX INDEX4 (relationship)
);

Notes

The max size of an image is associated to the datatype for MySQL

Type Max Size
TINYBLOB 255 Bytes
BLOB 65,535 Bytes
MEDIUMBLOB 16,777,215 Bytes
LONGBLOB 4 GBytes

JDBC Sample code

storing the file in the table:

File image = new File("C:/image.jpg");
fis = new FileInputStream(image);

psmnt = connection.prepareStatement("insert into media(uuid, data) "+ "values(?,?)");
psmnt.setString(1,"1234-5678-9010");
psmnt.setBinaryStream(2, (InputStream)fis, (int)(image.length()));
psmnt.executeUpdate();

Add byte array to JDBC database

String sql = "INSERT INTO my_table (byte_array) VALUES (?)";
PreparedStatement statement = connection.prepareStatement(sql);
byte[] buffer = somwhere.getSomeByteArray();
statement.setBytes(1, buffer);
statement.executeUpdate();
statement.close();

Insert row into table using a statement

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

public class Main {
  public static void main(String[] argv) throws Exception {
    String driverName = "com.jnetdirect.jsql.JSQLDriver";
    Class.forName(driverName);

    String serverName = "127.0.0.1";
    String portNumber = "1433";
    String mydatabase = serverName + ":" + portNumber;
    String url = "jdbc:JSQLConnect://" + mydatabase;
    String username = "username";
    String password = "password";

    Connection connection = DriverManager.getConnection(url, username, password);
    Statement stmt = connection.createStatement();
    String sql = "INSERT INTO my_table (col_string) VALUES('a string')";

    stmt.executeUpdate(sql);
  }
}

Insert rows into table using a prepared statement

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

public class Main {
  public static void main(String[] argv) throws Exception {
    String driverName = "com.jnetdirect.jsql.JSQLDriver";
    Class.forName(driverName);

    String serverName = "127.0.0.1";
    String portNumber = "1433";
    String mydatabase = serverName + ":" + portNumber;
    String url = "jdbc:JSQLConnect://" + mydatabase;
    String username = "username";
    String password = "password";

    Connection connection = DriverManager.getConnection(url, username, password);
    String sql = "INSERT INTO my_table (col_string) VALUES(?)";
    PreparedStatement pstmt = connection.prepareStatement(sql);

    // Insert 10 rows
    for (int i = 0; i < 10; i++) {
      // Set the value
      pstmt.setString(1, "row " + i);

      // Insert the row
      pstmt.executeUpdate();
    }

  }
}

Client uploading

Simple HTML form POST

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
   <head><title>File Upload To Database</title></head>
   <body>
      <h3>Please Choose a File and click Submit</h3>
      <form enctype="multipart/form-data" 
            action="http://localhost:8080/openptk/resources/engine/contexts/Employees-MySQL-JDBC/subjects/aa10127/relationships/photo" 
            method="post">
         <input type="hidden" name="MAX_FILE_SIZE" value="10000000" />
         <input type="file" name="media" />
         <input type="submit" value="Submit" />
      </form>
   </body>
</html>

It is important to use the enctype="multipart/form-data" so that the browser uploads the binary data correctly.


File Upload Processing, Server Side

This section identifies the options for processing the file being uploaded, from the perspective of the Server. All of these options will use the JAX-RS / Jersey API for interfacing with the HTTP Methods GET, PUT, POST, DELETE.

Apache Common, File Upload

  • This technique uses POST
  • Handles multiple files per single HTML widget, sent using multipart/mixed encoding type, as specified by RFC 1867.
  • FileUpload Java API Docs
RFC1867 This proposal makes two changes to HTML:
  1. Add a FILE option for the TYPE attribute of INPUT.
  2. Allow an ACCEPT attribute for INPUT tag, which is a list of media types or type patterns allowed for the input.
    In addition, it defines a new MIME media type, multipart/form-data, and specifies the behavior of HTML user agents when interpreting a form with ENCTYPE="multipart/form-data" and/or <INPUT type="file"> tags. These changes might be considered independently, but are all necessary for reasonable file upload. The author of an HTML form who wants to request one or more files from a user would write (for example):
    <FORM ENCTYPE="multipart/form-data" ACTION="_URL_" METHOD=POST>
    
    File to process: <INPUT NAME="userfile1" TYPE="file">
    
    <INPUT TYPE="submit" VALUE="Send File">
    
    </FORM>
    
package rest.resource;

import java.io.File;
import java.util.Iterator;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@Path("fileupload")
public class FileUploadResource
{

   @POST
   @Produces("text/plain")
   public String loadFile(@Context HttpServletRequest request)
   {
      String resultStatus = "fail";
      String fileRepository = "I:\\testRepo\\";
      if (ServletFileUpload.isMultipartContent(request))
      {
         FileItemFactory factory = new DiskFileItemFactory();
         ServletFileUpload upload = new ServletFileUpload(factory);
         List<FileItem> items = null;
         try
         {
            items = upload.parseRequest(request);
         }
         catch (FileUploadException e)
         {

            e.printStackTrace();
         }
         if (items != null)
         {
            Iterator<FileItem> iter = items.iterator();
            while (iter.hasNext())
            {
               FileItem item = iter.next();
               if (!item.isFormField() && item.getSize() > 0)
               {
                  String fileName = processFileName(item.getName());
                  try
                  {
                     item.write(new File(fileRepository + fileName));
                  }
                  catch (Exception e)
                  {
                     e.printStackTrace();
                  }
                  resultStatus = "ok";
               }
            }
         }
      }
      return resultStatus;
   }

   private String processFileName(String fileNameInput)
   {
      String fileNameOutput = null;
      fileNameOutput = fileNameInput.substring(fileNameInput.lastIndexOf("\\") + 1, fileNameInput.length());
      return fileNameOutput;
   }
}

PUT with byte array

    @PUT
    public Response putItem(
            @Context HttpHeaders headers,
            byte[] data) {
        System.out.println("PUT ITEM " + container + " " + item);
        
        URI uri = uriInfo.getAbsolutePath();
        MediaType mimeType = headers.getMediaType();
        GregorianCalendar gc = new GregorianCalendar();
        gc.set(GregorianCalendar.MILLISECOND, 0);
        Item i = new Item(item, uri.toString(), mimeType.toString(), gc);
        String digest = computeDigest(data);
        i.setDigest(digest);
        
        Response r;
        if (!MemoryStore.MS.hasItem(container, item)) {
            r = Response.created(uri).build();
        } else {
            r = Response.noContent().build();
        }
        
        Item ii = MemoryStore.MS.createOrUpdateItem(container, i, data);
        if (ii == null) {
            // Create the container if one has not been created
            URI containerUri = uriInfo.getAbsolutePathBuilder().path("..").
                    build().normalize();
            Container c = new Container(container, containerUri.toString());
            MemoryStore.MS.createContainer(c);
            i = MemoryStore.MS.createOrUpdateItem(container, i, data);
            if (i == null)
                throw new NotFoundException("Container not found");
        }
        
        return r;
    }    

POST with Jersey FormParam InputStream

the client is sending multipart/form-data.

   @Consumes("multipart/form-data") 
   @POST 
   public void post(@FormParam("file") InputStream file) { 
     ... 
   } 

POST with Jersey FormDataMultiPart

   @Consumes("multipart/form-data") 
   @POST 
   public void post(FormDataMultiPart formData) { 
    FormDataPart p = formData.getField("file"); 
    InputStream file = p.getValueAs(InputStream.class); 
     ... 
   } 

InputStream to Byte Array

InputStream in=xmlClob().getAsciiStream();
int c;
while ((c = in.read()) != -1) {
byteArrayOutputStream.write((char) c);
}
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
import java.io.*;
public class InputstramToByteArray
{
public static void main(String args[])throws IOException
  {
  InputStream itStrm=new FileInputStream(
 "InputstramToByteArray.java" );
  String str=itStrm.toString();
  byte[] b3=str.getBytes();
  for(int i=0;i<b3.length;i++)
  System.out.print(b3[i]+"\t");
  }
}
int bytesRead=0;
int bytesToRead=1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
  int result = in.read(input, bytesRead, bytesToRead - bytesRead);
  if (result == -1) break;
  bytesRead += result;
}

For either of the above you require a dependency on JavaMail:

Checking the results of the prepared statement

Connection conn = getConnection();
    conn.setAutoCommit(false);
    Statement st = conn.createStatement();

    st.executeUpdate("create table survey (id int, name VARCHAR(30) );");

    String INSERT_RECORD = "insert into survey(id, name) values(?,?)";

    PreparedStatement pstmt = conn.prepareStatement(INSERT_RECORD);

    pstmt.setInt(1, 1);
    pstmt.setString(2, "name");
    pstmt.executeUpdate();

    // Get warnings on PreparedStatement object
    SQLWarning warning = pstmt.getWarnings();
    while (warning != null) {
      // Process statement warnings...
      String message = warning.getMessage();
      String sqlState = warning.getSQLState();
      int errorCode = warning.getErrorCode();
      warning = warning.getNextWarning();
    }

OpenPTK-Plugin-MimeUtil

A new OpenPTK project that provides an interface to the mime-util service. The mime-util service is used to determine the Mime-Type of a document.

ext
   Plugin
     MimeUtil
       src
         java
  Misc
  Service
Name Description
org.openptk.plugin Plugin interface and abstract base clases
org.openptk.plugin.mimeutil Java Package namespace for the classes that implement the Mime-Util plugin

This project will require these jar files:

  • mime-util-2.1.2.jar
  • slf4j-api-1.5.8.jar
  • slf4j-log4j12-1.5.8.jar
  • log4j-1.2.15.jar

Apache Jackrabbit

Apache Jackrabbit  is a open source Content Management System that implements the Java Specification Request (JSR 170) for the Java Content Repository (JCR 1.0) API. In order to use the "local" API of Jackrabbit/JCR it MUST be installed in the same web container as the application (OpenPTK). The application (OpenPTK) accesses the Jackrabbit/JCR API by obtaining its ServletContext, from the same web container.

Downloads

Glassfish v3 Install

Description Command / Process
Set an environment variable GF_HOME to reference the Glassfish v3 deployment
export GF_HOME=/usr/local/glassfish
Copy the JCR 1.0 jar file to the web container lib directory
cp jcr-1.0.jar ${GF_HOME}/domains/domain1/lib
Start the domain
${GF_HOME}/bin/asadmin start-domain
Name of the domain started: [domain1] and its location: [/usr/local/glassfish/domains/domain1].
Admin port for the domain: [4848].
Deploy Jackrabbit war file to Glassfish. The contextroot is jcr
${GF_HOME}/bin/asadmin deploy --contextroot /jcr --name "JCR-Jackrabbit" ./jackrabbit-webapp-1.6.0.war 
List deployed applications
${GF_HOME}/bin/asadmin list-applications
OpenPTK-Server <web>
JCR-Jackrabbit <web>
OpenPTK-Samples-Taglib <web>
OpenPTK-Apps-UML <web>
Stop Glassfish
${GF_HOME}/bin/asadmin stop-domain
Copy the default (empty) repository and config files. The the contents of the WEB-INF folder to the deployed application. This will override the web.xml and install a default repository.
cp ${SVN_TRUNK}/openptk/ext/Misc/Apache/Jackrabbit/WEB-INF/* ${GF_HOME}/domains/domain1/applications/JCR-Jackrabbit/WEB-INF
Start Glassfish
${GF_HOME}/bin/asadmin start-domain
The WebDAV Servlet is enabled by default. The "default" workspace can be access from a web browser and from the MacOS Finder.
Web browser: http://localhost:8080/jcr/repository/default


Finder: "Go" -> "Connect to Server ..." ->

http://localhost:8080/jcr/repository/default

(optional) delete application
${GF_HOME}/bin/asadmin undeploy "JCR-Jackrabbit"

Reference

Form-based File Upload in HTML November 1995 RFC 1867 
Returning Values from Forms: multipart/form-data August 1998 RFC 2388