Thursday, 28 August 2014

Using docx4j to generate docx files

When it comes to generate a docx file in Java, there are not so many alternatives. Some are not free and some have a small set of features.

One Java library that offers a good set of features is docx4j. This library uses JAXB to convert between xml and Java objects. I played with it and I managed to create a docx file with needed functionality.

Microsoft docx uses a special measure unit called dxa which represents 1/20 from a point. You should use following conversions:

20 dxa = 1 point
1440 dxa = 1 inch = 72 points

In Java we need to know screen resolution and we can convert from pixels to dxa:

    // get dots per inch
    protected static int getDPI() {
          return GraphicsEnvironment.isHeadless() ? 96 :
                    Toolkit.getDefaultToolkit().getScreenResolution();
    }

    private int pixelsToDxa(int pixels) {
        return  ( 1440 * pixels / getDPI() );         
    }   

We initialize our docx generator:

    private static WordprocessingMLPackage wordMLPackage;
    private static ObjectFactory factory;
    .....
    boolean landscape = false;
    wordMLPackage = WordprocessingMLPackage.createPackage(PageSizePaper.A4, landscape);
    factory = Context.getWmlObjectFactory();    

We can set document page margins to 50 pixels:

    private void setPageMargins() {        
        try {
            Body body = wordMLPackage.getMainDocumentPart().getContents().getBody();
            PageDimensions page = new PageDimensions();
            PgMar pgMar = page.getPgMar();                  
            pgMar.setBottom(BigInteger.valueOf(pixelsToDxa(50)));
            pgMar.setTop(BigInteger.valueOf(pixelsToDxa(50)));
            pgMar.setLeft(BigInteger.valueOf(pixelsToDxa(50)));
            pgMar.setRight(BigInteger.valueOf(pixelsToDxa(50)));            
            SectPr sectPr = factory.createSectPr();   
            body.setSectPr(sectPr);                           
            sectPr.setPgMar(pgMar);  
        } catch (Docx4JException e) {            
            e.printStackTrace();
        }        
    }

We add a table and save the file:

    Tbl table = createTableWithContent();      
    wordMLPackage.getMainDocumentPart().addObject(table); 

    wordMLPackage.save(new File("C:/Test.docx"));

We define a DocxStyle bean class to be used with following properties:

    private boolean bold;
    private boolean italic;
    private boolean underline;
    private String fontSize;
    private String fontColor; 
    private String fontFamily;
    
    // cell margins
    private int left;
    private int bottom;
    private int top;
    private int right;
    
    private String background;
    private STVerticalJc verticalAlignment;
    private JcEnumeration horizAlignment;
    
    private boolean borderLeft;
    private boolean borderRight;
    private boolean borderTop;
    private boolean borderBottom;
    private boolean noWrap;

Our createTableWithContent method follows (we use dxa values). There are 4 rows, some cells with vertical merge, and some with horizontal merge.

    private Tbl createTableWithContent() {

        Tbl table = factory.createTbl();                

        // for TEST: this adds borders to all cells
        TblPr tblPr = new TblPr();
        TblStyle tblStyle = new TblStyle();
        tblStyle.setVal("TableGrid");
        tblPr.setTblStyle(tblStyle);
        table.setTblPr(tblPr);

        Tr tableRow = factory.createTr();        

        // a default table cell style
        DocxStyle defStyle = new DocxStyle();
        defStyle.setBold(false);
        defStyle.setItalic(false);
        defStyle.setUnderline(false);
        defStyle.setHorizAlignment(JcEnumeration.CENTER);

        // a specific table cell style 
        DocxStyle style = new DocxStyle();
        style.setBold(true);
        style.setItalic(true);
        style.setUnderline(true);
        style.setFontSize("40");
        style.setFontColor("FF0000");
        style.setFontFamily("Book Antiqua");
        style.setTop(300);
        style.setBackground("CCFFCC");        
        style.setVerticalAlignment(STVerticalJc.CENTER);
        style.setHorizAlignment(JcEnumeration.CENTER);
        style.setBorderTop(true);
        style.setBorderBottom(true);
        style.setNoWrap(true);

        addTableCell(tableRow, "Field 1", 3500, style, 1, null);
        // start vertical merge for Filed 2 and Field 3 on 3 rows
        addTableCell(tableRow, "Field 2", 3500, defStyle, 1, "restart");
        addTableCell(tableRow, "Field 3", 1500, defStyle, 1, "restart");        
        table.getContent().add(tableRow);        

        tableRow = factory.createTr();
        addTableCell(tableRow, "Text", 3500, defStyle, 1, null);
        addTableCell(tableRow, "", 3500, defStyle, 1, "");
        addTableCell(tableRow, "", 1500, defStyle, 1, "");
        table.getContent().add(tableRow);

        tableRow = factory.createTr();
        addTableCell(tableRow, "Interval", 3500, defStyle, 1, null);
        addTableCell(tableRow, "", 3500, defStyle, 1, "close");
        addTableCell(tableRow, "", 1500, defStyle, 1, "close");
        table.getContent().add(tableRow);                          

        // add an image horizontally merged on 3 cells
        String filenameHint = null;
        String altText = null;
        int id1 = 0;
        int id2 = 1; 
        byte[] bytes = getImageBytes();
        P pImage;
        try {
            pImage = newImage(wordMLPackage, bytes, filenameHint, altText, id1, id2, 8500);
            tableRow = factory.createTr();
            addTableCell(tableRow, pImage, 8500, defStyle, 3, null);
            table.getContent().add(tableRow);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        return table;
    }


The most important method is addTableCell with two signatures, one for an image and one for a text cell:
    private void addTableCell(Tr tableRow, P image, int width, DocxStyle style, 
                        int horizontalMergedCells, String verticalMergedVal) {
        Tc tableCell = factory.createTc();                
        addImageCellStyle(tableCell, image, style);                
        setCellWidth(tableCell, width);                
        setCellVMerge(tableCell, verticalMergedVal);                
        setCellHMerge(tableCell, horizontalMergedCells);                            
        tableRow.getContent().add(tableCell);
    }
    
    private void addTableCell(Tr tableRow, String content, int width,
               DocxStyle style, int horizontalMergedCells, String verticalMergedVal) {
        Tc tableCell = factory.createTc();                
        addCellStyle(tableCell, content, style);                 
        setCellWidth(tableCell, width);                
        setCellVMerge(tableCell, verticalMergedVal);                
        setCellHMerge(tableCell, horizontalMergedCells);
        if (style.isNoWrap()) {
            setCellNoWrap(tableCell);
        }        
        tableRow.getContent().add(tableCell);
    } 

Then we look at addCellStyle method:
 
    private void addCellStyle(Tc tableCell, String content, DocxStyle style) {
        if (style != null) {
                                   
            P paragraph = factory.createP();
    
            Text text = factory.createText();
            text.setValue(content);
    
            R run = factory.createR();
            run.getContent().add(text);
    
            paragraph.getContent().add(run);
            
            setHorizontalAlignment(paragraph, style.getHorizAlignment());
                    
            RPr runProperties = factory.createRPr();
            
            if (style.isBold()) {
                addBoldStyle(runProperties);
            }
            if (style.isItalic()) {
                addItalicStyle(runProperties);
            }
            if (style.isUnderline()) {
                addUnderlineStyle(runProperties);
            }
            
            setFontSize(runProperties, style.getFontSize());                
            setFontColor(runProperties, style.getFontColor());                
            setFontFamily(runProperties, style.getFontFamily());
                    
            setCellMargins(tableCell, style.getTop(), style.getRight(), 
                           style.getBottom(), style.getLeft());
            setCellColor(tableCell, style.getBackground());
            setVerticalAlignment(tableCell, style.getVerticalAlignment());
            
            setCellBorders(tableCell, style.isBorderTop(), style.isBorderRight(), 
                           style.isBorderBottom(), style.isBorderLeft());
    
            run.setRPr(runProperties);
    
            tableCell.getContent().add(paragraph);
        }
    }
and the same method for image:
    private void addImageCellStyle(Tc tableCell, P image, DocxStyle style) {        
        setCellMargins(tableCell, style.getTop(), style.getRight(), 
                       style.getBottom(), style.getLeft());
        setCellColor(tableCell,  style.getBackground());
        setVerticalAlignment(tableCell, style.getVerticalAlignment());        
        setHorizontalAlignment(image, style.getHorizAlignment());
        setCellBorders(tableCell, style.isBorderTop(), style.isBorderRight(), 
                       style.isBorderBottom(), style.isBorderLeft());
        tableCell.getContent().add(image);
    }

To create an image paragraph we will do the following:
     public P newImage(WordprocessingMLPackage wordMLPackage, byte[] bytes, 
                       String filenameHint, String altText, int id1,
            int id2, long cx) throws Exception {
        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage.createImagePart(wordMLPackage, bytes);
        Inline inline = imagePart.createImageInline(filenameHint, altText, id1, id2, cx, false);
        // Now add the inline in w:p/w:r/w:drawing
        ObjectFactory factory = Context.getWmlObjectFactory();
        P p = factory.createP();
        R run = factory.createR();
        p.getContent().add(run);
        Drawing drawing = factory.createDrawing();
        run.getContent().add(drawing);
        drawing.getAnchorOrInline().add(inline);
        return p;
     }

Then we present all methods used to set cell properties (names are self explanatory):

     private void setCellBorders(Tc tableCell, boolean borderTop, boolean borderRight, 
                                 boolean borderBottom, boolean borderLeft) {
                
        TcPr tableCellProperties = tableCell.getTcPr();
        if (tableCellProperties == null) {
            tableCellProperties = new TcPr();
            tableCell.setTcPr(tableCellProperties);
        }
        
        CTBorder border = new CTBorder();
        // border.setColor("auto");
        border.setColor("0000FF");
        border.setSz(new BigInteger("20"));
        border.setSpace(new BigInteger("0"));
        border.setVal(STBorder.SINGLE);            
        
        TcBorders borders = new TcBorders();
        if (borderBottom) {
            borders.setBottom(border);
        } 
        if (borderTop) {
            borders.setTop(border);
        } 
        if (borderLeft) {
            borders.setLeft(border);
        }
        if (borderRight) {
            borders.setRight(border);
        }
        tableCellProperties.setTcBorders(borders);        
    }

    private void setCellWidth(Tc tableCell, int width) {
        if (width > 0) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
            TblWidth tableWidth = new TblWidth();
            tableWidth.setType("dxa");
            tableWidth.setW(BigInteger.valueOf(width));
            tableCellProperties.setTcW(tableWidth);
        }
    }
    
    private void setCellNoWrap(Tc tableCell) {
        TcPr tableCellProperties = tableCell.getTcPr();
        if (tableCellProperties == null) {
            tableCellProperties = new TcPr();
            tableCell.setTcPr(tableCellProperties);
        }
        BooleanDefaultTrue b = new BooleanDefaultTrue();
        b.setVal(true);
        tableCellProperties.setNoWrap(b);        
    }

    private void setCellVMerge(Tc tableCell, String mergeVal) {
        if (mergeVal != null) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
            VMerge merge = new VMerge();
            if (!"close".equals(mergeVal)) {
                merge.setVal(mergeVal);
            }
            tableCellProperties.setVMerge(merge);
        }
    }
    
    private void setCellHMerge(Tc tableCell, int horizontalMergedCells) {
        if (horizontalMergedCells > 1) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
    
            GridSpan gridSpan = new GridSpan();
            gridSpan.setVal(new BigInteger(String.valueOf(horizontalMergedCells)));
    
            tableCellProperties.setGridSpan(gridSpan);
            tableCell.setTcPr(tableCellProperties);
        }                
    }    
    
    private void setCellColor(Tc tableCell, String color) {
        if (color != null) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
            CTShd shd = new CTShd();
            shd.setFill(color);
            tableCellProperties.setShd(shd);
        }
    }

    private void setCellMargins(Tc tableCell, int top, int right, int bottom, int left) {
        TcPr tableCellProperties = tableCell.getTcPr();
        if (tableCellProperties == null) {
            tableCellProperties = new TcPr();
            tableCell.setTcPr(tableCellProperties);
        }
        TcMar margins = new TcMar();

        if (bottom > 0) {
            TblWidth bW = new TblWidth();
            bW.setType("dxa");
            bW.setW(BigInteger.valueOf(bottom));
            margins.setBottom(bW);
        }

        if (top  > 0) {
            TblWidth tW = new TblWidth();
            tW.setType("dxa");
            tW.setW(BigInteger.valueOf(top));
            margins.setTop(tW);
        }

        if (left > 0) {
            TblWidth lW = new TblWidth();
            lW.setType("dxa");
            lW.setW(BigInteger.valueOf(left));
            margins.setLeft(lW);
        }

        if (right > 0) {
            TblWidth rW = new TblWidth();
            rW.setType("dxa");
            rW.setW(BigInteger.valueOf(right));
            margins.setRight(rW);
        }

        tableCellProperties.setTcMar(margins);
    }

    private void setVerticalAlignment(Tc tableCell, STVerticalJc align) {
        if (align != null) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
    
            CTVerticalJc valign = new CTVerticalJc();
            valign.setVal(align);
    
            tableCellProperties.setVAlign(valign);
        }
    } 
 
    private void setFontSize(RPr runProperties, String fontSize) {
       if (fontSize != null && !fontSize.isEmpty()) {
          HpsMeasure size = new HpsMeasure();
          size.setVal(new BigInteger(fontSize));
          runProperties.setSz(size);
          runProperties.setSzCs(size);
       }
    }
 
    private void setFontFamily(RPr runProperties, String fontFamily) {
       if (fontFamily != null) {
          RFonts rf = runProperties.getRFonts();
          if (rf == null) {
             rf = new RFonts();
             runProperties.setRFonts(rf);
          }
          rf.setAscii(fontFamily);
       }
    }
 
    private void setFontColor(RPr runProperties, String color) {
       if (color != null) {
          Color c = new Color();
          c.setVal(color);
          runProperties.setColor(c);
       } 
    }
 
    private void setHorizontalAlignment(P paragraph, JcEnumeration hAlign) {
       if (hAlign != null) {
          PPr pprop = new PPr();
          Jc align = new Jc();
          align.setVal(hAlign);
          pprop.setJc(align);
          paragraph.setPPr(pprop);
       }
    }

    private void addBoldStyle(RPr runProperties) {
       BooleanDefaultTrue b = new BooleanDefaultTrue();
       b.setVal(true);
       runProperties.setB(b);
    }

    private void addItalicStyle(RPr runProperties) {
       BooleanDefaultTrue b = new BooleanDefaultTrue();
       b.setVal(true);
       runProperties.setI(b);
    }

    private void addUnderlineStyle(RPr runProperties) {
       U val = new U();
       val.setVal(UnderlineEnumeration.SINGLE);
       runProperties.setU(val);
    }
As you see it takes some work to generate a simple file. Docx4j is also slow and it's api looks awful for Java programers with short method names and classes, but this is the result of internally using Jaxb mapping. Despite these things, if you need to generate a docx file, you should take Docx4j as a suitable candidate.

Thursday, 19 June 2014

Javascript window resize listeners force you to double check your code

Working to my NextCharts HTML5 library made me to think more defensive when writing Javascript code. NextCharts library is used by NextReports Server inside its Dashboards section. Here, any dashboard can contain a number of charts and widgets.

Any chart / widget has a resize event defined in its code , like :

window.addEventListener('resize', resize, false); 

So, when the browser is resized, all charts and widgets are also resized.

The problems may start to appear when we change between dashboards. Visually I saw, sometimes , that the animation is not working. Looking for Javascript errors, I saw that in some place my canvas element was null. How can it be? I was just doing:

var canvas = document.getElementById(id);

Why is not the element id found?

And then it struck me: The resize event is registered for some charts/widgets and when we change the dashboard those listeners are still active, the browser notifies them about the event, but the components are not found inside the page anymore, so the canvas is null for them.

Because we do not know when to remove the resize listener for a specific component inside dashboard, the solution is to check for canvas null values in our javascript functions:

if (canvas == null) {
      return;
}
This is something you cannot think until you use your Javascript code under a big umbrella.

Friday, 14 February 2014

Get JSON with JQuery as UTF-8

This is how I used JQuery, to read JSON from a url:

$.getJSON('data-html5.json').then( 
    function(data){  
        console.log(data);
        nextChart(data, 'canvas', 'tip');
    },
    function(jqXHR, textStatus, errorThrown) {
        alert("Error: " + textStatus + " errorThrown: " + errorThrown);
    } 
);

But this method does not interpret UTF-8 characters, making your UTF-8 text to look scrambled. The workaround is to use the following:

$.ajax({
    type: "POST",
    url: "data-html5.json",
    contentType: "application/json; charset=utf-8",
    dataType: "json",    
    success: function(data) {
        console.log(data);
        nextChart(data, 'canvas', 'tip');
    },
    error: function(jqXHR, textStatus, errorThrown) {
        alert("Error: " + textStatus + " errorThrown: " + errorThrown);
    }
});  

Thursday, 14 November 2013

HTML Multiple select and Android incompatibilities

Do you have a multiple select on your web site? Then you should see how ugly it looks on Android platform. Not only it looks different, but it also behaves differently.

In Wicket there is a Palette component (two multiple selects) which allows to select some options from a list to another list. You can add / remove selected options using two buttons ">" and "<".


I extended this component with some new buttons ">>" and "<<" to add all and remove all elements.

On Android platform:
  • the multiple select looks like a simple select without any option in it
  • select does not even has an arrow to indicate you are looking at a list
  • even if you make its height bigger you cannot see all the possible options inside a multiple select; (it is just an empty big space!)
  • when you click on it you can select what options you want
  • after selection you can see only the first selected option
Because all of these, there is also no need for "add all" and "remove all" buttons (even if it does the operation, not being able to see what are all options makes it unusable).

When I first re-sized the browser to become with less than 480 px width, I saw the result ok . I just hid "add all" and "remove all" buttons.


But when I looked on Android emulator I saw another story.


First I modified the css to show the two buttons inline.  The re-sized browser looks like:


And then I added the arrow icon by myself (it will have impact only on Android):

.paletteContainer table.palette td.pane select,
.paletteContainer table.palette td.pane select[size="0"],
.paletteContainer table.palette td.pane select[size="1"] {
    background-image: url(data:image/png;base64,R0lGODlhDQAEAIAAAAAAAP8A
/yH5BAEHAAEALAAAAAANAAQAAAILhA+hG5jMDpxvhgIAOw==);
    background-repeat: no-repeat;
    background-position: right center;
    padding-right: 1.8em;
  }

The result was much better:


When you click on select you see Android selection dialog:


After selecting, the first selected option is seen:


Making a Wicket web site to look good on Android phone

I wanted my Wicket application to look good on an Android phone. Delving into this task I found two important things:

In many cases you will not be able to show all the information. For me, it was ok to use css media query to have a different css for Android phone. I had to tweak with fonts size and hide some markups.

Inside my html I added the viewport metadata:

<meta name="viewport" content="width=device-width,initial-scale=1"/>  

and I entered my two style sheets:

<!-- for all screens -->
<link rel="stylesheet" href="css/styles.css" type="text/css" media="screen" />

<!--  for Android phone 480px wide or less -->
<link rel="stylesheet" href="css/min-styles.css" type="text/css" 
      media="only screen and (max-width: 480px)"/>    

All the tweaks were added to min-styles.css file.

Modal Windows are a big pain to use. I had to make them show near the top with fixed position like:
 
 .wicket-modal {
        position: fixed !important;
        top:5% !important;
}

Without this, on Webkit browser if you scroll down to click some link which opens a modal, you even won't be able to see the modal (depends on how much scrolling you need). A modal cannot be dragged independently on screen.  So I guess, if you can avoid modals, then do it. If you cannot avoid them, then keep them as small as possible.

Thursday, 31 October 2013

Take care about your comments format

These days I was tangling with some media query to allow using a different css file in mobile platforms than in desktop ones.

After I set inside my base page the viewport like:
<meta name="viewport" content="width=device-width,initial-scale=1"/>
and adding the two css files:
<link rel="stylesheet" href="css/styles.css" type="text/css" 
      media="screen"/> 
<link rel="stylesheet" href="css/min-styles.css" type="text/css" 
      media="only screen and (max-width: 480px)"/>
I just noticed that my css class  for second file (I had just one class at first for testing) is not loaded.

I put some other classes inside css file and use them inside different places, and they work! But the first did not. I saw that all other classes where used inside basic html pages, while the first was used inside a modal window, and I thought that there is a problem with the modal window and media queries ...

After some time I realized that the problem was generated by a wrong comment inside css file. It was something like:
<!--  phone in portrait mode, or PCs w/Browser windows reduces to 
      480px wide or less -->
And this is a comment format used inside XML files , not CSS! After I used the right format:
/*  phone in portrait mode, or PCs w/Browser windows reduces to 
    480px wide or less */
my first class was loaded.

The lesson learned from this is that always take care about your comments! If you copy them from somewhere else, be sure that the comment tags are suited for your specific file!

Wednesday, 16 October 2013

Eclipse & EGit Frustrations

Sometimes any developer has his moments when a feeling of frustration becomes overwhelming. This just happened to me when trying to create with Eclipse a new project from GitHub and using EGit plugin for synchronization with remote repository.

To create a project from GitHub sources you just have to do the following:
  1. Select "File -> Import"  menu and from there go inside the tree structure to  "Git -> Projects from Git"
  2. Then you have to enter the URI like https://github.com/nextreports/nextreports-engine . Here the first problem arose. I  could not go further because I received an error:
 

I knew I was behind a proxy and that I had it set. So I started to look for what people had to say. I found that you should add .git at the end of the url to work. I tried, but another error:


 I found that other people where successful by using http protocol instead of https. So, using a url like http://github.com/nextreports/nextreports-engine.git showed me the master branch. Hurray, I said!

Wizard from eclipse, after sources download, asked me to create the project. Because I entered a custom path, now I got another error (the following picture was taken from another project, so do not look at the name):


So the wizard is also buggy! OK. I closed the wizard and I created the project from scratch from downloaded sources. After setting build path and making it compile, I tried to use EGit plugin to synchronize with repository. A new error popped-up:

This told me that I could synchronize only with my local repository, but not with the remote git repository because fetch failed. Why? It didn't say.

I felt so frustrated after looking around for hours. Because I really needed to do some work and not just configurations, I gave up and I started to use git commands from command prompt to pull and push my work.

When I had time to investigate further, I started to look again because frustration grew bigger. Project had an Ivy dependency management and Ivy Resolving or Refreshing where not working from Eclipse, letting me no choice just to use ant commands from command prompt. Eclipse started to look just as a simple editor ...  Everything I needed to do requested using command prompt commands.

Looking inside Ivy console, helped me to see a "Connection timeout" for a specific library. This was just our engine library added to maven central repository through sonatype. Library could be accessed with no problem from browser. After another searching time, I just realized the big picture:

Our maven library had an url with https! So retrieving libraries through https did not work, only through http. This made me return to proxy settings and saw that proxy was indeed set, but only for http. So I edited the HTTPS entry and I added the proxy to it:


After that everything started to work:
- creating git project using link with  https
- Eclipse ivy resolving / refreshing
- EGit synchronization with remote git repository

And as a bonus, sometimes a plugin from eclipse could not be installed on my computer. This forced me to add the proxy settings directly to eclipse.ini file (-Dhttp.proxyPort=... -Dhttp.proxyHost=...) I realized that those plugins with https inside their URI were the culprit.

So, just a proxy setting caused so much trouble in so many places inside Eclipse.  Sometimes, solving a problem can take just a second, but only after hours or days of frustration.