Search this blog ...

Loading...

Thursday, May 16, 2013

Unused CSS rules - Optimize HTML pages by removing unreferenced CSS - for free!

Google provides some very useful tools and services that can be leveraged to audit web page performance and thus assist in optimizing HTML pages.  However these do have their warts.

Page Speed for example provides a free online audit check, as well as Firefox and Chrome browser plugins that can be leveraged to analyse a web page’s performance and assist with optimization.  There are two drawbacks with these tools/service

  1. The HTML file must be hosted on a HTTP server and a local file cannot be utilized.  Otherwise a message such as the following will occur “Unable to run PageSpeed Insights on file:///C:/Users/mshannon/Desktop/test2.html. Please navigate to a HTTP(s) web page.”
  2. The browser extensions can be used to “minify” both the HTML and CSS (by removing whitespace / formatting etc), but do not seem to accurately be able to detect unreferenced CSS

My solution to the hosted HTML files was to piggyback on dropbox.  I created myself a public folder in my dropbox account and uploaded the html and css files.  I then right-clicked on the uploaded files and obtained a public link (for example .. https://dl.dropboxusercontent.com/u/12345/test2.html)

Once I had the hosted HTML files, I could leverage the Page Speed service and utilize the browser developer tools.  The firefox Page Speed extension provides an addon to Firebug.  You can see in the image below it has an option to save optimized files to a local location.

image

I leveraged the Firefox Page Speed extension purely to get the cleaned/formatted html and css files.

What was left now to perform was to remove the unreferenced CSS rules.

Google chrome bundles a useful (crippled) developer tool that can be leveraged to audit web page performance and identify unused CSS rules.   Unlike Page Speed, this particular tool does a great job of identifying unreferenced rules.  It is accessed from Chrome through the following menu/popup combination: Tools > Developer Tools > Audits > Web Page Performance > Run

image

You can see by running the tool below, it found a huge number of unreferenced rules:

image

The problem with this tool however, is that it does not provide a mechanism to write out the good (referenced/used) CSS rules.

A bit of searching around, and I found this excellent post:

http://dev.bootstraponline.com/2011/11/automatically-remove-unused-css-rules.html

Essentially, the dude had patched the chrome developer tools to write out the good css rules when running the audit.  The problem for me though, was this was done using Linux builds, and also I wasn’t sure whether newer builds of chrome would offer better detection capabilities.

So I set about getting the latest windows chrome builds from http://commondatastorage.googleapis.com/chromium-browser-continuous/index.html

What I found however, is that they stopped shipping devtools_frontend.zip at build 158804. At the time of writing this, they were now up to build 200347.  Additionally, the contents of devtools_frontend.zip with build 158804 did not seem to match the files that boostraponline referenced in his/her patch.  So I went trawling backwards in builds until I found the very last version to ship with AuditRules.js inside devtools_frontend.zip.  This was build 152197 which is circa August 2012.

You need the following two files:

http://commondatastorage.googleapis.com/chromium-browser-continuous/Win/152197/chrome-win32.zip

http://commondatastorage.googleapis.com/chromium-browser-continuous/Win/152197/devtools_frontend.zip

Next …

  1. extract build 152197 of chrome-win32.zip (e.g. to desktop) ; this automatically creates a folder named "chrome-win32"
  2. extract build 152197 of devtools_frontend.zip to a *new* folder named "devtools_frontend" under "chrome-win32"; the "devtools_frontend" folder needs to be created
  3. create a new folder named "user-data" under "chrome-win32"
  4. create a run.bat file to trigger execution of chrome similar to the following:

@echo off
set CHROMEDIR=C:\Users\mshannon\Desktop\chrome-win32
%CHROMEDIR%\chrome.exe --user-data-dir=%CHROMEDIR%\user-data --debug-devtools-frontend=%CHROMEDIR%\devtools_frontend

Execute the bat file and see if chrome successfully loads.  Once confirmed, close down chrome.

The patch diff that bootstraponline provides is mostly accurate.  The one change though is you must now utilize “InspectorFrontendHost.save("used.css", usedCss, true);”

You can download the patched AuditRules.js file for the above build 152197 from my dropbox: http://db.tt/5ZUFkIqX

Simple replace devtools_frontend\AuditRules.js with the file above.

Having performed the patch, fire up chrome from the bat file.  Invoke the audit tool against the web page (or local file) and you will be presented with a save-as box should it detect unreferenced CSS rules. The file to be saved (used.css) contains the rules that were referenced.

image

Thanks bootstraponline and Google!

Saturday, April 13, 2013

Recover / Decrypt Weblogic password from boot.properties

When installing a Weblogic domain in development mode, the Configuration wizard will generate a boot identity file for the administration server containing the encrypted username and password of the initial administrative user. These credentials are then automatically leveraged when starting the admin server and avoid the need for the weblogic administrator to manually supply these. It is also possible to utilize a boot identify file (boot.properties) in production domains.    See the following link for more information: http://docs.oracle.com/cd/E14571_01/web.1111/e13708/overview.htm#i1068887

Recovering/decrypting a credential value from the boot identity file is reasonably straightforward should you have shell and executable access to the Weblogic installation.

First, obtain the DOMAIN_HOME value …

ps auxwww | grep Name=AdminServer | tr " " "\n" | grep "domain.home"

-Ddomain.home=/u01/app/oracle/product/Middleware/user_projects/domains/base_domain

Next, source the setDomainEnv.sh file …

export DOMAIN_HOME=/u01/app/oracle/product/Middleware/user_projects/domains/base_domain

source $DOMAIN_HOME/bin/setDomainEnv.sh

Extract the encrypted username and password credential from the boot identify file ...

USR=`grep username $DOMAIN_HOME/servers/AdminServer/security/boot.properties | sed -e "s/^username=\(.*\)/\1/"`

PW=`grep password $DOMAIN_HOME/servers/AdminServer/security/boot.properties | sed -e "s/^password=\(.*\)/\1/"`

Sample values …

mshannon@slc05elc% echo $USR
{AES}RI+L8BLQQc3mTwbCx59un+vcHJ4c30GMQ90ovDY7VLI=

mshannon@slc05elc% echo $PW
{AES}B9acQuaVUBNqsem1FzGROqu7w2tqZenm3StwYB3C+bM=

Create the small java Decrypt program and invoke it supplying the DOMAIN_HOME and encrypted value requiring decryption …

cat > /tmp/Decrypt.java <<EOF
public class Decrypt {
  public static void main(String[] args) {
    System.out.println("Decrypted value: " + new weblogic.security.internal.encryption.ClearOrEncryptedService(
      weblogic.security.internal.SerializedSystemIni.getEncryptionService(args[0])).
        decrypt(args[1]));
  }
}
EOF

$JAVA_HOME/bin/javac -d /tmp /tmp/Decrypt.java

$JAVA_HOME/bin/java -cp /tmp:$CLASSPATH Decrypt "$DOMAIN_HOME" "$USR"

$JAVA_HOME/bin/java -cp /tmp:$CLASSPATH Decrypt "$DOMAIN_HOME" "$PW"

Sample output … 

mshannon@slc05elc% $JAVA_HOME/bin/java -cp /tmp:$CLASSPATH Decrypt "$DOMAIN_HOME" "$USR"
Decrypted value: weblogic

mshannon@slc05elc% $JAVA_HOME/bin/java -cp /tmp:$CLASSPATH Decrypt "$DOMAIN_HOME" "$PW"
Decrypted value: welcome1

Wednesday, March 27, 2013

Simple Chrome black background dark theme extension - How to build one in 5 minutes

There are times when a white background is best replaced with a black one.  If you are using Chrome, you can build your own custom extension in just a few minutes that can easily modify colours of your favourite website(s). To do this, we leverage the Content Scripts feature available to Chrome Extensions that is kind of like a poor man’s version of GreaseMonkey.  Best of all though, there are no closed custom 3rd party Chrome extensions to install from some unknown developer, here you will be the developer!

Content Scripts provide a mechanism to manipulate the DOM. The DOM is essentially a tree of all objects that constitute the webpage (images / links / text / styles etc).

What we are going to do is create an unpacked extension that simply adds a new stylesheet link at the end of the page load which will change the background colour to black, and the text/link/visited-link colours to various shades of grey. For our test, we will modify all pages falling under cnn.com and yahoo.com.

To get started, first create yourself a folder (on your Desktop or wherever) that will host the two files that comprise our extension, for example "Black Background Extension"

Within this folder, create a file named toggle.js that has the following contents:

var cssStyle='* { background: black !important; color: #EEEEEE !important }'
+ ' :link, :link * { color: #A1A1A1 !important }'
+ ' :visited, :visited * { color: #505050 !important }';

if(document.createStyleSheet) {
  document.createStyleSheet("javascript:'"+cssStyle+"'");
} else {
  var cssLink = document.createElement('link');
  cssLink.rel = 'stylesheet';
  cssLink.href = 'data:text/css,'+escape(cssStyle);
  document.getElementsByTagName("head")[0].appendChild(cssLink);
}

Next, create a file named manifest.json that has the following contents:

{
  "name": "Black Background",
  "version": "1.0",
  "description": "Sets background colour to black, and text, link, and visited link to shades of grey",
  "manifest_version": 2,
  "content_scripts": [
    {
      "matches": ["http://*.cnn.com/*", "http://*.yahoo.com/*"],
      "js": ["toggle.js"],
      "run_at": "document_end",
      "all_frames": true
    }
  ]
}

Now, it is a simple matter of firing up Chrome, and typing in chrome://extensions in the address bar. Once the Extensions are displayed, activate the Developer mode option.

image

Next, choose the Load unpacked extension… button and navigate to the folder created above hosting our unpacked extension.

image

If all goes to plan, our extension should now be listed.  We also have the option of packing our extension in to a signed zip file that would allow us to redistribute it.

image

Finally, simply restart the Chrome browser and attempt to access a site from our match rules (cnn.com for example).  You should see that the background colour is now black!

image

For details on the content_scripts options leveraged in our manifest, refer to the Chrome documentation http://developer.chrome.com/extensions/content_scripts.html

 

Wednesday, March 20, 2013

How to determine if an Oracle LOB is stored as a SECUREFILE or BASICFILE

The DESCRIBE command on an Oracle table is not sufficient to determine whether a LOB column is stored as a SECUREFILE or a regular old BASICFILE. Instead you must query USER_LOBS (or DBA_LOBS etc), or alternatively leverage the PL/SQL dbms_lob.issecurefile function.

% sqlplus

SQL*Plus: Release 11.2.0.1.0 Production on Tue Mar 19 18:04:53 2013

Copyright (c) 1982, 2009, Oracle.  All rights reserved.

Enter user-name: / as sysdba

Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> show parameter compatible;

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
compatible                           string      11.2.0.0.0

SQL> show parameter db_securefile;

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
db_securefile                        string      PERMITTED

 

The DB_SECUREFILE parameter specifies whether or not to treat LOB files as SecureFiles by default.

NEVER - LOBs that are specified as SecureFiles are created as BasicFile LOBs. Any SecureFile-specific storage options specified will result in an exception.
PERMITTED - LOBs are allowed to be created as SecureFiles, but will be created as BasicFile by default.
ALWAYS - All LOBs created in the system are created as SecureFile LOBs.
IGNORE - The SECUREFILE keyword and all SecureFile options are ignored.

If the COMPATIBLE parameter is not set to 11.1 or higher, then LOBs are not treated as SecureFiles.

 

SQL> create user matt identified by welcome1;

User created.

SQL> grant create session to matt;

Grant succeeded.

SQL> grant create table to matt;

Grant succeeded.

SQL> grant unlimited tablespace to matt;

Grant succeeded.

SQL> conn matt/welcome1
Connected.

SQL>
CREATE TABLE test(
  lob1 BLOB
,lob2 BLOB
,lob3 BLOB
,lob4 BLOB
)
  LOB(lob2) STORE AS BASICFILE
,LOB(lob3) STORE AS SECUREFILE
,LOB(lob4) STORE AS SECUREFILE (
    ENABLE STORAGE IN ROW
    NOCACHE LOGGING
    COMPRESS MEDIUM
    DEDUPLICATE
  )
/

Table created.


Logging options:

LOGGING - LOB changes generate full entries in redo logs
NOLOGGING - LOB changes are not logged in the redo logs and cannot be replayed in the event of failure.

Caching options

CACHE - LOB data is placed in the buffer cache.
CACHE READS - LOB data is only placed in the buffer cache only during read operations but not during write operations.
NOCACHE - LOB data is not placed in the buffer cache, or brought in to the buffer cache and placed at the least recently used end of the LRU list.

SecureFile LOBs also support FILESYSTEM_LIKE_LOGGING logging option which is similar to metadata journaling of file systems

 

SQL> desc test;
Name                                      Null?    Type
----------------------------------------- -------- ----------------------------
LOB1                                               BLOB
LOB2                                               BLOB
LOB3                                               BLOB
LOB4                                               BLOB

SQL> desc user_lobs;
Name                                      Null?    Type
----------------------------------------- -------- ----------------------------
TABLE_NAME                                         VARCHAR2(30)
COLUMN_NAME                                        VARCHAR2(4000)
SEGMENT_NAME                                       VARCHAR2(30)
TABLESPACE_NAME                                    VARCHAR2(30)
INDEX_NAME                                         VARCHAR2(30)
CHUNK                                              NUMBER
PCTVERSION                                         NUMBER
RETENTION                                          NUMBER
FREEPOOLS                                          NUMBER
CACHE                                              VARCHAR2(10)
LOGGING                                            VARCHAR2(7)
ENCRYPT                                            VARCHAR2(4)
COMPRESSION                                        VARCHAR2(6)
DEDUPLICATION                                      VARCHAR2(15)
IN_ROW                                             VARCHAR2(3)
FORMAT                                             VARCHAR2(15)
PARTITIONED                                        VARCHAR2(3)
SECUREFILE                                         VARCHAR2(3)
SEGMENT_CREATED                                    VARCHAR2(3)

 

set linesize 100

col Column format a6
col isSecureFile format a12
col Compressed format a10
col DeDuplicated format a12
col Encrypted format a9
col StoredInRow format a11
col Logging format a7
col Cached format a10

SELECT
  column_name as "Column"
,securefile as "isSecureFile"
,compression as "Compressed"
,deduplication as "DeDuplicated"
,encrypt as "Encrypted"
,in_row as "StoredInRow"
,logging as "Logging"
,cache as "Cached"
FROM user_lobs
WHERE table_name = 'TEST'


Column isSecureFile Compressed DeDuplicated Encrypted StoredInRow Logging Cached
------ ------------ ---------- ------------ --------- ----------- ------- ----------
LOB1   NO           NONE       NONE         NONE      YES         YES     NO
LOB2   NO           NONE       NONE         NONE      YES         YES     NO
LOB3   YES          NO         NO           NO        YES         YES     NO
LOB4   YES          MEDIUM     LOB          NO        YES         YES     NO

 

SQL>

insert into test values(empty_blob(), empty_blob(), empty_blob(), empty_blob())

set serveroutput on

DECLARE
l1 BLOB; l2 BLOB; l3 BLOB; l4 BLOB;
BEGIN
  SELECT lob1, lob2, lob3, lob4
  INTO l1, l2, l3, l4
  FROM test
  WHERE rownum = 1;

  IF dbms_lob.issecurefile(l1) THEN
    dbms_output.put_line('Stored in a securefile');
  ELSE
    dbms_output.put_line('Not stored in a securefile');
  END IF;

  IF dbms_lob.issecurefile(l2) THEN
    dbms_output.put_line('Stored in a securefile');
  ELSE
    dbms_output.put_line('Not stored in a securefile');
  END IF;

  IF dbms_lob.issecurefile(l3) THEN
    dbms_output.put_line('Stored in a securefile');
  ELSE
    dbms_output.put_line('Not stored in a securefile');
  END IF;

  IF dbms_lob.issecurefile(l4) THEN
    dbms_output.put_line('Stored in a securefile');
  ELSE
    dbms_output.put_line('Not stored in a securefile');
  END IF;

END;
/


Not stored in a securefile
Not stored in a securefile
Stored in a securefile
Stored in a securefile

PL/SQL procedure successfully completed.

Wednesday, March 6, 2013

Disable Adobe Reader XI (11.x) Welcome Screen

 

Create and execute a registry file (disablewelcomescreen.reg) with contents:

REGEDIT4

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Adobe\Acrobat Reader\11.0\FeatureLockDown\cWelcomeScreen]
"bShowWelcomeScreen"=dword:00000000

Alternatively, open regedit and then navigate to Computer > HKEY_LOCAL_MACHINE > SOFTWARE > Policies > Adobe > Acrobat Reader > 11.0 > FeatureLockDown.

right click on FeatureLockDown and choose New > Key.

name the key cWelcomeScreen

right click the cWelcomeScreen key and choose New > DWORD (32-bit) Value

name the DWORD value bShowWelcomeScreen

leave the value as 0

Tuesday, February 26, 2013

xargs - is command executed once or multiple times – examples

xargs is used to execute a command one or potentially multiple times using the standard input as arguments to that command.

What sometimes in unclear is whether the command invoked by xargs is executed once or multiple times.

This blog post should clear things up …

Firstly, here is the xargs program usage in layman’s terms

xargs [xargs options] [command] [arguments for command based on xargs standard input]

The standard input would typically be output that was piped from an earlier command such as find / ls / echo etc.  It doesn’t have to be though, you can invoke xargs as a standalone no-arg command and simply type the input and terminate with a control-d to signal end-of-terminal.

Let’s try a simple example combining the xargs --verbose option so that we can see the command-line that xargs will execute on the standard error output before it is actually invoked. In the screenshot below, I invoke the xargs command, and then enter 1 to 5 separating each with a newline (enter), followed by a terminating ctrl-d.  As I provided the --verbose option to xargs, it wrote out the command that it will execute and the arguments that it is going to provide to that command.

image

The output above shows that be default if you don’t provide an explicit command for xargs to invoke, it will leverage /bin/echo. You can also see xargs invoked this echo command just a single time. What may not be obvious is how xargs processed the standard input to come up with arguments to supply the (in this case 'echo') command. xargs will by default treat whitespace and newlines as delimiters. In the example below, I entered: 1 TAB 2 NEWLINE 3 SPACE 4 NEWLINE 5 NEWLINE ctrl-d.

image

So how and under what conditions does the command that xargs executes get invoked multiple times?

Well.. you can either explicitly tell xargs that a particular command can only operate on a specific number of arguments at a time, and that the command should be reinvoked as required to work through the remaining arguments; OR … xargs may determine itself that the command to execute along with any arguments hits a maximum command-line length, in which case it automatically splits the arguments across multiple command invocations.

Let’s first explicitly tell xargs that a command invocation should only work on a maximum number of arguments at a time.  You do this through the -n option …

image

The above example first shows supplying a “-n1” option which results in a command being invocated for each argument. Later the “-n2” option is leveraged which result in a command being invoked for every two arguments. The final xargs test above shows how 1 TAB 2 NEWLINE 3 SPACE 4 NEWLINE 5 NEWLINE ctrl-d is processed with the “-n1” option.  You can see that xargs immediately starts invoking commands after each line of input is processed.

As mentioned prior, xargs may itself automatically split arguments across multiple commands if a maximum command-line character length is reached. The xargs binary will likely have a default size limit hardcoded to the operating system ARG_MAX length.

image

You can explicitly tell xargs the max command-line length using the --max-chars or -s options.  The length comprises the command and initial-arguments and the terminating nulls at the ends of the argument strings.  As seen below “/bin/echo 1” will take 12 chars, and “/bin/echo 1 2” will take 14 chars (including terminating null).

/bin/echo 1 2
12345678901234

Let’s try out this “-s” option…

image

Here is a final example that ties everything together.  It demonstrates the following:

  1. xargs receiving piped standard-input from a prior command (i.e tr / find).
  2. xargs using the --verbose option to output the command that will be invoked
  3. xargs being told to leverage an explicit delimiter “-d” option, e.g “\n” – newline
  4. xargs invoking an explicit command (the Unix “file” command)
  5. xargs invoking an explicit command once per argument (“-n1”)
  6. find command leveraging the “-print0” option to delimit search results by the null character (rather than newline), in conjunction with the “-0” (or --null) xargs option so that any search results which contain whitespace are treated correctly as a single command-argument.

image

image

Wednesday, February 6, 2013

Java Applet terminated in browser when debugging – solution

I recently had to troubleshoot an old applet that was not working as expected. I intended to leverage JDeveloper to debug the applet utilizing JPDA socket debug steps listed in: http://docs.oracle.com/javase/6/docs/technotes/guides/plugin/developer_guide/debugger.html

I was able to successfully connect/attach to the VM from JDeveloper and start stepping through code, however within a minute or so, the applet would unexpectedly terminate.  You can imagine how frustrating this was.  A quick search on google listed possible causes such as browser plugin watchdogs, need for administration privileges, JRE bugs etc. etc.

It turns out the problem was caused due to a breakdown in the heartbeat messages sent between the browser VM and client VM.  Invoking the browser with the environment variable JPI_PLUGIN2_NO_HEARTBEAT=1 enabled the debugging to proceed as intended without the applet/Java-plugin being killed / terminating.

Below are the steps I performed to successfully debug an applet:

From Windows Control Panel choose Java > "Java" tab > "View..." button for Java Runtime Environment Settings
Double click table column value for Runtime Parameters for the user JRE entry.

Set the value as
-agentlib:jdwp=transport=dt_socket,server=y,address=8453,suspend=n

click "OK"
Click "Apply" on the Java Runtime Environment Settings page
Click "OK" on the Java Runtime Environment Settings page

jre
For debugging, the following advanced options should also be set for the JRE:

"Advanced" tab > Settings > Debugging
Enable tracing
Enable logging

"Advanced" tab > Settings > Java console
Choose Show console

Having configured the JRE options, we now need to disable that heartbeat message mechanism before we launch the browser.  As mentioned above, failure to perform this will result in the Applet / JRE abnormally terminating typically under a minute or so of stepping in to code.
You will likely see an exception along the lines of
basic: JVM[id=0]-Heartbeat heartbeat dead, exception. dT=NNN seconds.

From command-prompt
set JPI_PLUGIN2_NO_HEARTBEAT=1

And then launch browser, e.g.

%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe --disable-hang-monitor

e.g. C:\Users\mshannon\AppData\Local\Google\Chrome\Application\chrome.exe --disable-hang-monitor

The "--disable-hang-monitor" option passed to Chrome prevents it from popping up the ‘plugin not responding’ dialog.

image

Additional command-line options for Browser JVM Troubleshooting can also be set; These result in verbose console messages such as rendering the parameters supplied to the applet.

set JPI_PLUGIN2_DEBUG=1
set JPI_PLUGIN2_VERBOSE=1

Check for JRE logs/traces in %USERPROFILE%\AppData\LocalLow\Sun\Java\Deployment\log

e.g. C:\Users\mshannon\AppData\LocalLow\Sun\Java\Deployment\log

Once I was able to properly step through code, I found the cause of the problem with our applet.  The applet was no longer able to obtain from the browser an OAM 11g webgate cookie which had been set to be httponly.

Simple Solution - Black Wallpaper Background in Android

I’ve just got hold of an Android Samsung Galaxy S2 phone, and I want a fixed background color, in particular black.  Why does Google make this so difficult.

Here is my solution that involves installing nil/zero android apps that took less than a minute.

Step 1. Go to GSMARENA.COM and lookup the Display size resolution for your phone.  For the galaxy S2, it is 480 x 800 pixels.

Step 2. Fire up a basic paint application, such as Windows paint.

Step 3. Resize the image to the resolution from above

Step 4. Choose the appropriate color you want for the wallpaper/background, and leverage the fill/bucket tool to fill the image

Step 5. Save the image as a GIF, JPG, or PNG

Step 6. Email the image to your gmail account accessible from your android phone

Step 7.Open Gmail application and the email containing image just sent

Step 8. Click Save and View on Image

Step 9. Press Menu button in Viewer, and Choose “Set as” option and Home screen wallpaper or Lock screen wallpaper etc.

Step 10. Choose the correct orientation for the image – portrait etc, and click Save.

Congratulation, you should now have a fixed background.

Snap1Snap2Snap3Snap4

If you have a 480x800 phone, you can probably just save the image linked below.

black

Saturday, September 15, 2012

JAXB - XSD to Java Map/HashMap example using xjc, bindings and XmlAdapter

I’ve been assigned the task of implementing for my specific product team a common RESTful API that is invoked as part of a cloud on-boarding process.  The spec provided to me describes the data structures comprising the request and responses – all of which will be encoded in JSON.  In an ideal world, I would leverage something like JAX-RS (using something like Jersey RI). But, alas, that would be too easy.  Instead I must host this API somehow on top of my product’s existing service-based architecture framework.  I went searching around for a JAXB equivalent for JSON – that would allow some type of JSON to Java binding. I was hoping I would find some type of JSON schema definition concept, an xjc and schemgen equivalent etc.  I came up short on my search for such tools, but I did discover that Jackson and Jersey can support de/serialization from/to JSON of Java objects that are annotated using JAXB (java.xml.bind.annotation). This was a welcome discovery and it meant I could set about trying to model the specification’s data structures with XSD.

My plan was to create the XML schema up front, and then leverage xjc to create the set of JAXB-annotated Java classes that map to the elements/types defined in the schema.  Everything was coming along nicely.  Whenever I got stuck on the XSD front, I would simply try and model the concept using some basic java classes, then fire up the schemagen tool to view the schema it generated, and incorporate the techniques/result back in to my own XSD. I completed the XSD, and invoked xjc and out came my auto-generated JAXB-annotated classes.  However, some of the classes and properties were not what I was expecting.  Where had my java.util.Map based properties gone?

If you take a simple sample like the following – a Person object with a map property of all their worldly gadgets (iPad/iPhones etc):

Person.java

import java.util.Map;

public class Person
{
  private String name;
  private Map<String, Gadget> gadgets;

  public void setName(String name)
  {
    this.name = name;
  }

  public String getName()
  {
    return name;
  }

  public void setGadgets(Map<String, Gadget> gadgets)
  {
    this.gadgets = gadgets;
  }

  public Map<String, Gadget> getGadgets()
  {
    return gadgets;
  }
}

Gadget.java

public class Gadget
{
  private String make;

  public void setMake(String make)
  {
    this.make = make;
  }

  public String getMake()
  {
    return make;
  }
}

Running schemagen on the above, you get the following schema:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="
http://www.w3.org/2001/XMLSchema">

  <xs:complexType name="gadget">
    <xs:sequence>
      <xs:element name="make" type="xs:string" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="person">
    <xs:sequence>
      <xs:element name="gadgets">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="entry" minOccurs="0" maxOccurs="unbounded">
              <xs:complexType>
                <xs:sequence>
                  <xs:element name="key" minOccurs="0" type="xs:string"/>
                  <xs:element name="value" minOccurs="0" type="gadget"/>
                </xs:sequence>
              </xs:complexType>
            </xs:element>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
      <xs:element name="name" type="xs:string" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

If I then feed the above schema back to xjc, the JAXB-annotated classes that get generated look like the following (javadoc removed to save on space):

Person.java

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "person", propOrder = {
    "gadgets",
    "name"
})
public class Person {

    @XmlElement(required = true)
    protected Person.Gadgets gadgets;
    protected String name;

    public Person.Gadgets getGadgets() {
        return gadgets;
    }

    public void setGadgets(Person.Gadgets value) {
        this.gadgets = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String value) {
        this.name = value;
    }


    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "", propOrder = {
        "entry"
    })
    public static class Gadgets {

        protected List<Person.Gadgets.Entry> entry;

        public List<Person.Gadgets.Entry> getEntry() {
            if (entry == null) {
                entry = new ArrayList<Person.Gadgets.Entry>();
            }
            return this.entry;
        }

        @XmlAccessorType(XmlAccessType.FIELD)
        @XmlType(name = "", propOrder = {
            "key",
            "value"
        })
        public static class Entry {

            protected String key;
            protected Gadget value;

            public String getKey() {
                return key;
            }

            public void setKey(String value) {
                this.key = value;
            }

            public Gadget getValue() {
                return value;
            }

            public void setValue(Gadget value) {
                this.value = value;
            }

        }

    }

}

Gadget.java

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "gadget", propOrder = {
    "make"
})
public class Gadget {

    protected String make;

    public String getMake() {
        return make;
    }

    public void setMake(String value) {
        this.make = value;
    }
}

As can be seen above in the generated Person class, the Map was not reinstated. Instead a new Person.Gadgets class was created containing a list of the new Person.Gadgets.Entry class. To be fair, the xjc tool treated the schema at face value.  How was it to know that this structure should be modelled by a Map.  Ideally, it would be nice if only a few instructions in a bindings file (supplied to xjc tool along with the schema file) were sufficient to auto-generate all required JAXB-annotated classes with full Map support.  Unfortunately this is not the case. Instead a bindings file must be created that targets appropriate elements in the schema and overrides their baseType with a fully-qualified custom Map subclass.  Custom java files must be hand-created for the Map subclass, and also an XmlAdapter subclass which contains the logic to unmarshal/marshal to/from the Map subclass.


What proceeds is a fully-worked example based on the Person / Gadget scenario above that restores Map support for set/getGadgets() methods of the Person class. It also has a few extra features thrown in including subclassing of Gadget. Credit for this technique must go to Aaron Anderson @ adventuresintechology.blogspot.com.

First, the xml schema.

schema.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="
http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
targetNamespace="
http://todayguesswhat.blogspot.com/test" xmlns:test="http://todayguesswhat.blogspot.com/test">

<xs:element name="Person">
  <xs:complexType>
   <xs:sequence>
    <xs:element name="name" type="xs:string"/>
    <xs:element name="gadgets" type="test:GadgetMapModeller" minOccurs="0"/>
   </xs:sequence>
  </xs:complexType>
</xs:element>

<xs:complexType name="GadgetMapModeller">
  <xs:sequence>
    <xs:element name="entry" minOccurs="0" maxOccurs="unbounded">
     <xs:complexType>
      <xs:sequence>
        <xs:element name="key" type="xs:string"/>
        <xs:element name="value" type="test:Gadget"/>
      </xs:sequence>
     </xs:complexType>
   </xs:element>
  </xs:sequence>
</xs:complexType>

<xs:complexType name="Gadget">
  <xs:sequence>
   <xs:element name="make" type="xs:string"/>
   <xs:element name="model" type="xs:string"/>
   <xs:element name="year" type="xs:int"/>
  </xs:sequence>
</xs:complexType>

<xs:complexType name="Computer">
  <xs:complexContent>
   <xs:extension base="test:Gadget">
    <xs:sequence>
     <xs:element name="speed" type="xs:int"/>
     <xs:element name="cpu" type="xs:string"/>
    </xs:sequence>
   </xs:extension>
  </xs:complexContent>
</xs:complexType>

</xs:schema>

The important bindings file that overrides the generated type of the gadgets element to the new Map subclass – GadgetMap<String, Gadget>

bindings.xml

<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.1">

<jaxb:bindings schemaLocation="schema.xsd">
 
  <jaxb:bindings node="//xs:element[@name='Person']//xs:element[@name='gadgets']">
   <jaxb:property>
    <jaxb:baseType name="com.blogspot.todayguesswhat.test.model.GadgetMap&lt;String,Gadget&gt;" />
   </jaxb:property>
  </jaxb:bindings>

</jaxb:bindings>

</jaxb:bindings>

The new Map subclass with XmlJavaTypeAdapter JAXB annotation defining the name of the adapter for unmarshalling/marshalling  to/from this type.

GadgetMap.java

package com.blogspot.todayguesswhat.test.model;

import java.util.HashMap;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlJavaTypeAdapter(GadgetMapAdapter.class)
public class GadgetMap<String,Gadget> extends HashMap<String,Gadget>
{
}

The XmlAdapter that does the important conversion from one type to another:

GadgetMapAdapter.java

package com.blogspot.todayguesswhat.test.model;

import java.util.Map;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class GadgetMapAdapter extends XmlAdapter<GadgetMapModeller, GadgetMap<String,Gadget>>
{
  @Override
  public GadgetMap<String,Gadget> unmarshal(GadgetMapModeller modeller)
  {
    GadgetMap<String,Gadget> map = new GadgetMap<String,Gadget>();
    for (GadgetMapModeller.Entry e : modeller.getEntry())
    {
      map.put(e.getKey(), e.getValue());
    }
    return map;
  }

  @Override
  public GadgetMapModeller marshal(GadgetMap<String,Gadget> map)
  {
    GadgetMapModeller modeller = new GadgetMapModeller();
    for (Map.Entry<String,Gadget> entry : map.entrySet())
    {
      GadgetMapModeller.Entry e = new GadgetMapModeller.Entry();
      e.setKey(entry.getKey());
      e.setValue(entry.getValue());
      modeller.getEntry().add(e);
    }
    return modeller;
  }
}

A few test classes.

MarshalTest.java

package com.blogspot.todayguesswhat.test.model;
 
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
 
public class MarshalTest
{
  public static void main(String[] args) throws Exception
  {
    JAXBContext jc = JAXBContext.newInstance(Person.class);
    ObjectFactory factory = new ObjectFactory();

    Person person = factory.createPerson();
    person.setName("Matt Shannon");

    GadgetMap<String, Gadget> map = new GadgetMap<String, Gadget>();

    Gadget gadget1 = new Gadget();
    gadget1.setMake("Apple");
    gadget1.setModel("iPod");
    gadget1.setYear(2002);

    Computer gadget2 = new Computer();
    gadget2.setMake("Lenovo");
    gadget2.setModel("Thinkpad X230");
    gadget2.setYear(2012);
    gadget2.setCpu("Intel i5-3320M");
    gadget2.setSpeed(2600);

    map.put("my ipad", gadget1);
    map.put("my laptop", gadget2);

    person.setGadgets(map);

    Marshaller marshaller = jc.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(person, System.out);
  }
}

UnmarshalTest.java

package com.blogspot.todayguesswhat.test.model;
 
import java.io.File;

import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
 
public class UnmarshalTest
{
  public static void main(String[] args) throws Exception
  {
    JAXBContext jc = JAXBContext.newInstance(Person.class);

    Unmarshaller u = jc.createUnmarshaller();

    Person person = (Person) u.unmarshal(new File(args[0]));
    logPerson(person);
  }

  public static void logPerson(Person p)
  {
    if (p != null)
    {
      System.out.println("Person [name=" + p.getName() + "]");
      GadgetMap<String, Gadget> map = p.getGadgets();
      if (map != null)
      {
        for (Map.Entry<String,Gadget> entry : map.entrySet())
        {
          Gadget g = entry.getValue();
          System.out.println("   " + entry.getKey() + " : " + gadgetToString(g));
        }
      }
    }
  }

  public static String gadgetToString(Gadget g)
  {
    String result = null;
    if (g != null)
    {
      result = "[make="+g.getMake()+"][model="+g.getModel()+"][year="+g.getYear()+"]";
      if (g instanceof Computer)
      {
        Computer c = (Computer) g;
        result = result + "[cpu="+c.getCpu()+"][speed="+c.getSpeed()+"]";
      }
    }
    return result;
  }
}

An XML file leveraged by the unmarshalling test

person.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Person xmlns="
http://todayguesswhat.blogspot.com/test">
    <name>Louise</name>
    <gadgets>
        <entry>
            <key>work laptop</key>
            <value xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance" xsi:type="Computer">
                <make>Lenovo</make>
                <model>Thinkpad X120e</model>
                <year>2011</year>
                <speed>1600</speed>
                <cpu>AMD E-350</cpu>
            </value>
        </entry>
        <entry>
            <key>my phone</key>
            <value>
                <make>Apple</make>
                <model>iPhone 3g</model>
                <year>2008</year>
            </value>
        </entry>
    </gadgets>
</Person>

And some windows bat files to invoke the various commands

clean.bat

@ECHO off
SET PROJECT_GENSRC=%~dp0\gensrc
IF EXIST "%PROJECT_GENSRC%" RMDIR /s /q "%PROJECT_GENSRC%"
SET PROJECT_JAVADOC=%~dp0\javadoc
IF EXIST "%PROJECT_JAVADOC%" RMDIR /s /q "%PROJECT_JAVADOC%"
SET PROJECT_CLASSES=%~dp0\classes
IF EXIST "%PROJECT_CLASSES%" RMDIR /s /q "%PROJECT_CLASSES%"
pause.

jaxb.bat

@ECHO off
SET JAVA_HOME=C:\Oracle\Middleware\jdk160_24
SET JAXBCMD=%JAVA_HOME%\bin\xjc.exe
SET PROJECT_GENSRC=%~dp0\gensrc
IF EXIST "%PROJECT_GENSRC%" RMDIR /s /q "%PROJECT_GENSRC%"
MKDIR "%PROJECT_GENSRC%"
"%JAXBCMD%" -no-header -d "%PROJECT_GENSRC%" -p com.blogspot.todayguesswhat.test.model -xmlschema "%~dp0\schema.xsd" -b "%~dp0\bindings.xml"
pause.

image

image

image

compile.bat

@ECHO off
SET JAVA_HOME=C:\Oracle\Middleware\jdk160_24
SET JAVACMPCMD=%JAVA_HOME%\bin\javac.exe
SET SRC_BASE_PKG=src\com\blogspot\todayguesswhat\test\model
SET GENSRC_BASE_PKG=gensrc\com\blogspot\todayguesswhat\test\model
SET PROJECT_CLASSES=%~dp0\classes
IF EXIST "%PROJECT_CLASSES%" RMDIR /s /q "%PROJECT_CLASSES%"
MKDIR "%PROJECT_CLASSES%"
"%JAVACMPCMD%" -d "%PROJECT_CLASSES%" %SRC_BASE_PKG%\*.java %GENSRC_BASE_PKG%\*.java
pause.

image

javadoc.bat

@ECHO off
SET JAVA_HOME=C:\Oracle\Middleware\jdk160_24
SET JAVADOCCMD=%JAVA_HOME%\bin\javadoc.exe
SET PROJECT_SRC=%~dp0\src
SET PROJECT_GENSRC=%~dp0\gensrc
SET PROJECT_JAVADOC=%~dp0\javadoc
IF EXIST "%PROJECT_JAVADOC%" RMDIR /s /q "%PROJECT_JAVADOC%"
MKDIR "%PROJECT_JAVADOC%"
"%JAVADOCCMD%" -sourcepath "%PROJECT_GENSRC%;%PROJECT_SRC%" -d "%PROJECT_JAVADOC%" -subpackages com.blogspot.todayguesswhat.test.model -protected
pause.

image

image

 

image

 

image

 

image

run-marshal-test.bat

@ECHO off
MODE 120,50
SET JAVA_HOME=C:\Oracle\Middleware\jdk160_24
SET JAVACMD=%JAVA_HOME%\bin\java.exe
SET PROJECT_CLASSES=%~dp0\classes
"%JAVACMD%" -classpath "%PROJECT_CLASSES%" com.blogspot.todayguesswhat.test.model.MarshalTest
pause.

image

image

run-unmarshal-test.bat

@ECHO off
MODE 120,50
SET JAVA_HOME=C:\Oracle\Middleware\jdk160_24
SET JAVACMD=%JAVA_HOME%\bin\java.exe
SET PROJECT_CLASSES=%~dp0\classes
"%JAVACMD%" -classpath "%PROJECT_CLASSES%" com.blogspot.todayguesswhat.test.model.UnmarshalTest "%~dp0\person.xml"
pause.

image

image

Click here to download a zip of this JAXB sample.

Friday, August 31, 2012

Windows 7 Replacement for UserAccounts.CommonDialog in VBScript

After 6 years of mostly trouble-free development/engineering, I finally retired my Windows XP-based Dell Latitude D620 from active work duty.  I had been holding out for a business laptop with USB 3.0 to become available on the internal procurement site, and were finally able to obtain a Lenovo X230 with an Ivy Bridge i5-3320M processor.  (Un)fortunately this new machine is running Windows 7 x64, and for that matter a bastardized version full of all the resource hungry corporate mandated bloat.  This is the first time I have seen/used Windows 7 (having managed to also completely avoid Vista). The first thing I find myself doing is trying to make Windows 7 feel and behave like Windows XP again.  After installing classic shell to get the old start button functionality back (http://classicshell.sourceforge.net/) and turning off all of the visual effects (aka – ‘Adjust for best performance’ setting), my desktop is starting to resemble and feel like the ugly but reliable XP again.  Now I’m slowly working through my kit bag of productivity scripts that I created for XP and trying to get these to function in Windows 7.

One of the more frequent scripts I leverage is a simple VBScript for upload and download of a file by invoking the command-line FTP utility shipped with Windows.  See the following article I wrote for the full original XP supported source code: http://todayguesswhat.blogspot.com.au/2010/06/vbscript-ftp-upload-sample-leverages.html

I found out that the UserAccounts.CommonDialog class/control is not available in Windows 7. I leveraged this control to allow the user to select a file for upload.  Original VBScript code shown below:

Function ChooseFile(initialDir)
  Set cd = CreateObject("UserAccounts.CommonDialog")

  cd.InitialDir = initialDir
  cd.Filter = "ZIP files|*.zip|Text Documents|*.txt|Shell Scripts|*.*sh|All Files|*.*"
  ' filter index 4 would show all files by default
  ' filter index 1 would should zip files by default
  cd.FilterIndex = 1
  If cd.ShowOpen = True Then
    ChooseFile = cd.FileName
  Else
    ChooseFile = ""
  End If
  Set cd = Nothing
End Function

For Windows 7, I’ve kludged together code to replace the above method using techniques/articles/suggestions borrowed from multiple parties.  If there is a cleaner mechanism to navigate and select a file using VBScript in Windows 7, please let me know :)

The code I developed/hacked-together creates a temporary powershell script that spawns System.Windows.Forms OpenFileDialog, and then writes the chosen file out to a temporary output text file. The VBScript then reads in the value from the output text file and returns that in the function.  Code is as follows:

Function ChooseFile (ByVal initialDir)

  Set shell = CreateObject("WScript.Shell")

  Set fso = CreateObject("Scripting.FileSystemObject")

  tempDir = shell.ExpandEnvironmentStrings("%TEMP%")

  tempFile = tempDir & "\" & fso.GetTempName

  ' temporary powershell script file to be invoked
  powershellFile = tempFile & ".ps1"

  ' temporary file to store standard output from command
  powershellOutputFile = tempFile & ".txt"

  'input script
  psScript = psScript & "[System.Reflection.Assembly]::LoadWithPartialName(""System.windows.forms"") | Out-Null" & vbCRLF
  psScript = psScript & "$dlg = New-Object System.Windows.Forms.OpenFileDialog" & vbCRLF
  psScript = psScript & "$dlg.initialDirectory = """ &initialDir & """" & vbCRLF
  psScript = psScript & "$dlg.filter = ""ZIP files|*.zip|Text Documents|*.txt|Shell Scripts|*.*sh|All Files|*.*""" & vbCRLF
  ' filter index 4 would show all files by default
  ' filter index 1 would should zip files by default
  psScript = psScript & "$dlg.FilterIndex = 4" & vbCRLF
  psScript = psScript & "$dlg.Title = ""Select a file to upload""" & vbCRLF
  psScript = psScript & "$dlg.ShowHelp = $True" & vbCRLF
  psScript = psScript & "$dlg.ShowDialog() | Out-Null" & vbCRLF
  psScript = psScript & "Set-Content """ &powershellOutputFile & """ $dlg.FileName" & vbCRLF
  MsgBox psScript
 
  Set textFile = fso.CreateTextFile(powershellFile, True)
  textFile.WriteLine(psScript)
  textFile.Close
  Set textFile = Nothing

  ' objShell.Run (strCommand, [intWindowStyle], [bWaitOnReturn])
  ' 0 Hide the window and activate another window.
  ' bWaitOnReturn set to TRUE - indicating script should wait for the program
  ' to finish executing before continuing to the next statement

  Dim appCmd
  appCmd = "powershell -ExecutionPolicy unrestricted &'" & powershellFile & "'"
  MsgBox appCmd
  shell.Run appCmd, 0, TRUE

  ' open file for reading, do not create if missing, using system default format
  Set textFile = fso.OpenTextFile(powershellOutputFile, 1, 0, -2)
  ChooseFile = textFile.ReadLine
  textFile.Close
  Set textFile = Nothing
  fso.DeleteFile(powershellFile)
  fso.DeleteFile(powershellOutputFile)

End Function

UPDATE – May 2013

Some commenters have suggested leveraging BrowseForFolder.  At least for me, this produces strange behaviour on Windows 7 and may return -2147467259 (80004005) error code for certain file types (for example txt files) - but not others (e.g. zip).  I would NOT recommend it.

Here is a a new and improved version which is must faster than above and should be backward compatible with XP:-

Set shell = CreateObject( "WScript.Shell" )
defaultLocalDir = shell.ExpandEnvironmentStrings("%USERPROFILE%") & "\Desktop"
Set shell = Nothing

file = ChooseFile(defaultLocalDir)
MsgBox file

Function ChooseFile (ByVal initialDir)
    Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")

    Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")
    Dim winVersion

    ' This collection should contain just the one item
    For Each objItem in colItems
        'Caption e.g. Microsoft Windows 7 Professional
        'Name e.g. Microsoft Windows 7 Professional |C:\windows|...
        'OSType e.g. 18 / OSArchitecture e.g 64-bit
        'Version e.g 6.1.7601 / BuildNumber e.g 7601
        winVersion = CInt(Left(objItem.version, 1))
    Next
    Set objWMIService = Nothing
    Set colItems = Nothing

    If (winVersion <= 5) Then
        ' Then we are running XP and can use the original mechanism
        Set cd = CreateObject("UserAccounts.CommonDialog")
        cd.InitialDir = initialDir
        cd.Filter = "ZIP files|*.zip|Text Documents|*.txt|Shell Scripts|*.*sh|All Files|*.*"
        ' filter index 4 would show all files by default
        ' filter index 1 would show zip files by default
        cd.FilterIndex = 1
        If cd.ShowOpen = True Then
            ChooseFile = cd.FileName
        Else
            ChooseFile = ""
        End If
        Set cd = Nothing    

    Else
        ' We are running Windows 7 or later
        Set shell = CreateObject( "WScript.Shell" )
        Set ex = shell.Exec( "mshta.exe ""about: <input type=file id=X><script>X.click();new ActiveXObject('Scripting.FileSystemObject').GetStandardStream(1).WriteLine(X.value);close();resizeTo(0,0);</script>""" )
        ChooseFile = Replace( ex.StdOut.ReadAll, vbCRLF, "" )

        Set ex = Nothing
        Set shell = Nothing
    End If
End Function   

Tuesday, July 17, 2012

File Download Java Servlet example - 2GB overflow workaround

I discovered a few days back an issue with our product on HTTP downloads > 2GB.  It appears to be a simple overflow on the HttpServletResponse.setContentLength method. You can probably excuse the API designers circa 1997 assuming a 32-bit signed Integer with max value 231-1 (2147483647 bytes) would be sufficient.  The Gigabit ethernet standard did not come for another year (1998)!

Here is the exception seen when you provide a long value greater than 2147483647 bytes to the setContentLength(int) method:

java.net.ProtocolException: Exceeded stated content-length of: '-XXXX' bytes
        at weblogic.servlet.internal.ServletOutputStreamImpl.checkCL(ServletOutputStreamImpl.java:200)

Below is a sample download servlet with workaround for the 2gb limitation.  It has been tested on Firefox 3.6 against WebLogic Server 10.3.6 with a 2.2GB download and worked perfectly.


import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class DownloadServlet
  extends HttpServlet
{
  @SuppressWarnings("compatibility:1533750721037291976")
  private static final long serialVersionUID = 1L;

  protected void doGet(HttpServletRequest request,
    HttpServletResponse response)
    throws ServletException, IOException
  {
    doPost(request, response);
  }

  protected void doPost(HttpServletRequest request,
    HttpServletResponse response)
    throws ServletException, IOException
  {
    // if no file parameter specified, download hosts file
    String file = request.getParameter("file");
    file = (file == null || file.length() == 0) ? "/etc/hosts" : file;

    File fileObj = new File(file);
    if ((!fileObj.exists()) || (!fileObj.isFile()) || (!fileObj.canRead()))
    {
      throw new IOException("'file' '" + file + "' cannot be read.");
    }

    ServletContext context = getServletConfig().getServletContext();

    String mimetype = context.getMimeType(file);
    response.setContentType(mimetype == null ? "application/octet-stream" :
        mimetype);

    long length = fileObj.length();
    if (length <= Integer.MAX_VALUE)
    {
      response.setContentLength((int)length);
    }
    else
    {
      response.addHeader("Content-Length", Long.toString(length));
    }

    response.setHeader("Content-Disposition",
        "attachment; filename=\"" + fileObj.getName() + "\"");

    ServletOutputStream out = response.getOutputStream();
    InputStream in = null;
    byte[] buffer = new byte[32768];
    try
    {
      in = new FileInputStream(fileObj);

      int bytesRead;
      while ((bytesRead = in.read(buffer)) >= 0)
      {
        out.write(buffer, 0, bytesRead);
      }
    }
    finally
    {
      if (in != null)
      {
        in.close();
      }
    }
  }
}