Posts Tagged ‘git

28
Dec
13

The 3 R’s of Spring Batch 3.0.x with Annotations

This page describes how you can Read, wRrite and perform aRithmetic on flat files using The Spring Batch Framework. We will take a comma separated file (csv) that contain employee information, add some information to it, and write it back to the file system.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

The basic building blocks of any batch process is

  1. Reading a Item
  2. Performing an operation on it
  3. Writing the Item back

Please take some time to review The Domain Language of Batch before proceeding. It covers much of the fundamental concepts we will be covering here.

Batch Steps

This page is focused on an individual step of the batch process.

The following is from the spring batch documentation

A Step is a domain object that encapsulates an independent, sequential phase of a batch job. Therefore, every Job is composed entirely of one or more steps. A Step contains all of the information necessary to define and control the actual batch processing. This is a necessarily vague description because the contents of any given Step are at the discretion of the developer writing a Job. A Step can be as simple or complex as the developer desires. A simple Step might load data from a file into the database, requiring little or no code. (depending upon the implementations used) A more complex Step may have complicated business rules that are applied as part of the processing.

Step Processing types

There are 2 ways a step can process data,

Tasklet

If the step requires only to execute a single task then you can use a tasklet. Typical use case for this is when you need to run a stored procedure, or copy a file from one location to the other. In the “Hello World” example we used a Tasklet to print the message to the console.

Chunk oriented

Chunk oriented processing involves specifying a reader, processor and writer. The input is read one item at a time in sequence and passed to the processor and eventually to the writer in chunks within a transaction boundary. Once the commit interval is reached the items are committed to the writer. Chunk oriented processing is what we will cover on this page.

Library Versions

  • Spring Batch 3.0.0-M3 or above

Input Data

The following is the input csv file that will be read. Please create the following file in the projects resource directory.

vi src/main/resources/input_data.txt

7876,ADAMS,CLERK,1100
7499,ALLEN,SALESMAN,1600
7698,BLAKE,MANAGER,2850
7782,CLARK,MANAGER,2450
7902,FORD,ANALYST,3000
7900,JAMES,CLERK,950
7566,JONES,MANAGER,2975
7839,KING,PRESIDENT,-5000
7654,MARTIN,SALESMAN,1250
7934,MILLER,CLERK,1300
7788,SCOTT,ANALYST,3000
7369,SMITH,CLERK,800
7844,TURNER,SALESMAN,1500
7521,WARD,SALESMAN,1250

Employee Bean

This is a simple bean that represents a single Employee.

vi src/main/java/com/test/Employee.java

package com.test;

public class Employee {

	private Integer empId;
	private String lastName;
	private String title;
	private Integer salary;
	private String rank;
	
	public Integer getEmpId() {
		return empId;
	}
	public void setEmpId(Integer empId) {
		this.empId = empId;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public Integer getSalary() {
		return salary;
	}
	public void setSalary(Integer salary) {
		this.salary = salary;
	}
	public void setRank(String rank) {
		this.rank = rank;
	}
	public String getRank() {
		return rank;
	}
	@Override
	public String toString() {
		return "Employee [empId=" + empId + ", lastName=" + lastName
				+ ", title=" + title + ", salary=" + salary + ", rank=" + rank
				+ "]";
	}	
}

Reading

The reader is configured in the ThreeRJobConfig.java ( see reader() method )

Arithmetic

Not really! All we are doing is assigning a Rank based on the salary amount. The item processor takes an input Bean and converts it to an output bean. In this case the beans are the same but they don’t have to be.

vi src/main/java/com/test/EmployeeProcessor.java

package com.test;

import org.springframework.batch.item.ItemProcessor;

public class EmployeeProcessor implements ItemProcessor<Employee, Employee> {

	public Employee process(Employee emp) throws Exception {
		// if salary >= 2500 then set rank as "Director"		
		if(emp.getSalary() >= 2500 ) {
			emp.setRank("Director");			
		} else {
			emp.setRank("N/A");
		}
		return emp;
	}

}

Writing

The reader is configured in the ThreeRJobConfig.java ( See writer() method )

Job Configuration

vi src/main/java/com/test/config/ThreeRJobConfig.java

package com.test.config;

import java.io.File;

import javax.sql.DataSource;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.file.LineMapper;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor;
import org.springframework.batch.item.file.transform.DelimitedLineAggregator;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;

import com.test.Employee;
import com.test.EmployeeProcessor;

@Configuration
@Import(StandaloneInfrastructureConfiguration.class)
public class ThreeRJobConfig {

	@Autowired
	private JobBuilderFactory jobBuilders;
	
	@Autowired
	private StepBuilderFactory stepBuilders;
	
	@Autowired
	private InfrastructureConfiguration infrastructureConfiguration;
	
	@Autowired
	private DataSource dataSource; // just for show...
	
	@Bean
	public Job threeRJob(){
		return jobBuilders.get("threeRJob")
				.start(step())
				.build();
	}
	
	@Bean
	public Step step(){
		return stepBuilders.get("step")
				.<Employee,Employee>chunk(1)
				.reader(reader())
				.processor(processor())
				.writer(writer())
				.build();
	}

	private ItemWriter<Employee> writer() {
		FlatFileItemWriter<Employee> itemWriter = new FlatFileItemWriter<Employee>();
		DelimitedLineAggregator<Employee> la = new DelimitedLineAggregator<Employee>();
		la.setDelimiter(",");
		BeanWrapperFieldExtractor<Employee> fieldExtractor = new BeanWrapperFieldExtractor<Employee>();
		fieldExtractor.setNames(new String[]{"empId","lastName","title","salary","rank"});
		la.setFieldExtractor(fieldExtractor);
		itemWriter.setLineAggregator(la);
		
		itemWriter.setResource(new FileSystemResource(new File("target/output_data.txt")));
		return itemWriter;
	}

	private ItemProcessor<Employee,Employee> processor() {
		return new EmployeeProcessor();
	}

	private ItemReader<Employee> reader() {
		FlatFileItemReader<Employee> itemReader = new FlatFileItemReader<Employee>();
		itemReader.setLineMapper(lineMapper());
		itemReader.setResource(new ClassPathResource("input_data.txt"));
		return itemReader;
	}

	private LineMapper<Employee> lineMapper() {
		DefaultLineMapper<Employee> lineMapper = new DefaultLineMapper<Employee>();
		DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
		lineTokenizer.setNames(new String[]{"empId","lastName","title","salary"});
		lineTokenizer.setIncludedFields(new int[]{0,1,2,3});
		BeanWrapperFieldSetMapper<Employee> fieldSetMapper = new BeanWrapperFieldSetMapper<Employee>();
		fieldSetMapper.setTargetType(Employee.class);
		lineMapper.setLineTokenizer(lineTokenizer);
		lineMapper.setFieldSetMapper(fieldSetMapper);
		return lineMapper;
	}
	

}

Execute the job

Go to the command line and type the following:

mvn compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="com.test.config.ThreeRJobConfig threeRJob"

View the Results

The output file will appear in the target/ folder of the project.

Further Reading

To keep things simple we were reading and writing files located in the project own folders. There are many enterprise design patterns that describe the best practices for feeding data into the batch programs. For further reading on this topic please see the Spring Integrations Framework Homepage.

27
Dec
13

Hello World With Spring Batch 3.0.x with Pure Annotations

This page describes how to get a Spring Batch application to print hello world to the console. This page provides a stepping stone to help you get up and running quickly. Since this is a quick and dirty method of getting up and running with spring batch, it does not cover the fundamental concepts. For further information please visit the Spring Batch project documentation site.

This page shows the latest techniques of configuring spring batch using pure java annotations. This results in a significant reduction in work necessary to get a spring batch job running.

An older version of the page is available here. https://numberformat.wordpress.com/2010/02/05/hello-world-with-spring-batch/

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

This page takes about 10 minutes to complete and have a working spring batch application.

Background

The following spring batch example program is the simplest way you could setup a job to run in Spring batch. As such there are some limitations with the following approach.

Use of an In Memory Database

The following program uses an uses in memory database to store information about batch execution runs. This means that there is no protection against duplicate job runs and it does not store when a job was started or completed. Since spring is a pluggable architecture you can always change to use a persistent database like mysql or oracle to store job information. I will describe this process in a future blog entry.

If you use scheduling tools like Autosys you should already have a system that maintains information about job executions. These tools would maintain the history of past runs and if the job succeeded or not etc…so using an in-memory database should not big deal.

Library Versions

  • Spring Batch 3.0.0-M3 or above

Modify the pom.xml

vi pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.test</groupId>
	<artifactId>spring-batch-helloworld</artifactId>
	<version>20131227</version>
	<name>spring batch hello world</name>
	<packaging>jar</packaging>

	<pluginRepositories>
		<pluginRepository> <!-- Ignore this repository. Its only used for document publication. -->
			<id>numberformat-releases</id>
			<url>https://raw.github.com/numberformat/20130213/master/repo</url>
		</pluginRepository>
	</pluginRepositories>

	<properties>
		<spring.framework.version>3.2.1.RELEASE</spring.framework.version>
		<spring.batch.version>3.0.0.M2</spring.batch.version>
	</properties>

	<repositories>
		<repository>
			<id>spring-s3</id>
			<name>Spring Maven MILESTONE Repository</name>
			<url>http://maven.springframework.org/milestone</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>2.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.batch</groupId>
			<artifactId>spring-batch-core</artifactId>
			<version>${spring.batch.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.batch</groupId>
			<artifactId>spring-batch-infrastructure</artifactId>
			<version>${spring.batch.version}</version>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.17</version>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.8.2</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring.framework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring.framework.version}</version>
		</dependency>
		<dependency>
			<groupId>hsqldb</groupId>
			<artifactId>hsqldb</artifactId>
			<version>1.8.0.7</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin> <!-- Ignore this plugin. Its only used for document publication. -->
				<groupId>github.numberformat</groupId>
				<artifactId>blog-plugin</artifactId>
				<version>1.0-SNAPSHOT</version>
				<configuration>
					<gitUrl>https://github.com/numberformat/wordpress/tree/master/${project.version}/${project.artifactId}</gitUrl>
				</configuration>
				<executions>
					<execution>
						<id>1</id>
						<phase>site</phase>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

<!-- Run the application using: 
mvn compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="com.test.config.HelloWorldJobConfig helloWorldJob"
-->
</project>

Setup the log4j configuration files. We will be using a very basic file that outputs to the console.
vi src/main/resources/log4j.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
   
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out"/>
        <param name="Threshold" value="INFO" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c - %m%n"/>
        </layout>
    </appender>
    <logger name="org.springframework" additivity="false">
        <level value="INFO"/>
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root>
        <level value="ERROR"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</log4j:configuration>

The job configuration is stored in the com.test.config package. The configuration for the infrastructure and the job is present in this package.

Config Interface

vi src/main/java/com/test/config/InfrastructureConfiguration.java

package com.test.config;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;

public interface InfrastructureConfiguration {

	@Bean
	public abstract DataSource dataSource();

}

The implementation

vi src/main/java/com/test/config/StandaloneInfrastructureConfiguration.java

package com.test.config;

import javax.sql.DataSource;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

@Configuration
@EnableBatchProcessing
public class StandaloneInfrastructureConfiguration implements InfrastructureConfiguration {
	
	@Bean
	public DataSource dataSource(){
		EmbeddedDatabaseBuilder embeddedDatabaseBuilder = new EmbeddedDatabaseBuilder();
		return embeddedDatabaseBuilder.addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql")
				.addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql")
				.setType(EmbeddedDatabaseType.HSQL)
				.build();
	}

}

Job Configuration

vi src/main/java/com/test/config/HelloWorldJobConfig.java

package com.test.config;

import javax.sql.DataSource;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.test.HelloWorldTasklet;

@Configuration
@Import(StandaloneInfrastructureConfiguration.class)
public class HelloWorldJobConfig {

	@Autowired
	private JobBuilderFactory jobBuilders;
	
	@Autowired
	private StepBuilderFactory stepBuilders;
	
	@Autowired
	private InfrastructureConfiguration infrastructureConfiguration;
	
	@Autowired
	private DataSource dataSource; // just for show...
	
	@Bean
	public Job helloWorldJob(){
		return jobBuilders.get("helloWorldJob")
				.start(step())
				.build();
	}
	
	@Bean
	public Step step(){
		return stepBuilders.get("step")
				.tasklet(tasklet())
				.build();
	}
	
	@Bean
	public Tasklet tasklet() {
		return new HelloWorldTasklet();
	}
}

The Tasklet

This class does the actual printing of the message to the console.

vi src/main/java/com/test/HelloWorldTasklet.java

package com.test;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;

public class HelloWorldTasklet implements Tasklet {
	 
    public RepeatStatus execute(StepContribution arg0, ChunkContext arg1)
            throws Exception {
        System.out.println("");
        System.out.println(" XXX XXX           XX      XX             ");
        System.out.println("  X   X             X       X             ");
        System.out.println("  X   X             X       X             ");
        System.out.println("  X   X   XXXXX     X       X     XXXXX   ");
        System.out.println("  XXXXX  X     X    X       X    X     X  ");
        System.out.println("  X   X  XXXXXXX    X       X    X     X  ");
        System.out.println("  X   X  X          X       X    X     X  ");
        System.out.println("  X   X  X     X    X       X    X     X  ");
        System.out.println(" XXX XXX  XXXXX   XXXXX   XXXXX   XXXXX   ");
        System.out.println("                                          ");
        System.out.println("                                          ");
        System.out.println("                                          ");
        System.out.println("                                          ");
        System.out.println(" XXX XXX                   XX        XX   ");
        System.out.println("  X   X                     X         X   ");
        System.out.println("  X   X                     X         X   ");
        System.out.println("  X   X   XXXXX  XXX XX     X     XXXXX   ");
        System.out.println("  X X X  X     X   XX  X    X    X    X   ");
        System.out.println("  X X X  X     X   X        X    X    X   ");
        System.out.println("  X X X  X     X   X        X    X    X   ");
        System.out.println("   X X   X     X   X        X    X    X   ");
        System.out.println("   X X    XXXXX  XXXXX    XXXXX   XXXXXX  ");
        System.out.println("");
        return RepeatStatus.FINISHED;
    }
}

Execute the Job

To run the job from the command line type the following.

mvn compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="com.test.config.HelloWorldJobConfig helloWorldJob"

Deploying the application

The following tutorial describes how to Package and deploy the application as a self contained jar. (just be mindful to change the commandline args to the one you see here.)

What’s Next?

In the next few articles I plan on describing how to:

  1. Read and write flat files.
  2. Write header and footer records in the output file.
  3. Replace the in-memory database with a HyperSQL Java database so we can have job information persist between job invocations.
  4. Throw an exception in the middle of a large batch job and restart the job execution from the point where it left off.
  5. Use validation framework like “commons-validator” to perform input file validation and create reject records for manual correction and later processing.
16
Oct
13

Java Debugging using a Custom AlertFrame

This page describes how to create a simple Alert Frame to display java variable during application runtime. It is similar to the alert window commonly displayed in javaScript. The AlertFrame can be used during development as well as in your final application.

An example would be to display the results of a database query you have run, or to display results of a web service call that was made. The AlertFrame has capability to display not only simple wrapper types but also complex beans. It inspects and finds getter methods using reflection and displays the results in the appropriate format. It also allows the user to click further into the bean to explore deeper.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

Code

The following is the code for the the AlertFrame.

vi src/main/java/AlertFrame.java

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;


/**
 * Displays List, Properties, and Table data depending on which constructor is
 * called. Constructors for Strings, Array of Strings, Maps of Strings and List
 * of Maps are available.
 */
public class AlertFrame extends JFrame {
	private static final long serialVersionUID = 1L;

	private static final GraphicsDevice gd = GraphicsEnvironment
			.getLocalGraphicsEnvironment().getDefaultScreenDevice();

	private JTextField selectedRowIndex;
	private JTextField selectedColIndex;
	private MapListTableModel mapListTableModel;
	private JTextArea textArea = new JTextArea();
	
	private void initCommon() {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
	}
	public <T> AlertFrame(List<T> list) {
        initCommon();
		final JTextArea jTextArea = new JTextArea();
		jTextArea.setPreferredSize(new Dimension(300, 300));
		final DefaultListModel listModel = new DefaultListModel();
		int i = 0;
		for (Object value : list) {
			listModel.add(i++, value);
		}
		JList jList = new JList(listModel);
		getContentPane().add(new JScrollPane(jList),
				BorderLayout.CENTER);
		final JScrollPane scrollPane = new JScrollPane(jTextArea);
		getContentPane().add(scrollPane,BorderLayout.SOUTH);
		setSize(gd.getDisplayMode().getWidth() / 2, gd.getDisplayMode()
				.getWidth() / 2);

		jList.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent e) {
				for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
					if (((JList) e.getSource()).isSelectedIndex(i)) {
						jTextArea.setText(String.valueOf(listModel.get(i)));
						scrollPane.invalidate(); // TODO: fix this.
					}
				}
			}
		});
		
		jList.addMouseListener(new MouseAdapter() {
		    public void mouseClicked(MouseEvent evt) {
		        JList list = (JList)evt.getSource();
		        if (evt.getClickCount() == 2) {
		            int index = list.locationToIndex(evt.getPoint());
		            Object obj = listModel.get(index);
		            new AlertFrame(obj);
		        } 
		    }
		});	
		
		showCentered(this);		
	}
	public <T> AlertFrame(Set<T> set) {
		this(new ArrayList<T>(set));
	}
	/**
	 * Tabular data (simple text) with an ordered list of columns.
	 */
	public <T> AlertFrame(List<Map<String, T>> mapList,
			List<String> colNameList) {
        initCommon();
		this.mapListTableModel = new MapListTableModel<T>(mapList, colNameList);

		selectedRowIndex = new JTextField();
		selectedRowIndex.getDocument().addDocumentListener(new DL(this));
		selectedColIndex = new JTextField();
		selectedColIndex.getDocument().addDocumentListener(new DL(this));
		// textArea.setPreferredSize()
		setSize(800, 600);

		JTable jTable = new JTable(mapListTableModel);
		// handle selection events and callbacks here.
		jTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		ListSelectionModel rsm = jTable.getSelectionModel();
		rsm.addListSelectionListener(new SelectionDebugger(selectedRowIndex,
				rsm));

		ListSelectionModel csm = jTable.getColumnModel().getSelectionModel();
		csm.addListSelectionListener(new SelectionDebugger(selectedColIndex,
				csm));

		jTable.setRowSelectionAllowed(false);
		jTable.setColumnSelectionAllowed(false);
		jTable.setCellSelectionEnabled(true);

		getContentPane().add(new JScrollPane(jTable), BorderLayout.CENTER);
		getContentPane().add(new JScrollPane(textArea), BorderLayout.SOUTH);

		pack();
		showCentered(this);
		setVisible(true);
	}
	public AlertFrame(Number value) {
		this(String.valueOf(value));
	}
	public AlertFrame(Object obj) {
        initCommon();
		try {
			Map<String,Object> beanMap = BeanUtils.describe(obj);
		for(String key : beanMap.keySet()) {
			beanMap.put(key, PropertyUtils.getSimpleProperty(obj, key));
		}
			processMap(beanMap);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		}
	}
	public AlertFrame(String value) {
        initCommon();
		textArea.setText(value);
		getContentPane().add(new JScrollPane(textArea));
		setSize(gd.getDisplayMode().getWidth() / 2, gd.getDisplayMode()
				.getWidth() / 2);
		showCentered(this);
	}

	public AlertFrame(String[] values) {
        initCommon();
		final JTextArea jTextArea = new JTextArea();
		jTextArea.setPreferredSize(new Dimension(300, 300));
		final DefaultListModel listModel = new DefaultListModel();
		int i = 0;
		for (Object value : values) {
			listModel.add(i++, String.valueOf(value));
		}
		JList jList = new JList(listModel);
		getContentPane().add(new JScrollPane(jList),
				BorderLayout.SOUTH);
		final JScrollPane scrollPane = new JScrollPane(jTextArea);
		getContentPane().add(scrollPane,BorderLayout.CENTER);
		setSize(gd.getDisplayMode().getWidth() / 2, gd.getDisplayMode()
				.getWidth() / 2);

		jList.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent e) {
//				if (e.getValueIsAdjusting() == false || e.getFirstIndex() == -1) {
//					return;
//				}
				for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
					if (((JList) e.getSource()).isSelectedIndex(i)) {
						jTextArea.setText(String.valueOf(listModel.get(i)));
						scrollPane.invalidate(); // TODO: fix this.
					}
				}
			}
		});
		showCentered(this);
	}

	public <T> AlertFrame(final Map<String, T> map) {
        initCommon();
		processMap(map);
	}

	private <T> void processMap(final Map<String, T> map) {
		final DefaultListModel listModel = new DefaultListModel();

		int i = 0;
		for (Object value : new TreeSet<Object>(map.keySet())) {
			listModel.add(i++, value);
		}
		JList jList = new JList(listModel);
		jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		jList.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent e) {
//				if (e.getValueIsAdjusting() == false || e.getFirstIndex() == -1) {
//					return;
//				}
				for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
					if (((JList) e.getSource()).isSelectedIndex(i)) {
						textArea.setText(String.valueOf(map.get(listModel.get(i))));
					}
				}
			}
		});
		jList.addMouseListener(new MouseAdapter() {
		    public void mouseClicked(MouseEvent evt) {
		        JList list = (JList)evt.getSource();
		        if (evt.getClickCount() == 2) {
		            int index = list.locationToIndex(evt.getPoint());
		            Object obj = map.get(listModel.get(index));
		            new AlertFrame(obj);
		        } 
		    }
		});		
		
		// jList.add
		getContentPane().add(new JScrollPane(jList), BorderLayout.NORTH);
		getContentPane().add(new JScrollPane(textArea), BorderLayout.CENTER);
		setSize(gd.getDisplayMode().getWidth() / 2, gd.getDisplayMode()
				.getWidth() / 2);
		showCentered(this);
	}
	public JTextArea getTextArea() {
		return textArea;
	}

	class DL implements DocumentListener {
		private AlertFrame parent;

		public DL(AlertFrame parent) {
			this.parent = parent;
		}

		public void removeUpdate(DocumentEvent event) {
			changed(event);
		}

		public void insertUpdate(DocumentEvent event) {
			changed(event);
		}

		public void changedUpdate(DocumentEvent event) {
			changed(event);
		}

		private void changed(DocumentEvent event) {
			if (!"".equals(parent.selectedRowIndex.getText())
					&& !"".equals(parent.selectedColIndex.getText())) {
				int intSelectedRowIndex = Integer
						.parseInt(parent.selectedRowIndex.getText());
				int intSelectedColIndex = Integer
						.parseInt(parent.selectedColIndex.getText());
				textArea.setText(String.valueOf(mapListTableModel.getValueAt(
						intSelectedRowIndex, intSelectedColIndex)));
			}
		}
	}
	public static void showCentered(JFrame frame) {
		GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
		final int x = (gd.getDisplayMode().getWidth() - frame.getWidth()) / 2;
		final int y = (gd.getDisplayMode().getHeight() - frame.getHeight()) / 2;
		frame.setLocation(x, y);
		frame.setVisible(true);
	}	

}

class SelectionDebugger implements ListSelectionListener {
	private ListSelectionModel listModel;
	private JTextField target;

	public SelectionDebugger(JTextField target, ListSelectionModel lsm) {
		this.listModel = lsm;
		this.target = target;
	}

	public void valueChanged(ListSelectionEvent lse) {
		if (!lse.getValueIsAdjusting()) {
			int[] selection = getSelectedIndices(
					listModel.getMinSelectionIndex(),
					listModel.getMaxSelectionIndex());
			if (selection.length == 0) {
			} else {
				for (int i = 0; i < selection.length; i++) {
					String text = String.valueOf(selection[i]);
					if (!"".equals(text.trim())) {
						target.setText(text);
					}
				}
			}

		}
	}

	protected int[] getSelectedIndices(int start, int stop) {
		if ((start == -1) || (stop == -1)) {
			// no selection, so return an empty array
			return new int[0];
		}
		int guesses[] = new int[stop - start + 1];
		int index = 0;
		// manually walk thru these
		for (int i = start; i <= stop; i++) {
			if (listModel.isSelectedIndex(i)) {
				guesses[index++] = i;
			}
		}
		int realthing[] = new int[index];
		System.arraycopy(guesses, 0, realthing, 0, index);
		return realthing;
	}
}

class MapListTableModel <T> extends AbstractTableModel {
	private static final long serialVersionUID = 1L;
	private List<Map<String, T>> mapList = new ArrayList<Map<String, T>>();
	private List<String> colNameList = new ArrayList<String>();

	public MapListTableModel(List<Map<String, T>> mapList,
			List<String> colNameList) {
		super();
		this.mapList = mapList;
		this.colNameList = colNameList;
	}

	public int getColumnCount() {
		return colNameList.size();
	}

	public String getColumnName(int columnIndex) {
		return colNameList.get(columnIndex);
	}

	public int getRowCount() {
		return mapList.size();
	}

	public Object getValueAt(int rowIndex, int columnIndex) {
		String columnName = getColumnName(columnIndex);
		Map<String, T> map = mapList.get(rowIndex);
		if (map == null)
			return null;
		return map.get(columnName);
	}
}

Sample Swing Test Application

vi src/main/java/AlertFrameTest.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class AlertFrameTest {
	public static void main(String args[]) throws Exception {
		testValue();
		testValueSet();
		testValueList();
		testValueMap();
		testValueListMap();		
	}

	/**
	 * Displays a single dialog with the value displayed. 
	 * 
	 * @throws Exception
	 */
	private static void testValue() throws Exception {
		new AlertFrame(123);
		new AlertFrame(123.5);
		new AlertFrame(new Integer(123));
		// Strings
		new AlertFrame("Testing Single String");
		// beans 
		// uses reflection to get a map of all the getters.
		// displays a jListbox with the getters.
		// clicking displays toString on bottom jTextArea.
		// double clicking opens new AlertFrame (value).
		new AlertFrame(new UserBean(1, "Bob", new UserBean(0, "Manager", null)));
	}

	/**
	 * Displays Dialog with a JListbox.
	 */
	private static void testValueSet() {
		// wrapped numbers 
		// clicking displays toString on bottom jTextArea.
		// double clicking does nothing.
		Set<Integer> intSet = new HashSet<Integer>();
		intSet.add(1);
		intSet.add(2);
		new AlertFrame(intSet);
		// Strings
		// clicking displays toString on bottom jTextArea.
		// double clicking does nothing.
		Set<String> set = new HashSet<String>();
		set.add("a");
		set.add("b");
		new AlertFrame(set);
		// beans 
		// clicking displays toString on bottom jTextArea.
		// double clicking opens new AlertFrame (value).
		Set<UserBean> userSet = new HashSet<UserBean>();
		userSet.add(new UserBean(1, "Bob", new UserBean(0, "Manager", null)));
		userSet.add(new UserBean(1, "Ted", new UserBean(0, "Manager", null)));
		new AlertFrame(userSet);
	}
	public static void testValueList() {	
		// Strings
		// clicking displays toString on bottom jTextArea.
		// double clicking does nothing.
		List<String> set = new ArrayList<String>();
		set.add("a");
		set.add("b");
		new AlertFrame(set);
		// beans 
		// clicking displays toString on bottom jTextArea.
		// double clicking opens new AlertFrame (value).
		List<UserBean> userSet = new ArrayList<UserBean>();
		userSet.add(new UserBean(1, "Bob", new UserBean(0, "Manager", null)));
		userSet.add(new UserBean(1, "Ted", new UserBean(0, "Manager", null)));
		new AlertFrame(userSet);
	}
	/**
	 * This is for properties. It displays a list box with the map keys with
	 * details displayed in a text area on the bottom.
	 */
	public static void testValueMap() {	
		// wrapped numbers (as map values)
		// clicking displays toString on bottom jTextArea.		
		// double clicking does nothing.
		Map <String, Integer> numericMap = new HashMap<String,Integer>();
		numericMap.put("a", 123);
		numericMap.put("b", 456);
		new AlertFrame(numericMap);
		// Strings (as map values)
		// clicking displays toString on bottom jTextArea.
		// double clicking does nothing.
		Map <String, String> stringMap = new HashMap<String,String>();
		stringMap.put("a", "string a");
		stringMap.put("b", "String b");
		new AlertFrame(stringMap);
		// beans (as map values)		
		// clicking displays toString on bottom jTextArea.
		// double clicking opens new AlertFrame (value).
		Map <String, UserBean> beanMap = new HashMap<String,UserBean>();
		beanMap.put("a", new UserBean(1, "Bob", new UserBean(0, "Manager", null)));
		beanMap.put("b", new UserBean(1, "Ted", new UserBean(0, "Manager", null)));
		new AlertFrame(beanMap);
	}
	/**
	 * This displays a table with columns that are based on map keys with 
	 * details displayed in the text area on the bottom.
	 */
	public static void testValueListMap() {	
		{
			// wrapped numbers (as table cell values)
			// clicking displays toString on bottom jTextArea.
			// double clicking does nothing.
			List<Map<String,Integer>> mapList = new ArrayList<Map<String,Integer>>();
			mapList.add(Collections.singletonMap("key", 123));
			mapList.add(Collections.singletonMap("key", 456));
			new AlertFrame(mapList, Arrays.asList(new String[] {"key"}));
		}{
			// Strings (as table cell values)
			// clicking displays toString on bottom jTextArea.
			// double clicking does nothing.
			List<Map<String,String>> mapList = new ArrayList<Map<String,String>>();
			mapList.add(Collections.singletonMap("key", "String-123"));
			mapList.add(Collections.singletonMap("key", "String-456"));			
			new AlertFrame(mapList, Arrays.asList(new String[] {"key"}));
		}{
			// beans (have beans as table cells)		
			// clicking displays toString on bottom jTextArea.
			// double clicking opens new AlertFrame (value).
			List<Map<String,Object>> mapList = new ArrayList<Map<String,Object>>();
			Map<String,Object> hmap = new HashMap<String,Object>();
			hmap.put("key", new UserBean(1, "Bob", new UserBean(0, "Manager", null)));
			hmap.put("key2", "test");
			hmap.put("key3", 123);
			mapList.add(hmap);
			hmap = new HashMap<String,Object>();
			hmap.put("key", new UserBean(1, "Ted", new UserBean(0, "Manager", null)));
			hmap.put("key2", "test");
			hmap.put("key3", 123);
			mapList.add(hmap);
			new AlertFrame(mapList, new ArrayList<String>(hmap.keySet()));
		}
	}
}

To run the app just type the following:

mvn exec:java -Dexec.mainClass=AlertFrame

Finally the pom.xml

vi pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>alertFrame</artifactId>
  <version>0.0.1-SNAPSHOT</version>
    <pluginRepositories>
      <pluginRepository>
        <id>numberformat-releases</id>
        <url>https://raw.github.com/numberformat/20130213/master/repo</url>
      </pluginRepository>
    </pluginRepositories>  
  
  <dependencies>
  	<dependency>
  		<groupId>junit</groupId>
  		<artifactId>junit</artifactId>
  		<version>4.11</version>
  	</dependency>
  	<dependency>
  		<groupId>commons-beanutils</groupId>
  		<artifactId>commons-beanutils</artifactId>
  		<version>1.8.3</version>
  	</dependency>
  </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>github.numberformat</groupId>
                <artifactId>blog-plugin</artifactId>
                <version>1.0-SNAPSHOT</version>
                <configuration>
                <gitUrl>https://github.com/numberformat/wordpress/tree/master/20131013/${artifactId}</gitUrl>
                </configuration>
            <executions>
              <execution>
                <id>1</id>
                <phase>site</phase>
                <goals>
                  <goal>generate</goal>
                </goals>
              </execution>
            </executions>
            </plugin>
        </plugins>
    </build>  
</project>
Full downloadable source for this page is available here.
16
Feb
13

Log4j2 Configuration with Multiple Web Apps

This page describes how to use the new Log4j 2 Framework within multiple web applications deployed to a single web container with a shared log4j jar.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

Background

Historically Java Web Application WAR files have included their own copy of the log4j jar files. This allowed multiple applications to co exist and use their own log4j configuration. However as web applications get smaller and focused smaller tasks (possibly RESTful applications) you will find yourself deploying multiple WAR files to the same web container. It doesnt make too much sense to have each WAR contain its own copy of log4j.jar.

Ideally it would be nice to have one log4j.jar file at the container level that each application can share. However, its not as simple as moving it out of the WAR file because of the way log4j configures itself.

This page covers a method described in the log4j 2 homepage.

Place the logging jars in the container’s classpath and use the default ClassLoaderContextSelector. Include the Log4jContextListener in each web application. Each ContextListener can be configured to share the same configuration used at the container or they can be individually configured. If status logging is set to debug in the configuration there will be output from when logging is initialized in the container and then again in each web application.

Requirements

  • Java 5 or later
  • Maven 2 or later
  • Log4j 2
  • Apache Tomcat 6

Procedure

We will create 2 apps and deploy them on Tomcat and verify that they are both logging to the console as well as individual files specified in their configs.

Any one who worked with log4j 1.x knows that this was a pain to setup with multiple web applications. Log4j 2 makes it a lot easier.

  • web-log-test – outputs to SYSOUT and ${user.home}/APPBASE/logs/web-log-test.log
  • web-log-test2 – outputs to SYSOUT and ${user.home}/APPBASE/logs/web-log-test2.log

Project Configuration

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>web-log-test</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

	<pluginRepositories>
	  <pluginRepository>
	    <id>numberformat-releases</id>
	    <url>https://raw.github.com/numberformat/20130213/master/repo</url>
	  </pluginRepository>
	</pluginRepositories>

  <dependencies>
  	<dependency>
  		<groupId>javax.servlet</groupId>
  		<artifactId>servlet-api</artifactId>
  		<version>2.5</version>
  		<scope>provided</scope>
  	</dependency>
  	<dependency>
  		<groupId>org.apache.logging.log4j</groupId>
  		<artifactId>log4j-core</artifactId>
  		<version>2.0-beta4</version>
  		<scope>provided</scope>
  	</dependency>
  	<dependency>
  		<groupId>org.apache.logging.log4j</groupId>
  		<artifactId>log4j-api</artifactId>
  		<version>2.0-beta4</version>
  		<scope>provided</scope>
  	</dependency>
  	<dependency>
  		<groupId>org.apache.logging.log4j.adapters</groupId>
  		<artifactId>log4j-web</artifactId>
  		<version>2.0-beta4</version>
  		<scope>provided</scope>
  	</dependency>
  	<dependency>
  		<groupId>org.apache.logging.log4j.adapters</groupId>
  		<artifactId>log4j-1.2-api</artifactId>
  		<version>2.0-beta4</version>
  		<scope>provided</scope>
  	</dependency>
  </dependencies>

	<build>
		<finalName>web-log-test</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.0.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
	 		<plugin>
	 			<groupId>github.numberformat</groupId>
	 			<artifactId>blog-plugin</artifactId>
	 			<version>1.0-SNAPSHOT</version>
	 			<configuration>
				<gitUrl>https://github.com/numberformat/20130216</gitUrl>
	 			</configuration>
	        <executions>
	          <execution>
	            <id>1</id>
	            <phase>site</phase>
	            <goals>
	              <goal>generate</goal>
	            </goals>
	          </execution>
	        </executions>
	 		</plugin>
		</plugins>
	</build>
</project>

Web Configuration

The following sets up the log4jContextListener by specifying the log4jConfiguration parameter pointing it to the location of your config.

You may specify a file in the classpath or an absolute location on your system. If you want to keep this file in your WEB-INF folder then you can extend the class and customize it to suit your requirement.

vi src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name>Sample Web Application</display-name>

	<context-param>
	   <param-name>log4jConfiguration</param-name>
	   <param-value>log4j2.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.apache.logging.log4j.core.web.Log4jContextListener</listener-class>
	</listener>

</web-app>

Log4j Configuration

In log4j 2 the name of the config file should be log4j2.xml. For this page our configuration file is very simple. See the log4j 2 site for further details about other configuration options.

vi src/main/resources/log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration strict="true">
<!-- include this to enable log4j internal debug messages: status="debug" -->
<appenders>
	<appender type="Console" name="STDOUT">
		<layout type="PatternLayout" pattern="%d %-5p %c - %m%n"/>
	</appender>
	<appender type="File" name="File" fileName="${sys:user.home}/APPBASE/logs/web-log-test.log">
		<layout type="PatternLayout">
			<pattern>%d %-5p %c - %m%n</pattern>
		</layout>
	</appender>
</appenders>

<loggers>
	<logger name="org.apache.jsp" level="debug">
		<appender-ref ref="File"/>
	</logger>
	<root level="trace">
		<appender-ref ref="STDOUT"/>
	</root>
</loggers>

</configuration>

Test Page

We can test the application by using a simple jsp.

vi src/main/webapp/snoop.jsp

<HTML>
<HEAD>
	<TITLE>JSP snoop page</TITLE>
	<%@ page import="org.apache.log4j.*" %>
</HEAD>
<BODY>

<H1>WebApp JSP Log4j Test</H1>

<h3>Apache Log4j Logging</h3>

<%
Logger logger = Logger.getLogger(getClass());
if("submit".equals((String)request.getParameter("log"))) {
	String text = (String)request.getParameter("text");
	String level = (String)request.getParameter("level");
	logger.log(Level.toLevel(level), text);
}
%>

<form action="" method="GET">
<TABLE border="1">
<TR valign=top>
	<TH align=left>Level</TH>
	<TH align=left>Test Message</TH>
</TR>
<TR valign=top>
	<TD>
		<select name="level">
			<option value="ERROR">ERROR</option>
			<option value="WARN">WARN</option>
			<option value="INFO" selected="selected">INFO</option>
			<option value="DEBUG">DEBUG</option>
			<option value="TRACE">TRACE</option>
		</select>
	</TD>
	<TD>
		<input name="text" type="text" size="35" value="<%=new java.util.Date()%>">
		<input name="log" type="submit" value="submit">
	</TD>
</TR>
</TABLE>
</form>

</BODY>
</HTML>

Finally copy the following files to the “tomcat6/lib” folder. You may obtain these files from the .m2 folder after the project is built. Or you can get them from the apache download site. (at the time of this writing only beta4 was the latest available)

  • log4j-core-2.0.jar – core libarary.
  • log4j-api-2.0.jar – API Shell Classes to be used by your code
  • log4j-web-2.0.jar – Servlet Listener to initialize Logging
  • log4j-1.2-api-2.0.jar – log4j 1.x support for legacy code that still uses it.

Build and Run the Application

perform the build by typing:

mvn clean compile package

You will notice that the WAR file does not contain any log4j jars.

Copy the war file into your tomcat6/webapps folder.

navigate to: http://localhost:8080/web-log-test/snoop.jsp

Modify and Deploy the Second Application

For the second application we want to modify the log4j2.xml to write the log to ${user.home}/APPBASE/logs/web-log-test2.log

Rename the WAR file to web-log-test2.war deploy to the same server.

Restart and test.

Using a new TAB, navigate to: http://localhost:8080/web-log-test2/snoop.jsp

Verify that the logs are being written the console and to each file independently.

Appendix

log4j 1.x configuration available here

Full downloadable source for this page is available here.
13
Feb
13

Create Custom Maven Plugin

This page describes how to create a custom maven plugin.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

Problem Statement

This site contains hundreds pages with small demo java projects embedded in its pages.

While working on this site I came across a few problems.

  1. How to reduce typo errors on the pages source code.
  2. How to keep the blog page up to date after code fixes.

The solution: Have a custom maven plugin generate the blog page with source code automatically. The plugin uses velocity template that contain the text of the blog page along with “#include” velocity directives that bring in the source code from the project.

Implementation

Follow these steps to create a custom plugin to generate HTML content from velocity templates.

Start by editing the pom.xml file to look like this.

vi pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>github.numberformat</groupId>
	<artifactId>blog-plugin</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>maven-plugin</packaging>

	<properties>
		<mavenVersion>2.0.6</mavenVersion>
	</properties>

	<pluginRepositories>
	  <pluginRepository>
	    <id>numberformat-releases</id>
	    <url>https://raw.github.com/numberformat/20130213/master/repo</url>
	  </pluginRepository>
	</pluginRepositories>

	<dependencies>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-plugin-api</artifactId>
			<version>2.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.velocity</groupId>
			<artifactId>velocity</artifactId>
			<version>1.7</version>
		</dependency>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-model</artifactId>
			<version>${mavenVersion}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-artifact</artifactId>
			<version>${mavenVersion}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-project</artifactId>
			<version>${mavenVersion}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-core</artifactId>
			<version>${mavenVersion}</version>
		</dependency>
	</dependencies>
	<name>Wordpress Page Generation Plugin</name>
	<description>Generates a wordpress page for the project.</description>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.0.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
	 		<plugin> 			
	 			<groupId>github.numberformat</groupId>
	 			<artifactId>blog-plugin</artifactId>
	 			<version>1.0-SNAPSHOT</version>
	 			<configuration>
				<gitUrl>https://github.com/numberformat/20130213</gitUrl>
	 			</configuration>
	        <executions>
	          <execution>
	            <id>1</id>
	            <phase>site</phase>
	            <goals>
	              <goal>generate</goal>
	            </goals>	            
	          </execution>
	        </executions>
	 		</plugin>
		</plugins>
	</build>
</project>

The following file is the plugin implementation class.

BlogMojo.java

package github.numberformat.plugin;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

/**
 * This is a simple plug-in that generates a blog page from a velocity
 * template. This allows developers to create very simple project documentation
 * pages for their projects. Developers would typically create a blog entry by
 * copying and pasting the HTML directly into the blog site.
 * 
 * @goal generate
 */
public class BlogMojo extends AbstractMojo {
	/**
	 * @parameter default-value="${basedir}"
	 * @required
	 * @readonly
	 */
	private File basedir;

	/**
	 * Represents the date first published.
	 * 
	 * @parameter default-value="https://github.com/numberformat"
	 * @required
	 * @readonly
	 */
	private String gitUrl;
	
	
	public void execute() throws MojoExecutionException {
		
		final File templateDir = basedir;
		final File targetBlog = new File(basedir, "target/blog");

		if(new File(templateDir, "src/blog/wordpress.vm").canRead()) {
			if(!targetBlog.exists()) {
				targetBlog.mkdirs();
			} else if(!targetBlog.isDirectory()) {
				throw new MojoExecutionException("Must be a directory: " + targetBlog.getAbsolutePath());
			}
	
	        VelocityEngine ve = new VelocityEngine();
	        ve.setProperty("file.resource.loader.path", templateDir.getAbsolutePath());
	        ve.init();
        
	        Template t = ve.getTemplate( "src/blog/wordpress.vm" );
	        VelocityContext context = new VelocityContext();
	        context.put("blog_header", getHeader());
	        context.put("blog_footer", getFooter());
	        context.put("blog_git_url", gitUrl);
	        
	        FileWriter writer = null;
			try {
				writer = new FileWriter(new File(targetBlog, "wordpress.html"));
		        t.merge( context, writer );				
			} catch (IOException e) {
				throw new MojoExecutionException(e.getMessage());
			} finally {
				try{writer.close();}catch(Exception e){}				
			}
		}
	}


	private Object getFooter() {
		return "<div style=\"font-size:13px;border:1px solid gray; " +
				"padding:5px;line-height:120%\">Full downloadable source for " +
				"this page is <a href=\""+gitUrl+"\">available here</a>. " +
				"</div>";
	}

	private Object getHeader() {
		return "<div style=\"font-size:13px;border:1px solid gray; " +
				"padding:5px;line-height:120%\">Full downloadable source for " +
				"this page is <a href=\""+gitUrl+"\">available here</a>. " +
				"Corrections and enhancements are welcome, fork, change and push " +
				"back to GitHub.</div>";
	}
}

Publish to Nexus or Website

You may deploy the plugin into a nexus repository a simple website.

Example Usage

For demonstration purposes I have published the plugin to the following URL, you may use it in your project by including it in your pom.xml.

vi pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>testProject</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <pluginRepositories>
    <pluginRepository>
      <id>numberformat-releases</id>
      <url>https://raw.github.com/numberformat/20130213/master/repo</url>
    </pluginRepository>
  </pluginRepositories>
  
  <build>
  	<plugins>
  		<plugin> 			
  			<groupId>github.numberformat</groupId>
  			<artifactId>blog-plugin</artifactId>
  			<version>1.0-SNAPSHOT</version>
	        <executions>
	          <execution>
	            <id>1</id>
	            <phase>site</phase>
	            <goals>
	              <goal>generate</goal>
	            </goals>	            
	          </execution>
	        </executions>
  		</plugin>
  	</plugins>
  </build>
</project>

As you can see from the above the plugin to generate the wordpress page is hooked into the “site” phase of the build lifecycle.

Velocity Template

Save the file into:

src/blog/wordpress.vm

#set( $foo = "Velocity" )
Hello $foo World!

(include sourcecode tag in square brackets around the include line below)
#include("src/main/java/App.java")
(include /sourcecode tag in square brackets around the include line above)

Run the Plugin

To run the plugin and you have specified the executions tag above just type

mvn site

As an alternative if you don’t want to hook it into the maven lifecycle then just delete the “executions” tag above and run the plugin by typing:

To run the plugin just type

mvn blog:generate

If you get a WARNING about plexus ignore it.

Upon successful build you can view the generated wordpress page in the target/blog folder of the project.

Next Steps

blog-plugin improvements:

  1. Enhance the plugin to generate pages in formats other than wordpress.
  2. Have plugin insert headers or footers on the generated pages. (done)
  3. Enhance the plugin to have blog pages contain a link to GitHub where visitors can simply checkout the project instead of copying and pasting source from the page. (done)
Full downloadable source for this page is available here.
09
Feb
13

Convert UTF-8 Unicode to ASCII Latin 1

This page describes how to convert utf-8 or Unicode strings with diacritical characters or Unicode punctuation marks into Latin 1 encoding with minimal loss of information.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

There is a 2 step process to get this done.

Step 1

Use java.text.Normalizer to convert diacritical characters with accents into ASCII characters.

str = Normalizer.normalize(str, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");

Step 2

Convert Symbols and punctuation marks into latin-1 equivalents.

Currently the best way to get this done is to search and replace as seen below.

Sample Swing App

The following app demonstrates how to convert Unicode to ASCII latin-1.

vi src/main/java/github/numberformat/utf/Norm.java

package github.numberformat.utf;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Label;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.Normalizer;
import java.util.HashMap;
import java.util.Iterator;

import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class Norm extends JFrame {
	private static final long serialVersionUID = 1L;

	private JComboBox normalizationTemplate;
	private JComboBox formComboBox;
	private JComponent paintingComponent;
	private HashMap<String, Normalizer.Form> formValues = new HashMap<String, Normalizer.Form>();
	private HashMap<String, String> templateValues = new HashMap<String, String>();

	public Norm() {
		init();
	}
	
	public void init() {
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setTitle("UTF-8 to ASCII");
		
		formValues.put("NFC", Normalizer.Form.NFC);
		formValues.put("NFD", Normalizer.Form.NFD);
		formValues.put("NFKC", Normalizer.Form.NFKC);
		formValues.put("NFKD", Normalizer.Form.NFKD);
		
		formComboBox = new JComboBox();
		for (Iterator it = formValues.keySet().iterator(); it.hasNext();) {
			formComboBox.addItem((String) it.next());
		}
		templateValues.put("acute accent", "\u2039touch" + "\u00e9\u2035");

		// text with ligature
		templateValues.put("ligature", "a" + "\ufb03" + "ance");

		// text with the cedilla
		templateValues.put("cedilla", "fa" + "\u00e7" + "ade");

		
		templateValues.put("half-width katakana",
				"\uff81\uff6e\uff7a\uff9a\uff70\uff84");

		normalizationTemplate = new JComboBox();

		for (Iterator it = templateValues.keySet().iterator(); it.hasNext();) {
			normalizationTemplate.addItem((String) it.next());
		}
		
		JPanel controls = new JPanel();

		controls.setLayout(new BoxLayout(controls, BoxLayout.X_AXIS));
		controls.add(new Label("Normalization Form: "));
		controls.add(formComboBox);
		controls.add(new Label("Normalization Template:"));
		controls.add(normalizationTemplate);
		formComboBox.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				paintingComponent.repaint();
			}
		});

		normalizationTemplate.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				paintingComponent.repaint();
			}
		});

		
		getContentPane().add(getCenter(), BorderLayout.CENTER);
		getContentPane().add(controls, BorderLayout.SOUTH);
		pack();
		setVisible(true);

	}

	private JComponent getCenter() {
		if(paintingComponent != null) return paintingComponent;
		
		paintingComponent = new JComponent() {
			static final long serialVersionUID = -3725620407788489160L;

			public Dimension getSize() {
				return new Dimension(550, 200);
			}

			public Dimension getPreferredSize() {
				return new Dimension(550, 200);
			}

			public Dimension getMinimumSize() {
				return new Dimension(550, 200);
			}

			public void paint(Graphics g) {
				Graphics2D g2 = (Graphics2D) g;

				g2.setFont(new Font("Serif", Font.PLAIN, 20));
				g2.setColor(Color.BLACK);
				g2.drawString("Original string:", 100, 80);
				g2.drawString("Normalized string:", 100, 120);
				g2.setFont(new Font("Serif", Font.BOLD, 24));

				// output of the original sample selected from the ComboBox

				String original_string = templateValues
						.get(normalizationTemplate.getSelectedItem());
				g2.drawString(original_string, 320, 80);

				// normalization and output of the normalized string

				String normalized_string = utf8ToLatin1(original_string);

				g2.drawString(normalized_string, 320, 120);
			}

			private String utf8ToLatin1(String original_string) {
				String normalized_string;
				java.text.Normalizer.Form currentForm = formValues
						.get(formComboBox.getSelectedItem());
				normalized_string = Normalizer.normalize(original_string,
						currentForm);

				normalized_string = normalized_string.replaceAll(
						"\\p{InCombiningDiacriticalMarks}+", "");

				String str = normalized_string;
				str = str
						.replaceAll(
								"[\u00AB\u2034\u2037\u00BB\u02BA\u030B\u030E\u201C\u201D\u201E\u201F\u2033\u2036\u3003\u301D\u301E]",
								"\"");
				str = str.replaceAll("[\u02CB\u0300\u2035]", "`");
				str = str.replaceAll("[\u02C4\u02C6\u0302\u2038\u2303]", "^");
				str = str.replaceAll("[\u02CD\u0331\u0332\u2017]", "_");
				str = str.replaceAll(
						"[\u00AD\u2010\u2011\u2012\u2013\u2014\u2212\u2015]",
						"-");
				str = str.replaceAll("[\u201A]", ",");
				str = str.replaceAll("[\u0589\u05C3\u2236]", ":");
				str = str.replaceAll("[\u01C3\u2762]", "!");
				str = str.replaceAll("[\u203D]", "?");
				str = str
						.replaceAll(
								"[\u00B4\u02B9\u02BC\u02C8\u0301\u200B\u2018\u2019\u201B\u2032]",
								"'");
				str = str.replaceAll("[\u27E6]", "[");
				str = str.replaceAll("[\u301B]", "]");
				str = str.replaceAll("[\u2983]", "{");
				str = str.replaceAll("[\u2984]", "}");
				str = str.replaceAll("[\u066D\u204E\u2217\u2731]", "*");
				str = str.replaceAll("[\u00F7\u0338\u2044\u2060\u2215]", "/");
				str = str.replaceAll("[\u20E5\u2216]", "\\");
				str = str.replaceAll("[\u266F]", "#");
				str = str.replaceAll("[\u066A\u2052]", "%");
				str = str.replaceAll("[\u2039\u2329\u27E8\u3008]", "<");
				str = str.replaceAll("[\u203A\u232A\u27E9\u3009]", ">");
				str = str.replaceAll("[\u01C0\u05C0\u2223\u2758]", "|");
				str = str.replaceAll("[\u02DC\u0303\u2053\u223C\u301C]", "~");
				normalized_string = str;
				return normalized_string;
			}
		}; 
				
				
		return paintingComponent;
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		new Norm();
	}

}

To run the app just type the following:

mvn exec:java -Dexec.mainClass=github.numberformat.utf.Norm
Full downloadable source for this page is available here.
18
Jun
12

Blank GWT Template Starter Application

This page describes the complete end-to-end process of creating and testing a blank “Hello World” type Google Web Tool kit (GWT) starter application using Maven. The page takes about 10-15 minutes to complete and have a working GWT application.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

The application described on this page displays a Send Button on the page. It displays a JavaScript alert() message when the button is clicked.

Background

The GWT SDK allows you to generate an application using their generation tool. However I never liked using this tool because the application it generated is useless to me unless I understand how the application is working. The following page breaks down a simple Hello World GWT application step by step and allows the reader to follow along. Once the application is complete the user can import it into eclipse and use the GWT tool to modify the application using the Screen Design Tools.

Requirements

  • Maven
  • M2 Eclipse plugin
  • Eclipse GWT plugin

This page covers GWT version 2.4.0.

First step is to create a simple maven project in eclipse.

Modify the pom.xml to look like this…

vi pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.test</groupId>
	<artifactId>gwt-hello</artifactId>
	<packaging>war</packaging>
	<version>20140429</version>

	<pluginRepositories>
		<pluginRepository> <!-- Ignore this repository. Its only used for document publication. -->
			<id>numberformat-releases</id>
			<url>https://raw.github.com/numberformat/20130213/master/repo</url>
		</pluginRepository>
	</pluginRepositories>

	<properties>
		<!-- Convenience property to set the GWT version -->
		<gwtVersion>2.4.0</gwtVersion>
		<!-- GWT needs at least java 1.5 -->
		<webappDirectory>${project.build.directory}/${project.build.finalName}</webappDirectory>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>com.google.gwt</groupId>
			<artifactId>gwt-servlet</artifactId>
			<version>${gwtVersion}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>com.google.gwt</groupId>
			<artifactId>gwt-user</artifactId>
			<version>${gwtVersion}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.validation</groupId>
			<artifactId>validation-api</artifactId>
			<version>1.0.0.GA</version>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.7</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<!-- Generate compiled stuff in the folder used for developing mode -->
		<outputDirectory>${webappDirectory}/WEB-INF/classes</outputDirectory>
		<plugins>
			<!-- GWT Maven Plugin -->
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>gwt-maven-plugin</artifactId>
				<version>2.4.0</version>
				<executions>
					<execution>
						<goals>
							<goal>compile</goal>
							<goal>test</goal>
							<goal>generateAsync</goal>
						</goals>
					</execution>
				</executions>
				<!-- Plugin configuration. There are many available options, see gwt-maven-plugin 
					documentation at codehaus.org -->
				<configuration>
					<runTarget>Matrix.html</runTarget>
					<hostedWebapp>${webappDirectory}</hostedWebapp>
				</configuration>
			</plugin>

			<!-- Copy static web files before executing gwt:run -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>2.1.1</version>
				<executions>
					<execution>
						<phase>compile</phase>
						<goals>
							<goal>exploded</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<webappDirectory>${webappDirectory}</webappDirectory>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			
			<plugin> <!-- Ignore this plugin. Its only used for document publication. -->
				<groupId>github.numberformat</groupId>
				<artifactId>blog-plugin</artifactId>
				<version>1.0-SNAPSHOT</version>
				<configuration>
					<gitUrl>https://github.com/numberformat/wordpress/tree/master/${project.version}/${project.artifactId}</gitUrl>
				</configuration>
				<executions>
					<execution>
						<id>1</id>
						<phase>site</phase>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
					
		</plugins>
		<pluginManagement>
			<plugins>
				<!--This plugin's configuration is used to store Eclipse m2e settings 
					only. It has no influence on the Maven build itself. -->
				<plugin>
					<groupId>org.eclipse.m2e</groupId>
					<artifactId>lifecycle-mapping</artifactId>
					<version>1.0.0</version>
					<configuration>
						<lifecycleMappingMetadata>
							<pluginExecutions>
								<pluginExecution>
									<pluginExecutionFilter>
										<groupId>
											org.apache.maven.plugins
										</groupId>
										<artifactId>
											maven-war-plugin
										</artifactId>
										<versionRange>
											[2.1.1,)
										</versionRange>
										<goals>
											<goal>exploded</goal>
										</goals>
									</pluginExecutionFilter>
									<action>
										<ignore></ignore>
									</action>
								</pluginExecution>
								<pluginExecution>
									<pluginExecutionFilter>
										<groupId>org.codehaus.mojo</groupId>
										<artifactId>
											gwt-maven-plugin
										</artifactId>
										<versionRange>
											[2.4.0,)
										</versionRange>
										<goals>
											<goal>generateAsync</goal>
										</goals>
									</pluginExecutionFilter>
									<action>
										<ignore></ignore>
									</action>
								</pluginExecution>
							</pluginExecutions>
						</lifecycleMappingMetadata>
					</configuration>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>

</project>

Host HTML Page

The following is the Host HTML Page. The page imports the generated Javascript and starts the Javascript application. Similar to the “Entry Point” main() method of many other programming languages.

vi src/main/webapp/Matrix.html

<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <link type="text/css" rel="stylesheet" href="Matrix.css">
<script language="javascript" src="test.Matrix/test.Matrix.nocache.js"></script>
  </head>
  <body>
    <noscript>
      <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
        Your web browser must have JavaScript enabled
        in order for this application to display correctly.
      </div>
    </noscript>
    <div id="sendButtonContainer"></div>
  </body>
</html>

CSS Styles

GWT components are highly customizable. It makes sense to define sizes, colors, alighment, images, and other visual aspects of the component in CSS.

vi src/main/webapp/Matrix.css

.sendButton {
  display: block;
  font-size: 12pt;
}

Web Application Descriptor

The following is a basic web.xml file for the application.

vi src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
 
<web-app>
 
  <!-- Servlets -->
  <!-- Servlet-Mapping -->
 
  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>Matrix.html</welcome-file>
  </welcome-file-list>
</web-app>

vi src/main/java/com/test/Matrix.gwt.xml

<module>
  <inherits name='com.google.gwt.user.User' />
  <inherits name='com.google.gwt.user.theme.standard.Standard' />
  <entry-point class='test.client.Matrix' />
</module>

vi src/main/java/test/client/Matrix.java

package test.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.RootPanel;
 
public class Matrix implements EntryPoint {
 
    public void onModuleLoad() {
        Button button = new Button("Send", new ClickHandler() {
            public void onClick(ClickEvent event) {
                Window.alert("Hello World!");
            }
        });
        button.setStyleName("sendButton");
        RootPanel.get("sendButtonContainer").add(button);
    }
}

Run the project

mvn clean compile gwt:run

Click on Launch Default Browser button. You should see a page with a button on the top left.

Import into Eclipse and Edit in Design View

The following procedure allows you to open the screen above in the “GWT Design View”.

  1. Right click -> Properties -> Google -> Web Toolkit.
  2. Add the Entry point Module Matrix to the list if not already there by clicking on the “Add” button.
  3. Right click on the Matrix.java File and choose Open With -> WindowBuilder Editor.
  4. The source file will open and allow you to click on the Design Tab.
  5. Change the button name to Send2.

Test the change by typing:

mvn clean compile gwt:run

Might need to click on Launch Default Browser button twice. You should see a page with a button titled “Send2” on the top left.

What’s Next?




Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 75 other followers

April 2017
S M T W T F S
« Mar    
 1
2345678
9101112131415
16171819202122
23242526272829
30  

Blog Stats

  • 806,836 hits