The JCP is a sham…

Reading the minutes from the key JCP EC meeting on Oracle’s refusal to grant Apache a TCK licence, I think this dialog is very telling -

Doug asked Oracle to acknowledge that it was asking the ECs to condone breaking the JSPA rules. He said that if he was put in a position where he had to condone breaking the rules he would have to resign. Ken said that he understood, and would regret it if Doug resigned, but pointed out the importance of allowing the conversation to go forward. Josh Bloch asked again whether Oracle, who were on record as saying that to deny Apache a license without FOU restrictions is a violation of the JSPA, still feel that way. Ken Glueck responded that Oracle were not prepared to answer a legal question. Josh responded that Oracle had been willing to vote on this matter twice in the past. Ken responded that this is the situation now. Tim Peierls noted that Oracle has had plenty of time to prepare an answer.

Ken pointed out again that the platform is stuck and we need to move forward.

(my emphasis).

Firstly, the “conversation” that Oracle are saying “must go forward” is the proposed Java SE 7/8 JSRs which were not the focus of this part of the discussion. Secondly Oracle is on the record (in the past) as saying that the TCK licence issue is at conflict with the JSPA rules, but is now unwilling to fix it or even discuss it.

Instead it is apparent from the above and the rest of the discussion too, that Oracle has used the stagnation of the Java platform as leverage for JCP members to turn a blind eye on Oracle’s unwillingness to address the problems with TCK licencing the overall governance of the JCP.

I can’t help but be reminded of “The Empire Strikes Back” -

LANDO: You said they’d be left in the city under my supervision!

VADER: I am altering the deal. Pray I don’t alter it any further.

Given the current state of affairs, JCP members may find they are not equal negotiating terms with Oracle and have a difficult choice between two risky options. Stay with Oracle and hope they make favorable choices, or leave the JCP and hope for a change coming from the community or from the pressure back on Oracle.

Oracle declared at the start of the call that they were prepared to act with or without the ECs blessing. Those remarks should not have gone unchallenged.

The JCP is a sham.

Posted in Java, Software Engineering | Tagged Java, open-source, soap-box | Leave a comment

visural-wicket 0.6.5 release is available!

It’s taken longer than I’d hoped, but the next release of the visural-wicket library for Apache Wicket is now available – 0.6.5

This version is mostly a minor enhancement and fix release, but includes almost 6 months worth of patches based on production use of the library.

There are a number of fixes and enhancements based on user requests, and some new features and fixes based on my own work.

Note: this release separates the visural-common and visural-wicket library JARs.

Previously they were bundled together into a single JAR file, however this caused issues with release cycles for each library. However, I have included a visural-common-and-wicket-0.6.5.jar in the release package which leaves them bundled if you’d rather not have two JAR files attached to your project.

Enhancements

New Component: Client-side Tab-pages component

This is a javascript based tab control, which renders all tabs contents into the page at render time, allowing instant tab switching after render.

Unlike other implementations I’ve seen, this tab control retains tab state even when updating via Ajax.

It also has an intuitive implementation pattern that requires only adding a wicket:id to a single wrapping <div> tag containing the tab’s <div>’s.

example of how the tab control looks

Fancybox updated to latest v1.3.1

The new version of Fancybox (the image gallery component) has been integrated into visural-wicket, and it features an improved API with more options.

These options have been exposed in visural-wicket and javadoc has been included for each based on Fancybox’s API docs.

Example app includes InputHint example

The “InputHintBehavior” hasn’t changed at all, but there is now an example for it in the “live demo site” and examples application.

DropDown control changes

The DropDown control has had several fixes and improvements.

  • The “Show All” / “Filter List” feature that would appear as the first line in the list is now optional and can be disabled.
  • The “arrow icon” that appears at the right of the control can now be switched off. This may be desirable when working as a “suggest” or “auto-complete” style control, rather than a DropDownChoice style control.
  • The drop list now appears as the width of the overall box and is positioned more consistently with the CSS styling of the control.

Dialog’s Open / Close Javascript methods now protected access

The dialog open and close Javascript generation methods are now protected. This allows developers to pre-fix or post-fix the open and close events with their own Javascript callbacks.

All Components – Allow disable of JS & CSS static header contributions

To assist with static resource packing and packaging all components now have a -

    /**
     * Override and return false to suppress static Javascript and CSS contributions.
     * (May be desired if you are concatenating / compressing resources as part of build process)
     * @return
     */
    protected boolean autoAddToHeader() {
        return true;
    }

Overriding, and returning false here will cause the component to avoid adding it’s resource contributions during construction. You are then free to repackage them as you see fit as part of your build process.

No longer necessary to manually add excanvas.js for BeautyTipsBehavior

Previously users had to manually add the ExCanvas.js header contributor for Internet Explorer support for the tool-tip behaviour – BeautyTips. This contributor will now be added automatically, so everything works “out of the box”. This won’t affect code that adds it manually, so it’s just a simplification for new users.

JQuery 1.4+ and other JS library compatibility

JQuery 1.4 comptability has been introduced and all components should now work under v1.4.

The JQuery resource reference now takes a version parameter and 1.3 and 1.4 versions of JQuery are included with visural-wicket out of the box.

For backward compatibility, if no version is provided 1.3 is used as the default. For existing projects this is no change.

Additionally, all of the generated code now uses “jQuery(…)” rather than “$(…)” so if you are using multiple libraries overriding the “$(..)” function then you should now be able to use visural-wicket in your project.

All components implement security interfaces and have a serialVersionUID

All of the visural-wicket components now implement ISecureEnableComponent, ISecureRenderComponent which lets them work seamlessly with the security features built into the library.

In additional all of the classes that may end up Serialized in a user session now have a serialVersionUID = 1, to reduce deserialization issues between releases.

Add German date form (dd.mm.yyyy) to DateInputBehavior

As per user request, the german-style date format of dd.mm.yyyy has been added DateFormat.DD_MM_YYYY_DOTS.

Bug Fixes

So as you can see, this is a pretty decent release even if it was a while coming :)

Hope you find these changes useful, as always I welcome feedback, so if you have any ideas, questions or problems then please drop me a line.

Posted in Java, Software Engineering, Wicket | Tagged Java, open-source, releases, visural-wicket, web, wicket | 4 Comments

How to: 10000 photos from Adobe Photoshop Album 2 to Picasa

If you’re (un)fortunate enough to have your entire photo collection stored in the now-ancient Adobe Photoshop Album 2, you’re going to be rather stuck.

In theory, it should be possible to do “backup” operation from Album 2.0 and import into it’s successor Photoshop Elements.

My research suggested that this may be hit-and-miss with several people experiencing bugs that rendered the backup useless on import. I tried – and experienced exactly the same issue.

Anyhow, after some consideration I decided to switch to Picasa, since it’s -

  • free
  • multi-platform
  • has face recognition
  • integrates with my Google account
  • stores meta data inside the image files by default (i.e. captions & tags)

So the next challenge was – how do I get all the photos from Photoshop Album to Picasa with all the meta-data intact?

Step 1 – Get the meta-data out of Photoshop Album

Fortunately someone has already solved this part of the problem. I found this post here which links to a (no longer available) tool for reading the meta data out of Album’s (Access format?) ODBC data source.

I’ve cached a copy of the tool here – psatools.zip

I tried running the tool on the actual Photoshop Album database in the C:\Program Files folder, however it failed with a weird error. I found a workaround to that issue – export a full backup through Photoshop Album, and then run the PSATool on the backup file, e.g.

psatool /if-print /fp /xml "backup.psb" > output.xml

This will output the content of the Photoshop Album backup as XML with all the meta-data intact (captions, tags, albums). The “/fp” switch is required to output the full path to the original image file. We’ll need this for the following step.

Step 2 – Clean up, reorganise and apply meta-data to image files

There are two components required for this step.

One is exiftool - a command line tool for reading and writing meta data from image files.

The second is a custom Java program which I wrote (listed below). The Java program iterates through the XML meta data exported from the Photoshop Album backup and simultaneously reorganises (renames and copies) the images based on their captions and collections and adds this meta data (captions and tags) into the image using exiftool.

You’ll need a couple of libraries for the Java code to run -

You’ll probably also need to revise the code below to suit your own needs. It’s posted as a starting point only.

Be aware this is really rough and nasty code – it’s not intended to be used more than once. :)

It assumes that the output XML from step 1 is in “I:\Temp\output.xml”, the source picture files are accessible on the current PC and that you want to output to “I:\Temp\output” folder for the converted images.

import com.visural.common.Function;
import com.visural.common.Function;
import com.visural.common.StringUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Set;
import javolution.text.CharArray;
import javolution.xml.stream.XMLInputFactory;
import javolution.xml.stream.XMLStreamConstants;
import javolution.xml.stream.XMLStreamReader;

/**
 *
 * @author Richard Nichols
 */
public class Main {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws Exception {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader reader = factory.createXMLStreamReader(new FileInputStream("i:/temp/output.xml"));

        Set<Photo> photos = new HashSet<Photo>();
        Photo photo = null;
        CharArray blank = new CharArray("");

        while (reader.getEventType() != XMLStreamConstants.END_DOCUMENT) {
            switch (reader.next()) {
                case XMLStreamConstants.START_ELEMENT:
                    if (reader.getLocalName().equals("file")) {
                        // Reads primitive types (int) attributes directly (no memory allocation).
                        String type = Function.nvl(reader.getAttributeValue(null, "type"), blank).toString();
                        String name = Function.nvl(reader.getAttributeValue(null, "name"), blank).toString();
                        String caption = Function.nvl(reader.getAttributeValue(null, "caption"), blank).toString();
                        if (type.equals("photo")) {
                            if (name.toLowerCase().endsWith(".jpg") || name.toLowerCase().endsWith(".jpeg")) {
                                photo = new Photo(name, caption);
                                photos.add(photo);
                            } else {
                                System.out.println("Skipped photo: "+name);
                            }
                        }
                    }
                    if (reader.getLocalName().equals("folder")) {
                        if (photo != null) {
                            String type = Function.nvl(reader.getAttributeValue(null, "type"), blank).toString();
                            String value = Function.nvl(reader.getElementText(), blank).toString();
                            if (StringUtil.isNotBlankStr(value)) {
                                if ("Tag".equalsIgnoreCase(type)) {
                                    photo.getTags().add(value);
                                }
                                if ("Collection".equalsIgnoreCase(type)) {
                                    if (value.startsWith("\"")) {
                                        value = value.substring(1);
                                    }
                                    if (value.endsWith("\"")) {
                                        value = value.substring(0, value.length()-1);
                                    }
                                    photo.getCollections().add(value);
                                }
                            }
                        }
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    if (reader.getLocalName().equals("file")) {
                        photo = null;
                    }
                    break;
            }
        }
        reader.close(); 

        // fix filenames & collections
        Set<String> collections = new HashSet<String>();
        int numTwo = 0;
        for (Photo p : photos) {
            // remove any unwanted collections here
            p.getCollections().remove("unwanted collection");

            if (p.getCollections().isEmpty()) {
                p.getCollections().add("No Collection");
            } else if (p.getCollections().size() > 1) {
                numTwo++;
            }
            collections.addAll(p.getCollections());
        }

        for (String s : collections) {
            new File("i:/temp/output/" + safeFilename(s)).mkdirs();
        }

        int n = 0;
        for (Photo p : photos) {
            n++;
            if (n%50 == 0) {
                System.out.println(n+" of "+photos.size());
            }

            String collection = p.getCollections().iterator().next();
            File i = new File(p.getFilename());
            if (!i.exists()) {
                System.err.println("MISSING FILE: "+p.getFilename());
                continue;
            }
            String outname = p.getCaption();
            if (StringUtil.isBlankStr(outname)) {
                outname = p.getFilename().substring(p.getFilename().lastIndexOf('\\'));
            }
            File o = new File("i:/temp/output/" + safeFilename(collection)+"/"+safeFilename(outname)+".jpg");
            if (o.exists()) { // allows continue after partial conversion
                continue;
            }
            copyFile(i, o);
            tagAndCapFile(o, p.getCaption(), p.getTags());
        }
    }

    private static String safeFilename(String name) {
        for (char c : StringUtil.FILENAME_INVALID_CHARS) {
            name = name.replace(String.valueOf(c), " ");
        }
        return name.trim();
    }

    public static void copyFile(File in, File out) throws IOException {
        FileChannel inChannel = new FileInputStream(in).getChannel();
        FileChannel outChannel = new FileOutputStream(out).getChannel();
        try {
            inChannel.transferTo(0, inChannel.size(),
                    outChannel);
        } catch (IOException e) {
            throw e;
        } finally {
            if (inChannel != null) {
                inChannel.close();
            }
            if (outChannel != null) {
                outChannel.close();
            }
        }
    }

    private static void tagAndCapFile(File o, String caption, Set<String> tags) throws IOException, InterruptedException {
        //System.out.println("Tagging and capping: "+o.getCanonicalPath());
        {
            String run = "lib\\exiftool.exe \"-iptc:caption-abstract="+caption.replace('\"', ' ')+"\" \""+o.getCanonicalPath()+"\" -overwrite_original";
            Process p = Runtime.getRuntime().exec(run);
            int i = p.waitFor();
            if (i != 0) {
                System.err.println("Failed running: "+i+"\n"+run);
            }
        }

        for (String tag : tags) {
            String run = "lib\\exiftool.exe \"-iptc:keywords+="+tag.replace('\"', ' ').trim()+"\" \""+o.getCanonicalPath()+"\" -overwrite_original";
            Process p = Runtime.getRuntime().exec(run);
            int i = p.waitFor();
            if (i != 0) {
                System.err.println("Failed running: "+i+"\n"+run);
            }
        }

    }
}

/**
 *
 * @author Richard Nichols
 */
public class Photo {
    private String filename;
    private String caption;
    private final Set<String> collections = new HashSet<String>();
    private final Set<String> tags = new HashSet<String>();

    public Photo(String filename, String caption) {
        this.filename = filename;
        this.caption = caption;
    }

    public String getCaption() {
        return caption;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public String getFilename() {
        return filename;
    }

    public Set<String> getCollections() {
        return collections;
    }

    public Set<String> getTags() {
        return tags;
    }

}

After an hour or so while this buzzed away on 10000 odd pics I then ended up with a nicely arranged image collection with all the meta-data attached!

Posted in General | Tagged automation, Java, off-topic | 5 Comments

Implementing a “Draft Mode” with Apache Wicket Forms

A question came up on wicket-users recently about whether it’s possible to implement a form in Wicket whereby you can bypass the validation temporarily so that users could save prior to submitting the form.

Think something along the lines of “save draft” in a mail client like GMail.

It’s possible but requires a bit of Wicket-foo. I did post a short snippet on the mailing list, but I thought a longer explanation might be helpful.

First of all, let’s get something out of the way; it’s not possible to accept data that the model backing your components do not accept.

For example, you can’t accept “asdf” into a model field backed by an Integer – it ain’t going to work whichever way you slice it. (Though, you can accept any input if you build a model that consists only of Strings and add validation for numerics etc. further down the line).

Here’s the key steps to making a “draft mode” Wicket form -

  1. Override the form’s process() method to set a “draft mode” flag based on the component that submitted the form.
  2. Create validators that apply based on the mode that form is in.
  3. You’re probably going to have multiple submit buttons on the screen which do different things, thus your form’s onSubmit() handler will probably do nothing.

This means that you can’t use Wicket’s built in validators as they are applied regardless of any notion of “mode” that you may have in your form.

Here’s an example of how this might hang together:

class MyForm extends Form {
    private final SubmitButton submitButton;
    private final SubmitButton saveDraftButton;
    private final TextField textField;
    private boolean draftMode = true;

    public MyForm(String id) {
        super(id);
        add(textField = new TextField("textField"));
        add(saveDraftButton= new SubmitButton("draft") {
            @Override
            public void onSubmit() {
                myService.saveDraft(); // or whatever
            }
        });
        add(submitButton = new SubmitButton("submit") {
            @Override
            public void onSubmit() {
                myService.submit(); // or whatever
            }
        });
        // works like a required field validator
        textField.add(new IValidator() {
            @Override
            public void validate(IValidatable validatable) {
                if (validatable != null && !MyForm.this.draftMode &&
                    validatable.getValue() == null || validatable.getValue().toString().equals(""))
                {
                    validatable.error(new IValidationError() {
                        public String getErrorMessage(IErrorMessageSource messageSource) {
                            return "Required.";
                        }
                    });
                }
            }
        });
    }

    @Override
    public void process(IFormSubmittingComponent submittingComponent) {
        draftMode = !(submittingComponent == submitButton);
        super.process(submittingComponent);
    }

    @Override
    public void onSubmit() {
        // do nothing
    }
}

This work can be standardised in your own sub-class of Form for example, with a bunch of validators that integrate with it.

Also, you can support even more complex use-cases by using a “mode” enum instead of a simple “draft” boolean. And make all the validators conditional on Mode.

This approach works just fine, but it would be nice if there was some sort of formal abstraction in Wicket to do this, since the current approach assumes quite a bit about the type of applications that will be implemented.

Posted in Java, Software Engineering, Wicket | Tagged guides, Java, ui, web, wicket | 3 Comments