quarta-feira, 3 de junho de 2009

String closures.

Since java has no closures, programming callbacks is a pain. Making a interface, using the interface in the function, implementing the interface and finally use the damn method. While java doesn't get interface injection, or whatever it is called when the callee defines the interface and callers automagically verify the type, there is always reflection.

I did a few utility classes to call methods reflectively for the main closure types in java: Action, Listener, Runnable and Swingworker. Of these the interesting case is SwingWorker that has two important methods. Worked around by saving the result of the first and passing it to the second.

Then i do code like this:
DynamicSwingWorker.createWithFinished(this,"swingWorkerReadFinished","swingWorkerRead",uri).start();

/**
* Returns if it had success or not
*/
public Boolean swingWorkerRead(URL uri) {
try {
Integer index = getBookMarks().getIndex(uri);
pane.read(uri, index);
return true;
} catch (IOException ex) {
getBookMarks().remove(uri);
return false;
} catch (Exception e){
return false;
}
}

/**
* Receives if had success
*/
public void swingWorkerReadFinished(Boolean success) {
unBlockView(success);
}

Easy.

quarta-feira, 22 de abril de 2009

Yahoo image search

Did a experiment in using the yahoo image search for my app, but it's very raw, yet, since :
1) It only allows filtering by size on the end of the the search,
2) My image list (bellow) is only prepared for one request at a time, not buffered requests.
3) additional search parameters like the selective "cover" just appear to make the search fail. Google image search behaves better, but has a API that only allows javascript (what is - everything - a fucking web app now?) and uses the developer key ( instead of ip) for the limits, and that is monumentally useless for a non-web app.

Besides it returns negatives left and right. There are other, more selective webservices like Library Thing but they also have a one developer key 1000 requests thing going. Pure stupidity. A developer makes an app, users don't use the same app (except in server applications).

Screw the cloud.

Also need a way to cancel asynchronous computations. The ChainRunnable interface already allows that, but when to invoke is the question.

segunda-feira, 20 de abril de 2009

Creating a Scalable thread pool

I already used the thread pool infrastructure around Executors before, but there is something missing from the Executors static factory class.

A scaling thread poll that discards threads after a timeout, but still has a maximum thread number (and puts additional tasks into a queue). This is the synthesis of two factory methods, newCachedThreadPool (as needed, reuse, 0 to +infinite threads, kills after timeout) and newFixedThreadPool (as needed, reuse, upper and lower thread limit).
I even bothered people on the java concurrency mailing list but, since it was quite complex to do myself, and their response was "would be nice to have in core" variant, so i passed over it.
Until i found this blog post, that describes exactly what i wanted and how
to do it:

http://www.kimchy.org/juc-executorservice-gotcha/

terça-feira, 7 de abril de 2009

Jlist is ugly.

Edit: I edited this to allow mutable listmodels now.

For about a week now I've been disemboweling java swing to make a prettier and performant image list. I think i have succeed finally.
The idea is to publish a interface that gives everything that a user needs to add images that horizontally wrap to a centered jlist, asynchronously or not, without having the danger of memory leaks (by itself).
This can be achieved by:
  1. Creating a custom JViewportLayoutManager (centered)
  2. Creating a custom JList or JList configuration class.
  3. Creating a custom ListCellRenderer (that calls the interface functions)
  4. Instead of making the interface image function return immediately (forcing your interface to be synchronous) make it return by calling another function (in the JList configuration class). This also serves as a kind of locking so that a loading image work is not repeated, and repaints after loading.
  5. On the listcellrenderer, dispose of images that are not visible atm.
The viewport class, there are bugs with positioning and color if used alone unfortunatly:
package util.swing.layouts;


import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.LayoutManager;
import java.awt.Point;
import javax.swing.JList;
import javax.swing.JViewport;
import javax.swing.Scrollable;

/**
* A viewport layout that tries to horizontally center a component in
* the viewport. You might have to disable scrollRectToVisible in the
* view class used unfortunatly, since the only way i found of centering
* CellRenderers in a viewport is using a negative start position (upper left)
* since the view always scrolls to the cell from 0 not from the negative start position.
* @author bio-aulas
*/
public class CenteredViewPortLayout implements LayoutManager {

public void addLayoutComponent(String name, Component comp) {/*nop*/}

public void removeLayoutComponent(Component comp) {/*nop*/}

public Dimension maximumLayoutSize(Container target) {
if (target == null) {
return new Dimension(0, 0);
}
return target.getPreferredSize();
}

public Dimension preferredLayoutSize(Container parent) {
Component view = ((JViewport) parent).getView();
if (view == null) {
return new Dimension(0, 0);
} else if (view instanceof Scrollable) {
return ((Scrollable) view).getPreferredScrollableViewportSize();
} else {
return view.getPreferredSize();
}
}

public Dimension minimumLayoutSize(Container parent) {
return new Dimension(4, 4);
}

public void layoutContainer(Container parent) {
JViewport vp = (JViewport) parent;
Component view = vp.getView();
if (view == null) {
return;
}
/**
* All of the dimensions below are in view coordinates.
*/
Dimension newViewSize = view.getPreferredSize();
Dimension maximumViewSize = vp.toViewCoordinates(vp.getSize());
/**
* If a JList the prefered size is the prefered size of the
* sum of cells fitting in a row.
* Also, the limit is the number of cells.
*/
if (view instanceof JList) {
int fixedCellWidth = ((JList) view).getFixedCellWidth();
int expandLimit = ((JList) view).getModel().getSize();
if (fixedCellWidth != -1) {
newViewSize.width = fixedCellWidth;
}
/**
* Multiply the cell width until it matches the
* n * viewPreferredSize.width + z = viewPort.width
* for a z < preferredSize and n <= expandLimit */ int n = 0; while (((n + 1) * newViewSize.width) <= maximumViewSize.width && (n + 1) <= expandLimit) { n++; } newViewSize.width = n * newViewSize.width; } //fill to maximum size if it doesn't fit or nothing there if (newViewSize.width == 0) { newViewSize.width = maximumViewSize.width; } /**_________ __________ * |X|XYX|Y| -> |X|YYYY|X|
* |X|XYX|Y| |X|YYYY|X|
*/
Point viewPosition = vp.getViewPosition();
int centerJustifiedX = (maximumViewSize.width - newViewSize.width) / 2;
viewPosition.x = -Math.max(0, centerJustifiedX);
vp.setViewPosition(viewPosition);
vp.setViewSize(newViewSize);
}
}

The Jlist wrapper:
package ui;

import java.awt.Component;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;

import util.swing.layouts.CenteredViewPortLayout;

public class ImageList {

private final JScrollPane pane = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
private final LazyImageListUpdate update;
private boolean cacheImages = false;
//Only use this cache on the EDT. Even if synchronized it would
//throw concurrent modification exceptions because iterators are used.
private final Map memoryCache = new HashMap();

public final void returnImage(final Object valueAsKey, final Image image) {

//if not null signal repaint...
if (image != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
//if null loading disabled.
memoryCache.put(valueAsKey, image);
repaintIfIndexVisible(valueAsKey);
}
});
}
}

private void repaintIfIndexVisible(Object obj) {
JList l = (JList) ((JComponent) pane.getComponent(0)).getComponent(0);
ListModel m = l.getModel();
int first = l.getFirstVisibleIndex();
int last = l.getLastVisibleIndex();
if (first > -1) {
for (int i = first; i <= last; i++) {
if (m.getElementAt(i).equals(obj)) {
pane.repaint();
}
}
}
}

/**
* As images are a heavyheight object ImageList has a lazy
* strategy to create and dispose them after uze. To implement
* this optimally, the getCellImage method should offload image
* creation to another thread - images are returned by calling
* ImageList.returnImage(Object value, Image). If you don't call it,
* images are assumed not to exist for the index, as if you call it
* with a null image.
*/
public interface LazyImageListUpdate {

/**
* Given a list object value and desired width and height give a
* appropriate image. This method has to call returnImage(Object value, Image)
* after the image is loaded (can be called in another thread) and only
* after the image is loaded. If you offload image loading to a thread
* call returnImage in the thread after loading.
* If you don't want to offload image loading read the image and then call
* returnImage.
*
* @param obj not null
* @param imageWidth
* @param imageHeight
*/
public void getCellImage(ImageList list, Object obj, int imageWidth, int imageHeight);

/**
* Given a list object and desired width and height give a
* appropriate cell text.
* @param obj not null
* @return cell text
*/
public String getCellText(ImageList list, Object obj);

/**
* Given a list object and desired width and height give a
* appropriate tooltip text.
* @param obj not null
* @return cell tooltip text
*/
public abstract String getTooltipText(ImageList list, Object obj);
}

public JComponent getView() {
return pane;
}

public ImageList(int cellWidth, int cellHeight, LazyImageListUpdate update, boolean cacheImagesInMemory) {
super();
this.update = update;
init(new WrappedList(), cellWidth, cellHeight, cacheImagesInMemory);
}

public ImageList(int cellWidth, int cellHeight, LazyImageListUpdate update, boolean cacheImagesInMemory, Object... listData) {
super();
this.update = update;
init(new WrappedList(listData), cellWidth, cellHeight, cacheImagesInMemory);
}

public ImageList(int cellWidth, int cellHeight, LazyImageListUpdate update, boolean cacheImagesInMemory, ListModel dataModel) {
super();
this.update = update;
init(new WrappedList(dataModel), cellWidth, cellHeight, cacheImagesInMemory);
}

private void init(JList list, int cellWidth, int cellHeight, boolean cacheImagesInMemory) {
cacheImages = cacheImagesInMemory;
//for text (? assume 30) and borders (20*2)
int imageHeight = Math.max(0, cellHeight - 70);
int imageWidth = Math.max(0, cellWidth - 40);
ImageFileListCellRenderer renderer = new ImageFileListCellRenderer(imageWidth, imageHeight);
list.setCellRenderer(renderer);
list.setFixedCellWidth(cellWidth);
list.setFixedCellHeight(cellHeight);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(-1);
//some laf listui lie and paint a component color
//different than reported. To minimize this make
//the list not opaque and paint bg on the viewport
//(the color might still be different from a normal list,
//but at least its not different from the parent bg)
list.setOpaque(false);
pane.getViewport().setBackground(list.getBackground());
pane.getViewport().setLayout(new CenteredViewPortLayout());
pane.getViewport().add(list);
}

/**
* Needed to disable scrollRectToVisible that doesn't work with the custom
* layout.
*/
private class WrappedList extends JList {

public WrappedList() {
super();
}

public WrappedList(Object[] listData) {
super(listData);
}

public WrappedList(ListModel dataModel) {
super(dataModel);
}

@Override
public void scrollRectToVisible(Rectangle aRect) {
//Do nothing to avoid a positioning bug with centered layout
}

@Override
public String getToolTipText(MouseEvent event) {
Point point = event.getPoint();
int index = this.locationToIndex(point);
//Get the value of the item in the list
if (index < 0) {
return null;
}
return update.getTooltipText(ImageList.this, getModel().getElementAt(index));
}
}

private class ImageFileListCellRenderer extends JLabel implements ListCellRenderer {
private List tmp = new ArrayList();
private int width, height;

public ImageFileListCellRenderer(int width, int height) {
super();
this.width = width;
this.height = height;
setVerticalAlignment(SwingConstants.CENTER);
setHorizontalAlignment(SwingConstants.CENTER);
setVerticalTextPosition(JLabel.BOTTOM);
setHorizontalTextPosition(JLabel.CENTER);
setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
}

@Override
public Component getListCellRendererComponent(JList list, Object value, final int index, boolean isSelected, boolean cellHasFocus) {
setText(update.getCellText(ImageList.this, value));
//gc the old images...
if (!cacheImages) {
disposeNonVisibleImages(list);
}

Image img = memoryCache.get(value);
//null was put there. No image in cache...yet
if (img == null && memoryCache.containsKey(value)) {
setIcon(null);
return this;
}
if (img == null) {
//return null until image is not null
memoryCache.put(value, null);
//should call repaint later
update.getCellImage(ImageList.this, value, width, height);
setIcon(null);
return this;
}

setIcon(new ImageIcon(img));
return this;
}

@Override
public void validate() {
}

@Override
public void invalidate() {
}

@Override
public void repaint() {
}

@Override
public void revalidate() {
}

@Override
public void repaint(long tm, int x, int y, int width, int height) {
}

@Override
public void repaint(Rectangle r) {
}

private void disposeNonVisibleImages(JList list) {
int firstIndex = list.getFirstVisibleIndex();
int lastIndex = list.getLastVisibleIndex();
//A direct index mapping wouldn't work since listmodel can be mutable
ListModel m = list.getModel();
for (int i = firstIndex; i <= lastIndex; i++) {
Object key = m.getElementAt(i);
if(memoryCache.containsKey(key)){
tmp.add(key);
tmp.add(memoryCache.remove(key));
}
}
Iterator values = memoryCache.values().iterator();
while (values.hasNext()) {
Image disposable = values.next();
if (disposable != null) {
disposable.getGraphics().dispose();
values.remove();
}
}
Iterator savedImgs = tmp.iterator();
while (savedImgs.hasNext()) {
Object key = savedImgs.next();
savedImgs.remove();
Image saved = (Image) savedImgs.next();
savedImgs.remove();
memoryCache.put(key, saved);
}

}
}
}


This is enough, if you implement LazyImageListUpdate correctly to make a pretty and performant JList without memory leaks - if you set cacheImagesInMemory to false in the constructor - thinking about removing that parameter . I tested mine today, but since it needs a lot of libraries, and many other classes, i'm just posting a
LazyImageListUpdate example. If you really want to run it get a recent svn from my bookjar project, open it in netbeans, modify the path to go to some rar or zip files with images and run the JlistTest file.

My example implementation, prepared to work on a ImageList that receives a File ListModel, using a few utility Chain-of-responsability classes that i made:
    private class LazyUpdate implements ImageList.LazyImageListUpdate {

ChainExecutorService ex = newFixedThreadPool(1);

@Override
public void getCellImage(final ImageList list, final Object obj, int imageWidth, int imageHeight) {

URI fileUri = ((File) obj).toURI();
try {
ChainRunnable disjuntive, firstAttempt, secondAttempt, returnLink;
returnLink = new ChainRunnable(){
@Override
protected BufferedImage runLink(BufferedImage arg) throws Throwable {
list.returnImage(obj, arg);
return arg;
}
};

firstAttempt = createChain(new ReadImageFromCacheAsJpg(fileUri),
returnLink);

secondAttempt = createChain(new ReadImageFromArchive(fileUri),
new ScaleImage(imageWidth, imageHeight),
new WriteImageToCacheAsJpg(fileUri),
returnLink);

disjuntive = createDisjuntiveChain(firstAttempt, secondAttempt);
ex.submit(disjuntive);

} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public String getCellText(ImageList list, Object obj) {
return ((File) obj).getName();
}

@Override
public String getTooltipText(ImageList list, Object obj) {
return ((File) obj).getName();
}
}



And a image of how the test looks (with a glazedlists textfield filter of course).