Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Sunday, October 2, 2011

MacOSX standard artwork in LiveCode

When you develop an application for Mac, users have certain expectations about how your app will look and behave. One of the often forgotten parts of your user interface is the icon artwork - it is very tempting to simply buy an icon collection and then use it everywhere in your cross-platform application. However, to help developers achieve a consistent user interface, across multiple versions of the operating system, Apple has been adding 'template' images to its Cocoa NSImage class.

Here are a few examples which I recently used in a project:


For a complete list of the available template images, look here.

Great stuff, but how are we going to get at it from LiveCode? Not directly, I'm afraid. But all is not lost: a while back, Apple's Java engineers added to their Java implementation on MacOS X 10.5 Leopard (and later) the ability to fetch these images using a simple call:
Toolkit.getDefaultToolkit().getImage("NSImage://NSColorPanel")

You can see where I'm heading with this: writing a helper Java class that you can execute with a simple 'shell' function call. So let's start by writing up the Java class code. We'll set it up with two parameters: the first parameter accepts either "*" (to say you want a complete set of images) or a comma-separated list of image names; the second parameter is the output directory where the available images will be saved in PNG format.

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.EnumSet;
import javax.imageio.ImageIO;

public class NSImageFetcher {

public enum NSImageName {
// Image Template Constants
NSQuickLookTemplate,
NSBluetoothTemplate,
NSIChatTheaterTemplate,
NSSlideshowTemplate,
NSActionTemplate,
NSSmartBadgeTemplate,
NSPathTemplate,
NSInvalidDataFreestandingTemplate,
NSLockLockedTemplate,
NSLockUnlockedTemplate,
NSGoRightTemplate,
NSGoLeftTemplate,
NSRightFacingTriangleTemplate,
NSLeftFacingTriangleTemplate,
NSAddTemplate,
NSRemoveTemplate,
NSRevealFreestandingTemplate,
NSFollowLinkFreestandingTemplate,
NSEnterFullScreenTemplate,
NSExitFullScreenTemplate,
NSStopProgressTemplate,
NSStopProgressFreestandingTemplate,
NSRefreshTemplate,
NSRefreshFreestandingTemplate,
NSFolder,
NSTrashEmpty,
NSTrashFull,
NSHomeTemplate,
NSBookmarksTemplate,
NSCaution,
NSStatusAvailable,
NSStatusPartiallyAvailable,
NSStatusUnavailable,
NSStatusNone,
NSApplicationIcon,
NSMenuOnStateTemplate,
NSMenuMixedStateTemplate,
NSUserGuest,
NSMobileMe,
// Multiple Documents Drag Image
NSMultipleDocuments,
// Sharing Permissions Names Images
NSUser,
NSUserGroup,
NSEveryone,
// System Entity Images
NSBonjour,
NSDotMac,
NSComputer,
NSFolderBurnable,
NSFolderSmart,
NSNetwork,
// Toolbar Named Images
NSUserAccounts,
NSPreferencesGeneral,
NSAdvanced,
NSInfo,
NSFontPanel,
NSColorPanel,
// View Type Template Images
NSIconViewTemplate,
NSListViewTemplate,
NSColumnViewTemplate,
NSFlowViewTemplate;
}


public static Image getNSImage(final NSImageName nsImageName) {
return getNSImage(nsImageName.name());
}

public static Image getNSImage(final String nsImageName) {
return Toolkit.getDefaultToolkit().getImage("NSImage://" + nsImageName);
}

public static void saveNSImage(final NSImageName nsImageName, final File outputFile) throws IOException {
saveNSImage(nsImageName.name(), outputFile);
}

public static void saveNSImage(final String nsImageName, final File outputFile) throws IOException {
final Image nsImage = getNSImage(nsImageName);
if (nsImage != null) {
BufferedImage bufferedImage = null;
if (nsImage instanceof BufferedImage) {
bufferedImage = (BufferedImage) nsImage;
} else {
bufferedImage = new BufferedImage(nsImage.getWidth(null), nsImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);
final Graphics bufImgGraphics = bufferedImage.getGraphics();
bufImgGraphics.drawImage(nsImage, 0, 0, null);
}
if (!outputFile.exists()) {
outputFile.createNewFile();
}
ImageIO.write(bufferedImage, "png", outputFile);
}
}

public static void main(String[] args) throws IOException {
// We can't run in headless mode or we get empty icons
// System.setProperty("java.awt.headless", "true");

if (args.length != 2) {
System.out.println("Usage: java NSImageFetcher <list of NSImageNames>,<output directory>");
return;
}
final File outputDirectory = new File(args[1]);
if ("*".equals(args[0])) {
for (final NSImageName nsImageName : EnumSet.allOf(NSImageName.class)) {
final File outputFile = new File(outputDirectory.getAbsolutePath() + File.separator + nsImageName.name() + ".png");
saveNSImage(nsImageName, outputFile);
}
} else {
for (final String nsImageName : args[0].split(",")) {
try {
final File outputFile = new File(outputDirectory.getAbsolutePath() + File.separator + nsImageName + ".png");
saveNSImage(nsImageName, outputFile);
} catch (IllegalArgumentException e) {
// ignore
}
}
}
}
}

And here's the LiveCode button script to put all the images into a subdirectory 'resources' in the directory where the stack is saved:

on mouseUp
-- save settings
local tOldDefaultFolder, tOldHideConsoleWindows
put the defaultFolder into tOldDefaultFolder
set the defaultFolder to StackFolderPath()
put the hideConsoleWindows into tOldHideConsoleWindows
set the hideConsoleWindows to true
-- call the command line via shell
local tShellCommand, tShellResult
put "java NSImageFetcher" && \
quote & "*" & quote && \
quote & StackFolderPath("resources") & quote into tShellCommand
answer tShellCommand & return & shell(tShellCommand)
-- restore settings
set the defaultFolder to tOldDefaultFolder
set the hideConsoleWindows to tOldHideConsoleWindows
-- go ahead and link
end mouseUp

function StackFolderPath pRelativePath
local tPath
put the effective filename of this stack into tPath
set the itemDelimiter to slash
if pRelativePath is empty then
delete item -1 of tPath
else
put pRelativePath into item -1 of tPath
end if
return tPath
end StackFolderPath

Okay, MacOSX comes with a preinstalled Java runtime so the above should work on MacOSX 10.5 Leopard, MacOSX 10.6 Snow Leopard and even on MacOSX 10.7 Lion (since there's still a Java 6 'out of the box'). Naturally, the artwork may change between operating system versions. So my approach would be to:
- check if there is a '<MyCompanyName>/<MyAppName>/resources' directory in the user's preferences directory (by means of the 'specialFolderPath' function);
- if the directory is absent, create it and use the 'shell' function as above to fetch the images I need, and save the 'systemVersion' in a text file in the same directory
- if the directory is present, compare the current value of the 'systemVersion' with the one saved in said directory, and once again fetch the images if there's a difference

It's up to you to either use references to these .PNG images by setting the 'filename' property of an 'image' control; or to then import the .PNG files into a stack.

Note that this artwork is copyrighted by Apple, Inc. You shouldn't extract it and then use it in your apps on any platform other than MacOSX. Oh, and please head the warning "You should always use named images according to their intended purpose, and not according to how the image appears when loaded. The appearance of images can change between releases. If you use an image for its intended purpose (and not because of it looks), your code should look correct from release to release."

Monday, May 30, 2011

Leveraging Java from LiveCode - experiments with multi-threading

For some time now, I've been promoting the idea of extending LiveCode using Java - both are wonderful cross-platform technologies, each with their own strengths and weaknesses. One area where Java has really made an impact is its built-in approach to multi-threading, by offering a way to split work into chunks that can be executed separately and simultaneously. In a world where chip makers turn to multiple cores to speed up processing, this is a very important technology to incorporate into our software designs.

LiveCode is single-threaded - well, mostly single-threaded: script execution happens on a single thread, but some parts, like socket communication, can be used with event callbacks. This means that when new data arrives, an event message is sent to a control and handled on the script execution thread; but the actual reading and writing can happen on other threads. This is a solid middle ground, as coders don't have to worry about data races and the other un-fun things that come with full-blown multi-threaded programming.

Now my earlier postrs on extending LiveCode with Java were based on a strategy of handing off some work to a Java process (via shell calls or process communication via pipes) and waiting for the result to come back. This is all nice and dandy if the work is done quickly, but what if the processing may take a long time to complete? One of the main reasons to employ multi-threading, after all, is to make sure we can update the user interface while some heavy processing continues in the background.

One approach could be to use the 'in time' variant of the 'read from process' command, and poll the other side for content, but I wanted to show a different approach, by wrapping our Java code in an ExecutorService. The basic idea: we can either 'call' a command immedialtely, or 'submit' it as a task to process in the background. Submitting a command returns a pending-result-id, which we can use to 'cancel' (if it hasn't started or finished yet), check on its 'status' (done, cancelled, pending) or even 'get' its result (waiting for the command to complete).

I know that's a lot of things in one paragraph, so let's put it in Java code:


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;


public class LiveCodeJavaExecutor {

private final static String EXIT_COMMAND = "exit";
private final static String CALL_COMMAND = "call";
private final static String SUBMIT_COMMAND = "submit";
private final static String CANCEL_COMMAND = "cancel";
private final static String STATUS_COMMAND = "status";
private final static String GET_COMMAND = "get";

private final static ExecutorService EXECUTOR_SERVICE;
private final static Map<Integer,Future<String>> PENDING_RESULTS;
private final static AtomicInteger NEXT_PENDING_ID;

static {
EXECUTOR_SERVICE = Executors.newCachedThreadPool();
NEXT_PENDING_ID = new AtomicInteger();
PENDING_RESULTS = new HashMap<Integer,Future<String>>();
}

/**
* @param args
*/
public static void main(String[] args) {
// Make sure to run in head-less mode
System.setProperty("java.awt.headless", "true");

String commandLine = ""; // the last received command line
System.out.println("LiveCodeJavaExecutor started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);

InfiniteLoop:
while(!EXIT_COMMAND.equalsIgnoreCase(commandLine)) {
try {
commandLine = bir.readLine();
} catch (IOException e) {
continue InfiniteLoop;
}
final String[] commandParts = commandLine.split("\t");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeJavaExecutor is shutting down");
EXECUTOR_SERVICE.shutdown();
System.out.println("LiveCodeJavaExecutor is exiting");
} else if (CALL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
final CallableCommand callableCommand = new CallableCommand(commandParts);
try {
result = callableCommand.call();
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (SUBMIT_COMMAND.equalsIgnoreCase(commandName)){
String result = "";
try {
final CallableCommand callableCommand = new CallableCommand(commandParts);
final Integer pendingResultId = Integer.valueOf(NEXT_PENDING_ID.incrementAndGet());
final Future pendingResult = EXECUTOR_SERVICE.submit(callableCommand);
PENDING_RESULTS.put(pendingResultId, pendingResult);
result = pendingResultId.toString();
} catch (RejectedExecutionException e) {
result = "error - could not submit command";
}
System.out.println(result);
} else if (CANCEL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = String.valueOf(pendingResult.cancel(true));
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: cancel<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (STATUS_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
if (pendingResult.isCancelled()) {
result = "cancelled";
} else if (pendingResult.isDone()) {
result = "done";
} else {
result = "pending";
}
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: status<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (GET_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = pendingResult.get();
PENDING_RESULTS.remove(pendingResultId);
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: get<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else {
System.out.println("unrecognized command: " + commandLine);
}
System.out.println('.');
}
}

private static class CallableCommand implements Callable<String> {
private final String[] commandParts;
public CallableCommand(final String[] commandParts) {
super();
this.commandParts = commandParts;
}
public String call() throws Exception {
String result = null;
result = dispatchCommand(commandParts);
return result;
}
}

public static String dispatchCommand(String[] commandParts) throws Exception {
String commandName = commandParts[1];
Thread.sleep(10000);
return commandName;
}

}


Admittedly, the actual command execution part is a bit silly to say the least: sleeping for ten seconds and then just returning the command name is not exactly business logic. But you can take a look at some of my previous examples, to put in code for concatenating PDF files, validating XML data using XSD schemas, image processing or anything else that could take quite a bit of time to finish processing.

Turning our attention to the LiveCode side, we would have a stack script looking something like this:

local sProcess

on JExecutor_StartHelper
if sProcess is empty then
local tDefaultFolder, tStackFolder
put ImageIO_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java LiveCodeJavaExecutor" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeJavaExecutor started." then
--> good to go
else
close process sProcess
put empty into sProcess
end if
end if
end JExecutor_StartHelper

on JExecutor_StopHelper
write ("exit") & return to process sProcess
close process sProcess
put empty into sProcess
end JExecutor_StopHelper

command JExecutor_CallCommand pCommand
write("call" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CallCommand

command JExecutor_SubmitCommand pCommand
write("submit" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_SubmitCommand

command JExecutor_CancelRequest pRequestId
write("cancel" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CancelRequest

function JExecutor_StatusOfRequest pRequestId
write("status" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_StatusOfRequest

function JExecutor_GetResultOfRequest pRequestId
write("get" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_GetResultOfRequest


Now we have the commands and functions we need to start the separate Java process, 'call' commands, 'submit' commands, 'cancel' pending requests, checking the 'status' of pending requests, and fetching the result with a 'get'. Enough to control the lifecycle of such submitted commands a.k.a. pending requests.

If we're sure the command is just a small bit of work, we use the 'call' command and wait for the result to appear; but if it may take a long time to complete, we can 'submit' a command and use the returned 'pending request id' to track its progress - and while the Java side is churning away, we can update our LiveCode user interface with a lovely indeterminate progress display, polling every second or so until the 'status' returns "done" and then use 'get' to fetch the completed command output.

So perhaps the title was a little misleading, as I've only given you a skeleton. Hopefully it will inspire you to combine this technique with code that I posted previously, and actually use it in your own projects. Happy coding!

Sunday, May 8, 2011

Presenting at RunRevLive.11 - the code

As I mentioned earlier, I presented two topics at the RunRevLive.11 developer conference. After returning home, I got clobbered with a backlog of work at the day-job; but today, I finally got around to uploading the example code.

- Advanced databases (or: migrating from a single-user desktop application to a multi-user networked database application)
- Extending LiveCode with Java (or: leveraging Java libraries from within LiveCode scripts)

Enjoy - and don't forget that if you couldn't attend it in person and bought a SimulCast pass, you can see the whole presentation through the RunRevLive SimulCast - or wait for the DVD to come out :-)

Sunday, April 17, 2011

Presenting at RunRevLive.11

Next week I'll be at the RunRevLive.11 conference in sunny San Jose, California, giving two presentations: Advanced Databases and Extending LiveCode with Java. Here is a brief description for both sessions.

--

Advanced databases:
You've just finished a desktop application that stores its data in a local SQLite database. All works well, and now you're asked to make a multi-user version, so that other users can access and update the data as well. Unfortunately, you can't just load the data into a MySQL dataabse and be done with it: your data structures and business logic have to be ready as well!
In this presentation, I'll take a desktop SQLite application and turn it into a front-end for a networked MySQL database. Along the way, you'll get crucial tips to avoid the pitfalls of such a transformation.

Extending LiveCode with Java:
We all love LiveCode for its cross-platform abilities, combined with an elegant and productive scripting language. But the 200-pound gorilla in this space is indubitably Java. In this presentation we will see how we can leverage Java libraries from within LiveCode through process communication.
This allows us to extend our LiveCode desktop applications with Zeroconf discovery, XML schemas, PDF file enhancements and image processing.

--

Last but not least, you're invited to drop by the LiveCode Marketplace vendor area and ask me any questions you have regarding the Quartam developer tools for LiveCode. You might even get a sneak preview of Quartam Reports 1.2 :-)

Sunday, March 6, 2011

Locale aware formatting in LiveCode using Java

Recently on the Use-LiveCode mailing list, Peter Haworth asked how he could get a hold of the date/times/number/currency format preferences to ensure that data display corresponded to the user locale. For date and time, you can use 'the system date' and 'the system time' for quick formatting, and 'the dateFormat' to figure out some of the details. But formatting numbers is quite a different challenge. And even if you can get the 'system' information, what to do if you're building a text that shouldn't be in English or user formatting?

At the risk of sounding like a broken record: let's use Java - it has powerful formatting capabilities for all the required items, and its concept of Locale is robust. To avoid startup overhead and improve response time, we'll use the same approach as for the Zeroconf example: process communication to start a helper process, interact with it, and finally close it when we're done.

So let's write a helper class in Java that will (a) give us the default Locale, (b) list the available locales, (c) allows us to format a LiveCode number as currency, integer or decimal number, applying a given or the default locale, (d) allows us to format a LiveCode date as short, medium, long or full date, applying a given or the default locale, and (e) allows us to format a LiveCode time as short, medium, long or full time, applying a given or the default locale. Sounds like a lot to do, but it's actually quite straightforward to implement:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class LiveCodeLocaleHelper {

private final static String EXIT_COMMAND = "exit";
private final static String LIST_COMMAND = "list";
private final static String DEFAULT_COMMAND = "default";
private final static String CURRENCY_COMMAND = "currency";
private final static String INTEGER_COMMAND = "integer";
private final static String NUMBER_COMMAND = "number";
private final static String DATE_COMMAND = "date";
private final static String TIME_COMMAND = "time";

private static final Map LOCALE_MAP;
private static final DateFormat LC_DATE_FORMAT;
private static final DateFormat LC_TIME_FORMAT;

static {
Locale en_US_locale = null;
LOCALE_MAP = new HashMap();
for (Locale locale : Locale.getAvailableLocales()) {
if ("en".equals(locale.getLanguage()) && "US".equals(locale.getCountry())) {
en_US_locale = locale;
}
LOCALE_MAP.put(locale.toString(), locale);
}
LC_DATE_FORMAT = DateFormat.getDateInstance(DateFormat.SHORT, en_US_locale);
LC_TIME_FORMAT = DateFormat.getTimeInstance(DateFormat.SHORT, en_US_locale);
}

public static void main(final String[] args) throws IOException {
String commandLine = ""; // the last received command line
System.out.println("LiveCodeLocaleHelper started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);

while(!EXIT_COMMAND.equalsIgnoreCase(commandLine)) {
commandLine = bir.readLine();
final String[] commandParts = commandLine.split("\t");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeLocaleHelper is exiting");
} else if (LIST_COMMAND.equalsIgnoreCase(commandName)) {
handleListCommand(commandParts);
} else if (DEFAULT_COMMAND.equalsIgnoreCase(commandName)) {
handleDefaultCommand(commandParts);
} else if (CURRENCY_COMMAND.equalsIgnoreCase(commandName)) {
handleCurrencyCommand(commandParts);
} else if (INTEGER_COMMAND.equalsIgnoreCase(commandName)) {
handleIntegerCommand(commandParts);
} else if (NUMBER_COMMAND.equalsIgnoreCase(commandName)) {
handleNumberCommand(commandParts);
} else if (DATE_COMMAND.equalsIgnoreCase(commandName)) {
handleDateCommand(commandParts);
} else if (TIME_COMMAND.equalsIgnoreCase(commandName)) {
handleTimeCommand(commandParts);
} else {
System.out.println("Unrecognized command: " + commandLine);
}
}
}

private static void handleListCommand(final String[] commandParts) {
final List localeIdentifiers = new ArrayList(LOCALE_MAP.keySet());
Collections.sort(localeIdentifiers);
for (String localeIdentifier : localeIdentifiers) {
System.out.println(localeIdentifier);
}
System.out.println(".");
}

private static void handleDefaultCommand(final String[] commandParts) {
System.out.println(Locale.getDefault().toString());
}

private static void handleCurrencyCommand(final String[] commandParts) {
double number;
try {
number = Double.parseDouble(commandParts[1]);
} catch (NumberFormatException e) {
System.out.println("Invalid number: " + commandParts[1]);
return;
}
NumberFormat currencyFormat = null;
if (commandParts.length > 2) {
final String localeIdentifier = commandParts[2];
currencyFormat = NumberFormat.getCurrencyInstance(LOCALE_MAP.get(localeIdentifier));
} else {
currencyFormat = NumberFormat.getCurrencyInstance();
}
System.out.println(currencyFormat.format(number));
}

private static void handleIntegerCommand(final String[] commandParts) {
double number;
try {
number = Double.parseDouble(commandParts[1]);
} catch (NumberFormatException e) {
System.out.println("Invalid number: " + commandParts[1]);
return;
}
NumberFormat integerFormat = null;
if (commandParts.length > 2) {
final String localeIdentifier = commandParts[2];
integerFormat = NumberFormat.getIntegerInstance(LOCALE_MAP.get(localeIdentifier));
} else {
integerFormat = NumberFormat.getIntegerInstance();
}
System.out.println(integerFormat.format(number));
}

private static void handleNumberCommand(final String[] commandParts) {
double number;
try {
number = Double.parseDouble(commandParts[1]);
} catch (NumberFormatException e) {
System.out.println("Invalid number: " + commandParts[1]);
return;
}
NumberFormat numberFormat = null;
if (commandParts.length > 2) {
final String localeIdentifier = commandParts[2];
numberFormat = NumberFormat.getNumberInstance(LOCALE_MAP.get(localeIdentifier));
} else {
numberFormat = NumberFormat.getNumberInstance();
}
System.out.println(numberFormat.format(number));
}

private static void handleDateCommand(final String[] commandParts) {
Date date = null;
try {
date = LC_DATE_FORMAT.parse(commandParts[1]);
} catch (ParseException e) {
System.out.println("Invalid date: " + commandParts[1]);
return;
}
int dateSize = DateFormat.SHORT;
final String dateFormatString = commandParts[2];
if ("medium".equalsIgnoreCase(dateFormatString)) {
dateSize = DateFormat.MEDIUM;
} else if ("long".equalsIgnoreCase(dateFormatString)) {
dateSize = DateFormat.LONG;
} else if ("full".equalsIgnoreCase(dateFormatString)) {
dateSize = DateFormat.FULL;
}
DateFormat dateFormat = null;
if (commandParts.length > 3) {
final String localeIdentifier = commandParts[3];
dateFormat = DateFormat.getDateInstance(dateSize, LOCALE_MAP.get(localeIdentifier));
} else {
dateFormat = DateFormat.getDateInstance(dateSize);
}
System.out.println(dateFormat.format(date));
}

private static void handleTimeCommand(final String[] commandParts) {
Date date = null;
try {
date = LC_TIME_FORMAT.parse(commandParts[1]);
} catch (ParseException e) {
System.out.println("Invalid time: " + commandParts[1]);
return;
}
int timeSize = DateFormat.SHORT;
final String dateFormatString = commandParts[2];
if ("medium".equalsIgnoreCase(dateFormatString)) {
timeSize = DateFormat.MEDIUM;
} else if ("long".equalsIgnoreCase(dateFormatString)) {
timeSize = DateFormat.LONG;
} else if ("full".equalsIgnoreCase(dateFormatString)) {
timeSize = DateFormat.FULL;
}
DateFormat dateFormat = null;
if (commandParts.length > 3) {
final String localeIdentifier = commandParts[3];
dateFormat = DateFormat.getTimeInstance(timeSize, LOCALE_MAP.get(localeIdentifier));
} else {
dateFormat = DateFormat.getTimeInstance(timeSize);
}
System.out.println(dateFormat.format(date));
}

}

With the Java helper class written, we can turn our attention to the LiveCode side. Let's create a new stack:



As you can see, we have two buttons at the top for starting/stopping the helper process, along with an option menu to list the available locales and a field to show the default locale. Now put the following into the stack script:
local sProcess

on Locale_StartHelper
if sProcess is empty then
local tDefaultFolder, tStackFolder
put Locale_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java LiveCodeLocaleHelper" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeLocaleHelper started." then
disable button "Start"
enable button "Stop"
else
close process sProcess
put empty into sProcess
end if
end if
end Locale_StartHelper

function Locale_AvailableLocales
local tLocaleList
write ("list") & return to process sProcess
repeat forever
read from process sProcess for 1 line
if it begins with "." then exit repeat
put it after tLocaleList
end repeat
delete char -1 of tLocaleList
return tLocaleList
end Locale_AvailableLocales

function Locale_DefaultLocale
write ("default") & return to process sProcess
read from process sProcess for 1 line
return it
end Locale_DefaultLocale

function Locale_FormatCurrency pAmount, pLocale
if pLocale is empty then
write ("currency" & tab & pAmount) & return to process sProcess
else
write ("currency" & tab & pAmount & tab & pLocale) & return to process sProcess
end if
read from process sProcess for 1 line
return line 1 of it
end Locale_FormatCurrency

function Locale_FormatInteger pAmount, pLocale
if pLocale is empty then
write ("integer" & tab & pAmount) & return to process sProcess
else
write ("integer" & tab & pAmount & tab & pLocale) & return to process sProcess
end if
read from process sProcess for 1 line
return line 1 of it
end Locale_FormatInteger

function Locale_FormatNumber pAmount, pLocale
if pLocale is empty then
write ("number" & tab & pAmount) & return to process sProcess
else
write ("number" & tab & pAmount & tab & pLocale) & return to process sProcess
end if
read from process sProcess for 1 line
return line 1 of it
end Locale_FormatNumber

function Locale_FormatDate pDate, pSize, pLocale
if pLocale is empty then
write ("date" & tab & pDate & tab & pSize) & return to process sProcess
else
write ("date" & tab & pDate & tab & pSize & tab & pLocale) & return to process sProcess
end if
read from process sProcess for 1 line
return line 1 of it
end Locale_FormatDate

function Locale_FormatTime pTime, pSize, pLocale
if pLocale is empty then
write ("time" & tab & pTime & tab & pSize) & return to process sProcess
else
write ("time" & tab & pTime & tab & pSize & tab & pLocale) & return to process sProcess
end if
read from process sProcess for 1 line
return line 1 of it
end Locale_FormatTime

on Locale_StopHelper
write ("exit") & return to process sProcess
close process sProcess
enable button "Start"
disable button "Stop"
put empty into sProcess
end Locale_StopHelper

function Locale_StackFolder
local tStackFolder
put the effective filename of this stack into tStackFolder
set the itemDelimiter to slash
delete item -1 of tStackFolder
return tStackFolder
end Locale_StackFolder


The script for the Start button is easy enough:
on mouseUp
disable button "Start"
Locale_StartHelper
put Locale_AvailableLocales() into button "AvailableLocales"
put Locale_DefaultLocale() into field "DefaultLocale"
enable button "Stop"
end mouseUp

And the script of the Stop button is equally trivial:
on mouseUp
disable button "Stop"
Locale_StopHelper
enable button "Start"
end mouseUp

The script for the Default button in the Number formatting section looks like this:
on mouseUp
local tNumber
put field "Number" into tNumber
put "Currency:" & tab & Locale_FormatCurrency(tNumber) & return & \
"Integer:" & tab & Locale_FormatInteger(tNumber) & return & \
"Number:" & tab & Locale_FormatNumber(tNumber) \
into field "FormattedNumbers"
end mouseUp

And the script for the Available button in the Number formatting section looks like this:
on mouseUp
local tNumber, tLocale
put field "Number" into tNumber
put the label of button "AvailableLocales" into tLocale
put "Currency:" & tab & Locale_FormatCurrency(tNumber, tLocale) & return & \
"Integer:" & tab & Locale_FormatInteger(tNumber, tLocale) & return & \
"Number:" & tab & Locale_FormatNumber(tNumber, tLocale) \
into field "FormattedNumbers"
end mouseUp

Likewise, the script for the Default button in the Date formatting section becomes:
on mouseUp
local tDate
put field "Date" into tDate
put "Short:" & tab & Locale_FormatDate(tDate, "short") & return & \
"Medium:" & tab & Locale_FormatDate(tDate, "medium") & return & \
"Long:" & tab & Locale_FormatDate(tDate, "long") & return & \
"Full:" & tab & Locale_FormatDate(tDate, "full") \
into field "FormattedDates"
end mouseUp

Hence, the script for the Available button in the Date formatting section becomes:
on mouseUp
local tDate, tLocale
put field "Date" into tDate
put the label of button "AvailableLocales" into tLocale
put "Short:" & tab & Locale_FormatDate(tDate, "short", tLocale) & return & \
"Medium:" & tab & Locale_FormatDate(tDate, "medium", tLocale) & return & \
"Long:" & tab & Locale_FormatDate(tDate, "long", tLocale) & return & \
"Full:" & tab & Locale_FormatDate(tDate, "full", tLocale) \
into field "FormattedDates"
end mouseUp

Almost done - here's the script for the Default button in the Time formatting section:
on mouseUp
local tTime
put field "Time" into tTime
put "Short:" & tab & Locale_FormatTime(tTime, "short") & return & \
"Medium:" & tab & Locale_FormatTime(tTime, "medium") & return & \
"Long:" & tab & Locale_FormatTime(tTime, "long") & return & \
"Full:" & tab & Locale_FormatTime(tTime, "full") \
into field "FormattedTimes"
end mouseUp

Finally, the logical extension for the Available button in the Time formatting section:
on mouseUp
local tTime, tLocale
put field "Time" into tTime
put the label of button "AvailableLocales" into tLocale
put "Short:" & tab & Locale_FormatTime(tTime, "short", tLocale) & return & \
"Medium:" & tab & Locale_FormatTime(tTime, "medium", tLocale) & return & \
"Long:" & tab & Locale_FormatTime(tTime, "long", tLocale) & return & \
"Full:" & tab & Locale_FormatTime(tTime, "full", tLocale) \
into field "FormattedTimes"
end mouseUp

Save the stack, and copy the compiled LiveCodeLocaleHelper.class file into the same directory where you saved the stack. Click the Start button, then play around with the rest of the user interface to enter different numbers, dates and times, clicking the buttons to see the formatting output, switching between the different available locales.

Download it here.

Sunday, January 16, 2011

ZeroConf/Bonjour in LiveCode with JmDNS

In the last few posts, we examined how LiveCode scripts can delegate work to Java classes using the shell function. Even though this generally works quite fast and reliably, the downside of this approach is that a new Java process is started every time.

In this post, we'll use process communication to start a helper process, interact with it, and finally close it when we're done. Our use case: ZeroConf / Bonjour registration and discovery of services. The JmDNS project offers a pure-Java implementation of Multicast DNS and DNS Service Discovery technologies.

LiveCode process communication revolves around opening a process (a.k.a. command-line interface application), writing data, reading data, and finally closing said process. But we'll get to that later; let's start by writing a LiveCodeJmDNSHelper class in Java. First download a copy of the JmDNS library - I picked version 3.0 as I was building this on an older Mac which can't run Java 6.

In the .zip file, you'll find a file 'jmdns.jar' which is all you need for the rest of this example. Copy it into your Eclipse project, add it to the Build path, and use the following code for the LiveCodeJmDNSHelper.java file:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;


public class LiveCodeJmDNSHelper {

private final static String EXIT_COMMAND = "exit";
private final static String LIST_COMMAND = "list";
private final static String REGISTER_COMMAND = "register";
private final static String UNREGISTER_COMMAND = "unregister";

private JmDNS jmdns;
private Map<String, ServiceInfo> regMap;

public LiveCodeJmDNSHelper() {
super();
}

public void start() throws IOException {
this.jmdns = JmDNS.create();
this.regMap = new HashMap<String, ServiceInfo>();
String commandLine = ""; // the last received command line
System.out.println("LiveCodeJmDNSHelper started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);

while(!EXIT_COMMAND.equals(commandLine)) {
commandLine = bir.readLine();
final String[] commandParts = commandLine.split(" ");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeHelper is exiting");
System.exit(0);
} else if (LIST_COMMAND.equalsIgnoreCase(commandName)) {
handleListCommand(commandParts);
} else if (REGISTER_COMMAND.equalsIgnoreCase(commandName)) {
handleRegisterCommand(commandParts);
} else if (UNREGISTER_COMMAND.equalsIgnoreCase(commandName)) {
handleUnregisterCommand(commandParts);
} else {
System.out.println("Unrecognized command: " + commandLine);
}
}
}

private void handleListCommand(final String[] commandParts) {
final String svcType = commandParts[1];
final ServiceInfo[] svcInfos = this.jmdns.list(svcType);
for (ServiceInfo svcInfo : svcInfos) {
System.out.println(svcInfo);
}
System.out.println(".");
}

private void handleRegisterCommand(final String[] commandParts) {
final String svcAlias = commandParts[1];
final String svcType = commandParts[2];
final String svcName = commandParts[3];
final int svcPort = Integer.parseInt(commandParts[4]);
final String svcText = commandParts[5];
ServiceInfo svcInfo = ServiceInfo.create(svcType, svcName, svcPort, svcText);
try {
this.jmdns.registerService(svcInfo);
this.regMap.put(svcAlias, svcInfo);
System.out.println("Registered service '" + svcAlias + "' as: " + svcInfo);
} catch (IOException e) {
System.out.println("Failed to register service '" + svcAlias + "'");
}
}

private void handleUnregisterCommand(final String[] commandParts) {
final String svcAlias = commandParts[1];
final ServiceInfo svcInfo = this.regMap.remove(svcAlias);
if (svcInfo != null) {
this.jmdns.unregisterService(svcInfo);
System.out.println("Unregistered service '" + svcAlias + "'");
} else {
System.out.println("Failed to unregister service '" + svcAlias + "'");
}
}

public static void main(String[] args) throws IOException {
final LiveCodeJmDNSHelper helper = new LiveCodeJmDNSHelper();
helper.start();
}

}


The start() method goes into an infinite loop, waiting for a line of text as its input, parsing the command and executing it. You can 'list' the ServiceInfo data for a given service type, 'register' your own services, and 'unregister' those services again. All nice and dandy, but how do we talk to it from LiveCode? Let's start by creating a new stack.



So we have two buttons at the top, to Start and Stop the helper process. Then we have a button to Register a service of our own, with fields for the Alias, Type, Name, Port and Text. Next there is a button to Unregister our own service, given an Alias. Finally, there is a button to List all the services of a specific Type in a scrolling field below. With the user interface laid out, we can put the following code into the stack script:
local sProcess

on JmDNS_StartHelper
if sProcess is empty then
local tClassPath
if the platform is "Win32" then
put ".;jmdns.jar" into tClassPath
else
put ".:jmdns.jar" into tClassPath
end if
local tDefaultFolder, tStackFolder
put JMDNS_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java -cp" && tClassPath && "LiveCodeJmDNSHelper" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeJmDNSHelper started." then
--> ready for interaction
else
close process sProcess
put empty into sProcess
end if
end if
end JmDNS_StartHelper

function JmDNS_GetList pServiceType
local tJmDNSList
write ("list" && pServiceType) & return to process sProcess
repeat forever
read from process sProcess for 1 line
if it begins with "." then exit repeat
put it after tJmDNSList
end repeat
delete char -1 of tJmDNSList
return tJmDNSList
end JmDNS_GetList

command JmDNS_RegisterService pAlias, pType, pName, pPort, pText
write ("register" && pAlias && pType && pName && pPort && pText) & return to process sProcess
read from process sProcess for 1 line
return it
end JmDNS_RegisterService

command JmDNS_UnregisterService pAlias
write ("unregister" && pAlias) & return to process sProcess
read from process sProcess for 1 line
return it
end JmDNS_UnregisterService

command JmDNS_StopHelper
write ("exit") & return to process sProcess
close process sProcess
put empty into sProcess
end JmDNS_StopHelper

function JmDNS_StackFolder
local tStackFolder
put the effective filename of this stack into tStackFolder
set the itemDelimiter to slash
delete item -1 of tStackFolder
return tStackFolder
end JmDNS_StackFolder

The script for the Start button is easy enough:
on mouseUp
JmDNS_StartHelper
disable button "Start"
enable button "Stop"
end mouseUp

And the script of the Stop button is equally trivial:
on mouseUp
JmDNS_StopHelper
enable button "Start"
disable button "Stop"
end mouseUp

The script of the Register button isn't too complicated either:
on mouseUp
JmDNS_RegisterService field "Alias", field "Type", field "Name", field "Port", field "Text"
answer the result
end mouseUp

Neither is the script of the Unregister button:
on mouseUp
ask "Unregister which alias?" with field "Alias"
if it is empty then exit mouseUp
JmDNS_UnregisterService it
answer the result
end mouseUp

Nor the script of the List button:
on mouseUp
ask "List services of which type?" with field "Type"
if it is empty then exit mouseUp
put JMDNS_GetList(it) into field "List"
end mouseUp


Save the stack, and copy jmdns.jar as well as the compiled LiveCodeJmDNSHelper.class into the same directory where you saved the stack. Click the Start button, Register a service of your own, click the List button to make sure it shows up, click the Unregister button to remove it again, and then click the List button to make sure it's gone. If you want, you can launch some of the Java samples that come with JmDNS to see that both Java and LiveCode recognize the same services.

For the purists: no, I didn't do proper error checking or exception handling in this example. Because it is just that: an example. Never promised it was production-ready, did I? ;-)
Download it here.

Friday, December 31, 2010

XML validation using a Schema

In earlier posts, I examined how we can use the built-in revXML library in LiveCode to validate XML data against a DTD, later refining it with a version check to match evolving requirements. Unfortunately, a Document Type Definition is quite a limited way of XML validation. So this time, we'll improve our defenses again, by incorporating XML Schemas.

Whereas a DTD is limited to defining the basic structure of the XML in terms of elements and attributes, XML Schemas allow you to define validation on the actual content of the elements and attributes. So you can be sure that an element defined as "xs:date" is actually a valid date, or that an attribute defines as "xs:positiveInteger" is actually a positive integer, etc. A full explanation of XML Schemas is beyond the scope of this post, you'll find plenty of information around the web - a good first stop is this W3Schools XML Schema tutorial.

This all sounds very good, but here's the rub: the revXML library offers no built-in support for XML Schemas. So yet again we turn to Java, with its built-in XML Validation API. We can easily execute Java code using LiveCode's shell function - so let's start by writing the XmlValidateSchema class:
import java.io.File;
import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

public class XmlValidateSchema {
public static void main(String[] args) throws SAXException, ParserConfigurationException, IOException {
final File xmlFile = new File(args[0]);
final File xsdFile = new File(args[1]);
// Load the XML file
final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
final Document document = docBuilder.parse(xmlFile);
// Load the XSD file
final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
final Schema schema = schemaFactory.newSchema(xsdFile);
final Validator validator = schema.newValidator();
// Validate the XML document against the XSL schema
final Source source = new DOMSource(document);
validator.validate(source);
}
}

In keeping with earlier Java examples, the code is a bit lazy when it comes to exception handling: if any exception is thrown, it will end up in the output of our shell function call. The only important thing to remember is that the first parameter is the XML file, and the second is the XML Schema Definition (XSD) file.

Let's go to LiveCode and create a new stack for the user interface.



As you can see, there's a field for the Schema text, a field for the XML text, and a button to Validate the XML against the Schema. Since it's perhaps a tad small, here's the content of the Schema field:
<?xml version="1.0"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.quartam.com"
xmlns="http://www.quartam.com"
elementFormDefault="qualified">

<xs:element name="RootNode" type="RootNode"/>

<xs:complexType name="RootNode">
<xs:sequence>
<xs:element name="BranchNode" type="BranchNode" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="SpecVersion" type="xs:string"/>
</xs:complexType>

<xs:complexType name="BranchNode">
<xs:sequence>
<xs:element name="LeafNode" type="xs:string" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>

</xs:schema>

And here's the content of the XML field:
<?xml version="1.0"?>
<RootNode
xmlns="http://www.quartam.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.quartam.com schema.xsd"
SpecVersion="1.0">
<BranchNode>
<LeafNode>The first leaf node</LeafNode>
<LeafNode>The second leaf node</LeafNode>
<LeafNode>The third leaf node</LeafNode>
</BranchNode>
<BranchNode>
<LeafNode>The fourth leaf node</LeafNode>
<LeafNode>The fifth leaf node</LeafNode>
</BranchNode>
</RootNode>

After saving the stack, we copy the compiled XmlValidateSchema.class file into the same directory as the stack. Now we can write the script for the 'Validate' button:
on mouseUp
--> write Schema and XML to temporary files
local tSchemaFile, tXmlFile
put the tempName into tSchemaFile
put field "Schema" into URL ("file:" & tSchemaFile)
put the tempName into tXmlFile
put field "XML" into URL ("file:" & tXmlFile)
--> assemble the shell command
local tShellCommand
put "java XmlValidateSchema" && \
ShellPath(tXmlFile) && \
ShellPath(tSchemaFile) \
into tShellCommand
--> execute the shell command
local tHideConsoleWindows, tDefaultFolder, tShellResult
put the hideConsoleWindows into tHideConsoleWindows
set the hideConsoleWindows to true
put the defaultFolder into tDefaultFolder
set the defaultFolder to AbsolutePathFromStack()
put shell(tShellCommand) into tShellResult
set the defaultFolder to tDefaultFolder
set the hideConsoleWindows to tHideConsoleWindows
--> cleanup the temporary files
delete file tSchemaFile
delete file tXmlFile
if tShellResult is not empty then
answer error tShellResult
end if
end mouseUp

function AbsolutePathFromStack pFileName
local tAbsolutePath
put the effective filename of this stack into tAbsolutePath
set the itemDelimiter to slash
if pFileName is not empty then
put pFileName into item -1 of tAbsolutePath
else
delete item -1 of tAbsolutePath
end if
return tAbsolutePath
end AbsolutePathFromStack

function ShellPath pPath
if the platform is "Win32" then
put quote & pPath & quote into pPath
else
replace space with backslash & space in pPath
end if
return pPath
end ShellPath

This time around, we didn't have to fiddle with the Java classpath, as the XML Validation API is built-in. However, we had to write the XML and Schema to temporary files, to avoid length limitations in the shell command. If we now make a deliberate mistake, say change one of the 'LeafNode' elements into a 'BeafNode' element, we see this error:



Again, the image is a bit small, so here's the content of the error:
ERROR:  'cvc-complex-type.2.4.a: Invalid content was found starting with element 'BeafNode'. One of '{"http://www.quartam.com":LeafNode}' is expected.'
Exception in thread "main" org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'BeafNode'. One of '{"http://www.quartam.com":LeafNode}' is expected.
at com.sun.org.apache.xerces.internal.jaxp.validation.Util.toSAXParseException(Util.java:109)
at com.sun.org.apache.xerces.internal.jaxp.validation.ErrorHandlerAdaptor.error(ErrorHandlerAdaptor.java:104)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:382)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:316)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator$XSIErrorReporter.reportError(XMLSchemaValidator.java:429)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.reportSchemaError(XMLSchemaValidator.java:3185)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.handleStartElement(XMLSchemaValidator.java:1831)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.startElement(XMLSchemaValidator.java:705)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorHandlerImpl.startElement(ValidatorHandlerImpl.java:335)
at com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.closeStartTag(ToXMLSAXHandler.java:205)
at com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.characters(ToXMLSAXHandler.java:524)
at com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.characters(ToXMLSAXHandler.java:467)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:229)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:121)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:85)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transformIdentity(TransformerImpl.java:615)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:661)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:300)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorImpl.process(ValidatorImpl.java:220)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorImpl.validate(ValidatorImpl.java:141)
at javax.xml.validation.Validator.validate(Validator.java:82)
at XmlValidateSchema.main(XmlValidateSchema.java:31)

Thanks to the combination of LiveCode and Java, we can develop cross-platform solution quickly, without having to give up the power of existing libraries. Unfortunately, loading Java every time for a shell call is not the optimal solution, so in another post, we'll investigate how we can run Java code using LiveCode's 'process' communication. Stay tuned...

Thursday, December 30, 2010

Stamping PDF files

In a previous post, I examined how we can use LiveCode and the Java-based iText library to concatenate a series of existing PDF files into a single PDF file. Now we will examine how we can 'stamp' a PDF file with an image using the same technique.

The first thing to code is the Java class that we will call using the shell function. Here's what I came up with:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import com.lowagie.text.DocumentException;
import com.lowagie.text.Image;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;

public class StampPdfFile {
public static void main(String[] args) throws IOException, DocumentException {
final String inputFile = args[0];
final String outputFile = args[1];
final String imageFile = args[2];
final String[] coords = args[3].split(",");
final PdfReader inputReader = new PdfReader(inputFile);
final OutputStream outputStream = new FileOutputStream(outputFile);
final PdfStamper outputStamper = new PdfStamper(inputReader, outputStream);
final int pageCount = inputReader.getNumberOfPages();
final Image image = Image.getInstance(imageFile);
final int left = Integer.parseInt(coords[0]);
final int top = Integer.parseInt(coords[1]);
final int right = Integer.parseInt(coords[2]);
final int bottom = Integer.parseInt(coords[3]);
final int height = bottom - top;
final int width = right - left;
image.scaleToFit(width, height);
for (int pageIndex = 1; pageIndex <= pageCount; pageIndex++) {
final PdfContentByte overContent = outputStamper.getOverContent(pageIndex);
final Rectangle pageSize = inputReader.getPageSize(pageIndex);
image.setAbsolutePosition(left, pageSize.getHeight() - bottom);
overContent.addImage(image);
}
outputStamper.close();
}
}

In a nutshell, the first parameter is the input file, the second the output file, the third the image file, and the fourth parameter is a comma-separated list of coordinates making up the target rectangle. As usual, the code is a tad lazy when it comes to faulty input parameters and exception handling - if there's a mistake you'll simply get the stacktrace as the output of the shell command.

The most important bit is in the loop over the pages, where we use the outputStamper.getOverContent() method to draw our image on top of the existing content. If you'd rather have the image in the back, as a watermark, you would use the outputStamper.getUnderContent() methopd instead. Also note that setting the image position coordinate system works from the bottomLeft of the page, so we have to use the original page height and subtract the bottom coordinate from it.

Now we can proceed with writing a LiveCode button script:
on mouseUp
--> determine the input, output and image files
local tInputFile, tOutputFile, tImageFile
put ShellPath(AbsolutePathFromStack("demo1.pdf")) \
into tInputFile
put ShellPath(AbsolutePathFromStack("stamp.pdf")) \
into tOutputFile
put ShellPath(AbsolutePathFromStack("Template.png")) \
into tImageFile
--> determine the image target rectangle
local tImageRect
put quote & "10,10,103,87" & quote \
into tImageRect
--> determine the class path
local tClassPath
if the platform is "Win32" then
put ".;iText-2.1.7.jar" into tClassPath
else
put ".:iText-2.1.7.jar" into tClassPath
end if
--> assemble the shell command
local tShellCommand
put "java -classpath" && tClassPath && \
"StampPdfFile" && \
tInputFile && \
tOutputFile && \
tImageFile && \
tImageRect \
into tShellCommand
--> execute the shell command
local tHideConsoleWindows, tDefaultFolder, tShellResult
put the hideConsoleWindows into tHideConsoleWindows
set the hideConsoleWindows to true
put the defaultFolder into tDefaultFolder
set the defaultFolder to AbsolutePathFromStack()
put shell(tShellCommand) into tShellResult
set the defaultFolder to tDefaultFolder
set the hideConsoleWindows to tHideConsoleWindows
if tShellResult is not empty then
answer error tShellResult
end if
end mouseUp

function AbsolutePathFromStack pFileName
local tAbsolutePath
put the effective filename of this stack into tAbsolutePath
set the itemDelimiter to slash
if pFileName is not empty then
put pFileName into item -1 of tAbsolutePath
else
delete item -1 of tAbsolutePath
end if
return tAbsolutePath
end AbsolutePathFromStack

function ShellPath pPath
if the platform is "Win32" then
put quote & pPath & quote into pPath
else
replace space with backslash & space in pPath
end if
return pPath
end ShellPath

Click the button, and it happily takes the existing PDF files (demo1.pdf), paints the image (Template.png) on top of all pages, and writes a new PDF file (stamp.pdf) in the same folder as our stack. There we have it, another example of using iText from within LiveCode.

Wednesday, December 29, 2010

Concatenating PDF files

Since the advent of LiveCode 4.5, developers have the ability to 'print' stack content directly to PDF files. And if you need pin-point control over what goes where, you can use Quartam PDF Library to generate PDF files from scripts. That's great if you are in full control of the content, but what if you need to work with existing PDF files? In the next few posts, we will examine how you can tap into the power of the Java-based iText library from LiveCode.
So let's start by downloading a copy of iText version 2.1.7 - do not use version 5.x as the API changed and the following example code won't work.

The first question is: how can we execute Java code from LiveCode? The simplest solution is the shell function: it allows you to execute DOS or Unix commands, as if you typed them in from the command line. Note that on Windows, using this function will show a DOS window, but you can control that by setting the hideConsoleWindows property before calling the shell function.
You can test it out by simply executing the following line from the message box:
  answer shell("java -version")

The second question is: what sort of Java code do we need to write? Well, I fired up a copy of Eclipse, started a new project, and created a new class 'ConcatPdfFiles' in the default package. Then I grabbed my paper copy of iText in action (first edition) and flipped to page 64 as this contains the examples for concatenating PDF files. A little bit of thinking, and I derived the following code:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.PdfCopy;
import com.lowagie.text.pdf.PdfReader;

public class ConcatPdfFiles {
public static void main(String[] args) throws DocumentException, IOException {
final String outputFilePath = args[0];
final OutputStream outputStream = new FileOutputStream(outputFilePath);
final Document outputDocument = new Document();
final PdfCopy outputCopy = new PdfCopy(outputDocument, outputStream);
outputDocument.open();
for (int i = 1; i < args.length; i++) {
final PdfReader inputPdfReader = new PdfReader(args[i]);
final int pageCount = inputPdfReader.getNumberOfPages();
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
outputCopy.addPage(outputCopy.getImportedPage(inputPdfReader, pageIndex + 1));
}
}
outputDocument.close();
}
}

As you can see, the code is a bit lazy when it comes to exception handling: I just let the exceptions get thrown, and this will be the output of our shell call if something goes wrong. Note also that the first argument is the output file, followed by the input files that you want to concatenate into the output file.

More importantly, at this point in time, the code doesn't compile. The problem is, we haven't yet told Eclipse where that iText-2.1.7.jar library file is, so compilation fails. This is sometimes referred to as 'classpath hell' - you have to give Java a list of paths where it can find the necessary additional libraries, not just at compile time but also at runtime as we'll see later.
Because I like to keep everything together in my Java projects, I added a new 'lib' folder to my project, and copied the iText2.1.7.jar file into it. At that point, you can use the contextual menu on the iText.2.1.7.jar file, and add it to the Build Path. Now the code I showed earlier compiles just fine, and we can proceed to the next stage.

The third question is: how do we put everything together in LiveCode? We'll begin by putting all the necessary parts into a single folder: the iText-2.1.7.jar library file, the ConcatPdfFiles.class compiled file and two example PDF files (demo1.pdf and demo2.pdf). Then we fire up LiveCode, create a new stack 'ConcatPdfFiles' and save it in the same folder as the other files, naming it "ConcatPdfFiles.liveCode'. Now we can drop a button onto the stack and start scripting.

Now we need to determine the correct command to be executed by the shell function. It should look something like:
java -classpath <class-path> ConcatPdfFiles <output-file> <input-file-1> <input-file-2> ...

The java executable needs the correct classpath, and we need to pass in compatible file paths.

Let's start with the classpath. This is a list of places that java needs to look for its .class files - as separate files in folders, or stored together in a .jar file. And for extra fun, the separator character is a colon on Unix-based platforms, and a semicolon on Windows. You can have relative paths in this classpath, and '.' (period) is short for the current directory. So rather than building a long class path, we can circumvent the issue by setting the defaultFolder property to change the working directory before calling the shell function. Then our classpath can be as short as:
.:iText-2.1.7.jar
on MacOS X/Linux and
.;iText-2.1.7.jar
on Windows.

The next bit is compatible file paths. The good news: LiveCode uses a '/' (slash) as separator, regardless of the underlying platform, and Java is more than happy to accept '/' in a path, even when it's running on Windows. However, if there are spaces in the path, we need to save them by putting quotes around the path on Windows, and escaping the spaces with a backslash on Unix-based platforms.
And to determine the paths relative to the stack's location on your hard disk, we'll need a helper function that uses the effective filename property of our stack.

So finally, we have a button script as follows:
on mouseUp
--> determine the input and output files
local tInputFiles, tOutputFile
put ShellPath(AbsolutePathFromStack("demo1.pdf")) && \
ShellPath(AbsolutePathFromStack("demo2.pdf")) \
into tInputFiles
put ShellPath(AbsolutePathFromStack("output.pdf")) \
into tOutputFile
--> determine the class path
local tClassPath
if the platform is "Win32" then
put ".;iText-2.1.7.jar" into tClassPath
else
put ".:iText-2.1.7.jar" into tClassPath
end if
--> assemble the shell command
local tShellCommand
put "java -classpath" && tClassPath && \
"ConcatPdfFiles" && \
tOutputFile && tInputFiles \
into tShellCommand
--> execute the shell command
local tHideConsoleWindows, tDefaultFolder, tShellResult
put the hideConsoleWindows into tHideConsoleWindows
set the hideConsoleWindows to true
put the defaultFolder into tDefaultFolder
set the defaultFolder to AbsolutePathFromStack()
put shell(tShellCommand) into tShellResult
set the defaultFolder to tDefaultFolder
set the hideConsoleWindows to tHideConsoleWindows
if tShellResult is not empty then
answer error tShellResult
end if
end mouseUp

function AbsolutePathFromStack pFileName
local tAbsolutePath
put the effective filename of this stack into tAbsolutePath
set the itemDelimiter to slash
if pFileName is not empty then
put pFileName into item -1 of tAbsolutePath
else
delete item -1 of tAbsolutePath
end if
return tAbsolutePath
end AbsolutePathFromStack

function ShellPath pPath
if the platform is "Win32" then
put quote & pPath & quote into pPath
else
replace space with backslash & space in pPath
end if
return pPath
end ShellPath

Click the button, and it happily concatenates the two PDF files (demo1.pdf and demo2.pdf) into a single PDF file (output.pdf) in the same folder as our stack. There we have it, our first use of iText from within LiveCode.

Sunday, November 21, 2010

I went to the Devoxx 2010 conference and...

and I got the t-shirt to prove it


100% Javaholic was the motto of this year's Java developer conference, organised by the Belkgian JUG and held in Antwerp.
Of course, I wasn't there to collect the t-shirt - unlike some others who shall remain unnamed and went from booth to booth to fetch a t-shirt, even if they weren't interested in the product. You know who you are :-p

and I met Bruno Lowagie

For those who don't know Bruno Lowagie, he's the primary developer behind the iText project, an open-source library for producing PDF files using Java. There must be something in the water around here, as he lives about 30 miles from my house - two Belgians, both writing PDF libraries - what are the odds?
As he was busy signing copies of iText in Action (second edition) the conversation was rather short, but he signed my copy of his book with "To a fellow PDF developer" - what a compliment :-)

and I managed to squeeze LiveCode onto a whiteboard


Given that someone actually added 'HyperTalk :-)' to the board right behind my entry, I think I wasn't the only guy there who knows LiveCode. Next time, poke me and I'll buy you a drink.

and had a very good time overall

Plenty of interesting sessions, friendly environment and excellent organization. I'll definitely go back next time I get the chance.

Saturday, August 8, 2009

Presenting at RunRevLive'09

Edinburgh is the hometown of Runtime Revolution Ltd, the guys behind Revolution - and the scene of this year's RunRevLive conference. After the huge success of last year's conference in Las Vegas, it has the makings of yet aother groundbreaking conference, focusing on the revWeb browser plug-in, the new product line-up (revMedia for free, revStudio and revEnterprise for extremely affordable prices) and the unveiling of revServer (the engine behind On-Rev).

When the crew asked me to present at this conference, I jumped at the chance to not just be there as one of the many attendees, but share my experiences with the rest of the community - as I have done for years on the use-revolution mailing list and the official forums. After submitting proposals, I got the green light for three presentations: 'Working with Java Classes', 'Desktop Databases with SQLite' and 'Basic Reports & Output.'

'Working with Java Classes' (Day Zero) delves into the various ways Revolution applications can interact with Java classes: a half-hour rollercoaster ride along shell commands, process comunication, socket communication, web services, message queues and externals.

'Desktop Databases with SQLite' (Day One) will take you on a trip through a straightforward desktop application where the data is stored in an SQLite database. By the end, you should have a pretty good idea how to quickly develop a database front-end with revStudio or revEnterprise.

'Basic Reports & Output' (Day One) will use the same desktop application to demonstrate various ways of reporting: generating HTML and RTF documents, using Excel as reporting vehicle in multiple ways, sending stuff to the printer using the built-in commands, and (obviously) how you ca make your life easier with Quartam Reports and Quartam PDF Library for Revolution.

I'm looking forward to seeing you all at the conference, both familiar and new faces - there are few things better than finally meeting someone you've only known through exchanging emails, or going for drinks with someone you only get to see via these get-togethers. Plenty of good stuff, and no jet-lag for me as it's only a hop accross the channel :-)

Saturday, May 23, 2009

Another Batch of New Items on my Bookshelf

Packt Publishing Ltd is celebrating its 5th Anniversay - and I couldn't resist their special deal of 5 books for 75 euros. So my book collection got expanded with the following titles.

Service Oriented Architecture with Java was the first book that caught my eye. It gives a broad overview of why SOA (Service Oriented Architecture) is a good approach to developing new and combining existing applications. It also does a good job explaining the difference between tradition EAI (Enterprise Application Integration) and SOA, and why using an ESB (Enterprise Service Bus) is a good idea to assemble all the parts.
Having finished this book the dame day as it arrived, I have to say I liked the content, but was a little confused by some of the sentence constructs; it doesn't help when both the authors and the reviewer are non-native English speakers; I had it a little easier since I worked with developers from India in a not-too-distant past. Even though a technical book doesn't have to read like poetry, an editor should prevent from adding to the complexity of the material.

Service Oriented Java Business Integration was the logical next item, as JBI is one of those items I have a particular interest in. Even though this book is centered around Apache ServiceMix, I'm sure I'll be able to apply its principles to the alternative Glassfish OpenESB open-source project backed by Sun. Especially when I combine its contents with what I learned from the book Building SOA-Based Composite Applications Using NetBeans IDE 6 that I picked up last year.

Java EE 5 Development with NetBeans 6 continues the theme of my desire to learn more about Java EE development with NetBeans and Glassfish. Currently about a third into this book, I find it a good companion to the book Java EE 5 Development using GlassFish Application Server by the same author, which I also picked up last year - naturally, there is some overlap, but I have the memory of a goldfish and the author really does a good job of explaining things.

EJB 3 Developer Guide wraps up the enterprise-oriented book tour. Packed with practical examples, it gets straight to business. When I want a more architectural point-of-view, I can always refer back to the book EJB 3 in Action, published by Manning, which is three times the size but also contains more of the theory and differences between EJB 3 and older versions - and how the introduction of Java EE 5 really did simplify development a lot.

Swing Extreme Testing may seem like the odd-one-out, but what's the use of building an enterprise application back-end, if no one can see the contents? Having built a number of Swing-based front-ends, I can attest it takes time to get it "right" - and I'm sure I'm still doing things wrong. I sure wish someone would publish a book that explains best practices for Swing user interface development like Effective Java does for the Java language.
But I digress. While I've gradually converted to using JUnit for writing unit tests for the classes that make up the domain model, I still have to decide on a framework for testing the user interface. Once I've worked my way through this book, I'll be sure to check out FEST (Fixtures for Easy Software Testing) as the online articles I've read are quite favorable to this framework.

Ah, so much to read, so many experiments to conduct - and only so little time...

Thursday, April 16, 2009

Vive la Revolution!

It has been a busy month of April at the Runtime Revolution HQ - last week we witnessed the release of Revolution 3.5, with its data grid and behavior scripting, and this week they've announced the On-Rev web hosting initiative.

Since the unveiling of the RunRev web strategy at the runrevlive '08 conference, last year in Las Vegas, Rev-afficionados around the globe have been eagerly anticipating the day where they can do away with the mix-and-match (or should that be 'mismatch'?) of technologies needed to build modern, Internet-enabled applications.
With customers and end-users requesting applications that work on desktops and web-browsers alike, developers have been forced to use combinations of JavaScript, Flash, .NET, Java and a myriad of frameworks to deliver solutions where not all features might have been fully implemented accross the different client interfaces.

With the Revolution of the Future, developers will finally be able to build those same solutions with a comprehensive technology stack based on one language, and have it work as a desktop application or in a web browser, backed by business logic with database access on the server-side.
Java promised us this 15 years ago, and then kinda-sorta forgot about this promise. Granted, Java SE 6u10 finally fixed the Applets experience, and Java EE 5 definitely simplified server-side development; but in their panic to provide an alternative to Flash, they produced JavaFX with a new scripting language that doesn't even work like Java.

In short, I think the RunRev team has a winner on their hands - and I am definitely looking forward to putting these new Revolution technologies to good use.

Wednesday, November 26, 2008

Future-proofing your Revolution scripts

While I'm still not giving out details on the new features or release date for Quartam PDF Library 1.1, I would like to share some insights that I gained as part of my efforts on producing this new version. Back in September, the team of Runtime Revolution shipped version 3.0 of its cross-platform development tool Revolution.

Overall, Revolution 3.0 is an impressive release, with a revamped script editor, gradients, nested arrays and much-improved out-of-the-box experience thanks to the Start Center and Resource Center. One of the items that was also introduced, but not mentioned in the press release, was the new 'byte' chunk type.

If you're wondering what it's good for or why you would want to use it instead of the good old 'character' chunk type, then allow me to explain the underlying reason for its introduction. Back in the HyperCard days, not much attention was given to languages that didn't fall neatly in the ASCII-domain, based on the English alphabet, where one character takes up a single byte and thus there are 256 possible characters (a number of which are control characters that can't even be displayed on screen).

Naturally, this isn't quite enough room for a couple of thousand Chinese or Japanese characters, and that's where Unicode comes into play. This is a set of standards that govern how this much larger number of characters can be stored and should be interpreted. The UTF-8 standard still relies on single bytes as its main storage system, but uses special bit-settings to determine if the next character is a single byte or more than one byte.

My other favorite cross-platform technology Java embraced Unicode from the very first day, which made perfect sense given its goal of "write once, run anywhere" global software. And to make this easier, if less memory-efficient for those circumstances where you only have to deal with the ASCII characters, it uses UTF-16 everywhere.

When Revolution 2.0 saw the daylight, it introduced support for Unicode text entry and manipulation. Rather than jumping on the UTF-16 bandwagon, the engineers decided to implement a more flexible system that would use plain-old ASCII for everyday operations, while allowing the developer to use UTF-16 only when necessary.

While the implementation could still use some love and care, Revolution's technology director Mark Waddingham has big plans for it and wants to evolve to a system that is far less painful than anything else out there, but as resource-efficient as the current implementation. Which means that in the future, Revolution developers will be able to use the 'character', 'word', 'item' and 'line' chunks without having to care about the underlying encoding - the engine will know what constitutes a word or a sentence in the language of that piece of text and will take care of collation and all the gruesome details of Unicode.

Traditionally, the one-byte-is-one-character paradigm of xCard meant that developers could use the 'character' keyword, along with the 'charToNum' and 'numToChar' functions, to manipulate data at the byte-level. But with this equivalence fading away at some point in the future, the new 'byte' chunk type is our saviour!

Quartam PDF Library reads PNG and JPEG image files as byte streams, extracting information about the image height and width, bit depth and other binary-encoded information. To ensure that this and other binary-reading code will keep working correctly in the future, I've decided to make the necessary changes in version 1.1 to use the 'byte' chunk type rather than the 'character' chunk type.

While this means that the next version will require developers have Revolution 3.0 or higher, I feel this is a fair trade-off. In my experience, the new Revolution version performs well with less problems and quirks than ever. And if I compare my glacial speed of development with their feature-packed release momentum, I'm sure they will be shipping their next version before I do ;-)

Just in case you'd like to know how I updated and thus future-proofed my code, here are some pointers:
- replace 'open file theFilePath for read' with 'open file theFilePath for binary read'
- replace 'open file theFilePath for write' with 'open file theFilePath for binary write'
- replace 'length(theBinaryVariable)' / 'the number of chars in theBinaryVariable' with 'the number of bytes in theBinaryVariable'
- replace 'char x to y of theBinaryVariable' with 'byte x to y of theBinaryVariable'
- replace 'charToNum(char x of theBinaryVariable)' with 'byteToNum(byte x of theBinaryVariable)'
- replace 'numToChar(theNumber)' with 'numToByte(theNumber)'

That's it - no other changes required! The modification was straightforward and consistent, and everything works just fine. I know that my code will continue to work in future versions, and I can enjoy the same linear speed that I have now with the 'character' chunk type once that part is overhauled for Unicode embracing.

If only all requested or necessary changes were so easy to implement...