diff options
author | Artur Signell <artur@vaadin.com> | 2012-08-13 18:34:33 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2012-08-13 19:18:33 +0300 |
commit | e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch) | |
tree | 9ab6f13f7188cab44bbd979b1cf620f15328a03f /server | |
parent | 14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff) | |
download | vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip |
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server')
408 files changed, 107042 insertions, 0 deletions
diff --git a/server/src/com/vaadin/Application.java b/server/src/com/vaadin/Application.java new file mode 100644 index 0000000000..1d31410185 --- /dev/null +++ b/server/src/com/vaadin/Application.java @@ -0,0 +1,2426 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.SocketException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.vaadin.annotations.EagerInit; +import com.vaadin.annotations.Theme; +import com.vaadin.annotations.Widgetset; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterFactory; +import com.vaadin.data.util.converter.DefaultConverterFactory; +import com.vaadin.event.EventRouter; +import com.vaadin.service.ApplicationContext; +import com.vaadin.terminal.AbstractErrorMessage; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedRequest.BrowserDetails; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; +import com.vaadin.terminal.gwt.server.BootstrapFragmentResponse; +import com.vaadin.terminal.gwt.server.BootstrapListener; +import com.vaadin.terminal.gwt.server.BootstrapPageResponse; +import com.vaadin.terminal.gwt.server.BootstrapResponse; +import com.vaadin.terminal.gwt.server.ChangeVariablesErrorEvent; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.terminal.gwt.server.WebApplicationContext; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.AbstractField; +import com.vaadin.ui.Root; +import com.vaadin.ui.Table; +import com.vaadin.ui.Window; + +/** + * <p> + * Base class required for all Vaadin applications. This class provides all the + * basic services required by Vaadin. These services allow external discovery + * and manipulation of the user, {@link com.vaadin.ui.Window windows} and + * themes, and starting and stopping the application. + * </p> + * + * <p> + * As mentioned, all Vaadin applications must inherit this class. However, this + * is almost all of what one needs to do to create a fully functional + * application. The only thing a class inheriting the <code>Application</code> + * needs to do is implement the <code>init</code> method where it creates the + * windows it needs to perform its function. Note that all applications must + * have at least one window: the main window. The first unnamed window + * constructed by an application automatically becomes the main window which + * behaves just like other windows with one exception: when accessing windows + * using URLs the main window corresponds to the application URL whereas other + * windows correspond to a URL gotten by catenating the window's name to the + * application URL. + * </p> + * + * <p> + * See the class <code>com.vaadin.demo.HelloWorld</code> for a simple example of + * a fully working application. + * </p> + * + * <p> + * <strong>Window access.</strong> <code>Application</code> provides methods to + * list, add and remove the windows it contains. + * </p> + * + * <p> + * <strong>Execution control.</strong> This class includes method to start and + * finish the execution of the application. Being finished means basically that + * no windows will be available from the application anymore. + * </p> + * + * <p> + * <strong>Theme selection.</strong> The theme selection process allows a theme + * to be specified at three different levels. When a window's theme needs to be + * found out, the window itself is queried for a preferred theme. If the window + * does not prefer a specific theme, the application containing the window is + * queried. If neither the application prefers a theme, the default theme for + * the {@link com.vaadin.terminal.Terminal terminal} is used. The terminal + * always defines a default theme. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Application implements Terminal.ErrorListener, Serializable { + + /** + * The name of the parameter that is by default used in e.g. web.xml to + * define the name of the default {@link Root} class. + */ + public static final String ROOT_PARAMETER = "root"; + + private static final Method BOOTSTRAP_FRAGMENT_METHOD = ReflectTools + .findMethod(BootstrapListener.class, "modifyBootstrapFragment", + BootstrapFragmentResponse.class); + private static final Method BOOTSTRAP_PAGE_METHOD = ReflectTools + .findMethod(BootstrapListener.class, "modifyBootstrapPage", + BootstrapPageResponse.class); + + /** + * A special application designed to help migrating applications from Vaadin + * 6 to Vaadin 7. The legacy application supports setting a main window, + * adding additional browser level windows and defining the theme for the + * entire application. + * + * @deprecated This class is only intended to ease migration and should not + * be used for new projects. + * + * @since 7.0 + */ + @Deprecated + public static class LegacyApplication extends Application { + /** + * Ignore initial / and then get everything up to the next / + */ + private static final Pattern WINDOW_NAME_PATTERN = Pattern + .compile("^/?([^/]+).*"); + + private Root.LegacyWindow mainWindow; + private String theme; + + private Map<String, Root.LegacyWindow> legacyRootNames = new HashMap<String, Root.LegacyWindow>(); + + /** + * Sets the main window of this application. Setting window as a main + * window of this application also adds the window to this application. + * + * @param mainWindow + * the root to set as the default window + */ + public void setMainWindow(Root.LegacyWindow mainWindow) { + if (this.mainWindow != null) { + throw new IllegalStateException( + "mainWindow has already been set"); + } + if (mainWindow.getApplication() == null) { + mainWindow.setApplication(this); + } else if (mainWindow.getApplication() != this) { + throw new IllegalStateException( + "mainWindow is attached to another application"); + } + this.mainWindow = mainWindow; + } + + /** + * Gets the mainWindow of the application. + * + * <p> + * The main window is the window attached to the application URL ( + * {@link #getURL()}) and thus which is show by default to the user. + * </p> + * <p> + * Note that each application must have at least one main window. + * </p> + * + * @return the root used as the default window + */ + public Root.LegacyWindow getMainWindow() { + return mainWindow; + } + + /** + * This implementation simulates the way of finding a window for a + * request by extracting a window name from the requested path and + * passes that name to {@link #getWindow(String)}. + * + * {@inheritDoc} + * + * @see #getWindow(String) + * @see Application#getRoot(WrappedRequest) + */ + + @Override + public Root.LegacyWindow getRoot(WrappedRequest request) { + String pathInfo = request.getRequestPathInfo(); + String name = null; + if (pathInfo != null && pathInfo.length() > 0) { + Matcher matcher = WINDOW_NAME_PATTERN.matcher(pathInfo); + if (matcher.matches()) { + // Skip the initial slash + name = matcher.group(1); + } + } + Root.LegacyWindow window = getWindow(name); + if (window != null) { + return window; + } + return mainWindow; + } + + /** + * Sets the application's theme. + * <p> + * Note that this theme can be overridden for a specific root with + * {@link Application#getThemeForRoot(Root)}. Setting theme to be + * <code>null</code> selects the default theme. For the available theme + * names, see the contents of the VAADIN/themes directory. + * </p> + * + * @param theme + * the new theme for this application. + */ + public void setTheme(String theme) { + this.theme = theme; + } + + /** + * Gets the application's theme. The application's theme is the default + * theme used by all the roots for which a theme is not explicitly + * defined. If the application theme is not explicitly set, + * <code>null</code> is returned. + * + * @return the name of the application's theme. + */ + public String getTheme() { + return theme; + } + + /** + * This implementation returns the theme that has been set using + * {@link #setTheme(String)} + * <p> + * {@inheritDoc} + */ + + @Override + public String getThemeForRoot(Root root) { + return theme; + } + + /** + * <p> + * Gets a root by name. Returns <code>null</code> if the application is + * not running or it does not contain a window corresponding to the + * name. + * </p> + * + * @param name + * the name of the requested window + * @return a root corresponding to the name, or <code>null</code> to use + * the default window + */ + public Root.LegacyWindow getWindow(String name) { + return legacyRootNames.get(name); + } + + /** + * Counter to get unique names for windows with no explicit name + */ + private int namelessRootIndex = 0; + + /** + * Adds a new browser level window to this application. Please note that + * Root doesn't have a name that is used in the URL - to add a named + * window you should instead use {@link #addWindow(Root, String)} + * + * @param root + * the root window to add to the application + * @return returns the name that has been assigned to the window + * + * @see #addWindow(Root, String) + */ + public void addWindow(Root.LegacyWindow root) { + if (root.getName() == null) { + String name = Integer.toString(namelessRootIndex++); + root.setName(name); + } + + legacyRootNames.put(root.getName(), root); + root.setApplication(this); + } + + /** + * Removes the specified window from the application. This also removes + * all name mappings for the window (see + * {@link #addWindow(Root, String) and #getWindowName(Root)}. + * + * <p> + * Note that removing window from the application does not close the + * browser window - the window is only removed from the server-side. + * </p> + * + * @param root + * the root to remove + */ + public void removeWindow(Root.LegacyWindow root) { + for (Entry<String, Root.LegacyWindow> entry : legacyRootNames + .entrySet()) { + if (entry.getValue() == root) { + legacyRootNames.remove(entry.getKey()); + } + } + } + + /** + * Gets the set of windows contained by the application. + * + * <p> + * Note that the returned set of windows can not be modified. + * </p> + * + * @return the unmodifiable collection of windows. + */ + public Collection<Root.LegacyWindow> getWindows() { + return Collections.unmodifiableCollection(legacyRootNames.values()); + } + } + + /** + * An event sent to {@link #start(ApplicationStartEvent)} when a new + * Application is being started. + * + * @since 7.0 + */ + public static class ApplicationStartEvent implements Serializable { + private final URL applicationUrl; + + private final Properties applicationProperties; + + private final ApplicationContext context; + + private final boolean productionMode; + + /** + * @param applicationUrl + * the URL the application should respond to. + * @param applicationProperties + * the Application properties as specified by the deployment + * configuration. + * @param context + * the context application will be running in. + * @param productionMode + * flag indicating whether the application is running in + * production mode. + */ + public ApplicationStartEvent(URL applicationUrl, + Properties applicationProperties, ApplicationContext context, + boolean productionMode) { + this.applicationUrl = applicationUrl; + this.applicationProperties = applicationProperties; + this.context = context; + this.productionMode = productionMode; + } + + /** + * Gets the URL the application should respond to. + * + * @return the URL the application should respond to or + * <code>null</code> if the URL is not defined. + * + * @see Application#getURL() + */ + public URL getApplicationUrl() { + return applicationUrl; + } + + /** + * Gets the Application properties as specified by the deployment + * configuration. + * + * @return the properties configured for the applciation. + * + * @see Application#getProperty(String) + */ + public Properties getApplicationProperties() { + return applicationProperties; + } + + /** + * Gets the context application will be running in. + * + * @return the context application will be running in. + * + * @see Application#getContext() + */ + public ApplicationContext getContext() { + return context; + } + + /** + * Checks whether the application is running in production mode. + * + * @return <code>true</code> if in production mode, else + * <code>false</code> + * + * @see Application#isProductionMode() + */ + public boolean isProductionMode() { + return productionMode; + } + } + + private final static Logger logger = Logger.getLogger(Application.class + .getName()); + + /** + * Application context the application is running in. + */ + private ApplicationContext context; + + /** + * The current user or <code>null</code> if no user has logged in. + */ + private Object user; + + /** + * The application's URL. + */ + private URL applicationUrl; + + /** + * Application status. + */ + private volatile boolean applicationIsRunning = false; + + /** + * Application properties. + */ + private Properties properties; + + /** + * Default locale of the application. + */ + private Locale locale; + + /** + * List of listeners listening user changes. + */ + private LinkedList<UserChangeListener> userChangeListeners = null; + + /** + * Application resource mapping: key <-> resource. + */ + private final Hashtable<ApplicationResource, String> resourceKeyMap = new Hashtable<ApplicationResource, String>(); + + private final Hashtable<String, ApplicationResource> keyResourceMap = new Hashtable<String, ApplicationResource>(); + + private long lastResourceKeyNumber = 0; + + /** + * URL where the user is redirected to on application close, or null if + * application is just closed without redirection. + */ + private String logoutURL = null; + + /** + * The default SystemMessages (read-only). Change by overriding + * getSystemMessages() and returning CustomizedSystemMessages + */ + private static final SystemMessages DEFAULT_SYSTEM_MESSAGES = new SystemMessages(); + + /** + * Application wide error handler which is used by default if an error is + * left unhandled. + */ + private Terminal.ErrorListener errorHandler = this; + + /** + * The converter factory that is used to provide default converters for the + * application. + */ + private ConverterFactory converterFactory = new DefaultConverterFactory(); + + private LinkedList<RequestHandler> requestHandlers = new LinkedList<RequestHandler>(); + + private int nextRootId = 0; + private Map<Integer, Root> roots = new HashMap<Integer, Root>(); + + private boolean productionMode = true; + + private final Map<String, Integer> retainOnRefreshRoots = new HashMap<String, Integer>(); + + private final EventRouter eventRouter = new EventRouter(); + + /** + * Keeps track of which roots have been inited. + * <p> + * TODO Investigate whether this might be derived from the different states + * in getRootForRrequest. + * </p> + */ + private Set<Integer> initedRoots = new HashSet<Integer>(); + + /** + * Gets the user of the application. + * + * <p> + * Vaadin doesn't define of use user object in any way - it only provides + * this getter and setter methods for convenience. The user is any object + * that has been stored to the application with {@link #setUser(Object)}. + * </p> + * + * @return the User of the application. + */ + public Object getUser() { + return user; + } + + /** + * <p> + * Sets the user of the application instance. An application instance may + * have a user associated to it. This can be set in login procedure or + * application initialization. + * </p> + * <p> + * A component performing the user login procedure can assign the user + * property of the application and make the user object available to other + * components of the application. + * </p> + * <p> + * Vaadin doesn't define of use user object in any way - it only provides + * getter and setter methods for convenience. The user reference stored to + * the application can be read with {@link #getUser()}. + * </p> + * + * @param user + * the new user. + */ + public void setUser(Object user) { + final Object prevUser = this.user; + if (user == prevUser || (user != null && user.equals(prevUser))) { + return; + } + + this.user = user; + if (userChangeListeners != null) { + final Object[] listeners = userChangeListeners.toArray(); + final UserChangeEvent event = new UserChangeEvent(this, user, + prevUser); + for (int i = 0; i < listeners.length; i++) { + ((UserChangeListener) listeners[i]) + .applicationUserChanged(event); + } + } + } + + /** + * Gets the URL of the application. + * + * <p> + * This is the URL what can be entered to a browser window to start the + * application. Navigating to the application URL shows the main window ( + * {@link #getMainWindow()}) of the application. Note that the main window + * can also be shown by navigating to the window url ( + * {@link com.vaadin.ui.Window#getURL()}). + * </p> + * + * @return the application's URL. + */ + public URL getURL() { + return applicationUrl; + } + + /** + * Ends the Application. + * + * <p> + * In effect this will cause the application stop returning any windows when + * asked. When the application is closed, its state is removed from the + * session and the browser window is redirected to the application logout + * url set with {@link #setLogoutURL(String)}. If the logout url has not + * been set, the browser window is reloaded and the application is + * restarted. + * </p> + * . + */ + public void close() { + applicationIsRunning = false; + } + + /** + * Starts the application on the given URL. + * + * <p> + * This method is called by Vaadin framework when a user navigates to the + * application. After this call the application corresponds to the given URL + * and it will return windows when asked for them. There is no need to call + * this method directly. + * </p> + * + * <p> + * Application properties are defined by servlet configuration object + * {@link javax.servlet.ServletConfig} and they are overridden by + * context-wide initialization parameters + * {@link javax.servlet.ServletContext}. + * </p> + * + * @param event + * the application start event containing details required for + * starting the application. + * + */ + public void start(ApplicationStartEvent event) { + applicationUrl = event.getApplicationUrl(); + productionMode = event.isProductionMode(); + properties = event.getApplicationProperties(); + context = event.getContext(); + init(); + applicationIsRunning = true; + } + + /** + * Tests if the application is running or if it has been finished. + * + * <p> + * Application starts running when its + * {@link #start(URL, Properties, ApplicationContext)} method has been + * called and stops when the {@link #close()} is called. + * </p> + * + * @return <code>true</code> if the application is running, + * <code>false</code> if not. + */ + public boolean isRunning() { + return applicationIsRunning; + } + + /** + * <p> + * Main initializer of the application. The <code>init</code> method is + * called by the framework when the application is started, and it should + * perform whatever initialization operations the application needs. + * </p> + */ + public void init() { + // Default implementation does nothing + } + + /** + * Returns an enumeration of all the names in this application. + * + * <p> + * See {@link #start(URL, Properties, ApplicationContext)} how properties + * are defined. + * </p> + * + * @return an enumeration of all the keys in this property list, including + * the keys in the default property list. + * + */ + public Enumeration<?> getPropertyNames() { + return properties.propertyNames(); + } + + /** + * Searches for the property with the specified name in this application. + * This method returns <code>null</code> if the property is not found. + * + * See {@link #start(URL, Properties, ApplicationContext)} how properties + * are defined. + * + * @param name + * the name of the property. + * @return the value in this property list with the specified key value. + */ + public String getProperty(String name) { + return properties.getProperty(name); + } + + /** + * Adds new resource to the application. The resource can be accessed by the + * user of the application. + * + * @param resource + * the resource to add. + */ + public void addResource(ApplicationResource resource) { + + // Check if the resource is already mapped + if (resourceKeyMap.containsKey(resource)) { + return; + } + + // Generate key + final String key = String.valueOf(++lastResourceKeyNumber); + + // Add the resource to mappings + resourceKeyMap.put(resource, key); + keyResourceMap.put(key, resource); + } + + /** + * Removes the resource from the application. + * + * @param resource + * the resource to remove. + */ + public void removeResource(ApplicationResource resource) { + final Object key = resourceKeyMap.get(resource); + if (key != null) { + resourceKeyMap.remove(resource); + keyResourceMap.remove(key); + } + } + + /** + * Gets the relative uri of the resource. This method is intended to be + * called only be the terminal implementation. + * + * This method can only be called from within the processing of a UIDL + * request, not from a background thread. + * + * @param resource + * the resource to get relative location. + * @return the relative uri of the resource or null if called in a + * background thread + * + * @deprecated this method is intended to be used by the terminal only. It + * may be removed or moved in the future. + */ + @Deprecated + public String getRelativeLocation(ApplicationResource resource) { + + // Gets the key + final String key = resourceKeyMap.get(resource); + + // If the resource is not registered, return null + if (key == null) { + return null; + } + + return context.generateApplicationResourceURL(resource, key); + } + + /** + * Gets the default locale for this application. + * + * By default this is the preferred locale of the user using the + * application. In most cases it is read from the browser defaults. + * + * @return the locale of this application. + */ + public Locale getLocale() { + if (locale != null) { + return locale; + } + return Locale.getDefault(); + } + + /** + * Sets the default locale for this application. + * + * By default this is the preferred locale of the user using the + * application. In most cases it is read from the browser defaults. + * + * @param locale + * the Locale object. + * + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * <p> + * An event that characterizes a change in the current selection. + * </p> + * Application user change event sent when the setUser is called to change + * the current user of the application. + * + * @version + * @VERSION@ + * @since 3.0 + */ + public class UserChangeEvent extends java.util.EventObject { + + /** + * New user of the application. + */ + private final Object newUser; + + /** + * Previous user of the application. + */ + private final Object prevUser; + + /** + * Constructor for user change event. + * + * @param source + * the application source. + * @param newUser + * the new User. + * @param prevUser + * the previous User. + */ + public UserChangeEvent(Application source, Object newUser, + Object prevUser) { + super(source); + this.newUser = newUser; + this.prevUser = prevUser; + } + + /** + * Gets the new user of the application. + * + * @return the new User. + */ + public Object getNewUser() { + return newUser; + } + + /** + * Gets the previous user of the application. + * + * @return the previous Vaadin user, if user has not changed ever on + * application it returns <code>null</code> + */ + public Object getPreviousUser() { + return prevUser; + } + + /** + * Gets the application where the user change occurred. + * + * @return the Application. + */ + public Application getApplication() { + return (Application) getSource(); + } + } + + /** + * The <code>UserChangeListener</code> interface for listening application + * user changes. + * + * @version + * @VERSION@ + * @since 3.0 + */ + public interface UserChangeListener extends EventListener, Serializable { + + /** + * The <code>applicationUserChanged</code> method Invoked when the + * application user has changed. + * + * @param event + * the change event. + */ + public void applicationUserChanged(Application.UserChangeEvent event); + } + + /** + * Adds the user change listener. + * + * This allows one to get notification each time {@link #setUser(Object)} is + * called. + * + * @param listener + * the user change listener to add. + */ + public void addListener(UserChangeListener listener) { + if (userChangeListeners == null) { + userChangeListeners = new LinkedList<UserChangeListener>(); + } + userChangeListeners.add(listener); + } + + /** + * Removes the user change listener. + * + * @param listener + * the user change listener to remove. + */ + public void removeListener(UserChangeListener listener) { + if (userChangeListeners == null) { + return; + } + userChangeListeners.remove(listener); + if (userChangeListeners.isEmpty()) { + userChangeListeners = null; + } + } + + /** + * Window detach event. + * + * This event is sent each time a window is removed from the application + * with {@link com.vaadin.Application#removeWindow(Window)}. + */ + public class WindowDetachEvent extends EventObject { + + private final Window window; + + /** + * Creates a event. + * + * @param window + * the Detached window. + */ + public WindowDetachEvent(Window window) { + super(Application.this); + this.window = window; + } + + /** + * Gets the detached window. + * + * @return the detached window. + */ + public Window getWindow() { + return window; + } + + /** + * Gets the application from which the window was detached. + * + * @return the Application. + */ + public Application getApplication() { + return (Application) getSource(); + } + } + + /** + * Window attach event. + * + * This event is sent each time a window is attached tothe application with + * {@link com.vaadin.Application#addWindow(Window)}. + */ + public class WindowAttachEvent extends EventObject { + + private final Window window; + + /** + * Creates a event. + * + * @param window + * the Attached window. + */ + public WindowAttachEvent(Window window) { + super(Application.this); + this.window = window; + } + + /** + * Gets the attached window. + * + * @return the attached window. + */ + public Window getWindow() { + return window; + } + + /** + * Gets the application to which the window was attached. + * + * @return the Application. + */ + public Application getApplication() { + return (Application) getSource(); + } + } + + /** + * Window attach listener interface. + */ + public interface WindowAttachListener extends Serializable { + + /** + * Window attached + * + * @param event + * the window attach event. + */ + public void windowAttached(WindowAttachEvent event); + } + + /** + * Window detach listener interface. + */ + public interface WindowDetachListener extends Serializable { + + /** + * Window detached. + * + * @param event + * the window detach event. + */ + public void windowDetached(WindowDetachEvent event); + } + + /** + * Returns the URL user is redirected to on application close. If the URL is + * <code>null</code>, the application is closed normally as defined by the + * application running environment. + * <p> + * Desktop application just closes the application window and + * web-application redirects the browser to application main URL. + * </p> + * + * @return the URL. + */ + public String getLogoutURL() { + return logoutURL; + } + + /** + * Sets the URL user is redirected to on application close. If the URL is + * <code>null</code>, the application is closed normally as defined by the + * application running environment: Desktop application just closes the + * application window and web-application redirects the browser to + * application main URL. + * + * @param logoutURL + * the logoutURL to set. + */ + public void setLogoutURL(String logoutURL) { + this.logoutURL = logoutURL; + } + + /** + * Gets the SystemMessages for this application. SystemMessages are used to + * notify the user of various critical situations that can occur, such as + * session expiration, client/server out of sync, and internal server error. + * + * You can customize the messages by "overriding" this method and returning + * {@link CustomizedSystemMessages}. To "override" this method, re-implement + * this method in your application (the class that extends + * {@link Application}). Even though overriding static methods is not + * possible in Java, Vaadin selects to call the static method from the + * subclass instead of the original {@link #getSystemMessages()} if such a + * method exists. + * + * @return the SystemMessages for this application + */ + public static SystemMessages getSystemMessages() { + return DEFAULT_SYSTEM_MESSAGES; + } + + /** + * <p> + * Invoked by the terminal on any exception that occurs in application and + * is thrown by the <code>setVariable</code> to the terminal. The default + * implementation sets the exceptions as <code>ComponentErrors</code> to the + * component that initiated the exception and prints stack trace to standard + * error stream. + * </p> + * <p> + * You can safely override this method in your application in order to + * direct the errors to some other destination (for example log). + * </p> + * + * @param event + * the change event. + * @see com.vaadin.terminal.Terminal.ErrorListener#terminalError(com.vaadin.terminal.Terminal.ErrorEvent) + */ + + @Override + public void terminalError(Terminal.ErrorEvent event) { + final Throwable t = event.getThrowable(); + if (t instanceof SocketException) { + // Most likely client browser closed socket + getLogger().info( + "SocketException in CommunicationManager." + + " Most likely client (browser) closed socket."); + return; + } + + // Finds the original source of the error/exception + Object owner = null; + if (event instanceof VariableOwner.ErrorEvent) { + owner = ((VariableOwner.ErrorEvent) event).getVariableOwner(); + } else if (event instanceof ChangeVariablesErrorEvent) { + owner = ((ChangeVariablesErrorEvent) event).getComponent(); + } + + // Shows the error in AbstractComponent + if (owner instanceof AbstractComponent) { + ((AbstractComponent) owner).setComponentError(AbstractErrorMessage + .getErrorMessageForException(t)); + } + + // also print the error on console + getLogger().log(Level.SEVERE, "Terminal error:", t); + } + + /** + * Gets the application context. + * <p> + * The application context is the environment where the application is + * running in. The actual implementation class of may contains quite a lot + * more functionality than defined in the {@link ApplicationContext} + * interface. + * </p> + * <p> + * By default, when you are deploying your application to a servlet + * container, the implementation class is {@link WebApplicationContext} - + * you can safely cast to this class and use the methods from there. When + * you are deploying your application as a portlet, context implementation + * is {@link PortletApplicationContext}. + * </p> + * + * @return the application context. + */ + public ApplicationContext getContext() { + return context; + } + + /** + * Override this method to return correct version number of your + * Application. Version information is delivered for example to Testing + * Tools test results. By default this returns a string "NONVERSIONED". + * + * @return version string + */ + public String getVersion() { + return "NONVERSIONED"; + } + + /** + * Gets the application error handler. + * + * The default error handler is the application itself. + * + * @return Application error handler + */ + public Terminal.ErrorListener getErrorHandler() { + return errorHandler; + } + + /** + * Sets the application error handler. + * + * The default error handler is the application itself. By overriding this, + * you can redirect the error messages to your selected target (log for + * example). + * + * @param errorHandler + */ + public void setErrorHandler(Terminal.ErrorListener errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Gets the {@link ConverterFactory} used to locate a suitable + * {@link Converter} for fields in the application. + * + * See {@link #setConverterFactory(ConverterFactory)} for more details + * + * @return The converter factory used in the application + */ + public ConverterFactory getConverterFactory() { + return converterFactory; + } + + /** + * Sets the {@link ConverterFactory} used to locate a suitable + * {@link Converter} for fields in the application. + * <p> + * The {@link ConverterFactory} is used to find a suitable converter when + * binding data to a UI component and the data type does not match the UI + * component type, e.g. binding a Double to a TextField (which is based on a + * String). + * </p> + * <p> + * The {@link Converter} for an individual field can be overridden using + * {@link AbstractField#setConverter(Converter)} and for individual property + * ids in a {@link Table} using + * {@link Table#setConverter(Object, Converter)}. + * </p> + * <p> + * The converter factory must never be set to null. + * + * @param converterFactory + * The converter factory used in the application + */ + public void setConverterFactory(ConverterFactory converterFactory) { + this.converterFactory = converterFactory; + } + + /** + * Contains the system messages used to notify the user about various + * critical situations that can occur. + * <p> + * Customize by overriding the static + * {@link Application#getSystemMessages()} and returning + * {@link CustomizedSystemMessages}. + * </p> + * <p> + * The defaults defined in this class are: + * <ul> + * <li><b>sessionExpiredURL</b> = null</li> + * <li><b>sessionExpiredNotificationEnabled</b> = true</li> + * <li><b>sessionExpiredCaption</b> = ""</li> + * <li><b>sessionExpiredMessage</b> = + * "Take note of any unsaved data, and <u>click here</u> to continue."</li> + * <li><b>communicationErrorURL</b> = null</li> + * <li><b>communicationErrorNotificationEnabled</b> = true</li> + * <li><b>communicationErrorCaption</b> = "Communication problem"</li> + * <li><b>communicationErrorMessage</b> = + * "Take note of any unsaved data, and <u>click here</u> to continue."</li> + * <li><b>internalErrorURL</b> = null</li> + * <li><b>internalErrorNotificationEnabled</b> = true</li> + * <li><b>internalErrorCaption</b> = "Internal error"</li> + * <li><b>internalErrorMessage</b> = "Please notify the administrator.<br/> + * Take note of any unsaved data, and <u>click here</u> to continue."</li> + * <li><b>outOfSyncURL</b> = null</li> + * <li><b>outOfSyncNotificationEnabled</b> = true</li> + * <li><b>outOfSyncCaption</b> = "Out of sync"</li> + * <li><b>outOfSyncMessage</b> = "Something has caused us to be out of sync + * with the server.<br/> + * Take note of any unsaved data, and <u>click here</u> to re-sync."</li> + * <li><b>cookiesDisabledURL</b> = null</li> + * <li><b>cookiesDisabledNotificationEnabled</b> = true</li> + * <li><b>cookiesDisabledCaption</b> = "Cookies disabled"</li> + * <li><b>cookiesDisabledMessage</b> = "This application requires cookies to + * function.<br/> + * Please enable cookies in your browser and <u>click here</u> to try again. + * </li> + * </ul> + * </p> + * + */ + public static class SystemMessages implements Serializable { + protected String sessionExpiredURL = null; + protected boolean sessionExpiredNotificationEnabled = true; + protected String sessionExpiredCaption = "Session Expired"; + protected String sessionExpiredMessage = "Take note of any unsaved data, and <u>click here</u> to continue."; + + protected String communicationErrorURL = null; + protected boolean communicationErrorNotificationEnabled = true; + protected String communicationErrorCaption = "Communication problem"; + protected String communicationErrorMessage = "Take note of any unsaved data, and <u>click here</u> to continue."; + + protected String authenticationErrorURL = null; + protected boolean authenticationErrorNotificationEnabled = true; + protected String authenticationErrorCaption = "Authentication problem"; + protected String authenticationErrorMessage = "Take note of any unsaved data, and <u>click here</u> to continue."; + + protected String internalErrorURL = null; + protected boolean internalErrorNotificationEnabled = true; + protected String internalErrorCaption = "Internal error"; + protected String internalErrorMessage = "Please notify the administrator.<br/>Take note of any unsaved data, and <u>click here</u> to continue."; + + protected String outOfSyncURL = null; + protected boolean outOfSyncNotificationEnabled = true; + protected String outOfSyncCaption = "Out of sync"; + protected String outOfSyncMessage = "Something has caused us to be out of sync with the server.<br/>Take note of any unsaved data, and <u>click here</u> to re-sync."; + + protected String cookiesDisabledURL = null; + protected boolean cookiesDisabledNotificationEnabled = true; + protected String cookiesDisabledCaption = "Cookies disabled"; + protected String cookiesDisabledMessage = "This application requires cookies to function.<br/>Please enable cookies in your browser and <u>click here</u> to try again."; + + /** + * Use {@link CustomizedSystemMessages} to customize + */ + private SystemMessages() { + + } + + /** + * @return null to indicate that the application will be restarted after + * session expired message has been shown. + */ + public String getSessionExpiredURL() { + return sessionExpiredURL; + } + + /** + * @return true to show session expiration message. + */ + public boolean isSessionExpiredNotificationEnabled() { + return sessionExpiredNotificationEnabled; + } + + /** + * @return "" to show no caption. + */ + public String getSessionExpiredCaption() { + return (sessionExpiredNotificationEnabled ? sessionExpiredCaption + : null); + } + + /** + * @return + * "Take note of any unsaved data, and <u>click here</u> to continue." + */ + public String getSessionExpiredMessage() { + return (sessionExpiredNotificationEnabled ? sessionExpiredMessage + : null); + } + + /** + * @return null to reload the application after communication error + * message. + */ + public String getCommunicationErrorURL() { + return communicationErrorURL; + } + + /** + * @return true to show the communication error message. + */ + public boolean isCommunicationErrorNotificationEnabled() { + return communicationErrorNotificationEnabled; + } + + /** + * @return "Communication problem" + */ + public String getCommunicationErrorCaption() { + return (communicationErrorNotificationEnabled ? communicationErrorCaption + : null); + } + + /** + * @return + * "Take note of any unsaved data, and <u>click here</u> to continue." + */ + public String getCommunicationErrorMessage() { + return (communicationErrorNotificationEnabled ? communicationErrorMessage + : null); + } + + /** + * @return null to reload the application after authentication error + * message. + */ + public String getAuthenticationErrorURL() { + return authenticationErrorURL; + } + + /** + * @return true to show the authentication error message. + */ + public boolean isAuthenticationErrorNotificationEnabled() { + return authenticationErrorNotificationEnabled; + } + + /** + * @return "Authentication problem" + */ + public String getAuthenticationErrorCaption() { + return (authenticationErrorNotificationEnabled ? authenticationErrorCaption + : null); + } + + /** + * @return + * "Take note of any unsaved data, and <u>click here</u> to continue." + */ + public String getAuthenticationErrorMessage() { + return (authenticationErrorNotificationEnabled ? authenticationErrorMessage + : null); + } + + /** + * @return null to reload the current URL after internal error message + * has been shown. + */ + public String getInternalErrorURL() { + return internalErrorURL; + } + + /** + * @return true to enable showing of internal error message. + */ + public boolean isInternalErrorNotificationEnabled() { + return internalErrorNotificationEnabled; + } + + /** + * @return "Internal error" + */ + public String getInternalErrorCaption() { + return (internalErrorNotificationEnabled ? internalErrorCaption + : null); + } + + /** + * @return "Please notify the administrator.<br/> + * Take note of any unsaved data, and <u>click here</u> to + * continue." + */ + public String getInternalErrorMessage() { + return (internalErrorNotificationEnabled ? internalErrorMessage + : null); + } + + /** + * @return null to reload the application after out of sync message. + */ + public String getOutOfSyncURL() { + return outOfSyncURL; + } + + /** + * @return true to enable showing out of sync message + */ + public boolean isOutOfSyncNotificationEnabled() { + return outOfSyncNotificationEnabled; + } + + /** + * @return "Out of sync" + */ + public String getOutOfSyncCaption() { + return (outOfSyncNotificationEnabled ? outOfSyncCaption : null); + } + + /** + * @return "Something has caused us to be out of sync with the server.<br/> + * Take note of any unsaved data, and <u>click here</u> to + * re-sync." + */ + public String getOutOfSyncMessage() { + return (outOfSyncNotificationEnabled ? outOfSyncMessage : null); + } + + /** + * Returns the URL the user should be redirected to after dismissing the + * "you have to enable your cookies" message. Typically null. + * + * @return A URL the user should be redirected to after dismissing the + * message or null to reload the current URL. + */ + public String getCookiesDisabledURL() { + return cookiesDisabledURL; + } + + /** + * Determines if "cookies disabled" messages should be shown to the end + * user or not. If the notification is disabled the user will be + * immediately redirected to the URL returned by + * {@link #getCookiesDisabledURL()}. + * + * @return true to show "cookies disabled" messages to the end user, + * false to redirect to the given URL directly + */ + public boolean isCookiesDisabledNotificationEnabled() { + return cookiesDisabledNotificationEnabled; + } + + /** + * Returns the caption of the message shown to the user when cookies are + * disabled in the browser. + * + * @return The caption of the "cookies disabled" message + */ + public String getCookiesDisabledCaption() { + return (cookiesDisabledNotificationEnabled ? cookiesDisabledCaption + : null); + } + + /** + * Returns the message shown to the user when cookies are disabled in + * the browser. + * + * @return The "cookies disabled" message + */ + public String getCookiesDisabledMessage() { + return (cookiesDisabledNotificationEnabled ? cookiesDisabledMessage + : null); + } + + } + + /** + * Contains the system messages used to notify the user about various + * critical situations that can occur. + * <p> + * Vaadin gets the SystemMessages from your application by calling a static + * getSystemMessages() method. By default the + * Application.getSystemMessages() is used. You can customize this by + * defining a static MyApplication.getSystemMessages() and returning + * CustomizedSystemMessages. Note that getSystemMessages() is static - + * changing the system messages will by default change the message for all + * users of the application. + * </p> + * <p> + * The default behavior is to show a notification, and restart the + * application the the user clicks the message. <br/> + * Instead of restarting the application, you can set a specific URL that + * the user is taken to.<br/> + * Setting both caption and message to null will restart the application (or + * go to the specified URL) without displaying a notification. + * set*NotificationEnabled(false) will achieve the same thing. + * </p> + * <p> + * The situations are: + * <li>Session expired: the user session has expired, usually due to + * inactivity.</li> + * <li>Communication error: the client failed to contact the server, or the + * server returned and invalid response.</li> + * <li>Internal error: unhandled critical server error (e.g out of memory, + * database crash) + * <li>Out of sync: the client is not in sync with the server. E.g the user + * opens two windows showing the same application, but the application does + * not support this and uses the same Window instance. When the user makes + * changes in one of the windows - the other window is no longer in sync, + * and (for instance) pressing a button that is no longer present in the UI + * will cause a out-of-sync -situation. + * </p> + */ + + public static class CustomizedSystemMessages extends SystemMessages + implements Serializable { + + /** + * Sets the URL to go to when the session has expired. + * + * @param sessionExpiredURL + * the URL to go to, or null to reload current + */ + public void setSessionExpiredURL(String sessionExpiredURL) { + this.sessionExpiredURL = sessionExpiredURL; + } + + /** + * Enables or disables the notification. If disabled, the set URL (or + * current) is loaded directly when next transaction between server and + * client happens. + * + * @param sessionExpiredNotificationEnabled + * true = enabled, false = disabled + */ + public void setSessionExpiredNotificationEnabled( + boolean sessionExpiredNotificationEnabled) { + this.sessionExpiredNotificationEnabled = sessionExpiredNotificationEnabled; + } + + /** + * Sets the caption of the notification. Set to null for no caption. If + * both caption and message are null, client automatically forwards to + * sessionExpiredUrl after timeout timer expires. Timer uses value read + * from HTTPSession.getMaxInactiveInterval() + * + * @param sessionExpiredCaption + * the caption + */ + public void setSessionExpiredCaption(String sessionExpiredCaption) { + this.sessionExpiredCaption = sessionExpiredCaption; + } + + /** + * Sets the message of the notification. Set to null for no message. If + * both caption and message are null, client automatically forwards to + * sessionExpiredUrl after timeout timer expires. Timer uses value read + * from HTTPSession.getMaxInactiveInterval() + * + * @param sessionExpiredMessage + * the message + */ + public void setSessionExpiredMessage(String sessionExpiredMessage) { + this.sessionExpiredMessage = sessionExpiredMessage; + } + + /** + * Sets the URL to go to when there is a authentication error. + * + * @param authenticationErrorURL + * the URL to go to, or null to reload current + */ + public void setAuthenticationErrorURL(String authenticationErrorURL) { + this.authenticationErrorURL = authenticationErrorURL; + } + + /** + * Enables or disables the notification. If disabled, the set URL (or + * current) is loaded directly. + * + * @param authenticationErrorNotificationEnabled + * true = enabled, false = disabled + */ + public void setAuthenticationErrorNotificationEnabled( + boolean authenticationErrorNotificationEnabled) { + this.authenticationErrorNotificationEnabled = authenticationErrorNotificationEnabled; + } + + /** + * Sets the caption of the notification. Set to null for no caption. If + * both caption and message is null, the notification is disabled; + * + * @param authenticationErrorCaption + * the caption + */ + public void setAuthenticationErrorCaption( + String authenticationErrorCaption) { + this.authenticationErrorCaption = authenticationErrorCaption; + } + + /** + * Sets the message of the notification. Set to null for no message. If + * both caption and message is null, the notification is disabled; + * + * @param authenticationErrorMessage + * the message + */ + public void setAuthenticationErrorMessage( + String authenticationErrorMessage) { + this.authenticationErrorMessage = authenticationErrorMessage; + } + + /** + * Sets the URL to go to when there is a communication error. + * + * @param communicationErrorURL + * the URL to go to, or null to reload current + */ + public void setCommunicationErrorURL(String communicationErrorURL) { + this.communicationErrorURL = communicationErrorURL; + } + + /** + * Enables or disables the notification. If disabled, the set URL (or + * current) is loaded directly. + * + * @param communicationErrorNotificationEnabled + * true = enabled, false = disabled + */ + public void setCommunicationErrorNotificationEnabled( + boolean communicationErrorNotificationEnabled) { + this.communicationErrorNotificationEnabled = communicationErrorNotificationEnabled; + } + + /** + * Sets the caption of the notification. Set to null for no caption. If + * both caption and message is null, the notification is disabled; + * + * @param communicationErrorCaption + * the caption + */ + public void setCommunicationErrorCaption( + String communicationErrorCaption) { + this.communicationErrorCaption = communicationErrorCaption; + } + + /** + * Sets the message of the notification. Set to null for no message. If + * both caption and message is null, the notification is disabled; + * + * @param communicationErrorMessage + * the message + */ + public void setCommunicationErrorMessage( + String communicationErrorMessage) { + this.communicationErrorMessage = communicationErrorMessage; + } + + /** + * Sets the URL to go to when an internal error occurs. + * + * @param internalErrorURL + * the URL to go to, or null to reload current + */ + public void setInternalErrorURL(String internalErrorURL) { + this.internalErrorURL = internalErrorURL; + } + + /** + * Enables or disables the notification. If disabled, the set URL (or + * current) is loaded directly. + * + * @param internalErrorNotificationEnabled + * true = enabled, false = disabled + */ + public void setInternalErrorNotificationEnabled( + boolean internalErrorNotificationEnabled) { + this.internalErrorNotificationEnabled = internalErrorNotificationEnabled; + } + + /** + * Sets the caption of the notification. Set to null for no caption. If + * both caption and message is null, the notification is disabled; + * + * @param internalErrorCaption + * the caption + */ + public void setInternalErrorCaption(String internalErrorCaption) { + this.internalErrorCaption = internalErrorCaption; + } + + /** + * Sets the message of the notification. Set to null for no message. If + * both caption and message is null, the notification is disabled; + * + * @param internalErrorMessage + * the message + */ + public void setInternalErrorMessage(String internalErrorMessage) { + this.internalErrorMessage = internalErrorMessage; + } + + /** + * Sets the URL to go to when the client is out-of-sync. + * + * @param outOfSyncURL + * the URL to go to, or null to reload current + */ + public void setOutOfSyncURL(String outOfSyncURL) { + this.outOfSyncURL = outOfSyncURL; + } + + /** + * Enables or disables the notification. If disabled, the set URL (or + * current) is loaded directly. + * + * @param outOfSyncNotificationEnabled + * true = enabled, false = disabled + */ + public void setOutOfSyncNotificationEnabled( + boolean outOfSyncNotificationEnabled) { + this.outOfSyncNotificationEnabled = outOfSyncNotificationEnabled; + } + + /** + * Sets the caption of the notification. Set to null for no caption. If + * both caption and message is null, the notification is disabled; + * + * @param outOfSyncCaption + * the caption + */ + public void setOutOfSyncCaption(String outOfSyncCaption) { + this.outOfSyncCaption = outOfSyncCaption; + } + + /** + * Sets the message of the notification. Set to null for no message. If + * both caption and message is null, the notification is disabled; + * + * @param outOfSyncMessage + * the message + */ + public void setOutOfSyncMessage(String outOfSyncMessage) { + this.outOfSyncMessage = outOfSyncMessage; + } + + /** + * Sets the URL to redirect to when the browser has cookies disabled. + * + * @param cookiesDisabledURL + * the URL to redirect to, or null to reload the current URL + */ + public void setCookiesDisabledURL(String cookiesDisabledURL) { + this.cookiesDisabledURL = cookiesDisabledURL; + } + + /** + * Enables or disables the notification for "cookies disabled" messages. + * If disabled, the URL returned by {@link #getCookiesDisabledURL()} is + * loaded directly. + * + * @param cookiesDisabledNotificationEnabled + * true to enable "cookies disabled" messages, false + * otherwise + */ + public void setCookiesDisabledNotificationEnabled( + boolean cookiesDisabledNotificationEnabled) { + this.cookiesDisabledNotificationEnabled = cookiesDisabledNotificationEnabled; + } + + /** + * Sets the caption of the "cookies disabled" notification. Set to null + * for no caption. If both caption and message is null, the notification + * is disabled. + * + * @param cookiesDisabledCaption + * the caption for the "cookies disabled" notification + */ + public void setCookiesDisabledCaption(String cookiesDisabledCaption) { + this.cookiesDisabledCaption = cookiesDisabledCaption; + } + + /** + * Sets the message of the "cookies disabled" notification. Set to null + * for no message. If both caption and message is null, the notification + * is disabled. + * + * @param cookiesDisabledMessage + * the message for the "cookies disabled" notification + */ + public void setCookiesDisabledMessage(String cookiesDisabledMessage) { + this.cookiesDisabledMessage = cookiesDisabledMessage; + } + + } + + /** + * Application error is an error message defined on the application level. + * + * When an error occurs on the application level, this error message type + * should be used. This indicates that the problem is caused by the + * application - not by the user. + */ + public class ApplicationError implements Terminal.ErrorEvent { + private final Throwable throwable; + + public ApplicationError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Gets a root for a request for which no root is already known. This method + * is called when the framework processes a request that does not originate + * from an existing root instance. This typically happens when a host page + * is requested. + * + * <p> + * Subclasses of Application may override this method to provide custom + * logic for choosing how to create a suitable root or for picking an + * already created root. If an existing root is picked, care should be taken + * to avoid keeping the same root open in multiple browser windows, as that + * will cause the states to go out of sync. + * </p> + * + * <p> + * If {@link BrowserDetails} are required to create a Root, the + * implementation can throw a {@link RootRequiresMoreInformationException} + * exception. In this case, the framework will instruct the browser to send + * the additional details, whereupon this method is invoked again with the + * browser details present in the wrapped request. Throwing the exception if + * the browser details are already available is not supported. + * </p> + * + * <p> + * The default implementation in {@link Application} creates a new instance + * of the Root class returned by {@link #getRootClassName(WrappedRequest)}, + * which in turn uses the {@value #ROOT_PARAMETER} parameter from web.xml. + * If {@link DeploymentConfiguration#getClassLoader()} for the request + * returns a {@link ClassLoader}, it is used for loading the Root class. + * Otherwise the {@link ClassLoader} used to load this class is used. + * </p> + * + * @param request + * the wrapped request for which a root is needed + * @return a root instance to use for the request + * @throws RootRequiresMoreInformationException + * may be thrown by an implementation to indicate that + * {@link BrowserDetails} are required to create a root + * + * @see #getRootClassName(WrappedRequest) + * @see Root + * @see RootRequiresMoreInformationException + * @see WrappedRequest#getBrowserDetails() + * + * @since 7.0 + */ + protected Root getRoot(WrappedRequest request) + throws RootRequiresMoreInformationException { + String rootClassName = getRootClassName(request); + try { + ClassLoader classLoader = request.getDeploymentConfiguration() + .getClassLoader(); + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } + Class<? extends Root> rootClass = Class.forName(rootClassName, + true, classLoader).asSubclass(Root.class); + try { + Root root = rootClass.newInstance(); + return root; + } catch (Exception e) { + throw new RuntimeException("Could not instantiate root class " + + rootClassName, e); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException("Could not load root class " + + rootClassName, e); + } + } + + /** + * Provides the name of the <code>Root</code> class that should be used for + * a request. The class must have an accessible no-args constructor. + * <p> + * The default implementation uses the {@value #ROOT_PARAMETER} parameter + * from web.xml. + * </p> + * <p> + * This method is mainly used by the default implementation of + * {@link #getRoot(WrappedRequest)}. If you override that method with your + * own functionality, the results of this method might not be used. + * </p> + * + * @param request + * the request for which a new root is required + * @return the name of the root class to use + * + * @since 7.0 + */ + protected String getRootClassName(WrappedRequest request) { + Object rootClassNameObj = properties.get(ROOT_PARAMETER); + if (rootClassNameObj instanceof String) { + return (String) rootClassNameObj; + } else { + throw new RuntimeException("No " + ROOT_PARAMETER + + " defined in web.xml"); + } + } + + /** + * Finds the theme to use for a specific root. If no specific theme is + * required, <code>null</code> is returned. + * + * TODO Tell what the default implementation does once it does something. + * + * @param root + * the root to get a theme for + * @return the name of the theme, or <code>null</code> if the default theme + * should be used + * + * @since 7.0 + */ + public String getThemeForRoot(Root root) { + Theme rootTheme = getAnnotationFor(root.getClass(), Theme.class); + if (rootTheme != null) { + return rootTheme.value(); + } else { + return null; + } + } + + /** + * Finds the widgetset to use for a specific root. If no specific widgetset + * is required, <code>null</code> is returned. + * + * TODO Tell what the default implementation does once it does something. + * + * @param root + * the root to get a widgetset for + * @return the name of the widgetset, or <code>null</code> if the default + * widgetset should be used + * + * @since 7.0 + */ + public String getWidgetsetForRoot(Root root) { + Widgetset rootWidgetset = getAnnotationFor(root.getClass(), + Widgetset.class); + if (rootWidgetset != null) { + return rootWidgetset.value(); + } else { + return null; + } + } + + /** + * Helper to get an annotation for a class. If the annotation is not present + * on the target class, it's superclasses and implemented interfaces are + * also searched for the annotation. + * + * @param type + * the target class from which the annotation should be found + * @param annotationType + * the annotation type to look for + * @return an annotation of the given type, or <code>null</code> if the + * annotation is not present on the class + */ + private static <T extends Annotation> T getAnnotationFor(Class<?> type, + Class<T> annotationType) { + // Find from the class hierarchy + Class<?> currentType = type; + while (currentType != Object.class) { + T annotation = currentType.getAnnotation(annotationType); + if (annotation != null) { + return annotation; + } else { + currentType = currentType.getSuperclass(); + } + } + + // Find from an implemented interface + for (Class<?> iface : type.getInterfaces()) { + T annotation = iface.getAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + } + + return null; + } + + /** + * Handles a request by passing it to each registered {@link RequestHandler} + * in turn until one produces a response. This method is used for requests + * that have not been handled by any specific functionality in the terminal + * implementation (e.g. {@link AbstractApplicationServlet}). + * <p> + * The request handlers are invoked in the revere order in which they were + * added to the application until a response has been produced. This means + * that the most recently added handler is used first and the first request + * handler that was added to the application is invoked towards the end + * unless any previous handler has already produced a response. + * </p> + * + * @param request + * the wrapped request to get information from + * @param response + * the response to which data can be written + * @return returns <code>true</code> if a {@link RequestHandler} has + * produced a response and <code>false</code> if no response has + * been written. + * @throws IOException + * + * @see #addRequestHandler(RequestHandler) + * @see RequestHandler + * + * @since 7.0 + */ + public boolean handleRequest(WrappedRequest request, + WrappedResponse response) throws IOException { + // Use a copy to avoid ConcurrentModificationException + for (RequestHandler handler : new ArrayList<RequestHandler>( + requestHandlers)) { + if (handler.handleRequest(this, request, response)) { + return true; + } + } + // If not handled + return false; + } + + /** + * Adds a request handler to this application. Request handlers can be added + * to provide responses to requests that are not handled by the default + * functionality of the framework. + * <p> + * Handlers are called in reverse order of addition, so the most recently + * added handler will be called first. + * </p> + * + * @param handler + * the request handler to add + * + * @see #handleRequest(WrappedRequest, WrappedResponse) + * @see #removeRequestHandler(RequestHandler) + * + * @since 7.0 + */ + public void addRequestHandler(RequestHandler handler) { + requestHandlers.addFirst(handler); + } + + /** + * Removes a request handler from the application. + * + * @param handler + * the request handler to remove + * + * @since 7.0 + */ + public void removeRequestHandler(RequestHandler handler) { + requestHandlers.remove(handler); + } + + /** + * Gets the request handlers that are registered to the application. The + * iteration order of the returned collection is the same as the order in + * which the request handlers will be invoked when a request is handled. + * + * @return a collection of request handlers, with the iteration order + * according to the order they would be invoked + * + * @see #handleRequest(WrappedRequest, WrappedResponse) + * @see #addRequestHandler(RequestHandler) + * @see #removeRequestHandler(RequestHandler) + * + * @since 7.0 + */ + public Collection<RequestHandler> getRequestHandlers() { + return Collections.unmodifiableCollection(requestHandlers); + } + + /** + * Find an application resource with a given key. + * + * @param key + * The key of the resource + * @return The application resource corresponding to the provided key, or + * <code>null</code> if no resource is registered for the key + * + * @since 7.0 + */ + public ApplicationResource getResource(String key) { + return keyResourceMap.get(key); + } + + /** + * Thread local for keeping track of currently used application instance + * + * @since 7.0 + */ + private static final ThreadLocal<Application> currentApplication = new ThreadLocal<Application>(); + + private boolean rootPreserved = false; + + /** + * Gets the currently used application. The current application is + * automatically defined when processing requests to the server. In other + * cases, (e.g. from background threads), the current application is not + * automatically defined. + * + * @return the current application instance if available, otherwise + * <code>null</code> + * + * @see #setCurrent(Application) + * + * @since 7.0 + */ + public static Application getCurrent() { + return currentApplication.get(); + } + + /** + * Sets the thread local for the current application. This method is used by + * the framework to set the current application whenever a new request is + * processed and it is cleared when the request has been processed. + * <p> + * The application developer can also use this method to define the current + * application outside the normal request handling, e.g. when initiating + * custom background threads. + * </p> + * + * @param application + * + * @see #getCurrent() + * @see ThreadLocal + * + * @since 7.0 + */ + public static void setCurrent(Application application) { + currentApplication.set(application); + } + + /** + * Check whether this application is in production mode. If an application + * is in production mode, certain debugging facilities are not available. + * + * @return the status of the production mode flag + * + * @since 7.0 + */ + public boolean isProductionMode() { + return productionMode; + } + + /** + * Finds the {@link Root} to which a particular request belongs. If the + * request originates from an existing Root, that root is returned. In other + * cases, the method attempts to create and initialize a new root and might + * throw a {@link RootRequiresMoreInformationException} if all required + * information is not available. + * <p> + * Please note that this method can also return a newly created + * <code>Root</code> which has not yet been initialized. You can use + * {@link #isRootInitPending(int)} with the root's id ( + * {@link Root#getRootId()} to check whether the initialization is still + * pending. + * </p> + * + * @param request + * the request for which a root is desired + * @return a root belonging to the request + * @throws RootRequiresMoreInformationException + * if no existing root could be found and creating a new root + * requires additional information from the browser + * + * @see #getRoot(WrappedRequest) + * @see RootRequiresMoreInformationException + * + * @since 7.0 + */ + public Root getRootForRequest(WrappedRequest request) + throws RootRequiresMoreInformationException { + Root root = Root.getCurrent(); + if (root != null) { + return root; + } + Integer rootId = getRootId(request); + + synchronized (this) { + BrowserDetails browserDetails = request.getBrowserDetails(); + boolean hasBrowserDetails = browserDetails != null + && browserDetails.getUriFragment() != null; + + root = roots.get(rootId); + + if (root == null && isRootPreserved()) { + // Check for a known root + if (!retainOnRefreshRoots.isEmpty()) { + + Integer retainedRootId; + if (!hasBrowserDetails) { + throw new RootRequiresMoreInformationException(); + } else { + String windowName = browserDetails.getWindowName(); + retainedRootId = retainOnRefreshRoots.get(windowName); + } + + if (retainedRootId != null) { + rootId = retainedRootId; + root = roots.get(rootId); + } + } + } + + if (root == null) { + // Throws exception if root can not yet be created + root = getRoot(request); + + // Initialize some fields for a newly created root + if (root.getApplication() == null) { + root.setApplication(this); + } + if (root.getRootId() < 0) { + + if (rootId == null) { + // Get the next id if none defined + rootId = Integer.valueOf(nextRootId++); + } + root.setRootId(rootId.intValue()); + roots.put(rootId, root); + } + } + + // Set thread local here so it is available in init + Root.setCurrent(root); + + if (!initedRoots.contains(rootId)) { + boolean initRequiresBrowserDetails = isRootPreserved() + || !root.getClass() + .isAnnotationPresent(EagerInit.class); + if (!initRequiresBrowserDetails || hasBrowserDetails) { + root.doInit(request); + + // Remember that this root has been initialized + initedRoots.add(rootId); + + // init() might turn on preserve so do this afterwards + if (isRootPreserved()) { + // Remember this root + String windowName = request.getBrowserDetails() + .getWindowName(); + retainOnRefreshRoots.put(windowName, rootId); + } + } + } + } // end synchronized block + + return root; + } + + /** + * Internal helper to finds the root id for a request. + * + * @param request + * the request to get the root id for + * @return a root id, or <code>null</code> if no root id is defined + * + * @since 7.0 + */ + private static Integer getRootId(WrappedRequest request) { + if (request instanceof CombinedRequest) { + // Combined requests has the rootid parameter in the second request + CombinedRequest combinedRequest = (CombinedRequest) request; + request = combinedRequest.getSecondRequest(); + } + String rootIdString = request + .getParameter(ApplicationConnection.ROOT_ID_PARAMETER); + Integer rootId = rootIdString == null ? null + : new Integer(rootIdString); + return rootId; + } + + /** + * Sets whether the same Root state should be reused if the framework can + * detect that the application is opened in a browser window where it has + * previously been open. The framework attempts to discover this by checking + * the value of window.name in the browser. + * <p> + * NOTE that you should avoid turning this feature on/off on-the-fly when + * the UI is already shown, as it might not be retained as intended. + * </p> + * + * @param rootPreserved + * <code>true</code>if the same Root instance should be reused + * e.g. when the browser window is refreshed. + */ + public void setRootPreserved(boolean rootPreserved) { + this.rootPreserved = rootPreserved; + if (!rootPreserved) { + retainOnRefreshRoots.clear(); + } + } + + /** + * Checks whether the same Root state should be reused if the framework can + * detect that the application is opened in a browser window where it has + * previously been open. The framework attempts to discover this by checking + * the value of window.name in the browser. + * + * @return <code>true</code>if the same Root instance should be reused e.g. + * when the browser window is refreshed. + */ + public boolean isRootPreserved() { + return rootPreserved; + } + + /** + * Checks whether there's a pending initialization for the root with the + * given id. + * + * @param rootId + * root id to check for + * @return <code>true</code> of the initialization is pending, + * <code>false</code> if the root id is not registered or if the + * root has already been initialized + * + * @see #getRootForRequest(WrappedRequest) + */ + public boolean isRootInitPending(int rootId) { + return !initedRoots.contains(Integer.valueOf(rootId)); + } + + /** + * Gets all the roots of this application. This includes roots that have + * been requested but not yet initialized. Please note, that roots are not + * automatically removed e.g. if the browser window is closed and that there + * is no way to manually remove a root. Inactive roots will thus not be + * released for GC until the entire application is released when the session + * has timed out (unless there are dangling references). Improved support + * for releasing unused roots is planned for an upcoming alpha release of + * Vaadin 7. + * + * @return a collection of roots belonging to this application + * + * @since 7.0 + */ + public Collection<Root> getRoots() { + return Collections.unmodifiableCollection(roots.values()); + } + + private int connectorIdSequence = 0; + + /** + * Generate an id for the given Connector. Connectors must not call this + * method more than once, the first time they need an id. + * + * @param connector + * A connector that has not yet been assigned an id. + * @return A new id for the connector + */ + public String createConnectorId(ClientConnector connector) { + return String.valueOf(connectorIdSequence++); + } + + private static final Logger getLogger() { + return Logger.getLogger(Application.class.getName()); + } + + /** + * Returns a Root with the given id. + * <p> + * This is meant for framework internal use. + * </p> + * + * @param rootId + * The root id + * @return The root with the given id or null if not found + */ + public Root getRootById(int rootId) { + return roots.get(rootId); + } + + public void addBootstrapListener(BootstrapListener listener) { + eventRouter.addListener(BootstrapFragmentResponse.class, listener, + BOOTSTRAP_FRAGMENT_METHOD); + eventRouter.addListener(BootstrapPageResponse.class, listener, + BOOTSTRAP_PAGE_METHOD); + } + + public void removeBootstrapListener(BootstrapListener listener) { + eventRouter.removeListener(BootstrapFragmentResponse.class, listener, + BOOTSTRAP_FRAGMENT_METHOD); + eventRouter.removeListener(BootstrapPageResponse.class, listener, + BOOTSTRAP_PAGE_METHOD); + } + + public void modifyBootstrapResponse(BootstrapResponse response) { + eventRouter.fireEvent(response); + } +} diff --git a/server/src/com/vaadin/RootRequiresMoreInformationException.java b/server/src/com/vaadin/RootRequiresMoreInformationException.java new file mode 100644 index 0000000000..ed0fa41437 --- /dev/null +++ b/server/src/com/vaadin/RootRequiresMoreInformationException.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin; + +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedRequest.BrowserDetails; + +/** + * Exception that is thrown to indicate that creating or initializing the root + * requires information detailed from the web browser ({@link BrowserDetails}) + * to be present. + * + * This exception may not be thrown if that information is already present in + * the current WrappedRequest. + * + * @see Application#getRoot(WrappedRequest) + * @see WrappedRequest#getBrowserDetails() + * + * @since 7.0 + */ +public class RootRequiresMoreInformationException extends Exception { + // Nothing of interest here +} diff --git a/server/src/com/vaadin/Vaadin.gwt.xml b/server/src/com/vaadin/Vaadin.gwt.xml new file mode 100644 index 0000000000..07d7c941e6 --- /dev/null +++ b/server/src/com/vaadin/Vaadin.gwt.xml @@ -0,0 +1,85 @@ +<module> + <!-- This GWT module inherits all Vaadin client side functionality modules. + This is the module you want to inherit in your client side project to be + able to use com.vaadin.* classes. --> + + <!-- Hint for WidgetSetBuilder not to automatically update the file --> + <!-- WS Compiler: manually edited --> + + <inherits name="com.google.gwt.user.User" /> + + <inherits name="com.google.gwt.http.HTTP" /> + + <inherits name="com.google.gwt.json.JSON" /> + + <inherits name="com.vaadin.terminal.gwt.VaadinBrowserSpecificOverrides" /> + + <source path="terminal/gwt/client" /> + <source path="shared" /> + + <!-- Use own Scheduler implementation to be able to track if commands are + running --> + <replace-with class="com.vaadin.terminal.gwt.client.VSchedulerImpl"> + <when-type-is class="com.google.gwt.core.client.impl.SchedulerImpl" /> + </replace-with> + + <!-- Generators for serializators for classes used in communication between + server and client --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.SerializerMapGenerator"> + <when-type-is + class="com.vaadin.terminal.gwt.client.communication.SerializerMap" /> + </generate-with> + + <replace-with class="com.vaadin.terminal.gwt.client.VDebugConsole"> + <when-type-is class="com.vaadin.terminal.gwt.client.Console" /> + </replace-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.EagerWidgetMapGenerator"> + <when-type-is class="com.vaadin.terminal.gwt.client.WidgetMap" /> + </generate-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.AcceptCriteriaFactoryGenerator"> + <when-type-is + class="com.vaadin.terminal.gwt.client.ui.dd.VAcceptCriterionFactory" /> + </generate-with> + + <!-- Generate client side proxies for client to server RPC interfaces --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.RpcProxyGenerator"> + <when-type-assignable + class="com.vaadin.shared.communication.ServerRpc" /> + </generate-with> + + <!-- Generate client side proxies for client to server RPC interfaces --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.RpcProxyCreatorGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.communication.RpcProxy.RpcProxyCreator" /> + </generate-with> + + <!-- Generate client side RPC manager for server to client RPC --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.GeneratedRpcMethodProviderGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.communication.GeneratedRpcMethodProvider" /> + </generate-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.ConnectorWidgetFactoryGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.ui.ConnectorWidgetFactory" /> + </generate-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.ConnectorStateFactoryGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.ui.ConnectorStateFactory" /> + </generate-with> + + <!-- Use the new cross site linker to get a nocache.js without document.write --> + <add-linker name="xsiframe" /> + +</module> diff --git a/server/src/com/vaadin/Version.java b/server/src/com/vaadin/Version.java new file mode 100644 index 0000000000..eb6d73e7e0 --- /dev/null +++ b/server/src/com/vaadin/Version.java @@ -0,0 +1,74 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin; + +import java.io.Serializable; + +public class Version implements Serializable { + /** + * The version number of this release. For example "6.2.0". Always in the + * format "major.minor.revision[.build]". The build part is optional. All of + * major, minor, revision must be integers. + */ + private static final String VERSION; + /** + * Major version number. For example 6 in 6.2.0. + */ + private static final int VERSION_MAJOR; + + /** + * Minor version number. For example 2 in 6.2.0. + */ + private static final int VERSION_MINOR; + + /** + * Version revision number. For example 0 in 6.2.0. + */ + private static final int VERSION_REVISION; + + /** + * Build identifier. For example "nightly-20091123-c9963" in + * 6.2.0.nightly-20091123-c9963. + */ + private static final String VERSION_BUILD; + + /* Initialize version numbers from string replaced by build-script. */ + static { + if ("@VERSION@".equals("@" + "VERSION" + "@")) { + VERSION = "9.9.9.INTERNAL-DEBUG-BUILD"; + } else { + VERSION = "@VERSION@"; + } + final String[] digits = VERSION.split("\\.", 4); + VERSION_MAJOR = Integer.parseInt(digits[0]); + VERSION_MINOR = Integer.parseInt(digits[1]); + VERSION_REVISION = Integer.parseInt(digits[2]); + if (digits.length == 4) { + VERSION_BUILD = digits[3]; + } else { + VERSION_BUILD = ""; + } + } + + public static String getFullVersion() { + return VERSION; + } + + public static int getMajorVersion() { + return VERSION_MAJOR; + } + + public static int getMinorVersion() { + return VERSION_MINOR; + } + + public static int getRevision() { + return VERSION_REVISION; + } + + public static String getBuildIdentifier() { + return VERSION_BUILD; + } + +} diff --git a/server/src/com/vaadin/annotations/AutoGenerated.java b/server/src/com/vaadin/annotations/AutoGenerated.java new file mode 100644 index 0000000000..72c9b62a91 --- /dev/null +++ b/server/src/com/vaadin/annotations/AutoGenerated.java @@ -0,0 +1,18 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.annotations; + +/** + * Marker annotation for automatically generated code elements. + * + * These elements may be modified or removed by code generation. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.0 + */ +public @interface AutoGenerated { + +} diff --git a/server/src/com/vaadin/annotations/EagerInit.java b/server/src/com/vaadin/annotations/EagerInit.java new file mode 100644 index 0000000000..c7c2702d2a --- /dev/null +++ b/server/src/com/vaadin/annotations/EagerInit.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.ui.Root; + +/** + * Indicates that the init method in a Root class can be called before full + * browser details ({@link WrappedRequest#getBrowserDetails()}) are available. + * This will make the UI appear more quickly, as ensuring the availability of + * this information typically requires an additional round trip to the client. + * + * @see Root#init(com.vaadin.terminal.WrappedRequest) + * @see WrappedRequest#getBrowserDetails() + * + * @since 7.0 + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EagerInit { + // No values +} diff --git a/server/src/com/vaadin/annotations/JavaScript.java b/server/src/com/vaadin/annotations/JavaScript.java new file mode 100644 index 0000000000..357bcc3649 --- /dev/null +++ b/server/src/com/vaadin/annotations/JavaScript.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * If this annotation is present on a {@link ClientConnector} class, the + * framework ensures the referenced JavaScript files are loaded before the init + * method for the corresponding client-side connector is invoked. + * <p> + * Absolute URLs including protocol and host are used as is on the client-side. + * Relative urls are mapped to APP/CONNECTOR/[url] which are by default served + * from the classpath relative to the class where the annotation is defined. + * <p> + * Example: {@code @JavaScript( "http://host.com/file1.js", "file2.js"})} on the + * class com.example.MyConnector would load the file http://host.com/file1.js as + * is and file2.js from /com/example/file2.js on the server's classpath using + * the ClassLoader that was used to load com.example.MyConnector. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface JavaScript { + /** + * JavaScript files to load before initializing the client-side connector. + * + * @return an array of JavaScript file urls + */ + public String[] value(); +} diff --git a/server/src/com/vaadin/annotations/StyleSheet.java b/server/src/com/vaadin/annotations/StyleSheet.java new file mode 100644 index 0000000000..d082cb8d30 --- /dev/null +++ b/server/src/com/vaadin/annotations/StyleSheet.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * If this annotation is present on a {@link ClientConnector} class, the + * framework ensures the referenced style sheets are loaded before the init + * method for the corresponding client-side connector is invoked. + * <p> + * Example: {@code @StyleSheet( "http://host.com/file1.css", "file2.css"})} on + * the class com.example.MyConnector would load the file + * http://host.com/file1.css as is and file2.css from /com/example/file2.css on + * the server's classpath using the ClassLoader that was used to load + * com.example.MyConnector. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface StyleSheet { + /** + * Style sheets to load before initializing the client-side connector. + * + * @return an array of style sheet urls + */ + public String[] value(); +} diff --git a/server/src/com/vaadin/annotations/Theme.java b/server/src/com/vaadin/annotations/Theme.java new file mode 100644 index 0000000000..7c62b07741 --- /dev/null +++ b/server/src/com/vaadin/annotations/Theme.java @@ -0,0 +1,24 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.ui.Root; + +/** + * Defines a specific theme for a {@link Root}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Theme { + /** + * @return simple name of the theme + */ + public String value(); +} diff --git a/server/src/com/vaadin/annotations/Widgetset.java b/server/src/com/vaadin/annotations/Widgetset.java new file mode 100644 index 0000000000..99113f73f9 --- /dev/null +++ b/server/src/com/vaadin/annotations/Widgetset.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.ui.Root; + +/** + * Defines a specific theme for a {@link Root}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Widgetset { + /** + * @return name of the widgetset + */ + public String value(); + +} diff --git a/server/src/com/vaadin/annotations/package.html b/server/src/com/vaadin/annotations/package.html new file mode 100644 index 0000000000..d789e9b5df --- /dev/null +++ b/server/src/com/vaadin/annotations/package.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> +</head> + +<body bgcolor="white"> + +<p>Contains annotations used in Vaadin. Note that some annotations +are also found in other packages e.g., {@link com.vaadin.ui.ClientWidget}.</p> + +</body> +</html> diff --git a/server/src/com/vaadin/data/Buffered.java b/server/src/com/vaadin/data/Buffered.java new file mode 100644 index 0000000000..1387cb965b --- /dev/null +++ b/server/src/com/vaadin/data/Buffered.java @@ -0,0 +1,280 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; + +import com.vaadin.data.Validator.InvalidValueException; + +/** + * <p> + * Defines the interface to commit and discard changes to an object, supporting + * read-through and write-through modes. + * </p> + * + * <p> + * <i>Read-through mode</i> means that the value read from the buffered object + * is constantly up to date with the data source. <i>Write-through</i> mode + * means that all changes to the object are immediately updated to the data + * source. + * </p> + * + * <p> + * Since these modes are independent, their combinations may result in some + * behaviour that may sound surprising. + * </p> + * + * <p> + * For example, if a <code>Buffered</code> object is in read-through mode but + * not in write-through mode, the result is an object whose value is updated + * directly from the data source only if it's not locally modified. If the value + * is locally modified, retrieving the value from the object would result in a + * value that is different than the one stored in the data source, even though + * the object is in read-through mode. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Buffered extends Serializable { + + /** + * Updates all changes since the previous commit to the data source. The + * value stored in the object will always be updated into the data source + * when <code>commit</code> is called. + * + * @throws SourceException + * if the operation fails because of an exception is thrown by + * the data source. The cause is included in the exception. + * @throws InvalidValueException + * if the operation fails because validation is enabled and the + * values do not validate + */ + public void commit() throws SourceException, InvalidValueException; + + /** + * Discards all changes since last commit. The object updates its value from + * the data source. + * + * @throws SourceException + * if the operation fails because of an exception is thrown by + * the data source. The cause is included in the exception. + */ + public void discard() throws SourceException; + + /** + * Tests if the object is in write-through mode. If the object is in + * write-through mode, all modifications to it will result in + * <code>commit</code> being called after the modification. + * + * @return <code>true</code> if the object is in write-through mode, + * <code>false</code> if it's not. + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Deprecated + public boolean isWriteThrough(); + + /** + * Sets the object's write-through mode to the specified status. When + * switching the write-through mode on, the <code>commit</code> operation + * will be performed. + * + * @param writeThrough + * Boolean value to indicate if the object should be in + * write-through mode after the call. + * @throws SourceException + * If the operation fails because of an exception is thrown by + * the data source. + * @throws InvalidValueException + * If the implicit commit operation fails because of a + * validation error. + * + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Deprecated + public void setWriteThrough(boolean writeThrough) throws SourceException, + InvalidValueException; + + /** + * Tests if the object is in read-through mode. If the object is in + * read-through mode, retrieving its value will result in the value being + * first updated from the data source to the object. + * <p> + * The only exception to this rule is that when the object is not in + * write-through mode and it's buffer contains a modified value, the value + * retrieved from the object will be the locally modified value in the + * buffer which may differ from the value in the data source. + * </p> + * + * @return <code>true</code> if the object is in read-through mode, + * <code>false</code> if it's not. + * @deprecated Use {@link #isBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Deprecated + public boolean isReadThrough(); + + /** + * Sets the object's read-through mode to the specified status. When + * switching read-through mode on, the object's value is updated from the + * data source. + * + * @param readThrough + * Boolean value to indicate if the object should be in + * read-through mode after the call. + * + * @throws SourceException + * If the operation fails because of an exception is thrown by + * the data source. The cause is included in the exception. + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Deprecated + public void setReadThrough(boolean readThrough) throws SourceException; + + /** + * Sets the object's buffered mode to the specified status. + * <p> + * When the object is in buffered mode, an internal buffer will be used to + * store changes until {@link #commit()} is called. Calling + * {@link #discard()} will revert the internal buffer to the value of the + * data source. + * </p> + * <p> + * This is an easier way to use {@link #setReadThrough(boolean)} and + * {@link #setWriteThrough(boolean)} and not as error prone. Changing + * buffered mode will change both the read through and write through state + * of the object. + * </p> + * <p> + * Mixing calls to {@link #setBuffered(boolean)}/{@link #isBuffered()} and + * {@link #setReadThrough(boolean)}/{@link #isReadThrough()} or + * {@link #setWriteThrough(boolean)}/{@link #isWriteThrough()} is generally + * a bad idea. + * </p> + * + * @param buffered + * true if buffered mode should be turned on, false otherwise + * @since 7.0 + */ + public void setBuffered(boolean buffered); + + /** + * Checks the buffered mode of this Object. + * <p> + * This method only returns true if both read and write buffering is used. + * </p> + * + * @return true if buffered mode is on, false otherwise + * @since 7.0 + */ + public boolean isBuffered(); + + /** + * Tests if the value stored in the object has been modified since it was + * last updated from the data source. + * + * @return <code>true</code> if the value in the object has been modified + * since the last data source update, <code>false</code> if not. + */ + public boolean isModified(); + + /** + * An exception that signals that one or more exceptions occurred while a + * buffered object tried to access its data source or if there is a problem + * in processing a data source. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("serial") + public class SourceException extends RuntimeException implements + Serializable { + + /** Source class implementing the buffered interface */ + private final Buffered source; + + /** Original cause of the source exception */ + private Throwable[] causes = {}; + + /** + * Creates a source exception that does not include a cause. + * + * @param source + * the source object implementing the Buffered interface. + */ + public SourceException(Buffered source) { + this.source = source; + } + + /** + * Creates a source exception from a cause exception. + * + * @param source + * the source object implementing the Buffered interface. + * @param cause + * the original cause for this exception. + */ + public SourceException(Buffered source, Throwable cause) { + this.source = source; + causes = new Throwable[] { cause }; + } + + /** + * Creates a source exception from multiple causes. + * + * @param source + * the source object implementing the Buffered interface. + * @param causes + * the original causes for this exception. + */ + public SourceException(Buffered source, Throwable[] causes) { + this.source = source; + this.causes = causes; + } + + /** + * Gets the cause of the exception. + * + * @return The (first) cause for the exception, null if no cause. + */ + @Override + public final Throwable getCause() { + if (causes.length == 0) { + return null; + } + return causes[0]; + } + + /** + * Gets all the causes for this exception. + * + * @return throwables that caused this exception + */ + public final Throwable[] getCauses() { + return causes; + } + + /** + * Gets a source of the exception. + * + * @return the Buffered object which generated this exception. + */ + public Buffered getSource() { + return source; + } + + } +} diff --git a/server/src/com/vaadin/data/BufferedValidatable.java b/server/src/com/vaadin/data/BufferedValidatable.java new file mode 100644 index 0000000000..ce1d44fce6 --- /dev/null +++ b/server/src/com/vaadin/data/BufferedValidatable.java @@ -0,0 +1,35 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; + +/** + * <p> + * This interface defines the combination of <code>Validatable</code> and + * <code>Buffered</code> interfaces. The combination of the interfaces defines + * if the invalid data is committed to datasource. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface BufferedValidatable extends Buffered, Validatable, + Serializable { + + /** + * Tests if the invalid data is committed to datasource. The default is + * <code>false</code>. + */ + public boolean isInvalidCommitted(); + + /** + * Sets if the invalid data should be committed to datasource. The default + * is <code>false</code>. + */ + public void setInvalidCommitted(boolean isCommitted); +} diff --git a/server/src/com/vaadin/data/Collapsible.java b/server/src/com/vaadin/data/Collapsible.java new file mode 100644 index 0000000000..06c96b7ea7 --- /dev/null +++ b/server/src/com/vaadin/data/Collapsible.java @@ -0,0 +1,68 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data; + +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.Container.Ordered; + +/** + * Container needed by large lazy loading hierarchies displayed e.g. in + * TreeTable. + * <p> + * Container of this type gets notified when a subtree is opened/closed in a + * component displaying its content. This allows container to lazy load subtrees + * and release memory when a sub-tree is no longer displayed. + * <p> + * Methods from {@link Container.Ordered} (and from {@linkContainer.Indexed} if + * implemented) are expected to work as in "preorder" of the currently visible + * hierarchy. This means for example that the return value of size method + * changes when subtree is collapsed/expanded. In other words items in collapsed + * sub trees should be "ignored" by container when the container is accessed + * with methods introduced in {@link Container.Ordered} or + * {@linkContainer.Indexed}. From the accessors point of view, items in + * collapsed subtrees don't exist. + * <p> + * + */ +public interface Collapsible extends Hierarchical, Ordered { + + /** + * <p> + * Collapsing the {@link Item} indicated by <code>itemId</code> hides all + * children, and their respective children, from the {@link Container}. + * </p> + * + * <p> + * If called on a leaf {@link Item}, this method does nothing. + * </p> + * + * @param itemId + * the identifier of the collapsed {@link Item} + * @param collapsed + * <code>true</code> if you want to collapse the children below + * this {@link Item}. <code>false</code> if you want to + * uncollapse the children. + */ + public void setCollapsed(Object itemId, boolean collapsed); + + /** + * <p> + * Checks whether the {@link Item}, identified by <code>itemId</code> is + * collapsed or not. + * </p> + * + * <p> + * If an {@link Item} is "collapsed" its children are not included in + * methods used to list Items in this container. + * </p> + * + * @param itemId + * The {@link Item}'s identifier that is to be checked. + * @return <code>true</code> iff the {@link Item} identified by + * <code>itemId</code> is currently collapsed, otherwise + * <code>false</code>. + */ + public boolean isCollapsed(Object itemId); + +} diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java new file mode 100644 index 0000000000..f4c0ed9794 --- /dev/null +++ b/server/src/com/vaadin/data/Container.java @@ -0,0 +1,1105 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Collection; + +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * <p> + * A specialized set of identified Items. Basically the Container is a set of + * {@link Item}s, but it imposes certain constraints on its contents. These + * constraints state the following: + * </p> + * + * <ul> + * <li>All Items in the Container must have the same number of Properties. + * <li>All Items in the Container must have the same Property ID's (see + * {@link Item#getItemPropertyIds()}). + * <li>All Properties in the Items corresponding to the same Property ID must + * have the same data type. + * <li>All Items within a container are uniquely identified by their non-null + * IDs. + * </ul> + * + * <p> + * The Container can be visualized as a representation of a relational database + * table. Each Item in the Container represents a row in the table, and all + * cells in a column (identified by a Property ID) have the same data type. Note + * that as with the cells in a database table, no Property in a Container may be + * empty, though they may contain <code>null</code> values. + * </p> + * + * <p> + * Note that though uniquely identified, the Items in a Container are not + * necessarily {@link Container.Ordered ordered} or {@link Container.Indexed + * indexed}. + * </p> + * + * <p> + * Containers can derive Item ID's from the item properties or use other, + * container specific or user specified identifiers. + * </p> + * + * <p> + * If a container is {@link Filterable filtered} or {@link Sortable sorted}, + * most of the the methods of the container interface and its subinterfaces + * (container size, {@link #containsId(Object)}, iteration and indices etc.) + * relate to the filtered and sorted view, not to the full container contents. + * See individual method javadoc for exceptions to this (adding and removing + * items). + * </p> + * + * <p> + * <img src=doc-files/Container_full.gif> + * </p> + * + * <p> + * The Container interface is split to several subinterfaces so that a class can + * implement only the ones it needs. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Container extends Serializable { + + /** + * Gets the {@link Item} with the given Item ID from the Container. If the + * Container does not contain the requested Item, <code>null</code> is + * returned. + * + * Containers should not return Items that are filtered out. + * + * @param itemId + * ID of the {@link Item} to retrieve + * @return the {@link Item} with the given ID or <code>null</code> if the + * Item is not found in the Container + */ + public Item getItem(Object itemId); + + /** + * Gets the ID's of all Properties stored in the Container. The ID's cannot + * be modified through the returned collection. + * + * @return unmodifiable collection of Property IDs + */ + public Collection<?> getContainerPropertyIds(); + + /** + * Gets the ID's of all visible (after filtering and sorting) Items stored + * in the Container. The ID's cannot be modified through the returned + * collection. + * + * If the container is {@link Ordered}, the collection returned by this + * method should follow that order. If the container is {@link Sortable}, + * the items should be in the sorted order. + * + * Calling this method for large lazy containers can be an expensive + * operation and should be avoided when practical. + * + * @return unmodifiable collection of Item IDs + */ + public Collection<?> getItemIds(); + + /** + * Gets the Property identified by the given itemId and propertyId from the + * Container. If the Container does not contain the item or it is filtered + * out, or the Container does not have the Property, <code>null</code> is + * returned. + * + * @param itemId + * ID of the visible Item which contains the Property + * @param propertyId + * ID of the Property to retrieve + * @return Property with the given ID or <code>null</code> + */ + public Property<?> getContainerProperty(Object itemId, Object propertyId); + + /** + * Gets the data type of all Properties identified by the given Property ID. + * + * @param propertyId + * ID identifying the Properties + * @return data type of the Properties + */ + public Class<?> getType(Object propertyId); + + /** + * Gets the number of visible Items in the Container. + * + * Filtering can hide items so that they will not be visible through the + * container API. + * + * @return number of Items in the Container + */ + public int size(); + + /** + * Tests if the Container contains the specified Item. + * + * Filtering can hide items so that they will not be visible through the + * container API, and this method should respect visibility of items (i.e. + * only indicate visible items as being in the container) if feasible for + * the container. + * + * @param itemId + * ID the of Item to be tested + * @return boolean indicating if the Container holds the specified Item + */ + public boolean containsId(Object itemId); + + /** + * Creates a new Item with the given ID in the Container. + * + * <p> + * The new Item is returned, and it is ready to have its Properties + * modified. Returns <code>null</code> if the operation fails or the + * Container already contains a Item with the given ID. + * </p> + * + * <p> + * This functionality is optional. + * </p> + * + * @param itemId + * ID of the Item to be created + * @return Created new Item, or <code>null</code> in case of a failure + * @throws UnsupportedOperationException + * if adding an item with an explicit item ID is not supported + * by the container + */ + public Item addItem(Object itemId) throws UnsupportedOperationException; + + /** + * Creates a new Item into the Container, and assign it an automatic ID. + * + * <p> + * The new ID is returned, or <code>null</code> if the operation fails. + * After a successful call you can use the {@link #getItem(Object ItemId) + * <code>getItem</code>}method to fetch the Item. + * </p> + * + * <p> + * This functionality is optional. + * </p> + * + * @return ID of the newly created Item, or <code>null</code> in case of a + * failure + * @throws UnsupportedOperationException + * if adding an item without an explicit item ID is not + * supported by the container + */ + public Object addItem() throws UnsupportedOperationException; + + /** + * Removes the Item identified by <code>ItemId</code> from the Container. + * + * <p> + * Containers that support filtering should also allow removing an item that + * is currently filtered out. + * </p> + * + * <p> + * This functionality is optional. + * </p> + * + * @param itemId + * ID of the Item to remove + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the container does not support removing individual items + */ + public boolean removeItem(Object itemId) + throws UnsupportedOperationException; + + /** + * Adds a new Property to all Items in the Container. The Property ID, data + * type and default value of the new Property are given as parameters. + * + * This functionality is optional. + * + * @param propertyId + * ID of the Property + * @param type + * Data type of the new Property + * @param defaultValue + * The value all created Properties are initialized to + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the container does not support explicitly adding container + * properties + */ + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException; + + /** + * Removes a Property specified by the given Property ID from the Container. + * Note that the Property will be removed from all Items in the Container. + * + * This functionality is optional. + * + * @param propertyId + * ID of the Property to remove + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the container does not support removing container + * properties + */ + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException; + + /** + * Removes all Items from the Container. + * + * <p> + * Note that Property ID and type information is preserved. This + * functionality is optional. + * </p> + * + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the container does not support removing all items + */ + public boolean removeAllItems() throws UnsupportedOperationException; + + /** + * Interface for Container classes whose {@link Item}s can be traversed in + * order. + * + * <p> + * If the container is filtered or sorted, the traversal applies to the + * filtered and sorted view. + * </p> + * <p> + * The <code>addItemAfter()</code> methods should apply filters to the added + * item after inserting it, possibly hiding it immediately. If the container + * is being sorted, they may add items at the correct sorted position + * instead of the given position. See also {@link Filterable} and + * {@link Sortable} for more information. + * </p> + */ + public interface Ordered extends Container { + + /** + * Gets the ID of the Item following the Item that corresponds to + * <code>itemId</code>. If the given Item is the last or not found in + * the Container, <code>null</code> is returned. + * + * @param itemId + * ID of a visible Item in the Container + * @return ID of the next visible Item or <code>null</code> + */ + public Object nextItemId(Object itemId); + + /** + * Gets the ID of the Item preceding the Item that corresponds to + * <code>itemId</code>. If the given Item is the first or not found in + * the Container, <code>null</code> is returned. + * + * @param itemId + * ID of a visible Item in the Container + * @return ID of the previous visible Item or <code>null</code> + */ + public Object prevItemId(Object itemId); + + /** + * Gets the ID of the first Item in the Container. + * + * @return ID of the first visible Item in the Container + */ + public Object firstItemId(); + + /** + * Gets the ID of the last Item in the Container.. + * + * @return ID of the last visible Item in the Container + */ + public Object lastItemId(); + + /** + * Tests if the Item corresponding to the given Item ID is the first + * Item in the Container. + * + * @param itemId + * ID of an Item in the Container + * @return <code>true</code> if the Item is first visible item in the + * Container, <code>false</code> if not + */ + public boolean isFirstId(Object itemId); + + /** + * Tests if the Item corresponding to the given Item ID is the last Item + * in the Container. + * + * @return <code>true</code> if the Item is last visible item in the + * Container, <code>false</code> if not + */ + public boolean isLastId(Object itemId); + + /** + * Adds a new item after the given item. + * <p> + * Adding an item after null item adds the item as first item of the + * ordered container. + * </p> + * + * @see Ordered Ordered: adding items in filtered or sorted containers + * + * @param previousItemId + * Id of the visible item in ordered container after which to + * insert the new item. + * @return item id the the created new item or null if the operation + * fails. + * @throws UnsupportedOperationException + * if the operation is not supported by the container + */ + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException; + + /** + * Adds a new item after the given item. + * <p> + * Adding an item after null item adds the item as first item of the + * ordered container. + * </p> + * + * @see Ordered Ordered: adding items in filtered or sorted containers + * + * @param previousItemId + * Id of the visible item in ordered container after which to + * insert the new item. + * @param newItemId + * Id of the new item to be added. + * @return new item or null if the operation fails. + * @throws UnsupportedOperationException + * if the operation is not supported by the container + */ + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException; + + } + + /** + * Interface for Container classes whose {@link Item}s can be sorted. + * <p> + * When an {@link Ordered} or {@link Indexed} container is sorted, all + * relevant operations of these interfaces should only use the filtered and + * sorted contents and the filtered indices to the container. Indices or + * item identifiers in the public API refer to the visible view unless + * otherwise stated. However, the <code>addItem*()</code> methods may add + * items that will be filtered out after addition or moved to another + * position based on sorting. + * </p> + * <p> + * How sorting is performed when a {@link Hierarchical} container implements + * {@link Sortable} is implementation specific and should be documented in + * the implementing class. However, the recommended approach is sorting the + * roots and the sets of children of each item separately. + * </p> + * <p> + * Depending on the container type, sorting a container may permanently + * change the internal order of items in the container. + * </p> + */ + public interface Sortable extends Ordered { + + /** + * Sort method. + * + * Sorts the container items. + * + * Sorting a container can irreversibly change the order of its items or + * only change the order temporarily, depending on the container. + * + * @param propertyId + * Array of container property IDs, whose values are used to + * sort the items in container as primary, secondary, ... + * sorting criterion. All of the item IDs must be in the + * collection returned by + * {@link #getSortableContainerPropertyIds()} + * @param ascending + * Array of sorting order flags corresponding to each + * property ID used in sorting. If this array is shorter than + * propertyId array, ascending order is assumed for items + * where the order is not specified. Use <code>true</code> to + * sort in ascending order, <code>false</code> to use + * descending order. + */ + void sort(Object[] propertyId, boolean[] ascending); + + /** + * Gets the container property IDs which can be used to sort the items. + * + * @return the IDs of the properties that can be used for sorting the + * container + */ + Collection<?> getSortableContainerPropertyIds(); + + } + + /** + * Interface for Container classes whose {@link Item}s can be accessed by + * their position in the container. + * <p> + * If the container is filtered or sorted, all indices refer to the filtered + * and sorted view. However, the <code>addItemAt()</code> methods may add + * items that will be filtered out after addition or moved to another + * position based on sorting. + * </p> + */ + public interface Indexed extends Ordered { + + /** + * Gets the index of the Item corresponding to the itemId. The following + * is <code>true</code> for the returned index: 0 <= index < size(), or + * index = -1 if there is no visible item with that id in the container. + * + * @param itemId + * ID of an Item in the Container + * @return index of the Item, or -1 if (the filtered and sorted view of) + * the Container does not include the Item + */ + public int indexOfId(Object itemId); + + /** + * Gets the ID of an Item by an index number. + * + * @param index + * Index of the requested id in (the filtered and sorted view + * of) the Container + * @return ID of the Item in the given index + */ + public Object getIdByIndex(int index); + + /** + * Adds a new item at given index (in the filtered view). + * <p> + * The indices of the item currently in the given position and all the + * following items are incremented. + * </p> + * <p> + * This method should apply filters to the added item after inserting + * it, possibly hiding it immediately. If the container is being sorted, + * the item may be added at the correct sorted position instead of the + * given position. See {@link Indexed}, {@link Ordered}, + * {@link Filterable} and {@link Sortable} for more information. + * </p> + * + * @param index + * Index (in the filtered and sorted view) to add the new + * item. + * @return item id of the created item or null if the operation fails. + * @throws UnsupportedOperationException + * if the operation is not supported by the container + */ + public Object addItemAt(int index) throws UnsupportedOperationException; + + /** + * Adds a new item at given index (in the filtered view). + * <p> + * The indexes of the item currently in the given position and all the + * following items are incremented. + * </p> + * <p> + * This method should apply filters to the added item after inserting + * it, possibly hiding it immediately. If the container is being sorted, + * the item may be added at the correct sorted position instead of the + * given position. See {@link Indexed}, {@link Filterable} and + * {@link Sortable} for more information. + * </p> + * + * @param index + * Index (in the filtered and sorted view) at which to add + * the new item. + * @param newItemId + * Id of the new item to be added. + * @return new {@link Item} or null if the operation fails. + * @throws UnsupportedOperationException + * if the operation is not supported by the container + */ + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException; + + } + + /** + * <p> + * Interface for <code>Container</code> classes whose Items can be arranged + * hierarchically. This means that the Items in the container belong in a + * tree-like structure, with the following quirks: + * </p> + * + * <ul> + * <li>The Item structure may have more than one root elements + * <li>The Items in the hierarchy can be declared explicitly to be able or + * unable to have children. + * </ul> + */ + public interface Hierarchical extends Container { + + /** + * Gets the IDs of all Items that are children of the specified Item. + * The returned collection is unmodifiable. + * + * @param itemId + * ID of the Item whose children the caller is interested in + * @return An unmodifiable {@link java.util.Collection collection} + * containing the IDs of all other Items that are children in + * the container hierarchy + */ + public Collection<?> getChildren(Object itemId); + + /** + * Gets the ID of the parent Item of the specified Item. + * + * @param itemId + * ID of the Item whose parent the caller wishes to find out. + * @return the ID of the parent Item. Will be <code>null</code> if the + * specified Item is a root element. + */ + public Object getParent(Object itemId); + + /** + * Gets the IDs of all Items in the container that don't have a parent. + * Such items are called <code>root</code> Items. The returned + * collection is unmodifiable. + * + * @return An unmodifiable {@link java.util.Collection collection} + * containing IDs of all root elements of the container + */ + public Collection<?> rootItemIds(); + + /** + * <p> + * Sets the parent of an Item. The new parent item must exist and be + * able to have children. ( + * <code>{@link #areChildrenAllowed(Object)} == true</code> ). It is + * also possible to detach a node from the hierarchy (and thus make it + * root) by setting the parent <code>null</code>. + * </p> + * + * <p> + * This operation is optional. + * </p> + * + * @param itemId + * ID of the item to be set as the child of the Item + * identified with <code>newParentId</code> + * @param newParentId + * ID of the Item that's to be the new parent of the Item + * identified with <code>itemId</code> + * @return <code>true</code> if the operation succeeded, + * <code>false</code> if not + */ + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException; + + /** + * Tests if the Item with given ID can have children. + * + * @param itemId + * ID of the Item in the container whose child capability is + * to be tested + * @return <code>true</code> if the specified Item exists in the + * Container and it can have children, <code>false</code> if + * it's not found from the container or it can't have children. + */ + public boolean areChildrenAllowed(Object itemId); + + /** + * <p> + * Sets the given Item's capability to have children. If the Item + * identified with <code>itemId</code> already has children and + * <code>{@link #areChildrenAllowed(Object)}</code> is false this method + * fails and <code>false</code> is returned. + * </p> + * <p> + * The children must be first explicitly removed with + * {@link #setParent(Object itemId, Object newParentId)}or + * {@link com.vaadin.data.Container#removeItem(Object itemId)}. + * </p> + * + * <p> + * This operation is optional. If it is not implemented, the method + * always returns <code>false</code>. + * </p> + * + * @param itemId + * ID of the Item in the container whose child capability is + * to be set + * @param areChildrenAllowed + * boolean value specifying if the Item can have children or + * not + * @return <code>true</code> if the operation succeeded, + * <code>false</code> if not + */ + public boolean setChildrenAllowed(Object itemId, + boolean areChildrenAllowed) + throws UnsupportedOperationException; + + /** + * Tests if the Item specified with <code>itemId</code> is a root Item. + * The hierarchical container can have more than one root and must have + * at least one unless it is empty. The {@link #getParent(Object itemId)} + * method always returns <code>null</code> for root Items. + * + * @param itemId + * ID of the Item whose root status is to be tested + * @return <code>true</code> if the specified Item is a root, + * <code>false</code> if not + */ + public boolean isRoot(Object itemId); + + /** + * <p> + * Tests if the Item specified with <code>itemId</code> has child Items + * or if it is a leaf. The {@link #getChildren(Object itemId)} method + * always returns <code>null</code> for leaf Items. + * </p> + * + * <p> + * Note that being a leaf does not imply whether or not an Item is + * allowed to have children. + * </p> + * . + * + * @param itemId + * ID of the Item to be tested + * @return <code>true</code> if the specified Item has children, + * <code>false</code> if not (is a leaf) + */ + public boolean hasChildren(Object itemId); + + /** + * <p> + * Removes the Item identified by <code>ItemId</code> from the + * Container. + * </p> + * + * <p> + * Note that this does not remove any children the item might have. + * </p> + * + * @param itemId + * ID of the Item to remove + * @return <code>true</code> if the operation succeeded, + * <code>false</code> if not + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException; + } + + /** + * Interface that is implemented by containers which allow reducing their + * visible contents based on a set of filters. This interface has been + * renamed from {@link Filterable}, and implementing the new + * {@link Filterable} instead of or in addition to {@link SimpleFilterable} + * is recommended. This interface might be removed in future Vaadin + * versions. + * <p> + * When a set of filters are set, only items that match all the filters are + * included in the visible contents of the container. Still new items that + * do not match filters can be added to the container. Multiple filters can + * be added and the container remembers the state of the filters. When + * multiple filters are added, all filters must match for an item to be + * visible in the container. + * </p> + * <p> + * When an {@link Ordered} or {@link Indexed} container is filtered, all + * operations of these interfaces should only use the filtered contents and + * the filtered indices to the container. + * </p> + * <p> + * How filtering is performed when a {@link Hierarchical} container + * implements {@link SimpleFilterable} is implementation specific and should + * be documented in the implementing class. + * </p> + * <p> + * Adding items (if supported) to a filtered {@link Ordered} or + * {@link Indexed} container should insert them immediately after the + * indicated visible item. The unfiltered position of items added at index + * 0, at index {@link com.vaadin.data.Container#size()} or at an undefined + * position is up to the implementation. + * </p> + * <p> + * The functionality of SimpleFilterable can be implemented using the + * {@link Filterable} API and {@link SimpleStringFilter}. + * </p> + * + * @since 5.0 (renamed from Filterable to SimpleFilterable in 6.6) + */ + public interface SimpleFilterable extends Container, Serializable { + + /** + * Add a filter for given property. + * + * The API {@link Filterable#addContainerFilter(Filter)} is recommended + * instead of this method. A {@link SimpleStringFilter} can be used with + * the new API to implement the old string filtering functionality. + * + * The filter accepts items for which toString() of the value of the + * given property contains or starts with given filterString. Other + * items are not visible in the container when filtered. + * + * If a container has multiple filters, only items accepted by all + * filters are visible. + * + * @param propertyId + * Property for which the filter is applied to. + * @param filterString + * String that must match the value of the property + * @param ignoreCase + * Determine if the casing can be ignored when comparing + * strings. + * @param onlyMatchPrefix + * Only match prefixes; no other matches are included. + */ + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix); + + /** + * Remove all filters from all properties. + */ + public void removeAllContainerFilters(); + + /** + * Remove all filters from the given property. + * + * @param propertyId + * for which to remove filters + */ + public void removeContainerFilters(Object propertyId); + } + + /** + * Filter interface for container filtering. + * + * If a filter does not support in-memory filtering, + * {@link #passesFilter(Item)} should throw + * {@link UnsupportedOperationException}. + * + * Lazy containers must be able to map filters to their internal + * representation (e.g. SQL or JPA 2.0 Criteria). + * + * An {@link UnsupportedFilterException} can be thrown by the container if a + * particular filter is not supported by the container. + * + * An {@link Filter} should implement {@link #equals(Object)} and + * {@link #hashCode()} correctly to avoid duplicate filter registrations + * etc. + * + * @see Filterable + * + * @since 6.6 + */ + public interface Filter extends Serializable { + + /** + * Check if an item passes the filter (in-memory filtering). + * + * @param itemId + * identifier of the item being filtered; may be null when + * the item is being added to the container + * @param item + * the item being filtered + * @return true if the item is accepted by this filter + * @throws UnsupportedOperationException + * if the filter cannot be used for in-memory filtering + */ + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException; + + /** + * Check if a change in the value of a property can affect the filtering + * result. May always return true, at the cost of performance. + * + * If the filter cannot determine whether it may depend on the property + * or not, should return true. + * + * @param propertyId + * @return true if the filtering result may/does change based on changes + * to the property identified by propertyId + */ + public boolean appliesToProperty(Object propertyId); + + } + + /** + * Interface that is implemented by containers which allow reducing their + * visible contents based on a set of filters. + * <p> + * When a set of filters are set, only items that match all the filters are + * included in the visible contents of the container. Still new items that + * do not match filters can be added to the container. Multiple filters can + * be added and the container remembers the state of the filters. When + * multiple filters are added, all filters must match for an item to be + * visible in the container. + * </p> + * <p> + * When an {@link Ordered} or {@link Indexed} container is filtered, all + * operations of these interfaces should only use the filtered and sorted + * contents and the filtered indices to the container. Indices or item + * identifiers in the public API refer to the visible view unless otherwise + * stated. However, the <code>addItem*()</code> methods may add items that + * will be filtered out after addition or moved to another position based on + * sorting. + * </p> + * <p> + * How filtering is performed when a {@link Hierarchical} container + * implements {@link Filterable} is implementation specific and should be + * documented in the implementing class. + * </p> + * <p> + * Adding items (if supported) to a filtered {@link Ordered} or + * {@link Indexed} container should insert them immediately after the + * indicated visible item. However, the unfiltered position of items added + * at index 0, at index {@link com.vaadin.data.Container#size()} or at an + * undefined position is up to the implementation. + * </p> + * + * <p> + * This API replaces the old Filterable interface, renamed to + * {@link SimpleFilterable} in Vaadin 6.6. + * </p> + * + * @since 6.6 + */ + public interface Filterable extends Container, Serializable { + /** + * Adds a filter for the container. + * + * If a container has multiple filters, only items accepted by all + * filters are visible. + * + * @throws UnsupportedFilterException + * if the filter is not supported by the container + */ + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException; + + /** + * Removes a filter from the container. + * + * This requires that the equals() method considers the filters as + * equivalent (same instance or properly implemented equals() method). + */ + public void removeContainerFilter(Filter filter); + + /** + * Remove all active filters from the container. + */ + public void removeAllContainerFilters(); + + } + + /** + * Interface implemented by viewer classes capable of using a Container as a + * data source. + */ + public interface Viewer extends Serializable { + + /** + * Sets the Container that serves as the data source of the viewer. + * + * @param newDataSource + * The new data source Item + */ + public void setContainerDataSource(Container newDataSource); + + /** + * Gets the Container serving as the data source of the viewer. + * + * @return data source Container + */ + public Container getContainerDataSource(); + + } + + /** + * <p> + * Interface implemented by the editor classes supporting editing the + * Container. Implementing this interface means that the Container serving + * as the data source of the editor can be modified through it. + * </p> + * <p> + * Note that not implementing the <code>Container.Editor</code> interface + * does not restrict the class from editing the Container contents + * internally. + * </p> + */ + public interface Editor extends Container.Viewer, Serializable { + + } + + /* Contents change event */ + + /** + * An <code>Event</code> object specifying the Container whose Item set has + * changed (items added, removed or reordered). + * + * A simple property value change is not an item set change. + */ + public interface ItemSetChangeEvent extends Serializable { + + /** + * Gets the Property where the event occurred. + * + * @return source of the event + */ + public Container getContainer(); + } + + /** + * Container Item set change listener interface. + * + * An item set change refers to addition, removal or reordering of items in + * the container. A simple property value change is not an item set change. + */ + public interface ItemSetChangeListener extends Serializable { + + /** + * Lets the listener know a Containers visible (filtered and/or sorted, + * if applicable) Item set has changed. + * + * @param event + * change event text + */ + public void containerItemSetChange(Container.ItemSetChangeEvent event); + } + + /** + * The interface for adding and removing <code>ItemSetChangeEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>ItemSetChangeEvent</code> when its contents + * are modified. + * + * An item set change refers to addition, removal or reordering of items in + * the container. A simple property value change is not an item set change. + * + * <p> + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + */ + public interface ItemSetChangeNotifier extends Serializable { + + /** + * Adds an Item set change listener for the object. + * + * @param listener + * listener to be added + */ + public void addListener(Container.ItemSetChangeListener listener); + + /** + * Removes the Item set change listener from the object. + * + * @param listener + * listener to be removed + */ + public void removeListener(Container.ItemSetChangeListener listener); + } + + /* Property set change event */ + + /** + * An <code>Event</code> object specifying the Container whose Property set + * has changed. + * + * A property set change means the addition, removal or other structural + * changes to the properties of a container. Changes concerning the set of + * items in the container and their property values are not property set + * changes. + */ + public interface PropertySetChangeEvent extends Serializable { + + /** + * Retrieves the Container whose contents have been modified. + * + * @return Source Container of the event. + */ + public Container getContainer(); + } + + /** + * The listener interface for receiving <code>PropertySetChangeEvent</code> + * objects. + * + * A property set change means the addition, removal or other structural + * change of the properties (supported property IDs) of a container. Changes + * concerning the set of items in the container and their property values + * are not property set changes. + */ + public interface PropertySetChangeListener extends Serializable { + + /** + * Notifies this listener that the set of property IDs supported by the + * Container has changed. + * + * @param event + * Change event. + */ + public void containerPropertySetChange( + Container.PropertySetChangeEvent event); + } + + /** + * <p> + * The interface for adding and removing <code>PropertySetChangeEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>PropertySetChangeEvent</code> when the set + * of property IDs supported by the container is modified. + * </p> + * + * <p> + * A property set change means the addition, removal or other structural + * changes to the properties of a container. Changes concerning the set of + * items in the container and their property values are not property set + * changes. + * </p> + * + * <p> + * Note that the general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + */ + public interface PropertySetChangeNotifier extends Serializable { + + /** + * Registers a new Property set change listener for this Container. + * + * @param listener + * The new Listener to be registered + */ + public void addListener(Container.PropertySetChangeListener listener); + + /** + * Removes a previously registered Property set change listener. + * + * @param listener + * Listener to be removed + */ + public void removeListener(Container.PropertySetChangeListener listener); + } +} diff --git a/server/src/com/vaadin/data/Item.java b/server/src/com/vaadin/data/Item.java new file mode 100644 index 0000000000..98b95aecff --- /dev/null +++ b/server/src/com/vaadin/data/Item.java @@ -0,0 +1,180 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Collection; + +/** + * <p> + * Provides a mechanism for handling a set of Properties, each associated to a + * locally unique non-null identifier. The interface is split into subinterfaces + * to enable a class to implement only the functionalities it needs. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Item extends Serializable { + + /** + * Gets the Property corresponding to the given Property ID stored in the + * Item. If the Item does not contain the Property, <code>null</code> is + * returned. + * + * @param id + * identifier of the Property to get + * @return the Property with the given ID or <code>null</code> + */ + public Property<?> getItemProperty(Object id); + + /** + * Gets the collection of IDs of all Properties stored in the Item. + * + * @return unmodifiable collection containing IDs of the Properties stored + * the Item + */ + public Collection<?> getItemPropertyIds(); + + /** + * Tries to add a new Property into the Item. + * + * <p> + * This functionality is optional. + * </p> + * + * @param id + * ID of the new Property + * @param property + * the Property to be added and associated with the id + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the operation is not supported. + */ + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException; + + /** + * Removes the Property identified by ID from the Item. + * + * <p> + * This functionality is optional. + * </p> + * + * @param id + * ID of the Property to be removed + * @return <code>true</code> if the operation succeeded + * @throws UnsupportedOperationException + * if the operation is not supported. <code>false</code> if not + */ + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException; + + /** + * Interface implemented by viewer classes capable of using an Item as a + * data source. + */ + public interface Viewer extends Serializable { + + /** + * Sets the Item that serves as the data source of the viewer. + * + * @param newDataSource + * The new data source Item + */ + public void setItemDataSource(Item newDataSource); + + /** + * Gets the Item serving as the data source of the viewer. + * + * @return data source Item + */ + public Item getItemDataSource(); + } + + /** + * Interface implemented by the <code>Editor</code> classes capable of + * editing the Item. Implementing this interface means that the Item serving + * as the data source of the editor can be modified through it. + * <p> + * Note : Not implementing the <code>Item.Editor</code> interface does not + * restrict the class from editing the contents of an internally. + * </p> + */ + public interface Editor extends Item.Viewer, Serializable { + + } + + /* Property set change event */ + + /** + * An <code>Event</code> object specifying the Item whose contents has been + * changed through the <code>Property</code> interface. + * <p> + * Note: The values stored in the Properties may change without triggering + * this event. + * </p> + */ + public interface PropertySetChangeEvent extends Serializable { + + /** + * Retrieves the Item whose contents has been modified. + * + * @return source Item of the event + */ + public Item getItem(); + } + + /** + * The listener interface for receiving <code>PropertySetChangeEvent</code> + * objects. + */ + public interface PropertySetChangeListener extends Serializable { + + /** + * Notifies this listener that the Item's property set has changed. + * + * @param event + * Property set change event object + */ + public void itemPropertySetChange(Item.PropertySetChangeEvent event); + } + + /** + * The interface for adding and removing <code>PropertySetChangeEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>PropertySetChangeEvent</code> when its + * Property set is modified. + * <p> + * Note : The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + */ + public interface PropertySetChangeNotifier extends Serializable { + + /** + * Registers a new property set change listener for this Item. + * + * @param listener + * The new Listener to be registered. + */ + public void addListener(Item.PropertySetChangeListener listener); + + /** + * Removes a previously registered property set change listener. + * + * @param listener + * Listener to be removed. + */ + public void removeListener(Item.PropertySetChangeListener listener); + } +} diff --git a/server/src/com/vaadin/data/Property.java b/server/src/com/vaadin/data/Property.java new file mode 100644 index 0000000000..9fab642381 --- /dev/null +++ b/server/src/com/vaadin/data/Property.java @@ -0,0 +1,402 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; + +/** + * <p> + * The <code>Property</code> is a simple data object that contains one typed + * value. This interface contains methods to inspect and modify the stored value + * and its type, and the object's read-only state. + * </p> + * + * <p> + * The <code>Property</code> also defines the events + * <code>ReadOnlyStatusChangeEvent</code> and <code>ValueChangeEvent</code>, and + * the associated <code>listener</code> and <code>notifier</code> interfaces. + * </p> + * + * <p> + * The <code>Property.Viewer</code> interface should be used to attach the + * Property to an external data source. This way the value in the data source + * can be inspected using the <code>Property</code> interface. + * </p> + * + * <p> + * The <code>Property.editor</code> interface should be implemented if the value + * needs to be changed through the implementing class. + * </p> + * + * @param T + * type of values of the property + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Property<T> extends Serializable { + + /** + * Gets the value stored in the Property. The returned object is compatible + * with the class returned by getType(). + * + * @return the value stored in the Property + */ + public T getValue(); + + /** + * Sets the value of the Property. + * <p> + * Implementing this functionality is optional. If the functionality is + * missing, one should declare the Property to be in read-only mode and + * throw <code>Property.ReadOnlyException</code> in this function. + * </p> + * + * Note : Since Vaadin 7.0, setting the value of a non-String property as a + * String is no longer supported. + * + * @param newValue + * New value of the Property. This should be assignable to the + * type returned by getType + * + * @throws Property.ReadOnlyException + * if the object is in read-only mode + */ + public void setValue(Object newValue) throws Property.ReadOnlyException; + + /** + * Returns the type of the Property. The methods <code>getValue</code> and + * <code>setValue</code> must be compatible with this type: one must be able + * to safely cast the value returned from <code>getValue</code> to the given + * type and pass any variable assignable to this type as an argument to + * <code>setValue</code>. + * + * @return type of the Property + */ + public Class<? extends T> getType(); + + /** + * Tests if the Property is in read-only mode. In read-only mode calls to + * the method <code>setValue</code> will throw + * <code>ReadOnlyException</code> and will not modify the value of the + * Property. + * + * @return <code>true</code> if the Property is in read-only mode, + * <code>false</code> if it's not + */ + public boolean isReadOnly(); + + /** + * Sets the Property's read-only mode to the specified status. + * + * This functionality is optional, but all properties must implement the + * <code>isReadOnly</code> mode query correctly. + * + * @param newStatus + * new read-only status of the Property + */ + public void setReadOnly(boolean newStatus); + + /** + * A Property that is capable of handle a transaction that can end in commit + * or rollback. + * + * Note that this does not refer to e.g. database transactions but rather + * two-phase commit that allows resetting old field values on a form etc. if + * the commit of one of the properties fails after others have already been + * committed. If + * + * @param <T> + * The type of the property + * @author Vaadin Ltd + * @version @version@ + * @since 7.0 + */ + public interface Transactional<T> extends Property<T> { + + /** + * Starts a transaction. + * + * <p> + * If the value is set during a transaction the value must not replace + * the original value until {@link #commit()} is called. Still, + * {@link #getValue()} must return the current value set in the + * transaction. Calling {@link #rollback()} while in a transaction must + * rollback the value to what it was before the transaction started. + * </p> + * <p> + * {@link ValueChangeEvent}s must not be emitted for internal value + * changes during a transaction. If the value changes as a result of + * {@link #commit()}, a {@link ValueChangeEvent} should be emitted. + * </p> + */ + public void startTransaction(); + + /** + * Commits and ends the transaction that is in progress. + * <p> + * If the value is changed as a result of this operation, a + * {@link ValueChangeEvent} is emitted if such are supported. + * <p> + * This method has no effect if there is no transaction is in progress. + * <p> + * This method must never throw an exception. + */ + public void commit(); + + /** + * Aborts and rolls back the transaction that is in progress. + * <p> + * The value is reset to the value before the transaction started. No + * {@link ValueChangeEvent} is emitted as a result of this. + * <p> + * This method has no effect if there is no transaction is in progress. + * <p> + * This method must never throw an exception. + */ + public void rollback(); + } + + /** + * <code>Exception</code> object that signals that a requested Property + * modification failed because it's in read-only mode. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("serial") + public class ReadOnlyException extends RuntimeException { + + /** + * Constructs a new <code>ReadOnlyException</code> without a detail + * message. + */ + public ReadOnlyException() { + } + + /** + * Constructs a new <code>ReadOnlyException</code> with the specified + * detail message. + * + * @param msg + * the detail message + */ + public ReadOnlyException(String msg) { + super(msg); + } + } + + /** + * Interface implemented by the viewer classes capable of using a Property + * as a data source. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Viewer extends Serializable { + + /** + * Sets the Property that serves as the data source of the viewer. + * + * @param newDataSource + * the new data source Property + */ + public void setPropertyDataSource(Property newDataSource); + + /** + * Gets the Property serving as the data source of the viewer. + * + * @return the Property serving as the viewers data source + */ + public Property getPropertyDataSource(); + } + + /** + * Interface implemented by the editor classes capable of editing the + * Property. + * <p> + * Implementing this interface means that the Property serving as the data + * source of the editor can be modified through the editor. It does not + * restrict the editor from editing the Property internally, though if the + * Property is in a read-only mode, attempts to modify it will result in the + * <code>ReadOnlyException</code> being thrown. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Editor extends Property.Viewer, Serializable { + + } + + /* Value change event */ + + /** + * An <code>Event</code> object specifying the Property whose value has been + * changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ValueChangeEvent extends Serializable { + + /** + * Retrieves the Property that has been modified. + * + * @return source Property of the event + */ + public Property getProperty(); + } + + /** + * The <code>listener</code> interface for receiving + * <code>ValueChangeEvent</code> objects. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ValueChangeListener extends Serializable { + + /** + * Notifies this listener that the Property's value has changed. + * + * @param event + * value change event object + */ + public void valueChange(Property.ValueChangeEvent event); + } + + /** + * The interface for adding and removing <code>ValueChangeEvent</code> + * listeners. If a Property wishes to allow other objects to receive + * <code>ValueChangeEvent</code> generated by it, it must implement this + * interface. + * <p> + * Note : The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ValueChangeNotifier extends Serializable { + + /** + * Registers a new value change listener for this Property. + * + * @param listener + * the new Listener to be registered + */ + public void addListener(Property.ValueChangeListener listener); + + /** + * Removes a previously registered value change listener. + * + * @param listener + * listener to be removed + */ + public void removeListener(Property.ValueChangeListener listener); + } + + /* ReadOnly Status change event */ + + /** + * An <code>Event</code> object specifying the Property whose read-only + * status has been changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ReadOnlyStatusChangeEvent extends Serializable { + + /** + * Property whose read-only state has changed. + * + * @return source Property of the event. + */ + public Property getProperty(); + } + + /** + * The listener interface for receiving + * <code>ReadOnlyStatusChangeEvent</code> objects. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ReadOnlyStatusChangeListener extends Serializable { + + /** + * Notifies this listener that a Property's read-only status has + * changed. + * + * @param event + * Read-only status change event object + */ + public void readOnlyStatusChange( + Property.ReadOnlyStatusChangeEvent event); + } + + /** + * The interface for adding and removing + * <code>ReadOnlyStatusChangeEvent</code> listeners. If a Property wishes to + * allow other objects to receive <code>ReadOnlyStatusChangeEvent</code> + * generated by it, it must implement this interface. + * <p> + * Note : The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ReadOnlyStatusChangeNotifier extends Serializable { + + /** + * Registers a new read-only status change listener for this Property. + * + * @param listener + * the new Listener to be registered + */ + public void addListener(Property.ReadOnlyStatusChangeListener listener); + + /** + * Removes a previously registered read-only status change listener. + * + * @param listener + * listener to be removed + */ + public void removeListener( + Property.ReadOnlyStatusChangeListener listener); + } +} diff --git a/server/src/com/vaadin/data/Validatable.java b/server/src/com/vaadin/data/Validatable.java new file mode 100644 index 0000000000..4a7a0fda10 --- /dev/null +++ b/server/src/com/vaadin/data/Validatable.java @@ -0,0 +1,110 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Collection; + +/** + * <p> + * Interface for validatable objects. Defines methods to verify if the object's + * value is valid or not, and to add, remove and list registered validators of + * the object. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @see com.vaadin.data.Validator + */ +public interface Validatable extends Serializable { + + /** + * <p> + * Adds a new validator for this object. The validator's + * {@link Validator#validate(Object)} method is activated every time the + * object's value needs to be verified, that is, when the {@link #isValid()} + * method is called. This usually happens when the object's value changes. + * </p> + * + * @param validator + * the new validator + */ + void addValidator(Validator validator); + + /** + * <p> + * Removes a previously registered validator from the object. The specified + * validator is removed from the object and its <code>validate</code> method + * is no longer called in {@link #isValid()}. + * </p> + * + * @param validator + * the validator to remove + */ + void removeValidator(Validator validator); + + /** + * <p> + * Lists all validators currently registered for the object. If no + * validators are registered, returns <code>null</code>. + * </p> + * + * @return collection of validators or <code>null</code> + */ + public Collection<Validator> getValidators(); + + /** + * <p> + * Tests the current value of the object against all registered validators. + * The registered validators are iterated and for each the + * {@link Validator#validate(Object)} method is called. If any validator + * throws the {@link Validator.InvalidValueException} this method returns + * <code>false</code>. + * </p> + * + * @return <code>true</code> if the registered validators concur that the + * value is valid, <code>false</code> otherwise + */ + public boolean isValid(); + + /** + * <p> + * Checks the validity of the validatable. If the validatable is valid this + * method should do nothing, and if it's not valid, it should throw + * <code>Validator.InvalidValueException</code> + * </p> + * + * @throws Validator.InvalidValueException + * if the value is not valid + */ + public void validate() throws Validator.InvalidValueException; + + /** + * <p> + * Checks the validabtable object accept invalid values.The default value is + * <code>true</code>. + * </p> + * + */ + public boolean isInvalidAllowed(); + + /** + * <p> + * Should the validabtable object accept invalid values. Supporting this + * configuration possibility is optional. By default invalid values are + * allowed. + * </p> + * + * @param invalidValueAllowed + * + * @throws UnsupportedOperationException + * if the setInvalidAllowed is not supported. + */ + public void setInvalidAllowed(boolean invalidValueAllowed) + throws UnsupportedOperationException; + +} diff --git a/server/src/com/vaadin/data/Validator.java b/server/src/com/vaadin/data/Validator.java new file mode 100644 index 0000000000..768a02babe --- /dev/null +++ b/server/src/com/vaadin/data/Validator.java @@ -0,0 +1,175 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data; + +import java.io.Serializable; + +import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; + +/** + * Interface that implements a method for validating if an {@link Object} is + * valid or not. + * <p> + * Implementors of this class can be added to any + * {@link com.vaadin.data.Validatable Validatable} implementor to verify its + * value. + * </p> + * <p> + * {@link #validate(Object)} can be used to check if a value is valid. An + * {@link InvalidValueException} with an appropriate validation error message is + * thrown if the value is not valid. + * </p> + * <p> + * Validators must not have any side effects. + * </p> + * <p> + * Since Vaadin 7, the method isValid(Object) does not exist in the interface - + * {@link #validate(Object)} should be used instead, and the exception caught + * where applicable. Concrete classes implementing {@link Validator} can still + * internally implement and use isValid(Object) for convenience or to ease + * migration from earlier Vaadin versions. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Validator extends Serializable { + + /** + * Checks the given value against this validator. If the value is valid the + * method does nothing. If the value is invalid, an + * {@link InvalidValueException} is thrown. + * + * @param value + * the value to check + * @throws Validator.InvalidValueException + * if the value is invalid + */ + public void validate(Object value) throws Validator.InvalidValueException; + + /** + * Exception that is thrown by a {@link Validator} when a value is invalid. + * + * <p> + * The default implementation of InvalidValueException does not support HTML + * in error messages. To enable HTML support, override + * {@link #getHtmlMessage()} and use the subclass in validators. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("serial") + public class InvalidValueException extends RuntimeException { + + /** + * Array of one or more validation errors that are causing this + * validation error. + */ + private InvalidValueException[] causes = null; + + /** + * Constructs a new {@code InvalidValueException} with the specified + * message. + * + * @param message + * The detail message of the problem. + */ + public InvalidValueException(String message) { + this(message, new InvalidValueException[] {}); + } + + /** + * Constructs a new {@code InvalidValueException} with a set of causing + * validation exceptions. The causing validation exceptions are included + * when the exception is painted to the client. + * + * @param message + * The detail message of the problem. + * @param causes + * One or more {@code InvalidValueException}s that caused + * this exception. + */ + public InvalidValueException(String message, + InvalidValueException[] causes) { + super(message); + if (causes == null) { + throw new NullPointerException( + "Possible causes array must not be null"); + } + + this.causes = causes; + } + + /** + * Check if the error message should be hidden. + * + * An empty (null or "") message is invisible unless it contains nested + * exceptions that are visible. + * + * @return true if the error message should be hidden, false otherwise + */ + public boolean isInvisible() { + String msg = getMessage(); + if (msg != null && msg.length() > 0) { + return false; + } + if (causes != null) { + for (int i = 0; i < causes.length; i++) { + if (!causes[i].isInvisible()) { + return false; + } + } + } + return true; + } + + /** + * Returns the message of the error in HTML. + * + * Note that this API may change in future versions. + */ + public String getHtmlMessage() { + return AbstractApplicationServlet + .safeEscapeForHtml(getLocalizedMessage()); + } + + /** + * Returns the {@code InvalidValueExceptions} that caused this + * exception. + * + * @return An array containing the {@code InvalidValueExceptions} that + * caused this exception. Returns an empty array if this + * exception was not caused by other exceptions. + */ + public InvalidValueException[] getCauses() { + return causes; + } + + } + + /** + * A specific type of {@link InvalidValueException} that indicates that + * validation failed because the value was empty. What empty means is up to + * the thrower. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.3.0 + */ + @SuppressWarnings("serial") + public class EmptyValueException extends Validator.InvalidValueException { + + public EmptyValueException(String message) { + super(message); + } + + } +} diff --git a/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java new file mode 100644 index 0000000000..b8efa5b1e4 --- /dev/null +++ b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.fieldgroup; + +import java.lang.reflect.Method; + +import com.vaadin.data.Item; +import com.vaadin.data.util.BeanItem; +import com.vaadin.data.validator.BeanValidator; +import com.vaadin.ui.Field; + +public class BeanFieldGroup<T> extends FieldGroup { + + private Class<T> beanType; + + private static Boolean beanValidationImplementationAvailable = null; + + public BeanFieldGroup(Class<T> beanType) { + this.beanType = beanType; + } + + @Override + protected Class<?> getPropertyType(Object propertyId) { + if (getItemDataSource() != null) { + return super.getPropertyType(propertyId); + } else { + // Data source not set so we need to figure out the type manually + /* + * toString should never really be needed as propertyId should be of + * form "fieldName" or "fieldName.subField[.subField2]" but the + * method declaration comes from parent. + */ + java.lang.reflect.Field f; + try { + f = getField(beanType, propertyId.toString()); + return f.getType(); + } catch (SecurityException e) { + throw new BindException("Cannot determine type of propertyId '" + + propertyId + "'.", e); + } catch (NoSuchFieldException e) { + throw new BindException("Cannot determine type of propertyId '" + + propertyId + "'. The propertyId was not found in " + + beanType.getName(), e); + } + } + } + + private static java.lang.reflect.Field getField(Class<?> cls, + String propertyId) throws SecurityException, NoSuchFieldException { + if (propertyId.contains(".")) { + String[] parts = propertyId.split("\\.", 2); + // Get the type of the field in the "cls" class + java.lang.reflect.Field field1 = getField(cls, parts[0]); + // Find the rest from the sub type + return getField(field1.getType(), parts[1]); + } else { + try { + // Try to find the field directly in the given class + java.lang.reflect.Field field1 = cls + .getDeclaredField(propertyId); + return field1; + } catch (NoSuchFieldError e) { + // Try super classes until we reach Object + Class<?> superClass = cls.getSuperclass(); + if (superClass != Object.class) { + return getField(superClass, propertyId); + } else { + throw e; + } + } + } + } + + /** + * Helper method for setting the data source directly using a bean. This + * method wraps the bean in a {@link BeanItem} and calls + * {@link #setItemDataSource(Item)}. + * + * @param bean + * The bean to use as data source. + */ + public void setItemDataSource(T bean) { + setItemDataSource(new BeanItem(bean)); + } + + @Override + public void setItemDataSource(Item item) { + if (!(item instanceof BeanItem)) { + throw new RuntimeException(getClass().getSimpleName() + + " only supports BeanItems as item data source"); + } + super.setItemDataSource(item); + } + + @Override + public BeanItem<T> getItemDataSource() { + return (BeanItem<T>) super.getItemDataSource(); + } + + @Override + public void bind(Field field, Object propertyId) { + if (getItemDataSource() != null) { + // The data source is set so the property must be found in the item. + // If it is not we try to add it. + try { + getItemProperty(propertyId); + } catch (BindException e) { + // Not found, try to add a nested property; + // BeanItem property ids are always strings so this is safe + getItemDataSource().addNestedProperty((String) propertyId); + } + } + + super.bind(field, propertyId); + } + + @Override + protected void configureField(Field<?> field) { + super.configureField(field); + // Add Bean validators if there are annotations + if (isBeanValidationImplementationAvailable()) { + BeanValidator validator = new BeanValidator(beanType, + getPropertyId(field).toString()); + field.addValidator(validator); + if (field.getLocale() != null) { + validator.setLocale(field.getLocale()); + } + } + } + + /** + * Checks whether a bean validation implementation (e.g. Hibernate Validator + * or Apache Bean Validation) is available. + * + * TODO move this method to some more generic location + * + * @return true if a JSR-303 bean validation implementation is available + */ + protected static boolean isBeanValidationImplementationAvailable() { + if (beanValidationImplementationAvailable != null) { + return beanValidationImplementationAvailable; + } + try { + Class<?> validationClass = Class + .forName("javax.validation.Validation"); + Method buildFactoryMethod = validationClass + .getMethod("buildDefaultValidatorFactory"); + Object factory = buildFactoryMethod.invoke(null); + beanValidationImplementationAvailable = (factory != null); + } catch (Exception e) { + // no bean validation implementation available + beanValidationImplementationAvailable = false; + } + return beanValidationImplementationAvailable; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/fieldgroup/Caption.java b/server/src/com/vaadin/data/fieldgroup/Caption.java new file mode 100644 index 0000000000..b990b720cd --- /dev/null +++ b/server/src/com/vaadin/data/fieldgroup/Caption.java @@ -0,0 +1,15 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.fieldgroup; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Caption { + String value(); +} diff --git a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java new file mode 100644 index 0000000000..be0db328f2 --- /dev/null +++ b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.fieldgroup; + +import java.util.EnumSet; + +import com.vaadin.data.Item; +import com.vaadin.data.fieldgroup.FieldGroup.BindException; +import com.vaadin.ui.AbstractSelect; +import com.vaadin.ui.AbstractTextField; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.Field; +import com.vaadin.ui.ListSelect; +import com.vaadin.ui.NativeSelect; +import com.vaadin.ui.OptionGroup; +import com.vaadin.ui.RichTextArea; +import com.vaadin.ui.Table; +import com.vaadin.ui.TextField; + +public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory { + + public static final Object CAPTION_PROPERTY_ID = "Caption"; + + @Override + public <T extends Field> T createField(Class<?> type, Class<T> fieldType) { + if (Enum.class.isAssignableFrom(type)) { + return createEnumField(type, fieldType); + } else if (Boolean.class.isAssignableFrom(type) + || boolean.class.isAssignableFrom(type)) { + return createBooleanField(fieldType); + } + if (AbstractTextField.class.isAssignableFrom(fieldType)) { + return fieldType.cast(createAbstractTextField(fieldType + .asSubclass(AbstractTextField.class))); + } else if (fieldType == RichTextArea.class) { + return fieldType.cast(createRichTextArea()); + } + return createDefaultField(type, fieldType); + } + + protected RichTextArea createRichTextArea() { + RichTextArea rta = new RichTextArea(); + rta.setImmediate(true); + + return rta; + } + + private <T extends Field> T createEnumField(Class<?> type, + Class<T> fieldType) { + if (AbstractSelect.class.isAssignableFrom(fieldType)) { + AbstractSelect s = createCompatibleSelect((Class<? extends AbstractSelect>) fieldType); + populateWithEnumData(s, (Class<? extends Enum>) type); + return (T) s; + } + + return null; + } + + protected AbstractSelect createCompatibleSelect( + Class<? extends AbstractSelect> fieldType) { + AbstractSelect select; + if (fieldType.isAssignableFrom(ListSelect.class)) { + select = new ListSelect(); + select.setMultiSelect(false); + } else if (fieldType.isAssignableFrom(NativeSelect.class)) { + select = new NativeSelect(); + } else if (fieldType.isAssignableFrom(OptionGroup.class)) { + select = new OptionGroup(); + select.setMultiSelect(false); + } else if (fieldType.isAssignableFrom(Table.class)) { + Table t = new Table(); + t.setSelectable(true); + select = t; + } else { + select = new ComboBox(null); + } + select.setImmediate(true); + select.setNullSelectionAllowed(false); + + return select; + } + + protected <T extends Field> T createBooleanField(Class<T> fieldType) { + if (fieldType.isAssignableFrom(CheckBox.class)) { + CheckBox cb = new CheckBox(null); + cb.setImmediate(true); + return (T) cb; + } else if (AbstractTextField.class.isAssignableFrom(fieldType)) { + return (T) createAbstractTextField((Class<? extends AbstractTextField>) fieldType); + } + + return null; + } + + protected <T extends AbstractTextField> T createAbstractTextField( + Class<T> fieldType) { + if (fieldType == AbstractTextField.class) { + fieldType = (Class<T>) TextField.class; + } + try { + T field = fieldType.newInstance(); + field.setImmediate(true); + return field; + } catch (Exception e) { + throw new BindException("Could not create a field of type " + + fieldType, e); + } + } + + /** + * Fallback when no specific field has been created. Typically returns a + * TextField. + * + * @param <T> + * The type of field to create + * @param type + * The type of data that should be edited + * @param fieldType + * The type of field to create + * @return A field capable of editing the data or null if no field could be + * created + */ + protected <T extends Field> T createDefaultField(Class<?> type, + Class<T> fieldType) { + if (fieldType.isAssignableFrom(TextField.class)) { + return fieldType.cast(createAbstractTextField(TextField.class)); + } + return null; + } + + /** + * Populates the given select with all the enums in the given {@link Enum} + * class. Uses {@link Enum}.toString() for caption. + * + * @param select + * The select to populate + * @param enumClass + * The Enum class to use + */ + protected void populateWithEnumData(AbstractSelect select, + Class<? extends Enum> enumClass) { + select.removeAllItems(); + for (Object p : select.getContainerPropertyIds()) { + select.removeContainerProperty(p); + } + select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, ""); + select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID); + @SuppressWarnings("unchecked") + EnumSet<?> enumSet = EnumSet.allOf(enumClass); + for (Object r : enumSet) { + Item newItem = select.addItem(r); + newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(r.toString()); + } + } +} diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java new file mode 100644 index 0000000000..3df19f5bc9 --- /dev/null +++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java @@ -0,0 +1,978 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.fieldgroup; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.logging.Logger; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.util.TransactionalPropertyWrapper; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.DefaultFieldFactory; +import com.vaadin.ui.Field; +import com.vaadin.ui.Form; + +/** + * FieldGroup provides an easy way of binding fields to data and handling + * commits of these fields. + * <p> + * The functionality of FieldGroup is similar to {@link Form} but + * {@link FieldGroup} does not handle layouts in any way. The typical use case + * is to create a layout outside the FieldGroup and then use FieldGroup to bind + * the fields to a data source. + * </p> + * <p> + * {@link FieldGroup} is not a UI component so it cannot be added to a layout. + * Using the buildAndBind methods {@link FieldGroup} can create fields for you + * using a FieldGroupFieldFactory but you still have to add them to the correct + * position in your layout. + * </p> + * + * @author Vaadin Ltd + * @version @version@ + * @since 7.0 + */ +public class FieldGroup implements Serializable { + + private static final Logger logger = Logger.getLogger(FieldGroup.class + .getName()); + + private Item itemDataSource; + private boolean buffered = true; + + private boolean enabled = true; + private boolean readOnly = false; + + private HashMap<Object, Field<?>> propertyIdToField = new HashMap<Object, Field<?>>(); + private LinkedHashMap<Field<?>, Object> fieldToPropertyId = new LinkedHashMap<Field<?>, Object>(); + private List<CommitHandler> commitHandlers = new ArrayList<CommitHandler>(); + + /** + * The field factory used by builder methods. + */ + private FieldGroupFieldFactory fieldFactory = new DefaultFieldGroupFieldFactory(); + + /** + * Constructs a field binder. Use {@link #setItemDataSource(Item)} to set a + * data source for the field binder. + * + */ + public FieldGroup() { + + } + + /** + * Constructs a field binder that uses the given data source. + * + * @param itemDataSource + * The data source to bind the fields to + */ + public FieldGroup(Item itemDataSource) { + setItemDataSource(itemDataSource); + } + + /** + * Updates the item that is used by this FieldBinder. Rebinds all fields to + * the properties in the new item. + * + * @param itemDataSource + * The new item to use + */ + public void setItemDataSource(Item itemDataSource) { + this.itemDataSource = itemDataSource; + + for (Field<?> f : fieldToPropertyId.keySet()) { + bind(f, fieldToPropertyId.get(f)); + } + } + + /** + * Gets the item used by this FieldBinder. Note that you must call + * {@link #commit()} for the item to be updated unless buffered mode has + * been switched off. + * + * @see #setBuffered(boolean) + * @see #commit() + * + * @return The item used by this FieldBinder + */ + public Item getItemDataSource() { + return itemDataSource; + } + + /** + * Checks the buffered mode for the bound fields. + * <p> + * + * @see #setBuffered(boolean) for more details on buffered mode + * + * @see Field#isBuffered() + * @return true if buffered mode is on, false otherwise + * + */ + public boolean isBuffered() { + return buffered; + } + + /** + * Sets the buffered mode for the bound fields. + * <p> + * When buffered mode is on the item will not be updated until + * {@link #commit()} is called. If buffered mode is off the item will be + * updated once the fields are updated. + * </p> + * <p> + * The default is to use buffered mode. + * </p> + * + * @see Field#setBuffered(boolean) + * @param buffered + * true to turn on buffered mode, false otherwise + */ + public void setBuffered(boolean buffered) { + if (buffered == this.buffered) { + return; + } + + this.buffered = buffered; + for (Field<?> field : getFields()) { + field.setBuffered(buffered); + } + } + + /** + * Returns the enabled status for the fields. + * <p> + * Note that this will not accurately represent the enabled status of all + * fields if you change the enabled status of the fields through some other + * method than {@link #setEnabled(boolean)}. + * + * @return true if the fields are enabled, false otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Updates the enabled state of all bound fields. + * + * @param fieldsEnabled + * true to enable all bound fields, false to disable them + */ + public void setEnabled(boolean fieldsEnabled) { + enabled = fieldsEnabled; + for (Field<?> field : getFields()) { + field.setEnabled(fieldsEnabled); + } + } + + /** + * Returns the read only status for the fields. + * <p> + * Note that this will not accurately represent the read only status of all + * fields if you change the read only status of the fields through some + * other method than {@link #setReadOnly(boolean)}. + * + * @return true if the fields are set to read only, false otherwise + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Updates the read only state of all bound fields. + * + * @param fieldsReadOnly + * true to set all bound fields to read only, false to set them + * to read write + */ + public void setReadOnly(boolean fieldsReadOnly) { + readOnly = fieldsReadOnly; + } + + /** + * Returns a collection of all fields that have been bound. + * <p> + * The fields are not returned in any specific order. + * </p> + * + * @return A collection with all bound Fields + */ + public Collection<Field<?>> getFields() { + return fieldToPropertyId.keySet(); + } + + /** + * Binds the field with the given propertyId from the current item. If an + * item has not been set then the binding is postponed until the item is set + * using {@link #setItemDataSource(Item)}. + * <p> + * This method also adds validators when applicable. + * </p> + * + * @param field + * The field to bind + * @param propertyId + * The propertyId to bind to the field + * @throws BindException + * If the property id is already bound to another field by this + * field binder + */ + public void bind(Field<?> field, Object propertyId) throws BindException { + if (propertyIdToField.containsKey(propertyId) + && propertyIdToField.get(propertyId) != field) { + throw new BindException("Property id " + propertyId + + " is already bound to another field"); + } + fieldToPropertyId.put(field, propertyId); + propertyIdToField.put(propertyId, field); + if (itemDataSource == null) { + // Will be bound when data source is set + return; + } + + field.setPropertyDataSource(wrapInTransactionalProperty(getItemProperty(propertyId))); + configureField(field); + } + + private <T> Property.Transactional<T> wrapInTransactionalProperty( + Property<T> itemProperty) { + return new TransactionalPropertyWrapper<T>(itemProperty); + } + + /** + * Gets the property with the given property id from the item. + * + * @param propertyId + * The id if the property to find + * @return The property with the given id from the item + * @throws BindException + * If the property was not found in the item or no item has been + * set + */ + protected Property<?> getItemProperty(Object propertyId) + throws BindException { + Item item = getItemDataSource(); + if (item == null) { + throw new BindException("Could not lookup property with id " + + propertyId + " as no item has been set"); + } + Property<?> p = item.getItemProperty(propertyId); + if (p == null) { + throw new BindException("A property with id " + propertyId + + " was not found in the item"); + } + return p; + } + + /** + * Detaches the field from its property id and removes it from this + * FieldBinder. + * <p> + * Note that the field is not detached from its property data source if it + * is no longer connected to the same property id it was bound to using this + * FieldBinder. + * + * @param field + * The field to detach + * @throws BindException + * If the field is not bound by this field binder or not bound + * to the correct property id + */ + public void unbind(Field<?> field) throws BindException { + Object propertyId = fieldToPropertyId.get(field); + if (propertyId == null) { + throw new BindException( + "The given field is not part of this FieldBinder"); + } + + Property fieldDataSource = field.getPropertyDataSource(); + if (fieldDataSource instanceof TransactionalPropertyWrapper) { + fieldDataSource = ((TransactionalPropertyWrapper) fieldDataSource) + .getWrappedProperty(); + } + if (fieldDataSource == getItemProperty(propertyId)) { + field.setPropertyDataSource(null); + } + fieldToPropertyId.remove(field); + propertyIdToField.remove(propertyId); + } + + /** + * Configures a field with the settings set for this FieldBinder. + * <p> + * By default this updates the buffered, read only and enabled state of the + * field. Also adds validators when applicable. + * + * @param field + * The field to update + */ + protected void configureField(Field<?> field) { + field.setBuffered(isBuffered()); + + field.setEnabled(isEnabled()); + field.setReadOnly(isReadOnly()); + } + + /** + * Gets the type of the property with the given property id. + * + * @param propertyId + * The propertyId. Must be find + * @return The type of the property + */ + protected Class<?> getPropertyType(Object propertyId) throws BindException { + if (getItemDataSource() == null) { + throw new BindException( + "Property type for '" + + propertyId + + "' could not be determined. No item data source has been set."); + } + Property<?> p = getItemDataSource().getItemProperty(propertyId); + if (p == null) { + throw new BindException( + "Property type for '" + + propertyId + + "' could not be determined. No property with that id was found."); + } + + return p.getType(); + } + + /** + * Returns a collection of all property ids that have been bound to fields. + * <p> + * Note that this will return property ids even before the item has been + * set. In that case it returns the property ids that will be bound once the + * item is set. + * </p> + * <p> + * No guarantee is given for the order of the property ids + * </p> + * + * @return A collection of bound property ids + */ + public Collection<Object> getBoundPropertyIds() { + return Collections.unmodifiableCollection(propertyIdToField.keySet()); + } + + /** + * Returns a collection of all property ids that exist in the item set using + * {@link #setItemDataSource(Item)} but have not been bound to fields. + * <p> + * Will always return an empty collection before an item has been set using + * {@link #setItemDataSource(Item)}. + * </p> + * <p> + * No guarantee is given for the order of the property ids + * </p> + * + * @return A collection of property ids that have not been bound to fields + */ + public Collection<Object> getUnboundPropertyIds() { + if (getItemDataSource() == null) { + return new ArrayList<Object>(); + } + List<Object> unboundPropertyIds = new ArrayList<Object>(); + unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds()); + unboundPropertyIds.removeAll(propertyIdToField.keySet()); + return unboundPropertyIds; + } + + /** + * Commits all changes done to the bound fields. + * <p> + * Calls all {@link CommitHandler}s before and after committing the field + * changes to the item data source. The whole commit is aborted and state is + * restored to what it was before commit was called if any + * {@link CommitHandler} throws a CommitException or there is a problem + * committing the fields + * + * @throws CommitException + * If the commit was aborted + */ + public void commit() throws CommitException { + if (!isBuffered()) { + // Not using buffered mode, nothing to do + return; + } + for (Field<?> f : fieldToPropertyId.keySet()) { + ((Property.Transactional<?>) f.getPropertyDataSource()) + .startTransaction(); + } + try { + firePreCommitEvent(); + // Commit the field values to the properties + for (Field<?> f : fieldToPropertyId.keySet()) { + f.commit(); + } + firePostCommitEvent(); + + // Commit the properties + for (Field<?> f : fieldToPropertyId.keySet()) { + ((Property.Transactional<?>) f.getPropertyDataSource()) + .commit(); + } + + } catch (Exception e) { + for (Field<?> f : fieldToPropertyId.keySet()) { + try { + ((Property.Transactional<?>) f.getPropertyDataSource()) + .rollback(); + } catch (Exception rollbackException) { + // FIXME: What to do ? + } + } + + throw new CommitException("Commit failed", e); + } + + } + + /** + * Sends a preCommit event to all registered commit handlers + * + * @throws CommitException + * If the commit should be aborted + */ + private void firePreCommitEvent() throws CommitException { + CommitHandler[] handlers = commitHandlers + .toArray(new CommitHandler[commitHandlers.size()]); + + for (CommitHandler handler : handlers) { + handler.preCommit(new CommitEvent(this)); + } + } + + /** + * Sends a postCommit event to all registered commit handlers + * + * @throws CommitException + * If the commit should be aborted + */ + private void firePostCommitEvent() throws CommitException { + CommitHandler[] handlers = commitHandlers + .toArray(new CommitHandler[commitHandlers.size()]); + + for (CommitHandler handler : handlers) { + handler.postCommit(new CommitEvent(this)); + } + } + + /** + * Discards all changes done to the bound fields. + * <p> + * Only has effect if buffered mode is used. + * + */ + public void discard() { + for (Field<?> f : fieldToPropertyId.keySet()) { + try { + f.discard(); + } catch (Exception e) { + // TODO: handle exception + // What can we do if discard fails other than try to discard all + // other fields? + } + } + } + + /** + * Returns the field that is bound to the given property id + * + * @param propertyId + * The property id to use to lookup the field + * @return The field that is bound to the property id or null if no field is + * bound to that property id + */ + public Field<?> getField(Object propertyId) { + return propertyIdToField.get(propertyId); + } + + /** + * Returns the property id that is bound to the given field + * + * @param field + * The field to use to lookup the property id + * @return The property id that is bound to the field or null if the field + * is not bound to any property id by this FieldBinder + */ + public Object getPropertyId(Field<?> field) { + return fieldToPropertyId.get(field); + } + + /** + * Adds a commit handler. + * <p> + * The commit handler is called before the field values are committed to the + * item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item + * has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a + * {@link CommitHandler} throws a CommitException the whole commit is + * aborted and the fields retain their old values. + * + * @param commitHandler + * The commit handler to add + */ + public void addCommitHandler(CommitHandler commitHandler) { + commitHandlers.add(commitHandler); + } + + /** + * Removes the given commit handler. + * + * @see #addCommitHandler(CommitHandler) + * + * @param commitHandler + * The commit handler to remove + */ + public void removeCommitHandler(CommitHandler commitHandler) { + commitHandlers.remove(commitHandler); + } + + /** + * Returns a list of all commit handlers for this {@link FieldGroup}. + * <p> + * Use {@link #addCommitHandler(CommitHandler)} and + * {@link #removeCommitHandler(CommitHandler)} to register or unregister a + * commit handler. + * + * @return A collection of commit handlers + */ + protected Collection<CommitHandler> getCommitHandlers() { + return Collections.unmodifiableCollection(commitHandlers); + } + + /** + * CommitHandlers are used by {@link FieldGroup#commit()} as part of the + * commit transactions. CommitHandlers can perform custom operations as part + * of the commit and cause the commit to be aborted by throwing a + * {@link CommitException}. + */ + public interface CommitHandler extends Serializable { + /** + * Called before changes are committed to the field and the item is + * updated. + * <p> + * Throw a {@link CommitException} to abort the commit. + * + * @param commitEvent + * An event containing information regarding the commit + * @throws CommitException + * if the commit should be aborted + */ + public void preCommit(CommitEvent commitEvent) throws CommitException; + + /** + * Called after changes are committed to the fields and the item is + * updated.. + * <p> + * Throw a {@link CommitException} to abort the commit. + * + * @param commitEvent + * An event containing information regarding the commit + * @throws CommitException + * if the commit should be aborted + */ + public void postCommit(CommitEvent commitEvent) throws CommitException; + } + + /** + * FIXME javadoc + * + */ + public static class CommitEvent implements Serializable { + private FieldGroup fieldBinder; + + private CommitEvent(FieldGroup fieldBinder) { + this.fieldBinder = fieldBinder; + } + + /** + * Returns the field binder that this commit relates to + * + * @return The FieldBinder that is being committed. + */ + public FieldGroup getFieldBinder() { + return fieldBinder; + } + + } + + /** + * Checks the validity of the bound fields. + * <p> + * Call the {@link Field#validate()} for the fields to get the individual + * error messages. + * + * @return true if all bound fields are valid, false otherwise. + */ + public boolean isValid() { + try { + for (Field<?> field : getFields()) { + field.validate(); + } + return true; + } catch (InvalidValueException e) { + return false; + } + } + + /** + * Checks if any bound field has been modified. + * + * @return true if at least on field has been modified, false otherwise + */ + public boolean isModified() { + for (Field<?> field : getFields()) { + if (field.isModified()) { + return true; + } + } + return false; + } + + /** + * Gets the field factory for the {@link FieldGroup}. The field factory is + * only used when {@link FieldGroup} creates a new field. + * + * @return The field factory in use + * + */ + public FieldGroupFieldFactory getFieldFactory() { + return fieldFactory; + } + + /** + * Sets the field factory for the {@link FieldGroup}. The field factory is + * only used when {@link FieldGroup} creates a new field. + * + * @param fieldFactory + * The field factory to use + */ + public void setFieldFactory(FieldGroupFieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + } + + /** + * Binds member fields found in the given object. + * <p> + * This method processes all (Java) member fields whose type extends + * {@link Field} and that can be mapped to a property id. Property id + * mapping is done based on the field name or on a @{@link PropertyId} + * annotation on the field. All non-null fields for which a property id can + * be determined are bound to the property id. + * </p> + * <p> + * For example: + * + * <pre> + * public class MyForm extends VerticalLayout { + * private TextField firstName = new TextField("First name"); + * @PropertyId("last") + * private TextField lastName = new TextField("Last name"); + * private TextField age = new TextField("Age"); ... } + * + * MyForm myForm = new MyForm(); + * ... + * fieldGroup.bindMemberFields(myForm); + * </pre> + * + * </p> + * This binds the firstName TextField to a "firstName" property in the item, + * lastName TextField to a "last" property and the age TextField to a "age" + * property. + * + * @param objectWithMemberFields + * The object that contains (Java) member fields to bind + * @throws BindException + * If there is a problem binding a field + */ + public void bindMemberFields(Object objectWithMemberFields) + throws BindException { + buildAndBindMemberFields(objectWithMemberFields, false); + } + + /** + * Binds member fields found in the given object and builds member fields + * that have not been initialized. + * <p> + * This method processes all (Java) member fields whose type extends + * {@link Field} and that can be mapped to a property id. Property id + * mapping is done based on the field name or on a @{@link PropertyId} + * annotation on the field. Fields that are not initialized (null) are built + * using the field factory. All non-null fields for which a property id can + * be determined are bound to the property id. + * </p> + * <p> + * For example: + * + * <pre> + * public class MyForm extends VerticalLayout { + * private TextField firstName = new TextField("First name"); + * @PropertyId("last") + * private TextField lastName = new TextField("Last name"); + * private TextField age; + * + * MyForm myForm = new MyForm(); + * ... + * fieldGroup.buildAndBindMemberFields(myForm); + * </pre> + * + * </p> + * <p> + * This binds the firstName TextField to a "firstName" property in the item, + * lastName TextField to a "last" property and builds an age TextField using + * the field factory and then binds it to the "age" property. + * </p> + * + * @param objectWithMemberFields + * The object that contains (Java) member fields to build and + * bind + * @throws BindException + * If there is a problem binding or building a field + */ + public void buildAndBindMemberFields(Object objectWithMemberFields) + throws BindException { + buildAndBindMemberFields(objectWithMemberFields, true); + } + + /** + * Binds member fields found in the given object and optionally builds + * member fields that have not been initialized. + * <p> + * This method processes all (Java) member fields whose type extends + * {@link Field} and that can be mapped to a property id. Property id + * mapping is done based on the field name or on a @{@link PropertyId} + * annotation on the field. Fields that are not initialized (null) are built + * using the field factory is buildFields is true. All non-null fields for + * which a property id can be determined are bound to the property id. + * </p> + * + * @param objectWithMemberFields + * The object that contains (Java) member fields to build and + * bind + * @throws BindException + * If there is a problem binding or building a field + */ + protected void buildAndBindMemberFields(Object objectWithMemberFields, + boolean buildFields) throws BindException { + Class<?> objectClass = objectWithMemberFields.getClass(); + + for (java.lang.reflect.Field memberField : objectClass + .getDeclaredFields()) { + + if (!Field.class.isAssignableFrom(memberField.getType())) { + // Process next field + continue; + } + + PropertyId propertyIdAnnotation = memberField + .getAnnotation(PropertyId.class); + + Class<? extends Field> fieldType = (Class<? extends Field>) memberField + .getType(); + + Object propertyId = null; + if (propertyIdAnnotation != null) { + // @PropertyId(propertyId) always overrides property id + propertyId = propertyIdAnnotation.value(); + } else { + propertyId = memberField.getName(); + } + + // Ensure that the property id exists + Class<?> propertyType; + + try { + propertyType = getPropertyType(propertyId); + } catch (BindException e) { + // Property id was not found, skip this field + continue; + } + + Field<?> field; + try { + // Get the field from the object + field = (Field<?>) ReflectTools.getJavaFieldValue( + objectWithMemberFields, memberField); + } catch (Exception e) { + // If we cannot determine the value, just skip the field and try + // the next one + continue; + } + + if (field == null && buildFields) { + Caption captionAnnotation = memberField + .getAnnotation(Caption.class); + String caption; + if (captionAnnotation != null) { + caption = captionAnnotation.value(); + } else { + caption = DefaultFieldFactory + .createCaptionByPropertyId(propertyId); + } + + // Create the component (Field) + field = build(caption, propertyType, fieldType); + + // Store it in the field + try { + ReflectTools.setJavaFieldValue(objectWithMemberFields, + memberField, field); + } catch (IllegalArgumentException e) { + throw new BindException("Could not assign value to field '" + + memberField.getName() + "'", e); + } catch (IllegalAccessException e) { + throw new BindException("Could not assign value to field '" + + memberField.getName() + "'", e); + } catch (InvocationTargetException e) { + throw new BindException("Could not assign value to field '" + + memberField.getName() + "'", e); + } + } + + if (field != null) { + // Bind it to the property id + bind(field, propertyId); + } + } + } + + public static class CommitException extends Exception { + + public CommitException() { + super(); + // TODO Auto-generated constructor stub + } + + public CommitException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + + public CommitException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + public CommitException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } + + } + + public static class BindException extends RuntimeException { + + public BindException(String message) { + super(message); + } + + public BindException(String message, Throwable t) { + super(message, t); + } + + } + + /** + * Builds a field and binds it to the given property id using the field + * binder. + * + * @param propertyId + * The property id to bind to. Must be present in the field + * finder. + * @throws BindException + * If there is a problem while building or binding + * @return The created and bound field + */ + public Field<?> buildAndBind(Object propertyId) throws BindException { + String caption = DefaultFieldFactory + .createCaptionByPropertyId(propertyId); + return buildAndBind(caption, propertyId); + } + + /** + * Builds a field using the given caption and binds it to the given property + * id using the field binder. + * + * @param caption + * The caption for the field + * @param propertyId + * The property id to bind to. Must be present in the field + * finder. + * @throws BindException + * If there is a problem while building or binding + * @return The created and bound field. Can be any type of {@link Field}. + */ + public Field<?> buildAndBind(String caption, Object propertyId) + throws BindException { + Class<?> type = getPropertyType(propertyId); + return buildAndBind(caption, propertyId, Field.class); + + } + + /** + * Builds a field using the given caption and binds it to the given property + * id using the field binder. Ensures the new field is of the given type. + * + * @param caption + * The caption for the field + * @param propertyId + * The property id to bind to. Must be present in the field + * finder. + * @throws BindException + * If the field could not be created + * @return The created and bound field. Can be any type of {@link Field}. + */ + + public <T extends Field> T buildAndBind(String caption, Object propertyId, + Class<T> fieldType) throws BindException { + Class<?> type = getPropertyType(propertyId); + + T field = build(caption, type, fieldType); + bind(field, propertyId); + + return field; + } + + /** + * Creates a field based on the given data type. + * <p> + * The data type is the type that we want to edit using the field. The field + * type is the type of field we want to create, can be {@link Field} if any + * Field is good. + * </p> + * + * @param caption + * The caption for the new field + * @param dataType + * The data model type that we want to edit using the field + * @param fieldType + * The type of field that we want to create + * @return A Field capable of editing the given type + * @throws BindException + * If the field could not be created + */ + protected <T extends Field> T build(String caption, Class<?> dataType, + Class<T> fieldType) throws BindException { + T field = getFieldFactory().createField(dataType, fieldType); + if (field == null) { + throw new BindException("Unable to build a field of type " + + fieldType.getName() + " for editing " + + dataType.getName()); + } + + field.setCaption(caption); + return field; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java new file mode 100644 index 0000000000..80c012cbdc --- /dev/null +++ b/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.fieldgroup; + +import java.io.Serializable; + +import com.vaadin.ui.Field; + +/** + * Factory interface for creating new Field-instances based on the data type + * that should be edited. + * + * @author Vaadin Ltd. + * @version @version@ + * @since 7.0 + */ +public interface FieldGroupFieldFactory extends Serializable { + /** + * Creates a field based on the data type that we want to edit + * + * @param dataType + * The type that we want to edit using the field + * @param fieldType + * The type of field we want to create. If set to {@link Field} + * then any type of field is accepted + * @return A field that can be assigned to the given fieldType and that is + * capable of editing the given type of data + */ + <T extends Field> T createField(Class<?> dataType, Class<T> fieldType); +} diff --git a/server/src/com/vaadin/data/fieldgroup/PropertyId.java b/server/src/com/vaadin/data/fieldgroup/PropertyId.java new file mode 100644 index 0000000000..268047401d --- /dev/null +++ b/server/src/com/vaadin/data/fieldgroup/PropertyId.java @@ -0,0 +1,15 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.fieldgroup; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface PropertyId { + String value(); +} diff --git a/server/src/com/vaadin/data/package.html b/server/src/com/vaadin/data/package.html new file mode 100644 index 0000000000..a14ea1ac88 --- /dev/null +++ b/server/src/com/vaadin/data/package.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> +</head> + +<body bgcolor="white"> + +<p>Contains interfaces for the data layer, mainly for binding typed +data and data collections to components, and for validating data.</p> + +<h2>Data binding</h2> + +<p>The package contains a three-tiered structure for typed data +objects and collections of them:</p> + +<ul> + <li>A {@link com.vaadin.data.Property Property} represents a + single, typed data value. + + <li>An {@link com.vaadin.data.Item Item} embodies a set of <i>Properties</i>. + A locally unique (inside the {@link com.vaadin.data.Item Item}) + Property identifier corresponds to each Property inside the Item.</li> + <li>A {@link com.vaadin.data.Container Container} contains a set + of Items, each corresponding to a locally unique Item identifier. Note + that Container imposes a few restrictions on the data stored in it, see + {@link com.vaadin.data.Container Container} for further information.</li> +</ul> + +<p>For more information on the data model, see the <a + href="http://vaadin.com/book/-/page/datamodel.html">Data model +chapter</a> in Book of Vaadin.</p> + +<h2>Buffering</h2> + +<p>A {@link com.vaadin.data.Buffered Buffered} implementor is able +to track and buffer changes and commit or discard them later.</p> + +<h2>Validation</h2> + +<p>{@link com.vaadin.data.Validator Validator} implementations are +used to validate data, typically the value of a {@link +com.vaadin.ui.Field Field}. One or more {@link com.vaadin.data.Validator +Validators} can be added to a {@link com.vaadin.data.Validatable +Validatable} implementor and then used to validate the value of the +Validatable. </p> + +<!-- Put @see and @since tags down here. --> +</body> +</html> diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java new file mode 100644 index 0000000000..2f428d2cb6 --- /dev/null +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -0,0 +1,856 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Filterable; +import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Container.SimpleFilterable; +import com.vaadin.data.Container.Sortable; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.util.MethodProperty.MethodException; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * An abstract base class for in-memory containers for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class and explicitly adding or removing properties is not + * supported. Only beans of the same type can be added to the container. + * </p> + * + * <p> + * Subclasses should implement any public methods adding items to the container, + * typically calling the protected methods {@link #addItem(Object, Object)}, + * {@link #addItemAfter(Object, Object, Object)} and + * {@link #addItemAt(int, Object, Object)}. + * </p> + * + * @param <IDTYPE> + * The type of the item identifier + * @param <BEANTYPE> + * The type of the Bean + * + * @since 6.5 + */ +public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends + AbstractInMemoryContainer<IDTYPE, String, BeanItem<BEANTYPE>> implements + Filterable, SimpleFilterable, Sortable, ValueChangeListener, + PropertySetChangeNotifier { + + /** + * Resolver that maps beans to their (item) identifiers, removing the need + * to explicitly specify item identifiers when there is no need to customize + * this. + * + * Note that beans can also be added with an explicit id even if a resolver + * has been set. + * + * @param <IDTYPE> + * @param <BEANTYPE> + * + * @since 6.5 + */ + public static interface BeanIdResolver<IDTYPE, BEANTYPE> extends + Serializable { + /** + * Return the item identifier for a bean. + * + * @param bean + * @return + */ + public IDTYPE getIdForBean(BEANTYPE bean); + } + + /** + * A item identifier resolver that returns the value of a bean property. + * + * The bean must have a getter for the property, and the getter must return + * an object of type IDTYPE. + */ + protected class PropertyBasedBeanIdResolver implements + BeanIdResolver<IDTYPE, BEANTYPE> { + + private final Object propertyId; + + public PropertyBasedBeanIdResolver(Object propertyId) { + if (propertyId == null) { + throw new IllegalArgumentException( + "Property identifier must not be null"); + } + this.propertyId = propertyId; + } + + @Override + @SuppressWarnings("unchecked") + public IDTYPE getIdForBean(BEANTYPE bean) + throws IllegalArgumentException { + VaadinPropertyDescriptor<BEANTYPE> pd = model.get(propertyId); + if (null == pd) { + throw new IllegalStateException("Property " + propertyId + + " not found"); + } + try { + Property<IDTYPE> property = (Property<IDTYPE>) pd + .createProperty(bean); + return property.getValue(); + } catch (MethodException e) { + throw new IllegalArgumentException(e); + } + } + + } + + /** + * The resolver that finds the item ID for a bean, or null not to use + * automatic resolving. + * + * Methods that add a bean without specifying an ID must not be called if no + * resolver has been set. + */ + private BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver = null; + + /** + * Maps all item ids in the container (including filtered) to their + * corresponding BeanItem. + */ + private final Map<IDTYPE, BeanItem<BEANTYPE>> itemIdToItem = new HashMap<IDTYPE, BeanItem<BEANTYPE>>(); + + /** + * The type of the beans in the container. + */ + private final Class<? super BEANTYPE> type; + + /** + * A description of the properties found in beans of type {@link #type}. + * Determines the property ids that are present in the container. + */ + private LinkedHashMap<String, VaadinPropertyDescriptor<BEANTYPE>> model; + + /** + * Constructs a {@code AbstractBeanContainer} for beans of the given type. + * + * @param type + * the type of the beans that will be added to the container. + * @throws IllegalArgumentException + * If {@code type} is null + */ + protected AbstractBeanContainer(Class<? super BEANTYPE> type) { + if (type == null) { + throw new IllegalArgumentException( + "The bean type passed to AbstractBeanContainer must not be null"); + } + this.type = type; + model = BeanItem.getPropertyDescriptors((Class<BEANTYPE>) type); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + @Override + public Class<?> getType(Object propertyId) { + return model.get(propertyId).getPropertyType(); + } + + /** + * Create a BeanItem for a bean using pre-parsed bean metadata (based on + * {@link #getBeanType()}). + * + * @param bean + * @return created {@link BeanItem} or null if bean is null + */ + protected BeanItem<BEANTYPE> createBeanItem(BEANTYPE bean) { + return bean == null ? null : new BeanItem<BEANTYPE>(bean, model); + } + + /** + * Returns the type of beans this Container can contain. + * + * This comes from the bean type constructor parameter, and bean metadata + * (including container properties) is based on this. + * + * @return + */ + public Class<? super BEANTYPE> getBeanType() { + return type; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + @Override + public Collection<String> getContainerPropertyIds() { + return model.keySet(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() { + int origSize = size(); + + internalRemoveAllItems(); + + // detach listeners from all Items + for (Item item : itemIdToItem.values()) { + removeAllValueChangeListeners(item); + } + itemIdToItem.clear(); + + // fire event only if the visible view changed, regardless of whether + // filtered out items were removed or not + if (origSize != 0) { + fireItemSetChange(); + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ + @Override + public BeanItem<BEANTYPE> getItem(Object itemId) { + // TODO return only if visible? + return getUnfilteredItem(itemId); + } + + @Override + protected BeanItem<BEANTYPE> getUnfilteredItem(Object itemId) { + return itemIdToItem.get(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItemIds() + */ + @Override + @SuppressWarnings("unchecked") + public List<IDTYPE> getItemIds() { + return (List<IDTYPE>) super.getItemIds(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + Item item = getItem(itemId); + if (item == null) { + return null; + } + return item.getItemProperty(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) { + // TODO should also remove items that are filtered out + int origSize = size(); + Item item = getItem(itemId); + int position = indexOfId(itemId); + + if (internalRemoveItem(itemId)) { + // detach listeners from Item + removeAllValueChangeListeners(item); + + // remove item + itemIdToItem.remove(itemId); + + // fire event only if the visible view changed, regardless of + // whether filtered out items were removed or not + if (size() != origSize) { + fireItemRemoved(position, itemId); + } + + return true; + } else { + return false; + } + } + + /** + * Re-filter the container when one of the monitored properties changes. + */ + @Override + public void valueChange(ValueChangeEvent event) { + // if a property that is used in a filter is changed, refresh filtering + filterAll(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Filterable#addContainerFilter(java.lang.Object, + * java.lang.String, boolean, boolean) + */ + @Override + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + try { + addFilter(new SimpleStringFilter(propertyId, filterString, + ignoreCase, onlyMatchPrefix)); + } catch (UnsupportedFilterException e) { + // the filter instance created here is always valid for in-memory + // containers + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#removeAllContainerFilters() + */ + @Override + public void removeAllContainerFilters() { + if (!getFilters().isEmpty()) { + for (Item item : itemIdToItem.values()) { + removeAllValueChangeListeners(item); + } + removeAllFilters(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Filterable#removeContainerFilters(java.lang + * .Object) + */ + @Override + public void removeContainerFilters(Object propertyId) { + Collection<Filter> removedFilters = super.removeFilters(propertyId); + if (!removedFilters.isEmpty()) { + // stop listening to change events for the property + for (Item item : itemIdToItem.values()) { + removeValueChangeListener(item, propertyId); + } + } + } + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + addFilter(filter); + } + + @Override + public void removeContainerFilter(Filter filter) { + removeFilter(filter); + } + + /** + * Make this container listen to the given property provided it notifies + * when its value changes. + * + * @param item + * The {@link Item} that contains the property + * @param propertyId + * The id of the property + */ + private void addValueChangeListener(Item item, Object propertyId) { + Property<?> property = item.getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + // avoid multiple notifications for the same property if + // multiple filters are in use + ValueChangeNotifier notifier = (ValueChangeNotifier) property; + notifier.removeListener(this); + notifier.addListener(this); + } + } + + /** + * Remove this container as a listener for the given property. + * + * @param item + * The {@link Item} that contains the property + * @param propertyId + * The id of the property + */ + private void removeValueChangeListener(Item item, Object propertyId) { + Property<?> property = item.getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property).removeListener(this); + } + } + + /** + * Remove this contains as a listener for all the properties in the given + * {@link Item}. + * + * @param item + * The {@link Item} that contains the properties + */ + private void removeAllValueChangeListeners(Item item) { + for (Object propertyId : item.getItemPropertyIds()) { + removeValueChangeListener(item, propertyId); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getSortablePropertyIds(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sortContainer(propertyId, ascending); + } + + @Override + public ItemSorter getItemSorter() { + return super.getItemSorter(); + } + + @Override + public void setItemSorter(ItemSorter itemSorter) { + super.setItemSorter(itemSorter); + } + + @Override + protected void registerNewItem(int position, IDTYPE itemId, + BeanItem<BEANTYPE> item) { + itemIdToItem.put(itemId, item); + + // add listeners to be able to update filtering on property + // changes + for (Filter filter : getFilters()) { + for (String propertyId : getContainerPropertyIds()) { + if (filter.appliesToProperty(propertyId)) { + // addValueChangeListener avoids adding duplicates + addValueChangeListener(item, propertyId); + } + } + } + } + + /** + * Check that a bean can be added to the container (is of the correct type + * for the container). + * + * @param bean + * @return + */ + private boolean validateBean(BEANTYPE bean) { + return bean != null && getBeanType().isAssignableFrom(bean.getClass()); + } + + /** + * Adds the bean to the Container. + * + * Note: the behavior of this method changed in Vaadin 6.6 - now items are + * added at the very end of the unfiltered container and not after the last + * visible item if filtering is used. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + protected BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) { + if (!validateBean(bean)) { + return null; + } + return internalAddItemAtEnd(itemId, createBeanItem(bean), true); + } + + /** + * Adds the bean after the given bean. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object) + */ + protected BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId, + IDTYPE newItemId, BEANTYPE bean) { + if (!validateBean(bean)) { + return null; + } + return internalAddItemAfter(previousItemId, newItemId, + createBeanItem(bean), true); + } + + /** + * Adds a new bean at the given index. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param index + * Index at which the bean should be added. + * @param newItemId + * The item id for the bean to add to the container. + * @param bean + * The bean to add to the container. + * + * @return Returns the new BeanItem or null if the operation fails. + */ + protected BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId, + BEANTYPE bean) { + if (!validateBean(bean)) { + return null; + } + return internalAddItemAt(index, newItemId, createBeanItem(bean), true); + } + + /** + * Adds a bean to the container using the bean item id resolver to find its + * identifier. + * + * A bean id resolver must be set before calling this method. + * + * @see #addItem(Object, Object) + * + * @param bean + * the bean to add + * @return BeanItem<BEANTYPE> item added or null + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if an identifier cannot be resolved for the bean + */ + protected BeanItem<BEANTYPE> addBean(BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + if (bean == null) { + return null; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + return addItem(itemId, bean); + } + + /** + * Adds a bean to the container after a specified item identifier, using the + * bean item id resolver to find its identifier. + * + * A bean id resolver must be set before calling this method. + * + * @see #addItemAfter(Object, Object, Object) + * + * @param previousItemId + * the identifier of the bean after which this bean should be + * added, null to add to the beginning + * @param bean + * the bean to add + * @return BeanItem<BEANTYPE> item added or null + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if an identifier cannot be resolved for the bean + */ + protected BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId, + BEANTYPE bean) throws IllegalStateException, + IllegalArgumentException { + if (bean == null) { + return null; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + return addItemAfter(previousItemId, itemId, bean); + } + + /** + * Adds a bean at a specified (filtered view) position in the container + * using the bean item id resolver to find its identifier. + * + * A bean id resolver must be set before calling this method. + * + * @see #addItemAfter(Object, Object, Object) + * + * @param index + * the index (in the filtered view) at which to add the item + * @param bean + * the bean to add + * @return BeanItem<BEANTYPE> item added or null + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if an identifier cannot be resolved for the bean + */ + protected BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + if (bean == null) { + return null; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + return addItemAt(index, itemId, bean); + } + + /** + * Adds all the beans from a {@link Collection} in one operation using the + * bean item identifier resolver. More efficient than adding them one by + * one. + * + * A bean id resolver must be set before calling this method. + * + * Note: the behavior of this method changed in Vaadin 6.6 - now items are + * added at the very end of the unfiltered container and not after the last + * visible item if filtering is used. + * + * @param collection + * The collection of beans to add. Must not be null. + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if the resolver returns a null itemId for one of the beans in + * the collection + */ + protected void addAll(Collection<? extends BEANTYPE> collection) + throws IllegalStateException, IllegalArgumentException { + boolean modified = false; + for (BEANTYPE bean : collection) { + // TODO skipping invalid beans - should not allow them in javadoc? + if (bean == null + || !getBeanType().isAssignableFrom(bean.getClass())) { + continue; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + + if (internalAddItemAtEnd(itemId, createBeanItem(bean), false) != null) { + modified = true; + } + } + + if (modified) { + // Filter the contents when all items have been added + if (isFiltered()) { + filterAll(); + } else { + fireItemSetChange(); + } + } + } + + /** + * Use the bean resolver to get the identifier for a bean. + * + * @param bean + * @return resolved bean identifier, null if could not be resolved + * @throws IllegalStateException + * if no bean resolver is set + */ + protected IDTYPE resolveBeanId(BEANTYPE bean) { + if (beanIdResolver == null) { + throw new IllegalStateException( + "Bean item identifier resolver is required."); + } + return beanIdResolver.getIdForBean(bean); + } + + /** + * Sets the resolver that finds the item id for a bean, or null not to use + * automatic resolving. + * + * Methods that add a bean without specifying an id must not be called if no + * resolver has been set. + * + * Note that methods taking an explicit id can be used whether a resolver + * has been defined or not. + * + * @param beanIdResolver + * to use or null to disable automatic id resolution + */ + protected void setBeanIdResolver( + BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) { + this.beanIdResolver = beanIdResolver; + } + + /** + * Returns the resolver that finds the item ID for a bean. + * + * @return resolver used or null if automatic item id resolving is disabled + */ + public BeanIdResolver<IDTYPE, BEANTYPE> getBeanIdResolver() { + return beanIdResolver; + } + + /** + * Create an item identifier resolver using a named bean property. + * + * @param propertyId + * property identifier, which must map to a getter in BEANTYPE + * @return created resolver + */ + protected BeanIdResolver<IDTYPE, BEANTYPE> createBeanPropertyResolver( + Object propertyId) { + return new PropertyBasedBeanIdResolver(propertyId); + } + + @Override + public void addListener(Container.PropertySetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + super.removeListener(listener); + } + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Use addNestedContainerProperty(String) to add container properties to a " + + getClass().getSimpleName()); + } + + /** + * Adds a property for the container and all its items. + * + * Primarily for internal use, may change in future versions. + * + * @param propertyId + * @param propertyDescriptor + * @return true if the property was added + */ + protected final boolean addContainerProperty(String propertyId, + VaadinPropertyDescriptor<BEANTYPE> propertyDescriptor) { + if (null == propertyId || null == propertyDescriptor) { + return false; + } + + // Fails if the Property is already present + if (model.containsKey(propertyId)) { + return false; + } + + model.put(propertyId, propertyDescriptor); + for (BeanItem<BEANTYPE> item : itemIdToItem.values()) { + item.addItemProperty(propertyId, + propertyDescriptor.createProperty(item.getBean())); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + + /** + * Adds a nested container property for the container, e.g. + * "manager.address.street". + * + * All intermediate getters must exist and must return non-null values when + * the property value is accessed. + * + * @see NestedMethodProperty + * + * @param propertyId + * @return true if the property was added + */ + public boolean addNestedContainerProperty(String propertyId) { + return addContainerProperty(propertyId, new NestedPropertyDescriptor( + propertyId, type)); + } + + /** + * Adds a nested container properties for all sub-properties of a named + * property to the container. The named property itself is removed from the + * model as its subproperties are added. + * + * All intermediate getters must exist and must return non-null values when + * the property value is accessed. + * + * @see NestedMethodProperty + * @see #addNestedContainerProperty(String) + * + * @param propertyId + */ + @SuppressWarnings("unchecked") + public void addNestedContainerBean(String propertyId) { + Class<?> propertyType = getType(propertyId); + LinkedHashMap<String, VaadinPropertyDescriptor<Object>> pds = BeanItem + .getPropertyDescriptors((Class<Object>) propertyType); + for (String subPropertyId : pds.keySet()) { + String qualifiedPropertyId = propertyId + "." + subPropertyId; + NestedPropertyDescriptor<BEANTYPE> pd = new NestedPropertyDescriptor<BEANTYPE>( + qualifiedPropertyId, (Class<BEANTYPE>) type); + model.put(qualifiedPropertyId, pd); + model.remove(propertyId); + for (BeanItem<BEANTYPE> item : itemIdToItem.values()) { + item.addItemProperty(propertyId, + pd.createProperty(item.getBean())); + item.removeItemProperty(propertyId); + } + } + + // Sends a change event + fireContainerPropertySetChange(); + } + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + // Fails if the Property is not present + if (!model.containsKey(propertyId)) { + return false; + } + + // Removes the Property to Property list and types + model.remove(propertyId); + + // If remove the Property from all Items + for (final Iterator<IDTYPE> i = getAllItemIds().iterator(); i.hasNext();) { + getUnfilteredItem(i.next()).removeItemProperty(propertyId); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + +} diff --git a/server/src/com/vaadin/data/util/AbstractContainer.java b/server/src/com/vaadin/data/util/AbstractContainer.java new file mode 100644 index 0000000000..7d96c2d757 --- /dev/null +++ b/server/src/com/vaadin/data/util/AbstractContainer.java @@ -0,0 +1,251 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.LinkedList; + +import com.vaadin.data.Container; + +/** + * Abstract container class that manages event listeners and sending events to + * them ({@link PropertySetChangeNotifier}, {@link ItemSetChangeNotifier}). + * + * Note that this class provides the internal implementations for both types of + * events and notifiers as protected methods, but does not implement the + * {@link PropertySetChangeNotifier} and {@link ItemSetChangeNotifier} + * interfaces directly. This way, subclasses can choose not to implement them. + * Subclasses implementing those interfaces should also override the + * corresponding {@link #addListener()} and {@link #removeListener()} methods to + * make them public. + * + * @since 6.6 + */ +public abstract class AbstractContainer implements Container { + + /** + * List of all Property set change event listeners. + */ + private Collection<Container.PropertySetChangeListener> propertySetChangeListeners = null; + + /** + * List of all container Item set change event listeners. + */ + private Collection<Container.ItemSetChangeListener> itemSetChangeListeners = null; + + /** + * An <code>event</code> object specifying the container whose Property set + * has changed. + * + * This class does not provide information about which properties were + * concerned by the change, but subclasses can provide additional + * information about the changes. + */ + protected static class BasePropertySetChangeEvent extends EventObject + implements Container.PropertySetChangeEvent, Serializable { + + protected BasePropertySetChangeEvent(Container source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + /** + * An <code>event</code> object specifying the container whose Item set has + * changed. + * + * This class does not provide information about the exact changes + * performed, but subclasses can add provide additional information about + * the changes. + */ + protected static class BaseItemSetChangeEvent extends EventObject implements + Container.ItemSetChangeEvent, Serializable { + + protected BaseItemSetChangeEvent(Container source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + // PropertySetChangeNotifier + + /** + * Implementation of the corresponding method in + * {@link PropertySetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + protected void addListener(Container.PropertySetChangeListener listener) { + if (getPropertySetChangeListeners() == null) { + setPropertySetChangeListeners(new LinkedList<Container.PropertySetChangeListener>()); + } + getPropertySetChangeListeners().add(listener); + } + + /** + * Implementation of the corresponding method in + * {@link PropertySetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see PropertySetChangeNotifier#removeListener(com.vaadin.data.Container. + * PropertySetChangeListener) + */ + protected void removeListener(Container.PropertySetChangeListener listener) { + if (getPropertySetChangeListeners() != null) { + getPropertySetChangeListeners().remove(listener); + } + } + + // ItemSetChangeNotifier + + /** + * Implementation of the corresponding method in + * {@link ItemSetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + protected void addListener(Container.ItemSetChangeListener listener) { + if (getItemSetChangeListeners() == null) { + setItemSetChangeListeners(new LinkedList<Container.ItemSetChangeListener>()); + } + getItemSetChangeListeners().add(listener); + } + + /** + * Implementation of the corresponding method in + * {@link ItemSetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + protected void removeListener(Container.ItemSetChangeListener listener) { + if (getItemSetChangeListeners() != null) { + getItemSetChangeListeners().remove(listener); + } + } + + /** + * Sends a simple Property set change event to all interested listeners. + */ + protected void fireContainerPropertySetChange() { + fireContainerPropertySetChange(new BasePropertySetChangeEvent(this)); + } + + /** + * Sends a Property set change event to all interested listeners. + * + * Use {@link #fireContainerPropertySetChange()} instead of this method + * unless additional information about the exact changes is available and + * should be included in the event. + * + * @param event + * the property change event to send, optionally with additional + * information + */ + protected void fireContainerPropertySetChange( + Container.PropertySetChangeEvent event) { + if (getPropertySetChangeListeners() != null) { + final Object[] l = getPropertySetChangeListeners().toArray(); + for (int i = 0; i < l.length; i++) { + ((Container.PropertySetChangeListener) l[i]) + .containerPropertySetChange(event); + } + } + } + + /** + * Sends a simple Item set change event to all interested listeners, + * indicating that anything in the contents may have changed (items added, + * removed etc.). + */ + protected void fireItemSetChange() { + fireItemSetChange(new BaseItemSetChangeEvent(this)); + } + + /** + * Sends an Item set change event to all registered interested listeners. + * + * @param event + * the item set change event to send, optionally with additional + * information + */ + protected void fireItemSetChange(ItemSetChangeEvent event) { + if (getItemSetChangeListeners() != null) { + final Object[] l = getItemSetChangeListeners().toArray(); + for (int i = 0; i < l.length; i++) { + ((Container.ItemSetChangeListener) l[i]) + .containerItemSetChange(event); + } + } + } + + /** + * Sets the property set change listener collection. For internal use only. + * + * @param propertySetChangeListeners + */ + protected void setPropertySetChangeListeners( + Collection<Container.PropertySetChangeListener> propertySetChangeListeners) { + this.propertySetChangeListeners = propertySetChangeListeners; + } + + /** + * Returns the property set change listener collection. For internal use + * only. + */ + protected Collection<Container.PropertySetChangeListener> getPropertySetChangeListeners() { + return propertySetChangeListeners; + } + + /** + * Sets the item set change listener collection. For internal use only. + * + * @param itemSetChangeListeners + */ + protected void setItemSetChangeListeners( + Collection<Container.ItemSetChangeListener> itemSetChangeListeners) { + this.itemSetChangeListeners = itemSetChangeListeners; + } + + /** + * Returns the item set change listener collection. For internal use only. + */ + protected Collection<Container.ItemSetChangeListener> getItemSetChangeListeners() { + return itemSetChangeListeners; + } + + public Collection<?> getListeners(Class<?> eventType) { + if (Container.PropertySetChangeEvent.class.isAssignableFrom(eventType)) { + if (propertySetChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertySetChangeListeners); + } + } else if (Container.ItemSetChangeEvent.class + .isAssignableFrom(eventType)) { + if (itemSetChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(itemSetChangeListeners); + } + } + + return Collections.EMPTY_LIST; + } +} diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java new file mode 100644 index 0000000000..b7832756f2 --- /dev/null +++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -0,0 +1,941 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * Abstract {@link Container} class that handles common functionality for + * in-memory containers. Concrete in-memory container classes can either inherit + * this class, inherit {@link AbstractContainer}, or implement the + * {@link Container} interface directly. + * + * Adding and removing items (if desired) must be implemented in subclasses by + * overriding the appropriate add*Item() and remove*Item() and removeAllItems() + * methods, calling the corresponding + * {@link #internalAddItemAfter(Object, Object, Item)}, + * {@link #internalAddItemAt(int, Object, Item)}, + * {@link #internalAddItemAtEnd(Object, Item, boolean)}, + * {@link #internalRemoveItem(Object)} and {@link #internalRemoveAllItems()} + * methods. + * + * By default, adding and removing container properties is not supported, and + * subclasses need to implement {@link #getContainerPropertyIds()}. Optionally, + * subclasses can override {@link #addContainerProperty(Object, Class, Object)} + * and {@link #removeContainerProperty(Object)} to implement them. + * + * Features: + * <ul> + * <li> {@link Container.Ordered} + * <li> {@link Container.Indexed} + * <li> {@link Filterable} and {@link SimpleFilterable} (internal implementation, + * does not implement the interface directly) + * <li> {@link Sortable} (internal implementation, does not implement the + * interface directly) + * </ul> + * + * To implement {@link Sortable}, subclasses need to implement + * {@link #getSortablePropertyIds()} and call the superclass method + * {@link #sortContainer(Object[], boolean[])} in the method + * <code>sort(Object[], boolean[])</code>. + * + * To implement {@link Filterable}, subclasses need to implement the methods + * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)} + * (calling {@link #addFilter(Filter)}), + * {@link Filterable#removeAllContainerFilters()} (calling + * {@link #removeAllFilters()}) and + * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)} + * (calling {@link #removeFilter(com.vaadin.data.Container.Filter)}). + * + * To implement {@link SimpleFilterable}, subclasses also need to implement the + * methods + * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)} + * and {@link SimpleFilterable#removeContainerFilters(Object)} calling + * {@link #addFilter(com.vaadin.data.Container.Filter)} and + * {@link #removeFilters(Object)} respectively. + * + * @param <ITEMIDTYPE> + * the class of item identifiers in the container, use Object if can + * be any class + * @param <PROPERTYIDCLASS> + * the class of property identifiers for the items in the container, + * use Object if can be any class + * @param <ITEMCLASS> + * the (base) class of the Item instances in the container, use + * {@link Item} if unknown + * + * @since 6.6 + */ +public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITEMCLASS extends Item> + extends AbstractContainer implements ItemSetChangeNotifier, + Container.Indexed { + + /** + * An ordered {@link List} of all item identifiers in the container, + * including those that have been filtered out. + * + * Must not be null. + */ + private List<ITEMIDTYPE> allItemIds; + + /** + * An ordered {@link List} of item identifiers in the container after + * filtering, excluding those that have been filtered out. + * + * This is what the external API of the {@link Container} interface and its + * subinterfaces shows (e.g. {@link #size()}, {@link #nextItemId(Object)}). + * + * If null, the full item id list is used instead. + */ + private List<ITEMIDTYPE> filteredItemIds; + + /** + * Filters that are applied to the container to limit the items visible in + * it + */ + private Set<Filter> filters = new HashSet<Filter>(); + + /** + * The item sorter which is used for sorting the container. + */ + private ItemSorter itemSorter = new DefaultItemSorter(); + + // Constructors + + /** + * Constructor for an abstract in-memory container. + */ + protected AbstractInMemoryContainer() { + setAllItemIds(new ListSet<ITEMIDTYPE>()); + } + + // Container interface methods with more specific return class + + // default implementation, can be overridden + @Override + public ITEMCLASS getItem(Object itemId) { + if (containsId(itemId)) { + return getUnfilteredItem(itemId); + } else { + return null; + } + } + + /** + * Get an item even if filtered out. + * + * For internal use only. + * + * @param itemId + * @return + */ + protected abstract ITEMCLASS getUnfilteredItem(Object itemId); + + // cannot override getContainerPropertyIds() and getItemIds(): if subclass + // uses Object as ITEMIDCLASS or PROPERTYIDCLASS, Collection<Object> cannot + // be cast to Collection<MyInterface> + + // public abstract Collection<PROPERTYIDCLASS> getContainerPropertyIds(); + // public abstract Collection<ITEMIDCLASS> getItemIds(); + + // Container interface method implementations + + @Override + public int size() { + return getVisibleItemIds().size(); + } + + @Override + public boolean containsId(Object itemId) { + // only look at visible items after filtering + if (itemId == null) { + return false; + } else { + return getVisibleItemIds().contains(itemId); + } + } + + @Override + public List<?> getItemIds() { + return Collections.unmodifiableList(getVisibleItemIds()); + } + + // Container.Ordered + + @Override + public ITEMIDTYPE nextItemId(Object itemId) { + int index = indexOfId(itemId); + if (index >= 0 && index < size() - 1) { + return getIdByIndex(index + 1); + } else { + // out of bounds + return null; + } + } + + @Override + public ITEMIDTYPE prevItemId(Object itemId) { + int index = indexOfId(itemId); + if (index > 0) { + return getIdByIndex(index - 1); + } else { + // out of bounds + return null; + } + } + + @Override + public ITEMIDTYPE firstItemId() { + if (size() > 0) { + return getIdByIndex(0); + } else { + return null; + } + } + + @Override + public ITEMIDTYPE lastItemId() { + if (size() > 0) { + return getIdByIndex(size() - 1); + } else { + return null; + } + } + + @Override + public boolean isFirstId(Object itemId) { + if (itemId == null) { + return false; + } + return itemId.equals(firstItemId()); + } + + @Override + public boolean isLastId(Object itemId) { + if (itemId == null) { + return false; + } + return itemId.equals(lastItemId()); + } + + // Container.Indexed + + @Override + public ITEMIDTYPE getIdByIndex(int index) { + return getVisibleItemIds().get(index); + } + + @Override + public int indexOfId(Object itemId) { + return getVisibleItemIds().indexOf(itemId); + } + + // methods that are unsupported by default, override to support + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Object addItem() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Removing items not supported. Override the removeItem() method if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Removing items not supported. Override the removeAllItems() method if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding container properties not supported. Override the addContainerProperty() method if required."); + } + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Removing container properties not supported. Override the addContainerProperty() method if required."); + } + + // ItemSetChangeNotifier + + @Override + public void addListener(Container.ItemSetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + super.removeListener(listener); + } + + // internal methods + + // Filtering support + + /** + * Filter the view to recreate the visible item list from the unfiltered + * items, and send a notification if the set of visible items changed in any + * way. + */ + protected void filterAll() { + if (doFilterContainer(!getFilters().isEmpty())) { + fireItemSetChange(); + } + } + + /** + * Filters the data in the container and updates internal data structures. + * This method should reset any internal data structures and then repopulate + * them so {@link #getItemIds()} and other methods only return the filtered + * items. + * + * @param hasFilters + * true if filters has been set for the container, false + * otherwise + * @return true if the item set has changed as a result of the filtering + */ + protected boolean doFilterContainer(boolean hasFilters) { + if (!hasFilters) { + boolean changed = getAllItemIds().size() != getVisibleItemIds() + .size(); + setFilteredItemIds(null); + return changed; + } + + // Reset filtered list + List<ITEMIDTYPE> originalFilteredItemIds = getFilteredItemIds(); + boolean wasUnfiltered = false; + if (originalFilteredItemIds == null) { + originalFilteredItemIds = Collections.emptyList(); + wasUnfiltered = true; + } + setFilteredItemIds(new ListSet<ITEMIDTYPE>()); + + // Filter + boolean equal = true; + Iterator<ITEMIDTYPE> origIt = originalFilteredItemIds.iterator(); + for (final Iterator<ITEMIDTYPE> i = getAllItemIds().iterator(); i + .hasNext();) { + final ITEMIDTYPE id = i.next(); + if (passesFilters(id)) { + // filtered list comes from the full list, can use == + equal = equal && origIt.hasNext() && origIt.next() == id; + getFilteredItemIds().add(id); + } + } + + return (wasUnfiltered && !getAllItemIds().isEmpty()) || !equal + || origIt.hasNext(); + } + + /** + * Checks if the given itemId passes the filters set for the container. The + * caller should make sure the itemId exists in the container. For + * non-existing itemIds the behavior is undefined. + * + * @param itemId + * An itemId that exists in the container. + * @return true if the itemId passes all filters or no filters are set, + * false otherwise. + */ + protected boolean passesFilters(Object itemId) { + ITEMCLASS item = getUnfilteredItem(itemId); + if (getFilters().isEmpty()) { + return true; + } + final Iterator<Filter> i = getFilters().iterator(); + while (i.hasNext()) { + final Filter f = i.next(); + if (!f.passesFilter(itemId, item)) { + return false; + } + } + return true; + } + + /** + * Adds a container filter and re-filter the view. + * + * The filter must implement Filter and its sub-filters (if any) must also + * be in-memory filterable. + * + * This can be used to implement + * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)} + * and optionally also + * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)} + * (with {@link SimpleStringFilter}). + * + * Note that in some cases, incompatible filters cannot be detected when + * added and an {@link UnsupportedFilterException} may occur when performing + * filtering. + * + * @throws UnsupportedFilterException + * if the filter is detected as not supported by the container + */ + protected void addFilter(Filter filter) throws UnsupportedFilterException { + getFilters().add(filter); + filterAll(); + } + + /** + * Remove a specific container filter and re-filter the view (if necessary). + * + * This can be used to implement + * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)} + * . + */ + protected void removeFilter(Filter filter) { + for (Iterator<Filter> iterator = getFilters().iterator(); iterator + .hasNext();) { + Filter f = iterator.next(); + if (f.equals(filter)) { + iterator.remove(); + filterAll(); + return; + } + } + } + + /** + * Remove all container filters for all properties and re-filter the view. + * + * This can be used to implement + * {@link Filterable#removeAllContainerFilters()}. + */ + protected void removeAllFilters() { + if (getFilters().isEmpty()) { + return; + } + getFilters().clear(); + filterAll(); + } + + /** + * Checks if there is a filter that applies to a given property. + * + * @param propertyId + * @return true if there is an active filter for the property + */ + protected boolean isPropertyFiltered(Object propertyId) { + if (getFilters().isEmpty() || propertyId == null) { + return false; + } + final Iterator<Filter> i = getFilters().iterator(); + while (i.hasNext()) { + final Filter f = i.next(); + if (f.appliesToProperty(propertyId)) { + return true; + } + } + return false; + } + + /** + * Remove all container filters for a given property identifier and + * re-filter the view. This also removes filters applying to multiple + * properties including the one identified by propertyId. + * + * This can be used to implement + * {@link Filterable#removeContainerFilters(Object)}. + * + * @param propertyId + * @return Collection<Filter> removed filters + */ + protected Collection<Filter> removeFilters(Object propertyId) { + if (getFilters().isEmpty() || propertyId == null) { + return Collections.emptyList(); + } + List<Filter> removedFilters = new LinkedList<Filter>(); + for (Iterator<Filter> iterator = getFilters().iterator(); iterator + .hasNext();) { + Filter f = iterator.next(); + if (f.appliesToProperty(propertyId)) { + removedFilters.add(f); + iterator.remove(); + } + } + if (!removedFilters.isEmpty()) { + filterAll(); + return removedFilters; + } + return Collections.emptyList(); + } + + // sorting + + /** + * Returns the ItemSorter used for comparing items in a sort. See + * {@link #setItemSorter(ItemSorter)} for more information. + * + * @return The ItemSorter used for comparing two items in a sort. + */ + protected ItemSorter getItemSorter() { + return itemSorter; + } + + /** + * Sets the ItemSorter used for comparing items in a sort. The + * {@link ItemSorter#compare(Object, Object)} method is called with item ids + * to perform the sorting. A default ItemSorter is used if this is not + * explicitly set. + * + * @param itemSorter + * The ItemSorter used for comparing two items in a sort (not + * null). + */ + protected void setItemSorter(ItemSorter itemSorter) { + this.itemSorter = itemSorter; + } + + /** + * Sort base implementation to be used to implement {@link Sortable}. + * + * Subclasses should call this from a public + * {@link #sort(Object[], boolean[])} method when implementing Sortable. + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + protected void sortContainer(Object[] propertyId, boolean[] ascending) { + if (!(this instanceof Sortable)) { + throw new UnsupportedOperationException( + "Cannot sort a Container that does not implement Sortable"); + } + + // Set up the item sorter for the sort operation + getItemSorter().setSortProperties((Sortable) this, propertyId, + ascending); + + // Perform the actual sort + doSort(); + + // Post sort updates + if (isFiltered()) { + filterAll(); + } else { + fireItemSetChange(); + } + + } + + /** + * Perform the sorting of the data structures in the container. This is + * invoked when the <code>itemSorter</code> has been prepared for the sort + * operation. Typically this method calls + * <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays + * (containing item ids) that need to be sorted. + * + */ + protected void doSort() { + Collections.sort(getAllItemIds(), getItemSorter()); + } + + /** + * Returns the sortable property identifiers for the container. Can be used + * to implement {@link Sortable#getSortableContainerPropertyIds()}. + */ + protected Collection<?> getSortablePropertyIds() { + LinkedList<Object> sortables = new LinkedList<Object>(); + for (Object propertyId : getContainerPropertyIds()) { + Class<?> propertyType = getType(propertyId); + if (Comparable.class.isAssignableFrom(propertyType) + || propertyType.isPrimitive()) { + sortables.add(propertyId); + } + } + return sortables; + } + + // removing items + + /** + * Removes all items from the internal data structures of this class. This + * can be used to implement {@link #removeAllItems()} in subclasses. + * + * No notification is sent, the caller has to fire a suitable item set + * change notification. + */ + protected void internalRemoveAllItems() { + // Removes all Items + getAllItemIds().clear(); + if (isFiltered()) { + getFilteredItemIds().clear(); + } + } + + /** + * Removes a single item from the internal data structures of this class. + * This can be used to implement {@link #removeItem(Object)} in subclasses. + * + * No notification is sent, the caller has to fire a suitable item set + * change notification. + * + * @param itemId + * the identifier of the item to remove + * @return true if an item was successfully removed, false if failed to + * remove or no such item + */ + protected boolean internalRemoveItem(Object itemId) { + if (itemId == null) { + return false; + } + + boolean result = getAllItemIds().remove(itemId); + if (result && isFiltered()) { + getFilteredItemIds().remove(itemId); + } + + return result; + } + + // adding items + + /** + * Adds the bean to all internal data structures at the given position. + * Fails if an item with itemId is already in the container. Returns a the + * item if it was added successfully, null otherwise. + * + * <p> + * Caller should initiate filtering after calling this method. + * </p> + * + * For internal use only - subclasses should use + * {@link #internalAddItemAtEnd(Object, Item, boolean)}, + * {@link #internalAddItemAt(int, Object, Item, boolean)} and + * {@link #internalAddItemAfter(Object, Object, Item, boolean)} instead. + * + * @param position + * The position at which the item should be inserted in the + * unfiltered collection of items + * @param itemId + * The item identifier for the item to insert + * @param item + * The item to insert + * + * @return ITEMCLASS if the item was added successfully, null otherwise + */ + private ITEMCLASS internalAddAt(int position, ITEMIDTYPE itemId, + ITEMCLASS item) { + if (position < 0 || position > getAllItemIds().size() || itemId == null + || item == null) { + return null; + } + // Make sure that the item has not been added previously + if (getAllItemIds().contains(itemId)) { + return null; + } + + // "filteredList" will be updated in filterAll() which should be invoked + // by the caller after calling this method. + getAllItemIds().add(position, itemId); + registerNewItem(position, itemId, item); + + return item; + } + + /** + * Add an item at the end of the container, and perform filtering if + * necessary. An event is fired if the filtered view changes. + * + * @param newItemId + * @param item + * new item to add + * @param filter + * true to perform filtering and send event after adding the + * item, false to skip these operations for batch inserts - if + * false, caller needs to make sure these operations are + * performed at the end of the batch + * @return item added or null if no item was added + */ + protected ITEMCLASS internalAddItemAtEnd(ITEMIDTYPE newItemId, + ITEMCLASS item, boolean filter) { + ITEMCLASS newItem = internalAddAt(getAllItemIds().size(), newItemId, + item); + if (newItem != null && filter) { + // TODO filter only this item, use fireItemAdded() + filterAll(); + if (!isFiltered()) { + // TODO hack: does not detect change in filterAll() in this case + fireItemAdded(indexOfId(newItemId), newItemId, item); + } + } + return newItem; + } + + /** + * Add an item after a given (visible) item, and perform filtering. An event + * is fired if the filtered view changes. + * + * The new item is added at the beginning if previousItemId is null. + * + * @param previousItemId + * item id of a visible item after which to add the new item, or + * null to add at the beginning + * @param newItemId + * @param item + * new item to add + * @param filter + * true to perform filtering and send event after adding the + * item, false to skip these operations for batch inserts - if + * false, caller needs to make sure these operations are + * performed at the end of the batch + * @return item added or null if no item was added + */ + protected ITEMCLASS internalAddItemAfter(ITEMIDTYPE previousItemId, + ITEMIDTYPE newItemId, ITEMCLASS item, boolean filter) { + // only add if the previous item is visible + ITEMCLASS newItem = null; + if (previousItemId == null) { + newItem = internalAddAt(0, newItemId, item); + } else if (containsId(previousItemId)) { + newItem = internalAddAt( + getAllItemIds().indexOf(previousItemId) + 1, newItemId, + item); + } + if (newItem != null && filter) { + // TODO filter only this item, use fireItemAdded() + filterAll(); + if (!isFiltered()) { + // TODO hack: does not detect change in filterAll() in this case + fireItemAdded(indexOfId(newItemId), newItemId, item); + } + } + return newItem; + } + + /** + * Add an item at a given (visible after filtering) item index, and perform + * filtering. An event is fired if the filtered view changes. + * + * @param index + * position where to add the item (visible/view index) + * @param newItemId + * @param item + * new item to add + * @param filter + * true to perform filtering and send event after adding the + * item, false to skip these operations for batch inserts - if + * false, caller needs to make sure these operations are + * performed at the end of the batch + * @return item added or null if no item was added + */ + protected ITEMCLASS internalAddItemAt(int index, ITEMIDTYPE newItemId, + ITEMCLASS item, boolean filter) { + if (index < 0 || index > size()) { + return null; + } else if (index == 0) { + // add before any item, visible or not + return internalAddItemAfter(null, newItemId, item, filter); + } else { + // if index==size(), adds immediately after last visible item + return internalAddItemAfter(getIdByIndex(index - 1), newItemId, + item, filter); + } + } + + /** + * Registers a new item as having been added to the container. This can + * involve storing the item or any relevant information about it in internal + * container-specific collections if necessary, as well as registering + * listeners etc. + * + * The full identifier list in {@link AbstractInMemoryContainer} has already + * been updated to reflect the new item when this method is called. + * + * @param position + * @param itemId + * @param item + */ + protected void registerNewItem(int position, ITEMIDTYPE itemId, + ITEMCLASS item) { + } + + // item set change notifications + + /** + * Notify item set change listeners that an item has been added to the + * container. + * + * Unless subclasses specify otherwise, the default notification indicates a + * full refresh. + * + * @param postion + * position of the added item in the view (if visible) + * @param itemId + * id of the added item + * @param item + * the added item + */ + protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) { + fireItemSetChange(); + } + + /** + * Notify item set change listeners that an item has been removed from the + * container. + * + * Unless subclasses specify otherwise, the default notification indicates a + * full refresh. + * + * @param postion + * position of the removed item in the view prior to removal (if + * was visible) + * @param itemId + * id of the removed item, of type {@link Object} to satisfy + * {@link Container#removeItem(Object)} API + */ + protected void fireItemRemoved(int position, Object itemId) { + fireItemSetChange(); + } + + // visible and filtered item identifier lists + + /** + * Returns the internal list of visible item identifiers after filtering. + * + * For internal use only. + */ + protected List<ITEMIDTYPE> getVisibleItemIds() { + if (isFiltered()) { + return getFilteredItemIds(); + } else { + return getAllItemIds(); + } + } + + /** + * Returns true is the container has active filters. + * + * @return true if the container is currently filtered + */ + protected boolean isFiltered() { + return filteredItemIds != null; + } + + /** + * Internal helper method to set the internal list of filtered item + * identifiers. Should not be used outside this class except for + * implementing clone(), may disappear from future versions. + * + * @param filteredItemIds + */ + @Deprecated + protected void setFilteredItemIds(List<ITEMIDTYPE> filteredItemIds) { + this.filteredItemIds = filteredItemIds; + } + + /** + * Internal helper method to get the internal list of filtered item + * identifiers. Should not be used outside this class except for + * implementing clone(), may disappear from future versions - use + * {@link #getVisibleItemIds()} in other contexts. + * + * @return List<ITEMIDTYPE> + */ + protected List<ITEMIDTYPE> getFilteredItemIds() { + return filteredItemIds; + } + + /** + * Internal helper method to set the internal list of all item identifiers. + * Should not be used outside this class except for implementing clone(), + * may disappear from future versions. + * + * @param allItemIds + */ + @Deprecated + protected void setAllItemIds(List<ITEMIDTYPE> allItemIds) { + this.allItemIds = allItemIds; + } + + /** + * Internal helper method to get the internal list of all item identifiers. + * Avoid using this method outside this class, may disappear in future + * versions. + * + * @return List<ITEMIDTYPE> + */ + protected List<ITEMIDTYPE> getAllItemIds() { + return allItemIds; + } + + /** + * Set the internal collection of filters without performing filtering. + * + * This method is mostly for internal use, use + * {@link #addFilter(com.vaadin.data.Container.Filter)} and + * <code>remove*Filter*</code> (which also re-filter the container) instead + * when possible. + * + * @param filters + */ + protected void setFilters(Set<Filter> filters) { + this.filters = filters; + } + + /** + * Returns the internal collection of filters. The returned collection + * should not be modified by callers outside this class. + * + * @return Set<Filter> + */ + protected Set<Filter> getFilters() { + return filters; + } + +} diff --git a/server/src/com/vaadin/data/util/AbstractProperty.java b/server/src/com/vaadin/data/util/AbstractProperty.java new file mode 100644 index 0000000000..373a8dfd58 --- /dev/null +++ b/server/src/com/vaadin/data/util/AbstractProperty.java @@ -0,0 +1,226 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; + +import com.vaadin.data.Property; + +/** + * Abstract base class for {@link Property} implementations. + * + * Handles listener management for {@link ValueChangeListener}s and + * {@link ReadOnlyStatusChangeListener}s. + * + * @since 6.6 + */ +public abstract class AbstractProperty<T> implements Property<T>, + Property.ValueChangeNotifier, Property.ReadOnlyStatusChangeNotifier { + + /** + * List of listeners who are interested in the read-only status changes of + * the Property + */ + private LinkedList<ReadOnlyStatusChangeListener> readOnlyStatusChangeListeners = null; + + /** + * List of listeners who are interested in the value changes of the Property + */ + private LinkedList<ValueChangeListener> valueChangeListeners = null; + + /** + * Is the Property read-only? + */ + private boolean readOnly; + + /** + * {@inheritDoc} + * + * Override for additional restrictions on what is considered a read-only + * property. + */ + @Override + public boolean isReadOnly() { + return readOnly; + } + + @Override + public void setReadOnly(boolean newStatus) { + boolean oldStatus = isReadOnly(); + readOnly = newStatus; + if (oldStatus != isReadOnly()) { + fireReadOnlyStatusChange(); + } + } + + /** + * Returns the value of the <code>Property</code> in human readable textual + * format. + * + * @return String representation of the value stored in the Property + * @deprecated use {@link #getValue()} instead and possibly toString on that + */ + @Deprecated + @Override + public String toString() { + throw new UnsupportedOperationException( + "Use Property.getValue() instead of " + getClass() + + ".toString()"); + } + + /* Events */ + + /** + * An <code>Event</code> object specifying the Property whose read-only + * status has been changed. + */ + protected static class ReadOnlyStatusChangeEvent extends + java.util.EventObject implements Property.ReadOnlyStatusChangeEvent { + + /** + * Constructs a new read-only status change event for this object. + * + * @param source + * source object of the event. + */ + protected ReadOnlyStatusChangeEvent(Property source) { + super(source); + } + + /** + * Gets the Property whose read-only state has changed. + * + * @return source Property of the event. + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + + } + + /** + * Registers a new read-only status change listener for this Property. + * + * @param listener + * the new Listener to be registered. + */ + @Override + public void addListener(Property.ReadOnlyStatusChangeListener listener) { + if (readOnlyStatusChangeListeners == null) { + readOnlyStatusChangeListeners = new LinkedList<ReadOnlyStatusChangeListener>(); + } + readOnlyStatusChangeListeners.add(listener); + } + + /** + * Removes a previously registered read-only status change listener. + * + * @param listener + * the listener to be removed. + */ + @Override + public void removeListener(Property.ReadOnlyStatusChangeListener listener) { + if (readOnlyStatusChangeListeners != null) { + readOnlyStatusChangeListeners.remove(listener); + } + } + + /** + * Sends a read only status change event to all registered listeners. + */ + protected void fireReadOnlyStatusChange() { + if (readOnlyStatusChangeListeners != null) { + final Object[] l = readOnlyStatusChangeListeners.toArray(); + final Property.ReadOnlyStatusChangeEvent event = new ReadOnlyStatusChangeEvent( + this); + for (int i = 0; i < l.length; i++) { + ((Property.ReadOnlyStatusChangeListener) l[i]) + .readOnlyStatusChange(event); + } + } + } + + /** + * An <code>Event</code> object specifying the Property whose value has been + * changed. + */ + private static class ValueChangeEvent extends java.util.EventObject + implements Property.ValueChangeEvent { + + /** + * Constructs a new value change event for this object. + * + * @param source + * source object of the event. + */ + protected ValueChangeEvent(Property source) { + super(source); + } + + /** + * Gets the Property whose value has changed. + * + * @return source Property of the event. + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + + } + + @Override + public void addListener(ValueChangeListener listener) { + if (valueChangeListeners == null) { + valueChangeListeners = new LinkedList<ValueChangeListener>(); + } + valueChangeListeners.add(listener); + + } + + @Override + public void removeListener(ValueChangeListener listener) { + if (valueChangeListeners != null) { + valueChangeListeners.remove(listener); + } + + } + + /** + * Sends a value change event to all registered listeners. + */ + protected void fireValueChange() { + if (valueChangeListeners != null) { + final Object[] l = valueChangeListeners.toArray(); + final Property.ValueChangeEvent event = new ValueChangeEvent(this); + for (int i = 0; i < l.length; i++) { + ((Property.ValueChangeListener) l[i]).valueChange(event); + } + } + } + + public Collection<?> getListeners(Class<?> eventType) { + if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) { + if (valueChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections.unmodifiableCollection(valueChangeListeners); + } + } else if (Property.ReadOnlyStatusChangeEvent.class + .isAssignableFrom(eventType)) { + if (readOnlyStatusChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(readOnlyStatusChangeListeners); + } + } + + return Collections.EMPTY_LIST; + } + +} diff --git a/server/src/com/vaadin/data/util/BeanContainer.java b/server/src/com/vaadin/data/util/BeanContainer.java new file mode 100644 index 0000000000..bc1ee3c39e --- /dev/null +++ b/server/src/com/vaadin/data/util/BeanContainer.java @@ -0,0 +1,168 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.util.Collection; + +/** + * An in-memory container for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class. Only beans of the same type can be added to the + * container. + * </p> + * + * <p> + * In BeanContainer (unlike {@link BeanItemContainer}), the item IDs do not have + * to be the beans themselves. The container can be used either with explicit + * item IDs or the item IDs can be generated when adding beans. + * </p> + * + * <p> + * To use explicit item IDs, use the methods {@link #addItem(Object, Object)}, + * {@link #addItemAfter(Object, Object, Object)} and + * {@link #addItemAt(int, Object, Object)}. + * </p> + * + * <p> + * If a bean id resolver is set using + * {@link #setBeanIdResolver(com.vaadin.data.util.AbstractBeanContainer.BeanIdResolver)} + * or {@link #setBeanIdProperty(Object)}, the methods {@link #addBean(Object)}, + * {@link #addBeanAfter(Object, Object)}, {@link #addBeanAt(int, Object)} and + * {@link #addAll(java.util.Collection)} can be used to add items to the + * container. If one of these methods is called, the resolver is used to + * generate an identifier for the item (must not return null). + * </p> + * + * <p> + * Note that explicit item identifiers can also be used when a resolver has been + * set by calling the addItem*() methods - the resolver is only used when adding + * beans using the addBean*() or {@link #addAll(Collection)} methods. + * </p> + * + * <p> + * It is not possible to add additional properties to the container and nested + * bean properties are not supported. + * </p> + * + * @param <IDTYPE> + * The type of the item identifier + * @param <BEANTYPE> + * The type of the Bean + * + * @see AbstractBeanContainer + * @see BeanItemContainer + * + * @since 6.5 + */ +public class BeanContainer<IDTYPE, BEANTYPE> extends + AbstractBeanContainer<IDTYPE, BEANTYPE> { + + public BeanContainer(Class<? super BEANTYPE> type) { + super(type); + } + + /** + * Adds the bean to the Container. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + public BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) { + if (itemId != null && bean != null) { + return super.addItem(itemId, bean); + } else { + return null; + } + } + + /** + * Adds the bean after the given item id. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object) + */ + @Override + public BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId, + IDTYPE newItemId, BEANTYPE bean) { + if (newItemId != null && bean != null) { + return super.addItemAfter(previousItemId, newItemId, bean); + } else { + return null; + } + } + + /** + * Adds a new bean at the given index. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param index + * Index at which the bean should be added. + * @param newItemId + * The item id for the bean to add to the container. + * @param bean + * The bean to add to the container. + * + * @return Returns the new BeanItem or null if the operation fails. + */ + @Override + public BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId, + BEANTYPE bean) { + if (newItemId != null && bean != null) { + return super.addItemAt(index, newItemId, bean); + } else { + return null; + } + } + + // automatic item id resolution + + /** + * Sets the bean id resolver to use a property of the beans as the + * identifier. + * + * @param propertyId + * the identifier of the property to use to find item identifiers + */ + public void setBeanIdProperty(Object propertyId) { + setBeanIdResolver(createBeanPropertyResolver(propertyId)); + } + + @Override + // overridden to make public + public void setBeanIdResolver( + BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) { + super.setBeanIdResolver(beanIdResolver); + } + + @Override + // overridden to make public + public BeanItem<BEANTYPE> addBean(BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBean(bean); + } + + @Override + // overridden to make public + public BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBeanAfter(previousItemId, bean); + } + + @Override + // overridden to make public + public BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBeanAt(index, bean); + } + + @Override + // overridden to make public + public void addAll(Collection<? extends BEANTYPE> collection) + throws IllegalStateException { + super.addAll(collection); + } + +} diff --git a/server/src/com/vaadin/data/util/BeanItem.java b/server/src/com/vaadin/data/util/BeanItem.java new file mode 100644 index 0000000000..94439471f5 --- /dev/null +++ b/server/src/com/vaadin/data/util/BeanItem.java @@ -0,0 +1,269 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A wrapper class for adding the Item interface to any Java Bean. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class BeanItem<BT> extends PropertysetItem { + + /** + * The bean which this Item is based on. + */ + private final BT bean; + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all properties + * of a Java Bean to it. The properties are identified by their respective + * bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * + */ + public BeanItem(BT bean) { + this(bean, getPropertyDescriptors((Class<BT>) bean.getClass())); + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> using a pre-computed set + * of properties. The properties are identified by their respective bean + * names. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyDescriptors + * pre-computed property descriptors + */ + BeanItem(BT bean, + Map<String, VaadinPropertyDescriptor<BT>> propertyDescriptors) { + + this.bean = bean; + + for (VaadinPropertyDescriptor<BT> pd : propertyDescriptors.values()) { + addItemProperty(pd.getName(), pd.createProperty(bean)); + } + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all listed + * properties of a Java Bean to it - in specified order. The properties are + * identified by their respective bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyIds + * id of the property. + */ + public BeanItem(BT bean, Collection<?> propertyIds) { + + this.bean = bean; + + // Create bean information + LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pds = getPropertyDescriptors((Class<BT>) bean + .getClass()); + + // Add all the bean properties as MethodProperties to this Item + for (Object id : propertyIds) { + VaadinPropertyDescriptor<BT> pd = pds.get(id); + if (pd != null) { + addItemProperty(pd.getName(), pd.createProperty(bean)); + } + } + + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all listed + * properties of a Java Bean to it - in specified order. The properties are + * identified by their respective bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyIds + * ids of the properties. + */ + public BeanItem(BT bean, String[] propertyIds) { + this(bean, Arrays.asList(propertyIds)); + } + + /** + * <p> + * Perform introspection on a Java Bean class to find its properties. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param beanClass + * the Java Bean class to get properties for. + * @return an ordered map from property names to property descriptors + */ + static <BT> LinkedHashMap<String, VaadinPropertyDescriptor<BT>> getPropertyDescriptors( + final Class<BT> beanClass) { + final LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pdMap = new LinkedHashMap<String, VaadinPropertyDescriptor<BT>>(); + + // Try to introspect, if it fails, we just have an empty Item + try { + List<PropertyDescriptor> propertyDescriptors = getBeanPropertyDescriptor(beanClass); + + // Add all the bean properties as MethodProperties to this Item + // later entries on the list overwrite earlier ones + for (PropertyDescriptor pd : propertyDescriptors) { + final Method getMethod = pd.getReadMethod(); + if ((getMethod != null) + && getMethod.getDeclaringClass() != Object.class) { + VaadinPropertyDescriptor<BT> vaadinPropertyDescriptor = new MethodPropertyDescriptor<BT>( + pd.getName(), pd.getPropertyType(), + pd.getReadMethod(), pd.getWriteMethod()); + pdMap.put(pd.getName(), vaadinPropertyDescriptor); + } + } + } catch (final java.beans.IntrospectionException ignored) { + } + + return pdMap; + } + + /** + * Returns the property descriptors of a class or an interface. + * + * For an interface, superinterfaces are also iterated as Introspector does + * not take them into account (Oracle Java bug 4275879), but in that case, + * both the setter and the getter for a property must be in the same + * interface and should not be overridden in subinterfaces for the discovery + * to work correctly. + * + * For interfaces, the iteration is depth first and the properties of + * superinterfaces are returned before those of their subinterfaces. + * + * @param beanClass + * @return + * @throws IntrospectionException + */ + private static List<PropertyDescriptor> getBeanPropertyDescriptor( + final Class<?> beanClass) throws IntrospectionException { + // Oracle bug 4275879: Introspector does not consider superinterfaces of + // an interface + if (beanClass.isInterface()) { + List<PropertyDescriptor> propertyDescriptors = new ArrayList<PropertyDescriptor>(); + + for (Class<?> cls : beanClass.getInterfaces()) { + propertyDescriptors.addAll(getBeanPropertyDescriptor(cls)); + } + + BeanInfo info = Introspector.getBeanInfo(beanClass); + propertyDescriptors.addAll(Arrays.asList(info + .getPropertyDescriptors())); + + return propertyDescriptors; + } else { + BeanInfo info = Introspector.getBeanInfo(beanClass); + return Arrays.asList(info.getPropertyDescriptors()); + } + } + + /** + * Expands nested bean properties by replacing a top-level property with + * some or all of its sub-properties. The expansion is not recursive. + * + * @param propertyId + * property id for the property whose sub-properties are to be + * expanded, + * @param subPropertyIds + * sub-properties to expand, all sub-properties are expanded if + * not specified + */ + public void expandProperty(String propertyId, String... subPropertyIds) { + Set<String> subPropertySet = new HashSet<String>( + Arrays.asList(subPropertyIds)); + + if (0 == subPropertyIds.length) { + // Enumerate all sub-properties + Class<?> propertyType = getItemProperty(propertyId).getType(); + Map<String, ?> pds = getPropertyDescriptors(propertyType); + subPropertySet.addAll(pds.keySet()); + } + + for (String subproperty : subPropertySet) { + String qualifiedPropertyId = propertyId + "." + subproperty; + addNestedProperty(qualifiedPropertyId); + } + + removeItemProperty(propertyId); + } + + /** + * Adds a nested property to the item. + * + * @param nestedPropertyId + * property id to add. This property must not exist in the item + * already and must of of form "field1.field2" where field2 is a + * field in the object referenced to by field1 + */ + public void addNestedProperty(String nestedPropertyId) { + addItemProperty(nestedPropertyId, new NestedMethodProperty<Object>( + getBean(), nestedPropertyId)); + } + + /** + * Gets the underlying JavaBean object. + * + * @return the bean object. + */ + public BT getBean() { + return bean; + } + +} diff --git a/server/src/com/vaadin/data/util/BeanItemContainer.java b/server/src/com/vaadin/data/util/BeanItemContainer.java new file mode 100644 index 0000000000..dc4deaebdc --- /dev/null +++ b/server/src/com/vaadin/data/util/BeanItemContainer.java @@ -0,0 +1,241 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.util.Collection; + +/** + * An in-memory container for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class. Only beans of the same type can be added to the + * container. + * </p> + * + * <p> + * BeanItemContainer uses the beans themselves as identifiers. The + * {@link Object#hashCode()} of a bean is used when storing and looking up beans + * so it must not change during the lifetime of the bean (it should not depend + * on any part of the bean that can be modified). Typically this restricts the + * implementation of {@link Object#equals(Object)} as well in order for it to + * fulfill the contract between {@code equals()} and {@code hashCode()}. + * </p> + * + * <p> + * To add items to the container, use the methods {@link #addBean(Object)}, + * {@link #addBeanAfter(Object, Object)} and {@link #addBeanAt(int, Object)}. + * Also {@link #addItem(Object)}, {@link #addItemAfter(Object, Object)} and + * {@link #addItemAt(int, Object)} can be used as synonyms for them. + * </p> + * + * <p> + * It is not possible to add additional properties to the container and nested + * bean properties are not supported. + * </p> + * + * @param <BEANTYPE> + * The type of the Bean + * + * @since 5.4 + */ +@SuppressWarnings("serial") +public class BeanItemContainer<BEANTYPE> extends + AbstractBeanContainer<BEANTYPE, BEANTYPE> { + + /** + * Bean identity resolver that returns the bean itself as its item + * identifier. + * + * This corresponds to the old behavior of {@link BeanItemContainer}, and + * requires suitable (identity-based) equals() and hashCode() methods on the + * beans. + * + * @param <BT> + * + * @since 6.5 + */ + private static class IdentityBeanIdResolver<BT> implements + BeanIdResolver<BT, BT> { + + @Override + public BT getIdForBean(BT bean) { + return bean; + } + + } + + /** + * Constructs a {@code BeanItemContainer} for beans of the given type. + * + * @param type + * the type of the beans that will be added to the container. + * @throws IllegalArgumentException + * If {@code type} is null + */ + public BeanItemContainer(Class<? super BEANTYPE> type) + throws IllegalArgumentException { + super(type); + super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>()); + } + + /** + * Constructs a {@code BeanItemContainer} and adds the given beans to it. + * The collection must not be empty. + * {@link BeanItemContainer#BeanItemContainer(Class)} can be used for + * creating an initially empty {@code BeanItemContainer}. + * + * Note that when using this constructor, the actual class of the first item + * in the collection is used to determine the bean properties supported by + * the container instance, and only beans of that class or its subclasses + * can be added to the collection. If this is problematic or empty + * collections need to be supported, use {@link #BeanItemContainer(Class)} + * and {@link #addAll(Collection)} instead. + * + * @param collection + * a non empty {@link Collection} of beans. + * @throws IllegalArgumentException + * If the collection is null or empty. + * + * @deprecated use {@link #BeanItemContainer(Class, Collection)} instead + */ + @SuppressWarnings("unchecked") + @Deprecated + public BeanItemContainer(Collection<? extends BEANTYPE> collection) + throws IllegalArgumentException { + // must assume the class is BT + // the class information is erased by the compiler + this((Class<BEANTYPE>) getBeanClassForCollection(collection), + collection); + } + + /** + * Internal helper method to support the deprecated {@link Collection} + * container. + * + * @param <BT> + * @param collection + * @return + * @throws IllegalArgumentException + */ + @SuppressWarnings("unchecked") + @Deprecated + private static <BT> Class<? extends BT> getBeanClassForCollection( + Collection<? extends BT> collection) + throws IllegalArgumentException { + if (collection == null || collection.isEmpty()) { + throw new IllegalArgumentException( + "The collection passed to BeanItemContainer constructor must not be null or empty. Use the other BeanItemContainer constructor."); + } + return (Class<? extends BT>) collection.iterator().next().getClass(); + } + + /** + * Constructs a {@code BeanItemContainer} and adds the given beans to it. + * + * @param type + * the type of the beans that will be added to the container. + * @param collection + * a {@link Collection} of beans (can be empty or null). + * @throws IllegalArgumentException + * If {@code type} is null + */ + public BeanItemContainer(Class<? super BEANTYPE> type, + Collection<? extends BEANTYPE> collection) + throws IllegalArgumentException { + super(type); + super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>()); + + if (collection != null) { + addAll(collection); + } + } + + /** + * Adds all the beans from a {@link Collection} in one go. More efficient + * than adding them one by one. + * + * @param collection + * The collection of beans to add. Must not be null. + */ + @Override + public void addAll(Collection<? extends BEANTYPE> collection) { + super.addAll(collection); + } + + /** + * Adds the bean after the given bean. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param previousItemId + * the bean (of type BT) after which to add newItemId + * @param newItemId + * the bean (of type BT) to add (not null) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object) + */ + @Override + @SuppressWarnings("unchecked") + public BeanItem<BEANTYPE> addItemAfter(Object previousItemId, + Object newItemId) throws IllegalArgumentException { + return super.addBeanAfter((BEANTYPE) previousItemId, + (BEANTYPE) newItemId); + } + + /** + * Adds a new bean at the given index. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param index + * Index at which the bean should be added. + * @param newItemId + * The bean to add to the container. + * @return Returns the new BeanItem or null if the operation fails. + */ + @Override + @SuppressWarnings("unchecked") + public BeanItem<BEANTYPE> addItemAt(int index, Object newItemId) + throws IllegalArgumentException { + return super.addBeanAt(index, (BEANTYPE) newItemId); + } + + /** + * Adds the bean to the Container. + * + * The bean is used both as the item contents and as the item identifier. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + @SuppressWarnings("unchecked") + public BeanItem<BEANTYPE> addItem(Object itemId) { + return super.addBean((BEANTYPE) itemId); + } + + /** + * Adds the bean to the Container. + * + * The bean is used both as the item contents and as the item identifier. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + public BeanItem<BEANTYPE> addBean(BEANTYPE bean) { + return addItem(bean); + } + + /** + * Unsupported in BeanItemContainer. + */ + @Override + protected void setBeanIdResolver( + AbstractBeanContainer.BeanIdResolver<BEANTYPE, BEANTYPE> beanIdResolver) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "BeanItemContainer always uses an IdentityBeanIdResolver"); + } + +} diff --git a/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java new file mode 100644 index 0000000000..717ce834cf --- /dev/null +++ b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java @@ -0,0 +1,792 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * <p> + * A wrapper class for adding external hierarchy to containers not implementing + * the {@link com.vaadin.data.Container.Hierarchical} interface. + * </p> + * + * <p> + * If the wrapped container is changed directly (that is, not through the + * wrapper), and does not implement Container.ItemSetChangeNotifier and/or + * Container.PropertySetChangeNotifier the hierarchy information must be updated + * with the {@link #updateHierarchicalWrapper()} method. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ContainerHierarchicalWrapper implements Container.Hierarchical, + Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier { + + /** The wrapped container */ + private final Container container; + + /** Set of IDs of those contained Items that can't have children. */ + private HashSet<Object> noChildrenAllowed = null; + + /** Mapping from Item ID to parent Item ID */ + private Hashtable<Object, Object> parent = null; + + /** Mapping from Item ID to a list of child IDs */ + private Hashtable<Object, LinkedList<Object>> children = null; + + /** List that contains all root elements of the container. */ + private LinkedHashSet<Object> roots = null; + + /** Is the wrapped container hierarchical by itself ? */ + private boolean hierarchical; + + /** + * A comparator that sorts the listed items before other items. Otherwise, + * the order is undefined. + */ + private static class ListedItemsFirstComparator implements + Comparator<Object>, Serializable { + private final Collection<?> itemIds; + + private ListedItemsFirstComparator(Collection<?> itemIds) { + this.itemIds = itemIds; + } + + @Override + public int compare(Object o1, Object o2) { + if (o1.equals(o2)) { + return 0; + } + for (Object id : itemIds) { + if (id == o1) { + return -1; + } else if (id == o2) { + return 1; + } + } + return 0; + } + }; + + /** + * Constructs a new hierarchical wrapper for an existing Container. Works + * even if the to-be-wrapped container already implements the + * <code>Container.Hierarchical</code> interface. + * + * @param toBeWrapped + * the container that needs to be accessed hierarchically + * @see #updateHierarchicalWrapper() + */ + public ContainerHierarchicalWrapper(Container toBeWrapped) { + + container = toBeWrapped; + hierarchical = container instanceof Container.Hierarchical; + + // Check arguments + if (container == null) { + throw new NullPointerException("Null can not be wrapped"); + } + + // Create initial order if needed + if (!hierarchical) { + noChildrenAllowed = new HashSet<Object>(); + parent = new Hashtable<Object, Object>(); + children = new Hashtable<Object, LinkedList<Object>>(); + roots = new LinkedHashSet<Object>(container.getItemIds()); + } + + updateHierarchicalWrapper(); + + } + + /** + * Updates the wrapper's internal hierarchy data to include all Items in the + * underlying container. If the contents of the wrapped container change + * without the wrapper's knowledge, this method needs to be called to update + * the hierarchy information of the Items. + */ + public void updateHierarchicalWrapper() { + + if (!hierarchical) { + + // Recreate hierarchy and data structures if missing + if (noChildrenAllowed == null || parent == null || children == null + || roots == null) { + noChildrenAllowed = new HashSet<Object>(); + parent = new Hashtable<Object, Object>(); + children = new Hashtable<Object, LinkedList<Object>>(); + roots = new LinkedHashSet<Object>(container.getItemIds()); + } + + // Check that the hierarchy is up-to-date + else { + + // ensure order of root and child lists is same as in wrapped + // container + Collection<?> itemIds = container.getItemIds(); + Comparator<Object> basedOnOrderFromWrappedContainer = new ListedItemsFirstComparator( + itemIds); + + // Calculate the set of all items in the hierarchy + final HashSet<Object> s = new HashSet<Object>(); + s.addAll(parent.keySet()); + s.addAll(children.keySet()); + s.addAll(roots); + + // Remove unnecessary items + for (final Iterator<Object> i = s.iterator(); i.hasNext();) { + final Object id = i.next(); + if (!container.containsId(id)) { + removeFromHierarchyWrapper(id); + } + } + + // Add all the missing items + final Collection<?> ids = container.getItemIds(); + for (final Iterator<?> i = ids.iterator(); i.hasNext();) { + final Object id = i.next(); + if (!s.contains(id)) { + addToHierarchyWrapper(id); + s.add(id); + } + } + + Object[] array = roots.toArray(); + Arrays.sort(array, basedOnOrderFromWrappedContainer); + roots = new LinkedHashSet<Object>(); + for (int i = 0; i < array.length; i++) { + roots.add(array[i]); + } + for (Object object : children.keySet()) { + LinkedList<Object> object2 = children.get(object); + Collections.sort(object2, basedOnOrderFromWrappedContainer); + } + + } + } + } + + /** + * Removes the specified Item from the wrapper's internal hierarchy + * structure. + * <p> + * Note : The Item is not removed from the underlying Container. + * </p> + * + * @param itemId + * the ID of the item to remove from the hierarchy. + */ + private void removeFromHierarchyWrapper(Object itemId) { + + LinkedList<Object> oprhanedChildren = children.remove(itemId); + if (oprhanedChildren != null) { + for (Object object : oprhanedChildren) { + // make orphaned children root nodes + setParent(object, null); + } + } + + roots.remove(itemId); + final Object p = parent.get(itemId); + if (p != null) { + final LinkedList<Object> c = children.get(p); + if (c != null) { + c.remove(itemId); + } + } + parent.remove(itemId); + noChildrenAllowed.remove(itemId); + } + + /** + * Adds the specified Item specified to the internal hierarchy structure. + * The new item is added as a root Item. The underlying container is not + * modified. + * + * @param itemId + * the ID of the item to add to the hierarchy. + */ + private void addToHierarchyWrapper(Object itemId) { + roots.add(itemId); + + } + + /* + * Can the specified Item have any children? Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container) + .areChildrenAllowed(itemId); + } + + if (noChildrenAllowed.contains(itemId)) { + return false; + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the children of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getChildren(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).getChildren(itemId); + } + + final Collection<?> c = children.get(itemId); + if (c == null) { + return null; + } + return Collections.unmodifiableCollection(c); + } + + /* + * Gets the ID of the parent of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object getParent(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).getParent(itemId); + } + + return parent.get(itemId); + } + + /* + * Is the Item corresponding to the given ID a leaf node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean hasChildren(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).hasChildren(itemId); + } + + LinkedList<Object> list = children.get(itemId); + return (list != null && !list.isEmpty()); + } + + /* + * Is the Item corresponding to the given ID a root node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).isRoot(itemId); + } + + if (parent.containsKey(itemId)) { + return false; + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the root elements in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> rootItemIds() { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).rootItemIds(); + } + + return Collections.unmodifiableCollection(roots); + } + + /** + * <p> + * Sets the given Item's capability to have children. If the Item identified + * with the itemId already has children and the areChildrenAllowed is false + * this method fails and <code>false</code> is returned; the children must + * be first explicitly removed with + * {@link #setParent(Object itemId, Object newParentId)} or + * {@link com.vaadin.data.Container#removeItem(Object itemId)}. + * </p> + * + * @param itemId + * the ID of the Item in the container whose child capability is + * to be set. + * @param childrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).setChildrenAllowed( + itemId, childrenAllowed); + } + + // Check that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Update status + if (childrenAllowed) { + noChildrenAllowed.remove(itemId); + } else { + noChildrenAllowed.add(itemId); + } + + return true; + } + + /** + * <p> + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (<code>canHaveChildren(newParentId) == true</code>). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent <code>null</code>. + * </p> + * + * @param itemId + * the ID of the item to be set as the child of the Item + * identified with newParentId. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).setParent(itemId, + newParentId); + } + + // Check that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Get the old parent + final Object oldParentId = parent.get(itemId); + + // Check if no change is necessary + if ((newParentId == null && oldParentId == null) + || (newParentId != null && newParentId.equals(oldParentId))) { + return true; + } + + // Making root + if (newParentId == null) { + + // Remove from old parents children list + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(itemId); + } + } + + // Add to be a root + roots.add(itemId); + + // Update parent + parent.remove(itemId); + + return true; + } + + // Check that the new parent exists in container and can have + // children + if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) { + return false; + } + + // Check that setting parent doesn't result to a loop + Object o = newParentId; + while (o != null && !o.equals(itemId)) { + o = parent.get(o); + } + if (o != null) { + return false; + } + + // Update parent + parent.put(itemId, newParentId); + LinkedList<Object> pcl = children.get(newParentId); + if (pcl == null) { + pcl = new LinkedList<Object>(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Remove from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + } + } + + return true; + } + + /** + * Creates a new Item into the Container, assigns it an automatic ID, and + * adds it to the hierarchy. + * + * @return the autogenerated ID of the new Item or <code>null</code> if the + * operation failed + * @throws UnsupportedOperationException + * if the addItem is not supported. + */ + @Override + public Object addItem() throws UnsupportedOperationException { + + final Object id = container.addItem(); + if (!hierarchical && id != null) { + addToHierarchyWrapper(id); + } + return id; + } + + /** + * Adds a new Item by its ID to the underlying container and to the + * hierarchy. + * + * @param itemId + * the ID of the Item to be created. + * @return the added Item or <code>null</code> if the operation failed. + * @throws UnsupportedOperationException + * if the addItem is not supported. + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + + // Null ids are not accepted + if (itemId == null) { + throw new NullPointerException("Container item id can not be null"); + } + + final Item item = container.addItem(itemId); + if (!hierarchical && item != null) { + addToHierarchyWrapper(itemId); + } + return item; + } + + /** + * Removes all items from the underlying container and from the hierarcy. + * + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeAllItems is not supported. + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + + final boolean success = container.removeAllItems(); + + if (!hierarchical && success) { + roots.clear(); + parent.clear(); + children.clear(); + noChildrenAllowed.clear(); + } + return success; + } + + /** + * Removes an Item specified by the itemId from the underlying container and + * from the hierarchy. + * + * @param itemId + * the ID of the Item to be removed. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeItem is not supported. + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + + final boolean success = container.removeItem(itemId); + + if (!hierarchical && success) { + removeFromHierarchyWrapper(itemId); + } + + return success; + } + + /** + * Removes the Item identified by given itemId and all its children. + * + * @see #removeItem(Object) + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public boolean removeItemRecursively(Object itemId) { + return HierarchicalContainer.removeItemRecursively(this, itemId); + } + + /** + * Adds a new Property to all Items in the Container. + * + * @param propertyId + * the ID of the new Property. + * @param type + * the Data type of the new Property. + * @param defaultValue + * the value all created Properties are initialized to. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the addContainerProperty is not supported. + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + + return container.addContainerProperty(propertyId, type, defaultValue); + } + + /** + * Removes the specified Property from the underlying container and from the + * hierarchy. + * <p> + * Note : The Property will be removed from all Items in the Container. + * </p> + * + * @param propertyId + * the ID of the Property to remove. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeContainerProperty is not supported. + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + return container.removeContainerProperty(propertyId); + } + + /* + * Does the container contain the specified Item? Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean containsId(Object itemId) { + return container.containsId(itemId); + } + + /* + * Gets the specified Item from the container. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Item getItem(Object itemId) { + return container.getItem(itemId); + } + + /* + * Gets the ID's of all Items stored in the Container Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getItemIds() { + return container.getItemIds(); + } + + /* + * Gets the Property identified by the given itemId and propertyId from the + * Container Don't add a JavaDoc comment here, we use the default + * documentation from implemented interface. + */ + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + return container.getContainerProperty(itemId, propertyId); + } + + /* + * Gets the ID's of all Properties stored in the Container Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getContainerPropertyIds() { + return container.getContainerPropertyIds(); + } + + /* + * Gets the data type of all Properties identified by the given Property ID. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Class<?> getType(Object propertyId) { + return container.getType(propertyId); + } + + /* + * Gets the number of Items in the Container. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public int size() { + return container.size(); + } + + /* + * Registers a new Item set change listener for this Container. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void addListener(Container.ItemSetChangeListener listener) { + if (container instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) container) + .addListener(new PiggybackListener(listener)); + } + } + + /* + * Removes a Item set change listener from the object. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + if (container instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) container) + .removeListener(new PiggybackListener(listener)); + } + } + + /* + * Registers a new Property set change listener for this Container. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void addListener(Container.PropertySetChangeListener listener) { + if (container instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) container) + .addListener(new PiggybackListener(listener)); + } + } + + /* + * Removes a Property set change listener from the object. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + if (container instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) container) + .removeListener(new PiggybackListener(listener)); + } + } + + /** + * This listener 'piggybacks' on the real listener in order to update the + * wrapper when needed. It proxies equals() and hashCode() to the real + * listener so that the correct listener gets removed. + * + */ + private class PiggybackListener implements + Container.PropertySetChangeListener, + Container.ItemSetChangeListener { + + Object listener; + + public PiggybackListener(Object realListener) { + listener = realListener; + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + updateHierarchicalWrapper(); + ((Container.ItemSetChangeListener) listener) + .containerItemSetChange(event); + + } + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + updateHierarchicalWrapper(); + ((Container.PropertySetChangeListener) listener) + .containerPropertySetChange(event); + + } + + @Override + public boolean equals(Object obj) { + return obj == listener || (obj != null && obj.equals(listener)); + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + + } +} diff --git a/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java new file mode 100644 index 0000000000..d3d6f88d3e --- /dev/null +++ b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java @@ -0,0 +1,644 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * <p> + * A wrapper class for adding external ordering to containers not implementing + * the {@link com.vaadin.data.Container.Ordered} interface. + * </p> + * + * <p> + * If the wrapped container is changed directly (that is, not through the + * wrapper), and does not implement Container.ItemSetChangeNotifier and/or + * Container.PropertySetChangeNotifier the hierarchy information must be updated + * with the {@link #updateOrderWrapper()} method. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ContainerOrderedWrapper implements Container.Ordered, + Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier { + + /** + * The wrapped container + */ + private final Container container; + + /** + * Ordering information, ie. the mapping from Item ID to the next item ID + */ + private Hashtable<Object, Object> next; + + /** + * Reverse ordering information for convenience and performance reasons. + */ + private Hashtable<Object, Object> prev; + + /** + * ID of the first Item in the container. + */ + private Object first; + + /** + * ID of the last Item in the container. + */ + private Object last; + + /** + * Is the wrapped container ordered by itself, ie. does it implement the + * Container.Ordered interface by itself? If it does, this class will use + * the methods of the underlying container directly. + */ + private boolean ordered = false; + + /** + * The last known size of the wrapped container. Used to check whether items + * have been added or removed to the wrapped container, when the wrapped + * container does not send ItemSetChangeEvents. + */ + private int lastKnownSize = -1; + + /** + * Constructs a new ordered wrapper for an existing Container. Works even if + * the to-be-wrapped container already implements the Container.Ordered + * interface. + * + * @param toBeWrapped + * the container whose contents need to be ordered. + */ + public ContainerOrderedWrapper(Container toBeWrapped) { + + container = toBeWrapped; + ordered = container instanceof Container.Ordered; + + // Checks arguments + if (container == null) { + throw new NullPointerException("Null can not be wrapped"); + } + + // Creates initial order if needed + updateOrderWrapper(); + } + + /** + * Removes the specified Item from the wrapper's internal hierarchy + * structure. + * <p> + * Note : The Item is not removed from the underlying Container. + * </p> + * + * @param id + * the ID of the Item to be removed from the ordering. + */ + private void removeFromOrderWrapper(Object id) { + if (id != null) { + final Object pid = prev.get(id); + final Object nid = next.get(id); + if (first.equals(id)) { + first = nid; + } + if (last.equals(id)) { + first = pid; + } + if (nid != null) { + prev.put(nid, pid); + } + if (pid != null) { + next.put(pid, nid); + } + next.remove(id); + prev.remove(id); + } + } + + /** + * Registers the specified Item to the last position in the wrapper's + * internal ordering. The underlying container is not modified. + * + * @param id + * the ID of the Item to be added to the ordering. + */ + private void addToOrderWrapper(Object id) { + + // Adds the if to tail + if (last != null) { + next.put(last, id); + prev.put(id, last); + last = id; + } else { + first = last = id; + } + } + + /** + * Registers the specified Item after the specified itemId in the wrapper's + * internal ordering. The underlying container is not modified. Given item + * id must be in the container, or must be null. + * + * @param id + * the ID of the Item to be added to the ordering. + * @param previousItemId + * the Id of the previous item. + */ + private void addToOrderWrapper(Object id, Object previousItemId) { + + if (last == previousItemId || last == null) { + addToOrderWrapper(id); + } else { + if (previousItemId == null) { + next.put(id, first); + prev.put(first, id); + first = id; + } else { + prev.put(id, previousItemId); + next.put(id, next.get(previousItemId)); + prev.put(next.get(previousItemId), id); + next.put(previousItemId, id); + } + } + } + + /** + * Updates the wrapper's internal ordering information to include all Items + * in the underlying container. + * <p> + * Note : If the contents of the wrapped container change without the + * wrapper's knowledge, this method needs to be called to update the + * ordering information of the Items. + * </p> + */ + public void updateOrderWrapper() { + + if (!ordered) { + + final Collection<?> ids = container.getItemIds(); + + // Recreates ordering if some parts of it are missing + if (next == null || first == null || last == null || prev != null) { + first = null; + last = null; + next = new Hashtable<Object, Object>(); + prev = new Hashtable<Object, Object>(); + } + + // Filter out all the missing items + final LinkedList<?> l = new LinkedList<Object>(next.keySet()); + for (final Iterator<?> i = l.iterator(); i.hasNext();) { + final Object id = i.next(); + if (!container.containsId(id)) { + removeFromOrderWrapper(id); + } + } + + // Adds missing items + for (final Iterator<?> i = ids.iterator(); i.hasNext();) { + final Object id = i.next(); + if (!next.containsKey(id)) { + addToOrderWrapper(id); + } + } + } + } + + /* + * Gets the first item stored in the ordered container Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object firstItemId() { + if (ordered) { + return ((Container.Ordered) container).firstItemId(); + } + return first; + } + + /* + * Tests if the given item is the first item in the container Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isFirstId(Object itemId) { + if (ordered) { + return ((Container.Ordered) container).isFirstId(itemId); + } + return first != null && first.equals(itemId); + } + + /* + * Tests if the given item is the last item in the container Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isLastId(Object itemId) { + if (ordered) { + return ((Container.Ordered) container).isLastId(itemId); + } + return last != null && last.equals(itemId); + } + + /* + * Gets the last item stored in the ordered container Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object lastItemId() { + if (ordered) { + return ((Container.Ordered) container).lastItemId(); + } + return last; + } + + /* + * Gets the item that is next from the specified item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object nextItemId(Object itemId) { + if (ordered) { + return ((Container.Ordered) container).nextItemId(itemId); + } + if (itemId == null) { + return null; + } + return next.get(itemId); + } + + /* + * Gets the item that is previous from the specified item. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object prevItemId(Object itemId) { + if (ordered) { + return ((Container.Ordered) container).prevItemId(itemId); + } + if (itemId == null) { + return null; + } + return prev.get(itemId); + } + + /** + * Registers a new Property to all Items in the Container. + * + * @param propertyId + * the ID of the new Property. + * @param type + * the Data type of the new Property. + * @param defaultValue + * the value all created Properties are initialized to. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + + return container.addContainerProperty(propertyId, type, defaultValue); + } + + /** + * Creates a new Item into the Container, assigns it an automatic ID, and + * adds it to the ordering. + * + * @return the autogenerated ID of the new Item or <code>null</code> if the + * operation failed + * @throws UnsupportedOperationException + * if the addItem is not supported. + */ + @Override + public Object addItem() throws UnsupportedOperationException { + + final Object id = container.addItem(); + if (!ordered && id != null) { + addToOrderWrapper(id); + } + return id; + } + + /** + * Registers a new Item by its ID to the underlying container and to the + * ordering. + * + * @param itemId + * the ID of the Item to be created. + * @return the added Item or <code>null</code> if the operation failed + * @throws UnsupportedOperationException + * if the addItem is not supported. + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + final Item item = container.addItem(itemId); + if (!ordered && item != null) { + addToOrderWrapper(itemId); + } + return item; + } + + /** + * Removes all items from the underlying container and from the ordering. + * + * @return <code>true</code> if the operation succeeded, otherwise + * <code>false</code> + * @throws UnsupportedOperationException + * if the removeAllItems is not supported. + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + final boolean success = container.removeAllItems(); + if (!ordered && success) { + first = last = null; + next.clear(); + prev.clear(); + } + return success; + } + + /** + * Removes an Item specified by the itemId from the underlying container and + * from the ordering. + * + * @param itemId + * the ID of the Item to be removed. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeItem is not supported. + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + + final boolean success = container.removeItem(itemId); + if (!ordered && success) { + removeFromOrderWrapper(itemId); + } + return success; + } + + /** + * Removes the specified Property from the underlying container and from the + * ordering. + * <p> + * Note : The Property will be removed from all the Items in the Container. + * </p> + * + * @param propertyId + * the ID of the Property to remove. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeContainerProperty is not supported. + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + return container.removeContainerProperty(propertyId); + } + + /* + * Does the container contain the specified Item? Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean containsId(Object itemId) { + return container.containsId(itemId); + } + + /* + * Gets the specified Item from the container. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Item getItem(Object itemId) { + return container.getItem(itemId); + } + + /* + * Gets the ID's of all Items stored in the Container Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getItemIds() { + return container.getItemIds(); + } + + /* + * Gets the Property identified by the given itemId and propertyId from the + * Container Don't add a JavaDoc comment here, we use the default + * documentation from implemented interface. + */ + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + return container.getContainerProperty(itemId, propertyId); + } + + /* + * Gets the ID's of all Properties stored in the Container Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getContainerPropertyIds() { + return container.getContainerPropertyIds(); + } + + /* + * Gets the data type of all Properties identified by the given Property ID. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Class<?> getType(Object propertyId) { + return container.getType(propertyId); + } + + /* + * Gets the number of Items in the Container. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public int size() { + int newSize = container.size(); + if (lastKnownSize != -1 && newSize != lastKnownSize + && !(container instanceof Container.ItemSetChangeNotifier)) { + // Update the internal cache when the size of the container changes + // and the container is incapable of sending ItemSetChangeEvents + updateOrderWrapper(); + } + lastKnownSize = newSize; + return newSize; + } + + /* + * Registers a new Item set change listener for this Container. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void addListener(Container.ItemSetChangeListener listener) { + if (container instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) container) + .addListener(new PiggybackListener(listener)); + } + } + + /* + * Removes a Item set change listener from the object. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + if (container instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) container) + .removeListener(new PiggybackListener(listener)); + } + } + + /* + * Registers a new Property set change listener for this Container. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void addListener(Container.PropertySetChangeListener listener) { + if (container instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) container) + .addListener(new PiggybackListener(listener)); + } + } + + /* + * Removes a Property set change listener from the object. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + if (container instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) container) + .removeListener(new PiggybackListener(listener)); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + + // If the previous item is not in the container, fail + if (previousItemId != null && !containsId(previousItemId)) { + return null; + } + + // Adds the item to container + final Item item = container.addItem(newItemId); + + // Puts the new item to its correct place + if (!ordered && item != null) { + addToOrderWrapper(newItemId, previousItemId); + } + + return item; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + + // If the previous item is not in the container, fail + if (previousItemId != null && !containsId(previousItemId)) { + return null; + } + + // Adds the item to container + final Object id = container.addItem(); + + // Puts the new item to its correct place + if (!ordered && id != null) { + addToOrderWrapper(id, previousItemId); + } + + return id; + } + + /** + * This listener 'piggybacks' on the real listener in order to update the + * wrapper when needed. It proxies equals() and hashCode() to the real + * listener so that the correct listener gets removed. + * + */ + private class PiggybackListener implements + Container.PropertySetChangeListener, + Container.ItemSetChangeListener { + + Object listener; + + public PiggybackListener(Object realListener) { + listener = realListener; + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + updateOrderWrapper(); + ((Container.ItemSetChangeListener) listener) + .containerItemSetChange(event); + + } + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + updateOrderWrapper(); + ((Container.PropertySetChangeListener) listener) + .containerPropertySetChange(event); + + } + + @Override + public boolean equals(Object obj) { + return obj == listener || (obj != null && obj.equals(listener)); + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + + } + +} diff --git a/server/src/com/vaadin/data/util/DefaultItemSorter.java b/server/src/com/vaadin/data/util/DefaultItemSorter.java new file mode 100644 index 0000000000..81b15ebd4f --- /dev/null +++ b/server/src/com/vaadin/data/util/DefaultItemSorter.java @@ -0,0 +1,210 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Sortable; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Provides a default implementation of an ItemSorter. The + * <code>DefaultItemSorter</code> adheres to the + * {@link Sortable#sort(Object[], boolean[])} rules and sorts the container + * according to the properties given using + * {@link #setSortProperties(Sortable, Object[], boolean[])}. + * <p> + * A Comparator is used for comparing the individual <code>Property</code> + * values. The comparator can be set using the constructor. If no comparator is + * provided a default comparator is used. + * + */ +public class DefaultItemSorter implements ItemSorter { + + private java.lang.Object[] sortPropertyIds; + private boolean[] sortDirections; + private Container container; + private Comparator<Object> propertyValueComparator; + + /** + * Constructs a DefaultItemSorter using the default <code>Comparator</code> + * for comparing <code>Property</code>values. + * + */ + public DefaultItemSorter() { + this(new DefaultPropertyValueComparator()); + } + + /** + * Constructs a DefaultItemSorter which uses the <code>Comparator</code> + * indicated by the <code>propertyValueComparator</code> parameter for + * comparing <code>Property</code>values. + * + * @param propertyValueComparator + * The comparator to use when comparing individual + * <code>Property</code> values + */ + public DefaultItemSorter(Comparator<Object> propertyValueComparator) { + this.propertyValueComparator = propertyValueComparator; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.ItemSorter#compare(java.lang.Object, + * java.lang.Object) + */ + @Override + public int compare(Object o1, Object o2) { + Item item1 = container.getItem(o1); + Item item2 = container.getItem(o2); + + /* + * Items can be null if the container is filtered. Null is considered + * "less" than not-null. + */ + if (item1 == null) { + if (item2 == null) { + return 0; + } else { + return 1; + } + } else if (item2 == null) { + return -1; + } + + for (int i = 0; i < sortPropertyIds.length; i++) { + + int result = compareProperty(sortPropertyIds[i], sortDirections[i], + item1, item2); + + // If order can be decided + if (result != 0) { + return result; + } + + } + + return 0; + } + + /** + * Compares the property indicated by <code>propertyId</code> in the items + * indicated by <code>item1</code> and <code>item2</code> for order. Returns + * a negative integer, zero, or a positive integer as the property value in + * the first item is less than, equal to, or greater than the property value + * in the second item. If the <code>sortDirection</code> is false the + * returned value is negated. + * <p> + * The comparator set for this <code>DefaultItemSorter</code> is used for + * comparing the two property values. + * + * @param propertyId + * The property id for the property that is used for comparison. + * @param sortDirection + * The direction of the sort. A false value negates the result. + * @param item1 + * The first item to compare. + * @param item2 + * The second item to compare. + * @return a negative, zero, or positive integer if the property value in + * the first item is less than, equal to, or greater than the + * property value in the second item. Negated if + * {@code sortDirection} is false. + */ + protected int compareProperty(Object propertyId, boolean sortDirection, + Item item1, Item item2) { + + // Get the properties to compare + final Property<?> property1 = item1.getItemProperty(propertyId); + final Property<?> property2 = item2.getItemProperty(propertyId); + + // Get the values to compare + final Object value1 = (property1 == null) ? null : property1.getValue(); + final Object value2 = (property2 == null) ? null : property2.getValue(); + + // Result of the comparison + int r = 0; + if (sortDirection) { + r = propertyValueComparator.compare(value1, value2); + } else { + r = propertyValueComparator.compare(value2, value1); + } + + return r; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.ItemSorter#setSortProperties(com.vaadin.data.Container + * .Sortable, java.lang.Object[], boolean[]) + */ + @Override + public void setSortProperties(Container.Sortable container, + Object[] propertyId, boolean[] ascending) { + this.container = container; + + // Removes any non-sortable property ids + final List<Object> ids = new ArrayList<Object>(); + final List<Boolean> orders = new ArrayList<Boolean>(); + final Collection<?> sortable = container + .getSortableContainerPropertyIds(); + for (int i = 0; i < propertyId.length; i++) { + if (sortable.contains(propertyId[i])) { + ids.add(propertyId[i]); + orders.add(Boolean.valueOf(i < ascending.length ? ascending[i] + : true)); + } + } + + sortPropertyIds = ids.toArray(); + sortDirections = new boolean[orders.size()]; + for (int i = 0; i < sortDirections.length; i++) { + sortDirections[i] = (orders.get(i)).booleanValue(); + } + + } + + /** + * Provides a default comparator used for comparing {@link Property} values. + * The <code>DefaultPropertyValueComparator</code> assumes all objects it + * compares can be cast to Comparable. + * + */ + public static class DefaultPropertyValueComparator implements + Comparator<Object>, Serializable { + + @Override + @SuppressWarnings("unchecked") + public int compare(Object o1, Object o2) { + int r = 0; + // Normal non-null comparison + if (o1 != null && o2 != null) { + // Assume the objects can be cast to Comparable, throw + // ClassCastException otherwise. + r = ((Comparable<Object>) o1).compareTo(o2); + } else if (o1 == o2) { + // Objects are equal if both are null + r = 0; + } else { + if (o1 == null) { + r = -1; // null is less than non-null + } else { + r = 1; // non-null is greater than null + } + } + + return r; + } + } + +} diff --git a/server/src/com/vaadin/data/util/FilesystemContainer.java b/server/src/com/vaadin/data/util/FilesystemContainer.java new file mode 100644 index 0000000000..cdfeb57e14 --- /dev/null +++ b/server/src/com/vaadin/data/util/FilesystemContainer.java @@ -0,0 +1,918 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.service.FileTypeResolver; +import com.vaadin.terminal.Resource; + +/** + * A hierarchical container wrapper for a filesystem. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class FilesystemContainer implements Container.Hierarchical { + + /** + * String identifier of a file's "name" property. + */ + public static String PROPERTY_NAME = "Name"; + + /** + * String identifier of a file's "size" property. + */ + public static String PROPERTY_SIZE = "Size"; + + /** + * String identifier of a file's "icon" property. + */ + public static String PROPERTY_ICON = "Icon"; + + /** + * String identifier of a file's "last modified" property. + */ + public static String PROPERTY_LASTMODIFIED = "Last Modified"; + + /** + * List of the string identifiers for the available properties. + */ + public static Collection<String> FILE_PROPERTIES; + + private final static Method FILEITEM_LASTMODIFIED; + + private final static Method FILEITEM_NAME; + + private final static Method FILEITEM_ICON; + + private final static Method FILEITEM_SIZE; + + static { + + FILE_PROPERTIES = new ArrayList<String>(); + FILE_PROPERTIES.add(PROPERTY_NAME); + FILE_PROPERTIES.add(PROPERTY_ICON); + FILE_PROPERTIES.add(PROPERTY_SIZE); + FILE_PROPERTIES.add(PROPERTY_LASTMODIFIED); + FILE_PROPERTIES = Collections.unmodifiableCollection(FILE_PROPERTIES); + try { + FILEITEM_LASTMODIFIED = FileItem.class.getMethod("lastModified", + new Class[] {}); + FILEITEM_NAME = FileItem.class.getMethod("getName", new Class[] {}); + FILEITEM_ICON = FileItem.class.getMethod("getIcon", new Class[] {}); + FILEITEM_SIZE = FileItem.class.getMethod("getSize", new Class[] {}); + } catch (final NoSuchMethodException e) { + throw new RuntimeException( + "Internal error finding methods in FilesystemContainer"); + } + } + + private File[] roots = new File[] {}; + + private FilenameFilter filter = null; + + private boolean recursive = true; + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified file + * as the root of the filesystem. The files are included recursively. + * + * @param root + * the root file for the new file-system container. Null values + * are ignored. + */ + public FilesystemContainer(File root) { + if (root != null) { + roots = new File[] { root }; + } + } + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified file + * as the root of the filesystem. The files are included recursively. + * + * @param root + * the root file for the new file-system container. + * @param recursive + * should the container recursively contain subdirectories. + */ + public FilesystemContainer(File root, boolean recursive) { + this(root); + setRecursive(recursive); + } + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified file + * as the root of the filesystem. + * + * @param root + * the root file for the new file-system container. + * @param extension + * the Filename extension (w/o separator) to limit the files in + * container. + * @param recursive + * should the container recursively contain subdirectories. + */ + public FilesystemContainer(File root, String extension, boolean recursive) { + this(root); + this.setFilter(extension); + setRecursive(recursive); + } + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified root + * and recursivity status. + * + * @param root + * the root file for the new file-system container. + * @param filter + * the Filename filter to limit the files in container. + * @param recursive + * should the container recursively contain subdirectories. + */ + public FilesystemContainer(File root, FilenameFilter filter, + boolean recursive) { + this(root); + this.setFilter(filter); + setRecursive(recursive); + } + + /** + * Adds new root file directory. Adds a file to be included as root file + * directory in the <code>FilesystemContainer</code>. + * + * @param root + * the File to be added as root directory. Null values are + * ignored. + */ + public void addRoot(File root) { + if (root != null) { + final File[] newRoots = new File[roots.length + 1]; + for (int i = 0; i < roots.length; i++) { + newRoots[i] = roots[i]; + } + newRoots[roots.length] = root; + roots = newRoots; + } + } + + /** + * Tests if the specified Item in the container may have children. Since a + * <code>FileSystemContainer</code> contains files and directories, this + * method returns <code>true</code> for directory Items only. + * + * @param itemId + * the id of the item. + * @return <code>true</code> if the specified Item is a directory, + * <code>false</code> otherwise. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + return itemId instanceof File && ((File) itemId).canRead() + && ((File) itemId).isDirectory(); + } + + /* + * Gets the ID's of all Items who are children of the specified Item. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Collection<File> getChildren(Object itemId) { + + if (!(itemId instanceof File)) { + return Collections.unmodifiableCollection(new LinkedList<File>()); + } + File[] f; + if (filter != null) { + f = ((File) itemId).listFiles(filter); + } else { + f = ((File) itemId).listFiles(); + } + + if (f == null) { + return Collections.unmodifiableCollection(new LinkedList<File>()); + } + + final List<File> l = Arrays.asList(f); + Collections.sort(l); + + return Collections.unmodifiableCollection(l); + } + + /* + * Gets the parent item of the specified Item. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Object getParent(Object itemId) { + + if (!(itemId instanceof File)) { + return null; + } + return ((File) itemId).getParentFile(); + } + + /* + * Tests if the specified Item has any children. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean hasChildren(Object itemId) { + + if (!(itemId instanceof File)) { + return false; + } + String[] l; + if (filter != null) { + l = ((File) itemId).list(filter); + } else { + l = ((File) itemId).list(); + } + return (l != null) && (l.length > 0); + } + + /* + * Tests if the specified Item is the root of the filesystem. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + + if (!(itemId instanceof File)) { + return false; + } + for (int i = 0; i < roots.length; i++) { + if (roots[i].equals(itemId)) { + return true; + } + } + return false; + } + + /* + * Gets the ID's of all root Items in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<File> rootItemIds() { + + File[] f; + + // in single root case we use children + if (roots.length == 1) { + if (filter != null) { + f = roots[0].listFiles(filter); + } else { + f = roots[0].listFiles(); + } + } else { + f = roots; + } + + if (f == null) { + return Collections.unmodifiableCollection(new LinkedList<File>()); + } + + final List<File> l = Arrays.asList(f); + Collections.sort(l); + + return Collections.unmodifiableCollection(l); + } + + /** + * Returns <code>false</code> when conversion from files to directories is + * not supported. + * + * @param itemId + * the ID of the item. + * @param areChildrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operaton is successful otherwise + * <code>false</code>. + * @throws UnsupportedOperationException + * if the setChildrenAllowed is not supported. + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException( + "Conversion file to/from directory is not supported"); + } + + /** + * Returns <code>false</code> when moving files around in the filesystem is + * not supported. + * + * @param itemId + * the ID of the item. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation is successful otherwise + * <code>false</code>. + * @throws UnsupportedOperationException + * if the setParent is not supported. + */ + @Override + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException("File moving is not supported"); + } + + /* + * Tests if the filesystem contains the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean containsId(Object itemId) { + + if (!(itemId instanceof File)) { + return false; + } + boolean val = false; + + // Try to match all roots + for (int i = 0; i < roots.length; i++) { + try { + val |= ((File) itemId).getCanonicalPath().startsWith( + roots[i].getCanonicalPath()); + } catch (final IOException e) { + // Exception ignored + } + + } + if (val && filter != null) { + val &= filter.accept(((File) itemId).getParentFile(), + ((File) itemId).getName()); + } + return val; + } + + /* + * Gets the specified Item from the filesystem. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Item getItem(Object itemId) { + + if (!(itemId instanceof File)) { + return null; + } + return new FileItem((File) itemId); + } + + /** + * Internal recursive method to add the files under the specified directory + * to the collection. + * + * @param col + * the collection where the found items are added + * @param f + * the root file where to start adding files + */ + private void addItemIds(Collection<File> col, File f) { + File[] l; + if (filter != null) { + l = f.listFiles(filter); + } else { + l = f.listFiles(); + } + if (l == null) { + // File.listFiles returns null if File does not exist or if there + // was an IO error (permission denied) + return; + } + final List<File> ll = Arrays.asList(l); + Collections.sort(ll); + + for (final Iterator<File> i = ll.iterator(); i.hasNext();) { + final File lf = i.next(); + col.add(lf); + if (lf.isDirectory()) { + addItemIds(col, lf); + } + } + } + + /* + * Gets the IDs of Items in the filesystem. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Collection<File> getItemIds() { + + if (recursive) { + final Collection<File> col = new ArrayList<File>(); + for (int i = 0; i < roots.length; i++) { + addItemIds(col, roots[i]); + } + return Collections.unmodifiableCollection(col); + } else { + File[] f; + if (roots.length == 1) { + if (filter != null) { + f = roots[0].listFiles(filter); + } else { + f = roots[0].listFiles(); + } + } else { + f = roots; + } + + if (f == null) { + return Collections + .unmodifiableCollection(new LinkedList<File>()); + } + + final List<File> l = Arrays.asList(f); + Collections.sort(l); + return Collections.unmodifiableCollection(l); + } + + } + + /** + * Gets the specified property of the specified file Item. The available + * file properties are "Name", "Size" and "Last Modified". If propertyId is + * not one of those, <code>null</code> is returned. + * + * @param itemId + * the ID of the file whose property is requested. + * @param propertyId + * the property's ID. + * @return the requested property's value, or <code>null</code> + */ + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + + if (!(itemId instanceof File)) { + return null; + } + + if (propertyId.equals(PROPERTY_NAME)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_NAME, null); + } + + if (propertyId.equals(PROPERTY_ICON)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_ICON, null); + } + + if (propertyId.equals(PROPERTY_SIZE)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_SIZE, null); + } + + if (propertyId.equals(PROPERTY_LASTMODIFIED)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_LASTMODIFIED, null); + } + + return null; + } + + /** + * Gets the collection of available file properties. + * + * @return Unmodifiable collection containing all available file properties. + */ + @Override + public Collection<String> getContainerPropertyIds() { + return FILE_PROPERTIES; + } + + /** + * Gets the specified property's data type. "Name" is a <code>String</code>, + * "Size" is a <code>Long</code>, "Last Modified" is a <code>Date</code>. If + * propertyId is not one of those, <code>null</code> is returned. + * + * @param propertyId + * the ID of the property whose type is requested. + * @return data type of the requested property, or <code>null</code> + */ + @Override + public Class<?> getType(Object propertyId) { + + if (propertyId.equals(PROPERTY_NAME)) { + return String.class; + } + if (propertyId.equals(PROPERTY_ICON)) { + return Resource.class; + } + if (propertyId.equals(PROPERTY_SIZE)) { + return Long.class; + } + if (propertyId.equals(PROPERTY_LASTMODIFIED)) { + return Date.class; + } + return null; + } + + /** + * Internal method to recursively calculate the number of files under a root + * directory. + * + * @param f + * the root to start counting from. + */ + private int getFileCounts(File f) { + File[] l; + if (filter != null) { + l = f.listFiles(filter); + } else { + l = f.listFiles(); + } + + if (l == null) { + return 0; + } + int ret = l.length; + for (int i = 0; i < l.length; i++) { + if (l[i].isDirectory()) { + ret += getFileCounts(l[i]); + } + } + return ret; + } + + /** + * Gets the number of Items in the container. In effect, this is the + * combined amount of files and directories. + * + * @return Number of Items in the container. + */ + @Override + public int size() { + + if (recursive) { + int counts = 0; + for (int i = 0; i < roots.length; i++) { + counts += getFileCounts(roots[i]); + } + return counts; + } else { + File[] f; + if (roots.length == 1) { + if (filter != null) { + f = roots[0].listFiles(filter); + } else { + f = roots[0].listFiles(); + } + } else { + f = roots; + } + + if (f == null) { + return 0; + } + return f.length; + } + } + + /** + * A Item wrapper for files in a filesystem. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class FileItem implements Item { + + /** + * The wrapped file. + */ + private final File file; + + /** + * Constructs a FileItem from a existing file. + */ + private FileItem(File file) { + this.file = file; + } + + /* + * Gets the specified property of this file. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Property<?> getItemProperty(Object id) { + return getContainerProperty(file, id); + } + + /* + * Gets the IDs of all properties available for this item Don't add a + * JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Collection<String> getItemPropertyIds() { + return getContainerPropertyIds(); + } + + /** + * Calculates a integer hash-code for the Property that's unique inside + * the Item containing the Property. Two different Properties inside the + * same Item contained in the same list always have different + * hash-codes, though Properties in different Items may have identical + * hash-codes. + * + * @return A locally unique hash-code as integer + */ + @Override + public int hashCode() { + return file.hashCode() ^ FilesystemContainer.this.hashCode(); + } + + /** + * Tests if the given object is the same as the this object. Two + * Properties got from an Item with the same ID are equal. + * + * @param obj + * an object to compare with this object. + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof FileItem)) { + return false; + } + final FileItem fi = (FileItem) obj; + return fi.getHost() == getHost() && fi.file.equals(file); + } + + /** + * Gets the host of this file. + */ + private FilesystemContainer getHost() { + return FilesystemContainer.this; + } + + /** + * Gets the last modified date of this file. + * + * @return Date + */ + public Date lastModified() { + return new Date(file.lastModified()); + } + + /** + * Gets the name of this file. + * + * @return file name of this file. + */ + public String getName() { + return file.getName(); + } + + /** + * Gets the icon of this file. + * + * @return the icon of this file. + */ + public Resource getIcon() { + return FileTypeResolver.getIcon(file); + } + + /** + * Gets the size of this file. + * + * @return size + */ + public long getSize() { + if (file.isDirectory()) { + return 0; + } + return file.length(); + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + if ("".equals(file.getName())) { + return file.getAbsolutePath(); + } + return file.getName(); + } + + /** + * Filesystem container does not support adding new properties. + * + * @see com.vaadin.data.Item#addItemProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("Filesystem container " + + "does not support adding new properties"); + } + + /** + * Filesystem container does not support removing properties. + * + * @see com.vaadin.data.Item#removeItemProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Filesystem container does not support property removal"); + } + + } + + /** + * Generic file extension filter for displaying only files having certain + * extension. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class FileExtensionFilter implements FilenameFilter, Serializable { + + private final String filter; + + /** + * Constructs a new FileExtensionFilter using given extension. + * + * @param fileExtension + * the File extension without the separator (dot). + */ + public FileExtensionFilter(String fileExtension) { + filter = "." + fileExtension; + } + + /** + * Allows only files with the extension and directories. + * + * @see java.io.FilenameFilter#accept(File, String) + */ + @Override + public boolean accept(File dir, String name) { + if (name.endsWith(filter)) { + return true; + } + return new File(dir, name).isDirectory(); + } + + } + + /** + * Returns the file filter used to limit the files in this container. + * + * @return Used filter instance or null if no filter is assigned. + */ + public FilenameFilter getFilter() { + return filter; + } + + /** + * Sets the file filter used to limit the files in this container. + * + * @param filter + * The filter to set. <code>null</code> disables filtering. + */ + public void setFilter(FilenameFilter filter) { + this.filter = filter; + } + + /** + * Sets the file filter used to limit the files in this container. + * + * @param extension + * the Filename extension (w/o separator) to limit the files in + * container. + */ + public void setFilter(String extension) { + filter = new FileExtensionFilter(extension); + } + + /** + * Is this container recursive filesystem. + * + * @return <code>true</code> if container is recursive, <code>false</code> + * otherwise. + */ + public boolean isRecursive() { + return recursive; + } + + /** + * Sets the container recursive property. Set this to false to limit the + * files directly under the root file. + * <p> + * Note : This is meaningful only if the root really is a directory. + * </p> + * + * @param recursive + * the New value for recursive property. + */ + public void setRecursive(boolean recursive) { + this.recursive = recursive; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem() + */ + @Override + public Object addItem() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object ) + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } +} diff --git a/server/src/com/vaadin/data/util/HierarchicalContainer.java b/server/src/com/vaadin/data/util/HierarchicalContainer.java new file mode 100644 index 0000000000..06ab77c0e7 --- /dev/null +++ b/server/src/com/vaadin/data/util/HierarchicalContainer.java @@ -0,0 +1,814 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; + +/** + * A specialized Container whose contents can be accessed like it was a + * tree-like structure. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class HierarchicalContainer extends IndexedContainer implements + Container.Hierarchical { + + /** + * Set of IDs of those contained Items that can't have children. + */ + private final HashSet<Object> noChildrenAllowed = new HashSet<Object>(); + + /** + * Mapping from Item ID to parent Item ID. + */ + private final HashMap<Object, Object> parent = new HashMap<Object, Object>(); + + /** + * Mapping from Item ID to parent Item ID for items included in the filtered + * container. + */ + private HashMap<Object, Object> filteredParent = null; + + /** + * Mapping from Item ID to a list of child IDs. + */ + private final HashMap<Object, LinkedList<Object>> children = new HashMap<Object, LinkedList<Object>>(); + + /** + * Mapping from Item ID to a list of child IDs when filtered + */ + private HashMap<Object, LinkedList<Object>> filteredChildren = null; + + /** + * List that contains all root elements of the container. + */ + private final LinkedList<Object> roots = new LinkedList<Object>(); + + /** + * List that contains all filtered root elements of the container. + */ + private LinkedList<Object> filteredRoots = null; + + /** + * Determines how filtering of the container is done. + */ + private boolean includeParentsWhenFiltering = true; + + private boolean contentChangedEventsDisabled = false; + + private boolean contentsChangedEventPending; + + /* + * Can the specified Item have any children? Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + if (noChildrenAllowed.contains(itemId)) { + return false; + } + return containsId(itemId); + } + + /* + * Gets the IDs of the children of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getChildren(Object itemId) { + LinkedList<Object> c; + + if (filteredChildren != null) { + c = filteredChildren.get(itemId); + } else { + c = children.get(itemId); + } + + if (c == null) { + return null; + } + return Collections.unmodifiableCollection(c); + } + + /* + * Gets the ID of the parent of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object getParent(Object itemId) { + if (filteredParent != null) { + return filteredParent.get(itemId); + } + return parent.get(itemId); + } + + /* + * Is the Item corresponding to the given ID a leaf node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean hasChildren(Object itemId) { + if (filteredChildren != null) { + return filteredChildren.containsKey(itemId); + } else { + return children.containsKey(itemId); + } + } + + /* + * Is the Item corresponding to the given ID a root node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + // If the container is filtered the itemId must be among filteredRoots + // to be a root. + if (filteredRoots != null) { + if (!filteredRoots.contains(itemId)) { + return false; + } + } else { + // Container is not filtered + if (parent.containsKey(itemId)) { + return false; + } + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the root elements in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> rootItemIds() { + if (filteredRoots != null) { + return Collections.unmodifiableCollection(filteredRoots); + } else { + return Collections.unmodifiableCollection(roots); + } + } + + /** + * <p> + * Sets the given Item's capability to have children. If the Item identified + * with the itemId already has children and the areChildrenAllowed is false + * this method fails and <code>false</code> is returned; the children must + * be first explicitly removed with + * {@link #setParent(Object itemId, Object newParentId)} or + * {@link com.vaadin.data.Container#removeItem(Object itemId)}. + * </p> + * + * @param itemId + * the ID of the Item in the container whose child capability is + * to be set. + * @param childrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) { + + // Checks that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Updates status + if (childrenAllowed) { + noChildrenAllowed.remove(itemId); + } else { + noChildrenAllowed.add(itemId); + } + + return true; + } + + /** + * <p> + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (<code>canHaveChildren(newParentId) == true</code>). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent <code>null</code>. + * </p> + * + * @param itemId + * the ID of the item to be set as the child of the Item + * identified with newParentId. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + + // Checks that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Gets the old parent + final Object oldParentId = parent.get(itemId); + + // Checks if no change is necessary + if ((newParentId == null && oldParentId == null) + || ((newParentId != null) && newParentId.equals(oldParentId))) { + return true; + } + + // Making root? + if (newParentId == null) { + // The itemId should become a root so we need to + // - Remove it from the old parent's children list + // - Add it as a root + // - Remove it from the item -> parent list (parent is null for + // roots) + + // Removes from old parents children list + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + + } + + // Add to be a root + roots.add(itemId); + + // Updates parent + parent.remove(itemId); + + if (hasFilters()) { + // Refilter the container if setParent is called when filters + // are applied. Changing parent can change what is included in + // the filtered version (if includeParentsWhenFiltering==true). + doFilterContainer(hasFilters()); + } + + fireItemSetChange(); + + return true; + } + + // We get here when the item should not become a root and we need to + // - Verify the new parent exists and can have children + // - Check that the new parent is not a child of the selected itemId + // - Updated the item -> parent mapping to point to the new parent + // - Remove the item from the roots list if it was a root + // - Remove the item from the old parent's children list if it was not a + // root + + // Checks that the new parent exists in container and can have + // children + if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) { + return false; + } + + // Checks that setting parent doesn't result to a loop + Object o = newParentId; + while (o != null && !o.equals(itemId)) { + o = parent.get(o); + } + if (o != null) { + return false; + } + + // Updates parent + parent.put(itemId, newParentId); + LinkedList<Object> pcl = children.get(newParentId); + if (pcl == null) { + // Create an empty list for holding children if one were not + // previously created + pcl = new LinkedList<Object>(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Removes from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + } + } + + if (hasFilters()) { + // Refilter the container if setParent is called when filters + // are applied. Changing parent can change what is included in + // the filtered version (if includeParentsWhenFiltering==true). + doFilterContainer(hasFilters()); + } + + fireItemSetChange(); + + return true; + } + + private boolean hasFilters() { + return (filteredRoots != null); + } + + /** + * Moves a node (an Item) in the container immediately after a sibling node. + * The two nodes must have the same parent in the container. + * + * @param itemId + * the identifier of the moved node (Item) + * @param siblingId + * the identifier of the reference node (Item), after which the + * other node will be located + */ + public void moveAfterSibling(Object itemId, Object siblingId) { + Object parent2 = getParent(itemId); + LinkedList<Object> childrenList; + if (parent2 == null) { + childrenList = roots; + } else { + childrenList = children.get(parent2); + } + if (siblingId == null) { + childrenList.remove(itemId); + childrenList.addFirst(itemId); + + } else { + int oldIndex = childrenList.indexOf(itemId); + int indexOfSibling = childrenList.indexOf(siblingId); + if (indexOfSibling != -1 && oldIndex != -1) { + int newIndex; + if (oldIndex > indexOfSibling) { + newIndex = indexOfSibling + 1; + } else { + newIndex = indexOfSibling; + } + childrenList.remove(oldIndex); + childrenList.add(newIndex, itemId); + } else { + throw new IllegalArgumentException( + "Given identifiers no not have the same parent."); + } + } + fireItemSetChange(); + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#addItem() + */ + @Override + public Object addItem() { + disableContentsChangeEvents(); + final Object itemId = super.addItem(); + if (itemId == null) { + return null; + } + + if (!roots.contains(itemId)) { + roots.add(itemId); + if (filteredRoots != null) { + if (passesFilters(itemId)) { + filteredRoots.add(itemId); + } + } + } + enableAndFireContentsChangeEvents(); + return itemId; + } + + @Override + protected void fireItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + if (contentsChangeEventsOn()) { + super.fireItemSetChange(event); + } else { + contentsChangedEventPending = true; + } + } + + private boolean contentsChangeEventsOn() { + return !contentChangedEventsDisabled; + } + + private void disableContentsChangeEvents() { + contentChangedEventsDisabled = true; + } + + private void enableAndFireContentsChangeEvents() { + contentChangedEventsDisabled = false; + if (contentsChangedEventPending) { + fireItemSetChange(); + } + contentsChangedEventPending = false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) { + disableContentsChangeEvents(); + final Item item = super.addItem(itemId); + if (item == null) { + return null; + } + + roots.add(itemId); + + if (filteredRoots != null) { + if (passesFilters(itemId)) { + filteredRoots.add(itemId); + } + } + enableAndFireContentsChangeEvents(); + return item; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#removeAllItems() + */ + @Override + public boolean removeAllItems() { + disableContentsChangeEvents(); + final boolean success = super.removeAllItems(); + + if (success) { + roots.clear(); + parent.clear(); + children.clear(); + noChildrenAllowed.clear(); + if (filteredRoots != null) { + filteredRoots = null; + } + if (filteredChildren != null) { + filteredChildren = null; + } + if (filteredParent != null) { + filteredParent = null; + } + } + enableAndFireContentsChangeEvents(); + return success; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#removeItem(java.lang.Object ) + */ + @Override + public boolean removeItem(Object itemId) { + disableContentsChangeEvents(); + final boolean success = super.removeItem(itemId); + + if (success) { + // Remove from roots if this was a root + if (roots.remove(itemId)) { + + // If filtering is enabled we might need to remove it from the + // filtered list also + if (filteredRoots != null) { + filteredRoots.remove(itemId); + } + } + + // Clear the children list. Old children will now become root nodes + LinkedList<Object> childNodeIds = children.remove(itemId); + if (childNodeIds != null) { + if (filteredChildren != null) { + filteredChildren.remove(itemId); + } + for (Object childId : childNodeIds) { + setParent(childId, null); + } + } + + // Parent of the item that we are removing will contain the item id + // in its children list + final Object parentItemId = parent.get(itemId); + if (parentItemId != null) { + final LinkedList<Object> c = children.get(parentItemId); + if (c != null) { + c.remove(itemId); + + if (c.isEmpty()) { + children.remove(parentItemId); + } + + // Found in the children list so might also be in the + // filteredChildren list + if (filteredChildren != null) { + LinkedList<Object> f = filteredChildren + .get(parentItemId); + if (f != null) { + f.remove(itemId); + if (f.isEmpty()) { + filteredChildren.remove(parentItemId); + } + } + } + } + } + parent.remove(itemId); + if (filteredParent != null) { + // Item id no longer has a parent as the item id is not in the + // container. + filteredParent.remove(itemId); + } + noChildrenAllowed.remove(itemId); + } + + enableAndFireContentsChangeEvents(); + + return success; + } + + /** + * Removes the Item identified by given itemId and all its children. + * + * @see #removeItem(Object) + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public boolean removeItemRecursively(Object itemId) { + disableContentsChangeEvents(); + boolean removeItemRecursively = removeItemRecursively(this, itemId); + enableAndFireContentsChangeEvents(); + return removeItemRecursively; + } + + /** + * Removes the Item identified by given itemId and all its children from the + * given Container. + * + * @param container + * the container where the item is to be removed + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public static boolean removeItemRecursively( + Container.Hierarchical container, Object itemId) { + boolean success = true; + Collection<?> children2 = container.getChildren(itemId); + if (children2 != null) { + Object[] array = children2.toArray(); + for (int i = 0; i < array.length; i++) { + boolean removeItemRecursively = removeItemRecursively( + container, array[i]); + if (!removeItemRecursively) { + success = false; + } + } + } + // remove the root of subtree if children where succesfully removed + if (success) { + success = container.removeItem(itemId); + } + return success; + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#doSort() + */ + @Override + protected void doSort() { + super.doSort(); + + Collections.sort(roots, getItemSorter()); + for (LinkedList<Object> childList : children.values()) { + Collections.sort(childList, getItemSorter()); + } + } + + /** + * Used to control how filtering works. @see + * {@link #setIncludeParentsWhenFiltering(boolean)} for more information. + * + * @return true if all parents for items that match the filter are included + * when filtering, false if only the matching items are included + */ + public boolean isIncludeParentsWhenFiltering() { + return includeParentsWhenFiltering; + } + + /** + * Controls how the filtering of the container works. Set this to true to + * make filtering include parents for all matched items in addition to the + * items themselves. Setting this to false causes the filtering to only + * include the matching items and make items with excluded parents into root + * items. + * + * @param includeParentsWhenFiltering + * true to include all parents for items that match the filter, + * false to only include the matching items + */ + public void setIncludeParentsWhenFiltering( + boolean includeParentsWhenFiltering) { + this.includeParentsWhenFiltering = includeParentsWhenFiltering; + if (filteredRoots != null) { + // Currently filtered so needs to be re-filtered + doFilterContainer(true); + } + } + + /* + * Overridden to provide filtering for root & children items. + * + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#updateContainerFiltering() + */ + @Override + protected boolean doFilterContainer(boolean hasFilters) { + if (!hasFilters) { + // All filters removed + filteredRoots = null; + filteredChildren = null; + filteredParent = null; + + return super.doFilterContainer(hasFilters); + } + + // Reset data structures + filteredRoots = new LinkedList<Object>(); + filteredChildren = new HashMap<Object, LinkedList<Object>>(); + filteredParent = new HashMap<Object, Object>(); + + if (includeParentsWhenFiltering) { + // Filter so that parents for items that match the filter are also + // included + HashSet<Object> includedItems = new HashSet<Object>(); + for (Object rootId : roots) { + if (filterIncludingParents(rootId, includedItems)) { + filteredRoots.add(rootId); + addFilteredChildrenRecursively(rootId, includedItems); + } + } + // includedItemIds now contains all the item ids that should be + // included. Filter IndexedContainer based on this + filterOverride = includedItems; + super.doFilterContainer(hasFilters); + filterOverride = null; + + return true; + } else { + // Filter by including all items that pass the filter and make items + // with no parent new root items + + // Filter IndexedContainer first so getItemIds return the items that + // match + super.doFilterContainer(hasFilters); + + LinkedHashSet<Object> filteredItemIds = new LinkedHashSet<Object>( + getItemIds()); + + for (Object itemId : filteredItemIds) { + Object itemParent = parent.get(itemId); + if (itemParent == null || !filteredItemIds.contains(itemParent)) { + // Parent is not included or this was a root, in both cases + // this should be a filtered root + filteredRoots.add(itemId); + } else { + // Parent is included. Add this to the children list (create + // it first if necessary) + addFilteredChild(itemParent, itemId); + } + } + + return true; + } + } + + /** + * Adds the given childItemId as a filteredChildren for the parentItemId and + * sets it filteredParent. + * + * @param parentItemId + * @param childItemId + */ + private void addFilteredChild(Object parentItemId, Object childItemId) { + LinkedList<Object> parentToChildrenList = filteredChildren + .get(parentItemId); + if (parentToChildrenList == null) { + parentToChildrenList = new LinkedList<Object>(); + filteredChildren.put(parentItemId, parentToChildrenList); + } + filteredParent.put(childItemId, parentItemId); + parentToChildrenList.add(childItemId); + + } + + /** + * Recursively adds all items in the includedItems list to the + * filteredChildren map in the same order as they are in the children map. + * Starts from parentItemId and recurses down as long as child items that + * should be included are found. + * + * @param parentItemId + * The item id to start recurse from. Not added to a + * filteredChildren list + * @param includedItems + * Set containing the item ids for the items that should be + * included in the filteredChildren map + */ + private void addFilteredChildrenRecursively(Object parentItemId, + HashSet<Object> includedItems) { + LinkedList<Object> childList = children.get(parentItemId); + if (childList == null) { + return; + } + + for (Object childItemId : childList) { + if (includedItems.contains(childItemId)) { + addFilteredChild(parentItemId, childItemId); + addFilteredChildrenRecursively(childItemId, includedItems); + } + } + } + + /** + * Scans the itemId and all its children for which items should be included + * when filtering. All items which passes the filters are included. + * Additionally all items that have a child node that should be included are + * also themselves included. + * + * @param itemId + * @param includedItems + * @return true if the itemId should be included in the filtered container. + */ + private boolean filterIncludingParents(Object itemId, + HashSet<Object> includedItems) { + boolean toBeIncluded = passesFilters(itemId); + + LinkedList<Object> childList = children.get(itemId); + if (childList != null) { + for (Object childItemId : children.get(itemId)) { + toBeIncluded |= filterIncludingParents(childItemId, + includedItems); + } + } + + if (toBeIncluded) { + includedItems.add(itemId); + } + return toBeIncluded; + } + + private Set<Object> filterOverride = null; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.IndexedContainer#passesFilters(java.lang.Object) + */ + @Override + protected boolean passesFilters(Object itemId) { + if (filterOverride != null) { + return filterOverride.contains(itemId); + } else { + return super.passesFilters(itemId); + } + } +} diff --git a/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java new file mode 100644 index 0000000000..172dc0dd4f --- /dev/null +++ b/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java @@ -0,0 +1,70 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.util.Collection; + +import com.vaadin.data.Container.Hierarchical; + +/** + * A wrapper class for adding external ordering to containers not implementing + * the {@link com.vaadin.data.Container.Ordered} interface while retaining + * {@link Hierarchical} features. + * + * @see ContainerOrderedWrapper + */ +@SuppressWarnings({ "serial" }) +public class HierarchicalContainerOrderedWrapper extends + ContainerOrderedWrapper implements Hierarchical { + + private Hierarchical hierarchical; + + public HierarchicalContainerOrderedWrapper(Hierarchical toBeWrapped) { + super(toBeWrapped); + hierarchical = toBeWrapped; + } + + @Override + public boolean areChildrenAllowed(Object itemId) { + return hierarchical.areChildrenAllowed(itemId); + } + + @Override + public Collection<?> getChildren(Object itemId) { + return hierarchical.getChildren(itemId); + } + + @Override + public Object getParent(Object itemId) { + return hierarchical.getParent(itemId); + } + + @Override + public boolean hasChildren(Object itemId) { + return hierarchical.hasChildren(itemId); + } + + @Override + public boolean isRoot(Object itemId) { + return hierarchical.isRoot(itemId); + } + + @Override + public Collection<?> rootItemIds() { + return hierarchical.rootItemIds(); + } + + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + return hierarchical.setChildrenAllowed(itemId, areChildrenAllowed); + } + + @Override + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + return hierarchical.setParent(itemId, newParentId); + } + +} diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java new file mode 100644 index 0000000000..b95b2c4de8 --- /dev/null +++ b/server/src/com/vaadin/data/util/IndexedContainer.java @@ -0,0 +1,1109 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * An implementation of the <code>{@link Container.Indexed}</code> interface + * with all important features.</p> + * + * Features: + * <ul> + * <li> {@link Container.Indexed} + * <li> {@link Container.Ordered} + * <li> {@link Container.Sortable} + * <li> {@link Container.Filterable} + * <li> {@link Cloneable} (deprecated, might be removed in the future) + * <li>Sends all needed events on content changes. + * </ul> + * + * @see com.vaadin.data.Container + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + +@SuppressWarnings("serial") +// item type is really IndexedContainerItem, but using Item not to show it in +// public API +public class IndexedContainer extends + AbstractInMemoryContainer<Object, Object, Item> implements + Container.PropertySetChangeNotifier, Property.ValueChangeNotifier, + Container.Sortable, Cloneable, Container.Filterable, + Container.SimpleFilterable { + + /* Internal structure */ + + /** + * Linked list of ordered Property IDs. + */ + private ArrayList<Object> propertyIds = new ArrayList<Object>(); + + /** + * Property ID to type mapping. + */ + private Hashtable<Object, Class<?>> types = new Hashtable<Object, Class<?>>(); + + /** + * Hash of Items, where each Item is implemented as a mapping from Property + * ID to Property value. + */ + private Hashtable<Object, Map<Object, Object>> items = new Hashtable<Object, Map<Object, Object>>(); + + /** + * Set of properties that are read-only. + */ + private HashSet<Property<?>> readOnlyProperties = new HashSet<Property<?>>(); + + /** + * List of all Property value change event listeners listening all the + * properties. + */ + private LinkedList<Property.ValueChangeListener> propertyValueChangeListeners = null; + + /** + * Data structure containing all listeners interested in changes to single + * Properties. The data structure is a hashtable mapping Property IDs to a + * hashtable that maps Item IDs to a linked list of listeners listening + * Property identified by given Property ID and Item ID. + */ + private Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>> singlePropertyValueChangeListeners = null; + + private HashMap<Object, Object> defaultPropertyValues; + + private int nextGeneratedItemId = 1; + + /* Container constructors */ + + public IndexedContainer() { + super(); + } + + public IndexedContainer(Collection<?> itemIds) { + this(); + if (items != null) { + for (final Iterator<?> i = itemIds.iterator(); i.hasNext();) { + Object itemId = i.next(); + internalAddItemAtEnd(itemId, new IndexedContainerItem(itemId), + false); + } + filterAll(); + } + } + + /* Container methods */ + + @Override + protected Item getUnfilteredItem(Object itemId) { + if (itemId != null && items.containsKey(itemId)) { + return new IndexedContainerItem(itemId); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + @Override + public Collection<?> getContainerPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Gets the type of a Property stored in the list. + * + * @param id + * the ID of the Property. + * @return Type of the requested Property + */ + @Override + public Class<?> getType(Object propertyId) { + return types.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + if (!containsId(itemId)) { + return null; + } + + return new IndexedContainerProperty(itemId, propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) { + + // Fails, if nulls are given + if (propertyId == null || type == null) { + return false; + } + + // Fails if the Property is already present + if (propertyIds.contains(propertyId)) { + return false; + } + + // Adds the Property to Property list and types + propertyIds.add(propertyId); + types.put(propertyId, type); + + // If default value is given, set it + if (defaultValue != null) { + // for existing rows + for (final Iterator<?> i = getAllItemIds().iterator(); i.hasNext();) { + getItem(i.next()).getItemProperty(propertyId).setValue( + defaultValue); + } + // store for next rows + if (defaultPropertyValues == null) { + defaultPropertyValues = new HashMap<Object, Object>(); + } + defaultPropertyValues.put(propertyId, defaultValue); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() { + int origSize = size(); + + internalRemoveAllItems(); + + items.clear(); + + // fire event only if the visible view changed, regardless of whether + // filtered out items were removed or not + if (origSize != 0) { + // Sends a change event + fireItemSetChange(); + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem() + */ + @Override + public Object addItem() { + + // Creates a new id + final Object id = generateId(); + + // Adds the Item into container + addItem(id); + + return id; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) { + Item item = internalAddItemAtEnd(itemId, new IndexedContainerItem( + itemId), false); + if (!isFiltered()) { + // always the last item + fireItemAdded(size() - 1, itemId, item); + } else if (passesFilters(itemId) && !containsId(itemId)) { + getFilteredItemIds().add(itemId); + // always the last item + fireItemAdded(size() - 1, itemId, item); + } + return item; + } + + /** + * Helper method to add default values for items if available + * + * @param t + * data table of added item + */ + private void addDefaultValues(Hashtable<Object, Object> t) { + if (defaultPropertyValues != null) { + for (Object key : defaultPropertyValues.keySet()) { + t.put(key, defaultPropertyValues.get(key)); + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) { + if (itemId == null || items.remove(itemId) == null) { + return false; + } + int origSize = size(); + int position = indexOfId(itemId); + if (internalRemoveItem(itemId)) { + // fire event only if the visible view changed, regardless of + // whether filtered out items were removed or not + if (size() != origSize) { + fireItemRemoved(position, itemId); + } + + return true; + } else { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object ) + */ + @Override + public boolean removeContainerProperty(Object propertyId) { + + // Fails if the Property is not present + if (!propertyIds.contains(propertyId)) { + return false; + } + + // Removes the Property to Property list and types + propertyIds.remove(propertyId); + types.remove(propertyId); + if (defaultPropertyValues != null) { + defaultPropertyValues.remove(propertyId); + } + + // If remove the Property from all Items + for (final Iterator<Object> i = getAllItemIds().iterator(); i.hasNext();) { + items.get(i.next()).remove(propertyId); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + + /* Container.Ordered methods */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) { + return internalAddItemAfter(previousItemId, newItemId, + new IndexedContainerItem(newItemId), true); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + @Override + public Object addItemAfter(Object previousItemId) { + + // Creates a new id + final Object id = generateId(); + + if (addItemAfter(previousItemId, id) != null) { + return id; + } else { + return null; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object) + */ + @Override + public Item addItemAt(int index, Object newItemId) { + return internalAddItemAt(index, newItemId, new IndexedContainerItem( + newItemId), true); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int) + */ + @Override + public Object addItemAt(int index) { + + // Creates a new id + final Object id = generateId(); + + // Adds the Item into container + addItemAt(index, id); + + return id; + } + + /** + * Generates an unique identifier for use as an item id. Guarantees that the + * generated id is not currently used as an id. + * + * @return + */ + private Serializable generateId() { + Serializable id; + do { + id = Integer.valueOf(nextGeneratedItemId++); + } while (items.containsKey(id)); + + return id; + } + + @Override + protected void registerNewItem(int index, Object newItemId, Item item) { + Hashtable<Object, Object> t = new Hashtable<Object, Object>(); + items.put(newItemId, t); + addDefaultValues(t); + } + + /* Event notifiers */ + + /** + * An <code>event</code> object specifying the list whose Item set has + * changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ItemSetChangeEvent extends BaseItemSetChangeEvent { + + private final int addedItemIndex; + + private ItemSetChangeEvent(IndexedContainer source, int addedItemIndex) { + super(source); + this.addedItemIndex = addedItemIndex; + } + + /** + * Iff one item is added, gives its index. + * + * @return -1 if either multiple items are changed or some other change + * than add is done. + */ + public int getAddedItemIndex() { + return addedItemIndex; + } + + } + + /** + * An <code>event</code> object specifying the Property in a list whose + * value has changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + private static class PropertyValueChangeEvent extends EventObject implements + Property.ValueChangeEvent, Serializable { + + private PropertyValueChangeEvent(Property source) { + super(source); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeEvent#getProperty() + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + + } + + @Override + public void addListener(Container.PropertySetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + super.removeListener(listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com. + * vaadin.data.Property.ValueChangeListener) + */ + @Override + public void addListener(Property.ValueChangeListener listener) { + if (propertyValueChangeListeners == null) { + propertyValueChangeListeners = new LinkedList<Property.ValueChangeListener>(); + } + propertyValueChangeListeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com + * .vaadin.data.Property.ValueChangeListener) + */ + @Override + public void removeListener(Property.ValueChangeListener listener) { + if (propertyValueChangeListeners != null) { + propertyValueChangeListeners.remove(listener); + } + } + + /** + * Sends a Property value change event to all interested listeners. + * + * @param source + * the IndexedContainerProperty object. + */ + private void firePropertyValueChange(IndexedContainerProperty source) { + + // Sends event to listeners listening all value changes + if (propertyValueChangeListeners != null) { + final Object[] l = propertyValueChangeListeners.toArray(); + final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent( + source); + for (int i = 0; i < l.length; i++) { + ((Property.ValueChangeListener) l[i]).valueChange(event); + } + } + + // Sends event to single property value change listeners + if (singlePropertyValueChangeListeners != null) { + final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(source.propertyId); + if (propertySetToListenerListMap != null) { + final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap + .get(source.itemId); + if (listenerList != null) { + final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent( + source); + Object[] listeners = listenerList.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Property.ValueChangeListener) listeners[i]) + .valueChange(event); + } + } + } + } + + } + + @Override + public Collection<?> getListeners(Class<?> eventType) { + if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) { + if (propertyValueChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertyValueChangeListeners); + } + } + return super.getListeners(eventType); + } + + @Override + protected void fireItemAdded(int position, Object itemId, Item item) { + if (position >= 0) { + fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, + position)); + } + } + + @Override + protected void fireItemSetChange() { + fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, -1)); + } + + /** + * Adds new single Property change listener. + * + * @param propertyId + * the ID of the Property to add. + * @param itemId + * the ID of the Item . + * @param listener + * the listener to be added. + */ + private void addSinglePropertyChangeListener(Object propertyId, + Object itemId, Property.ValueChangeListener listener) { + if (listener != null) { + if (singlePropertyValueChangeListeners == null) { + singlePropertyValueChangeListeners = new Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>(); + } + Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(propertyId); + if (propertySetToListenerListMap == null) { + propertySetToListenerListMap = new Hashtable<Object, List<Property.ValueChangeListener>>(); + singlePropertyValueChangeListeners.put(propertyId, + propertySetToListenerListMap); + } + List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap + .get(itemId); + if (listenerList == null) { + listenerList = new LinkedList<Property.ValueChangeListener>(); + propertySetToListenerListMap.put(itemId, listenerList); + } + listenerList.add(listener); + } + } + + /** + * Removes a previously registered single Property change listener. + * + * @param propertyId + * the ID of the Property to remove. + * @param itemId + * the ID of the Item. + * @param listener + * the listener to be removed. + */ + private void removeSinglePropertyChangeListener(Object propertyId, + Object itemId, Property.ValueChangeListener listener) { + if (listener != null && singlePropertyValueChangeListeners != null) { + final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(propertyId); + if (propertySetToListenerListMap != null) { + final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap + .get(itemId); + if (listenerList != null) { + listenerList.remove(listener); + if (listenerList.isEmpty()) { + propertySetToListenerListMap.remove(itemId); + } + } + if (propertySetToListenerListMap.isEmpty()) { + singlePropertyValueChangeListeners.remove(propertyId); + } + } + if (singlePropertyValueChangeListeners.isEmpty()) { + singlePropertyValueChangeListeners = null; + } + } + } + + /* Internal Item and Property implementations */ + + /* + * A class implementing the com.vaadin.data.Item interface to be contained + * in the list. + * + * @author Vaadin Ltd. + * + * @version @VERSION@ + * + * @since 3.0 + */ + class IndexedContainerItem implements Item { + + /** + * Item ID in the host container for this Item. + */ + private final Object itemId; + + /** + * Constructs a new ListItem instance and connects it to a host + * container. + * + * @param itemId + * the Item ID of the new Item. + */ + private IndexedContainerItem(Object itemId) { + + // Gets the item contents from the host + if (itemId == null) { + throw new NullPointerException(); + } + this.itemId = itemId; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Item#getItemProperty(java.lang.Object) + */ + @Override + public Property<?> getItemProperty(Object id) { + return new IndexedContainerProperty(itemId, id); + } + + @Override + public Collection<?> getItemPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Gets the <code>String</code> representation of the contents of the + * Item. The format of the string is a space separated catenation of the + * <code>String</code> representations of the values of the Properties + * contained by the Item. + * + * @return <code>String</code> representation of the Item contents + */ + @Override + public String toString() { + String retValue = ""; + + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + final Object propertyId = i.next(); + retValue += getItemProperty(propertyId).getValue(); + if (i.hasNext()) { + retValue += " "; + } + } + + return retValue; + } + + /** + * Calculates a integer hash-code for the Item that's unique inside the + * list. Two Items inside the same list have always different + * hash-codes, though Items in different lists may have identical + * hash-codes. + * + * @return A locally unique hash-code as integer + */ + @Override + public int hashCode() { + return itemId.hashCode(); + } + + /** + * Tests if the given object is the same as the this object. Two Items + * got from a list container with the same ID are equal. + * + * @param obj + * an object to compare with this object + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (obj == null + || !obj.getClass().equals(IndexedContainerItem.class)) { + return false; + } + final IndexedContainerItem li = (IndexedContainerItem) obj; + return getHost() == li.getHost() && itemId.equals(li.itemId); + } + + private IndexedContainer getHost() { + return IndexedContainer.this; + } + + /** + * IndexedContainerItem does not support adding new properties. Add + * properties at container level. See + * {@link IndexedContainer#addContainerProperty(Object, Class, Object)} + * + * @see com.vaadin.data.Item#addProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("Indexed container item " + + "does not support adding new properties"); + } + + /** + * Indexed container does not support removing properties. Remove + * properties at container level. See + * {@link IndexedContainer#removeContainerProperty(Object)} + * + * @see com.vaadin.data.Item#removeProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Indexed container item does not support property removal"); + } + + } + + /** + * A class implementing the {@link Property} interface to be contained in + * the {@link IndexedContainerItem} contained in the + * {@link IndexedContainer}. + * + * @author Vaadin Ltd. + * + * @version + * @VERSION@ + * @since 3.0 + */ + private class IndexedContainerProperty implements Property<Object>, + Property.ValueChangeNotifier { + + /** + * ID of the Item, where this property resides. + */ + private final Object itemId; + + /** + * Id of the Property. + */ + private final Object propertyId; + + /** + * Constructs a new {@link IndexedContainerProperty} object. + * + * @param itemId + * the ID of the Item to connect the new Property to. + * @param propertyId + * the Property ID of the new Property. + * @param host + * the list that contains the Item to contain the new + * Property. + */ + private IndexedContainerProperty(Object itemId, Object propertyId) { + if (itemId == null || propertyId == null) { + // Null ids are not accepted + throw new NullPointerException( + "Container item or property ids can not be null"); + } + this.propertyId = propertyId; + this.itemId = itemId; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#getType() + */ + @Override + public Class<?> getType() { + return types.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#getValue() + */ + @Override + public Object getValue() { + return items.get(itemId).get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#isReadOnly() + */ + @Override + public boolean isReadOnly() { + return readOnlyProperties.contains(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean newStatus) { + if (newStatus) { + readOnlyProperties.add(this); + } else { + readOnlyProperties.remove(this); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#setValue(java.lang.Object) + */ + @Override + public void setValue(Object newValue) throws Property.ReadOnlyException { + // Gets the Property set + final Map<Object, Object> propertySet = items.get(itemId); + + // Support null values on all types + if (newValue == null) { + propertySet.remove(propertyId); + } else if (getType().isAssignableFrom(newValue.getClass())) { + propertySet.put(propertyId, newValue); + } else { + throw new IllegalArgumentException( + "Value is of invalid type, got " + + newValue.getClass().getName() + " but " + + getType().getName() + " was expected"); + } + + // update the container filtering if this property is being filtered + if (isPropertyFiltered(propertyId)) { + filterAll(); + } + + firePropertyValueChange(this); + } + + /** + * Returns the value of the Property in human readable textual format. + * The return value should be assignable to the <code>setValue</code> + * method if the Property is not in read-only mode. + * + * @return <code>String</code> representation of the value stored in the + * Property + * @deprecated use {@link #getValue()} instead and possibly toString on + * that + */ + @Deprecated + @Override + public String toString() { + throw new UnsupportedOperationException( + "Use Property.getValue() instead of IndexedContainerProperty.toString()"); + } + + /** + * Calculates a integer hash-code for the Property that's unique inside + * the Item containing the Property. Two different Properties inside the + * same Item contained in the same list always have different + * hash-codes, though Properties in different Items may have identical + * hash-codes. + * + * @return A locally unique hash-code as integer + */ + @Override + public int hashCode() { + return itemId.hashCode() ^ propertyId.hashCode(); + } + + /** + * Tests if the given object is the same as the this object. Two + * Properties got from an Item with the same ID are equal. + * + * @param obj + * an object to compare with this object + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (obj == null + || !obj.getClass().equals(IndexedContainerProperty.class)) { + return false; + } + final IndexedContainerProperty lp = (IndexedContainerProperty) obj; + return lp.getHost() == getHost() + && lp.propertyId.equals(propertyId) + && lp.itemId.equals(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#addListener( + * com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void addListener(Property.ValueChangeListener listener) { + addSinglePropertyChangeListener(propertyId, itemId, listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener + * (com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void removeListener(Property.ValueChangeListener listener) { + removeSinglePropertyChangeListener(propertyId, itemId, listener); + } + + private IndexedContainer getHost() { + return IndexedContainer.this; + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sortContainer(propertyId, ascending); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds + * () + */ + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getSortablePropertyIds(); + } + + @Override + public ItemSorter getItemSorter() { + return super.getItemSorter(); + } + + @Override + public void setItemSorter(ItemSorter itemSorter) { + super.setItemSorter(itemSorter); + } + + /** + * Supports cloning of the IndexedContainer cleanly. + * + * @throws CloneNotSupportedException + * if an object cannot be cloned. . + * + * @deprecated cloning support might be removed from IndexedContainer in the + * future + */ + @Deprecated + @Override + public Object clone() throws CloneNotSupportedException { + + // Creates the clone + final IndexedContainer nc = new IndexedContainer(); + + // Clone the shallow properties + nc.setAllItemIds(getAllItemIds() != null ? (ListSet<Object>) ((ListSet<Object>) getAllItemIds()) + .clone() : null); + nc.setItemSetChangeListeners(getItemSetChangeListeners() != null ? new LinkedList<Container.ItemSetChangeListener>( + getItemSetChangeListeners()) : null); + nc.propertyIds = propertyIds != null ? (ArrayList<Object>) propertyIds + .clone() : null; + nc.setPropertySetChangeListeners(getPropertySetChangeListeners() != null ? new LinkedList<Container.PropertySetChangeListener>( + getPropertySetChangeListeners()) : null); + nc.propertyValueChangeListeners = propertyValueChangeListeners != null ? (LinkedList<Property.ValueChangeListener>) propertyValueChangeListeners + .clone() : null; + nc.readOnlyProperties = readOnlyProperties != null ? (HashSet<Property<?>>) readOnlyProperties + .clone() : null; + nc.singlePropertyValueChangeListeners = singlePropertyValueChangeListeners != null ? (Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>) singlePropertyValueChangeListeners + .clone() : null; + + nc.types = types != null ? (Hashtable<Object, Class<?>>) types.clone() + : null; + + nc.setFilters((HashSet<Filter>) ((HashSet<Filter>) getFilters()) + .clone()); + + nc.setFilteredItemIds(getFilteredItemIds() == null ? null + : (ListSet<Object>) ((ListSet<Object>) getFilteredItemIds()) + .clone()); + + // Clone property-values + if (items == null) { + nc.items = null; + } else { + nc.items = new Hashtable<Object, Map<Object, Object>>(); + for (final Iterator<?> i = items.keySet().iterator(); i.hasNext();) { + final Object id = i.next(); + final Hashtable<Object, Object> it = (Hashtable<Object, Object>) items + .get(id); + nc.items.put(id, (Map<Object, Object>) it.clone()); + } + } + + return nc; + } + + @Override + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + try { + addFilter(new SimpleStringFilter(propertyId, filterString, + ignoreCase, onlyMatchPrefix)); + } catch (UnsupportedFilterException e) { + // the filter instance created here is always valid for in-memory + // containers + } + } + + @Override + public void removeAllContainerFilters() { + removeAllFilters(); + } + + @Override + public void removeContainerFilters(Object propertyId) { + removeFilters(propertyId); + } + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + addFilter(filter); + } + + @Override + public void removeContainerFilter(Filter filter) { + removeFilter(filter); + } + +} diff --git a/server/src/com/vaadin/data/util/ItemSorter.java b/server/src/com/vaadin/data/util/ItemSorter.java new file mode 100644 index 0000000000..4399dbe292 --- /dev/null +++ b/server/src/com/vaadin/data/util/ItemSorter.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.Comparator; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Sortable; + +/** + * An item comparator which is compatible with the {@link Sortable} interface. + * The <code>ItemSorter</code> interface can be used in <code>Sortable</code> + * implementations to provide a custom sorting method. + */ +public interface ItemSorter extends Comparator<Object>, Cloneable, Serializable { + + /** + * Sets the parameters for an upcoming sort operation. The parameters + * determine what container to sort and how the <code>ItemSorter</code> + * sorts the container. + * + * @param container + * The container that will be sorted. The container must contain + * the propertyIds given in the <code>propertyId</code> + * parameter. + * @param propertyId + * The property ids used for sorting. The property ids must exist + * in the container and should only be used if they are also + * sortable, i.e include in the collection returned by + * <code>container.getSortableContainerPropertyIds()</code>. See + * {@link Sortable#sort(Object[], boolean[])} for more + * information. + * @param ascending + * Sorting order flags for each property id. See + * {@link Sortable#sort(Object[], boolean[])} for more + * information. + */ + void setSortProperties(Container.Sortable container, Object[] propertyId, + boolean[] ascending); + + /** + * Compares its two arguments for order. Returns a negative integer, zero, + * or a positive integer as the first argument is less than, equal to, or + * greater than the second. + * <p> + * The parameters for the <code>ItemSorter</code> <code>compare()</code> + * method must always be item ids which exist in the container set using + * {@link #setSortProperties(Sortable, Object[], boolean[])}. + * + * @see Comparator#compare(Object, Object) + */ + @Override + int compare(Object itemId1, Object itemId2); + +} diff --git a/server/src/com/vaadin/data/util/ListSet.java b/server/src/com/vaadin/data/util/ListSet.java new file mode 100644 index 0000000000..b71cc46898 --- /dev/null +++ b/server/src/com/vaadin/data/util/ListSet.java @@ -0,0 +1,264 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * ListSet is an internal Vaadin class which implements a combination of a List + * and a Set. The main purpose of this class is to provide a list with a fast + * {@link #contains(Object)} method. Each inserted object must by unique (as + * specified by {@link #equals(Object)}). The {@link #set(int, Object)} method + * allows duplicates because of the way {@link Collections#sort(java.util.List)} + * works. + * + * This class is subject to change and should not be used outside Vaadin core. + */ +public class ListSet<E> extends ArrayList<E> { + private HashSet<E> itemSet = null; + + /** + * Contains a map from an element to the number of duplicates it has. Used + * to temporarily allow duplicates in the list. + */ + private HashMap<E, Integer> duplicates = new HashMap<E, Integer>(); + + public ListSet() { + super(); + itemSet = new HashSet<E>(); + } + + public ListSet(Collection<? extends E> c) { + super(c); + itemSet = new HashSet<E>(c.size()); + itemSet.addAll(c); + } + + public ListSet(int initialCapacity) { + super(initialCapacity); + itemSet = new HashSet<E>(initialCapacity); + } + + // Delegate contains operations to the set + @Override + public boolean contains(Object o) { + return itemSet.contains(o); + } + + @Override + public boolean containsAll(Collection<?> c) { + return itemSet.containsAll(c); + } + + // Methods for updating the set when the list is updated. + @Override + public boolean add(E e) { + if (contains(e)) { + // Duplicates are not allowed + return false; + } + + if (super.add(e)) { + itemSet.add(e); + return true; + } else { + return false; + } + }; + + /** + * Works as java.util.ArrayList#add(int, java.lang.Object) but returns + * immediately if the element is already in the ListSet. + */ + @Override + public void add(int index, E element) { + if (contains(element)) { + // Duplicates are not allowed + return; + } + + super.add(index, element); + itemSet.add(element); + } + + @Override + public boolean addAll(Collection<? extends E> c) { + boolean modified = false; + Iterator<? extends E> i = c.iterator(); + while (i.hasNext()) { + E e = i.next(); + if (contains(e)) { + continue; + } + + if (add(e)) { + itemSet.add(e); + modified = true; + } + } + return modified; + } + + @Override + public boolean addAll(int index, Collection<? extends E> c) { + ensureCapacity(size() + c.size()); + + boolean modified = false; + Iterator<? extends E> i = c.iterator(); + while (i.hasNext()) { + E e = i.next(); + if (contains(e)) { + continue; + } + + add(index++, e); + itemSet.add(e); + modified = true; + } + + return modified; + } + + @Override + public void clear() { + super.clear(); + itemSet.clear(); + } + + @Override + public int indexOf(Object o) { + if (!contains(o)) { + return -1; + } + + return super.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + if (!contains(o)) { + return -1; + } + + return super.lastIndexOf(o); + } + + @Override + public E remove(int index) { + E e = super.remove(index); + + if (e != null) { + itemSet.remove(e); + } + + return e; + } + + @Override + public boolean remove(Object o) { + if (super.remove(o)) { + itemSet.remove(o); + return true; + } else { + return false; + } + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + HashSet<E> toRemove = new HashSet<E>(); + for (int idx = fromIndex; idx < toIndex; idx++) { + toRemove.add(get(idx)); + } + super.removeRange(fromIndex, toIndex); + itemSet.removeAll(toRemove); + } + + @Override + public E set(int index, E element) { + if (contains(element)) { + // Element already exist in the list + if (get(index) == element) { + // At the same position, nothing to be done + return element; + } else { + // Adding at another position. We assume this is a sort + // operation and temporarily allow it. + + // We could just remove (null) the old element and keep the list + // unique. This would require finding the index of the old + // element (indexOf(element)) which is not a fast operation in a + // list. So we instead allow duplicates temporarily. + addDuplicate(element); + } + } + + E old = super.set(index, element); + removeFromSet(old); + itemSet.add(element); + + return old; + } + + /** + * Removes "e" from the set if it no longer exists in the list. + * + * @param e + */ + private void removeFromSet(E e) { + Integer dupl = duplicates.get(e); + if (dupl != null) { + // A duplicate was present so we only decrement the duplicate count + // and continue + if (dupl == 1) { + // This is what always should happen. A sort sets the items one + // by one, temporarily breaking the uniqueness requirement. + duplicates.remove(e); + } else { + duplicates.put(e, dupl - 1); + } + } else { + // The "old" value is no longer in the list. + itemSet.remove(e); + } + + } + + /** + * Marks the "element" can be found more than once from the list. Allowed in + * {@link #set(int, Object)} to make sorting work. + * + * @param element + */ + private void addDuplicate(E element) { + Integer nr = duplicates.get(element); + if (nr == null) { + nr = 1; + } else { + nr++; + } + + /* + * Store the number of duplicates of this element so we know later on if + * we should remove an element from the set or if it was a duplicate (in + * removeFromSet) + */ + duplicates.put(element, nr); + + } + + @SuppressWarnings("unchecked") + @Override + public Object clone() { + ListSet<E> v = (ListSet<E>) super.clone(); + v.itemSet = new HashSet<E>(itemSet); + return v; + } + +} diff --git a/server/src/com/vaadin/data/util/MethodProperty.java b/server/src/com/vaadin/data/util/MethodProperty.java new file mode 100644 index 0000000000..0c64d90481 --- /dev/null +++ b/server/src/com/vaadin/data/util/MethodProperty.java @@ -0,0 +1,784 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Property; +import com.vaadin.util.SerializerHelper; + +/** + * <p> + * Proxy class for creating Properties from pairs of getter and setter methods + * of a Bean property. An instance of this class can be thought as having been + * attached to a field of an object. Accessing the object through the Property + * interface directly manipulates the underlying field. + * </p> + * + * <p> + * It's assumed that the return value returned by the getter method is + * assignable to the type of the property, and the setter method parameter is + * assignable to that value. + * </p> + * + * <p> + * A valid getter method must always be available, but instance of this class + * can be constructed with a <code>null</code> setter method in which case the + * resulting MethodProperty is read-only. + * </p> + * + * <p> + * MethodProperty implements Property.ValueChangeNotifier, but does not + * automatically know whether or not the getter method will actually return a + * new value - value change listeners are always notified when setValue is + * called, without verifying what the getter returns. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class MethodProperty<T> extends AbstractProperty<T> { + + /** + * The object that includes the property the MethodProperty is bound to. + */ + private transient Object instance; + + /** + * Argument arrays for the getter and setter methods. + */ + private transient Object[] setArgs, getArgs; + + /** + * The getter and setter methods. + */ + private transient Method setMethod, getMethod; + + /** + * Index of the new value in the argument list for the setter method. If the + * setter method requires several parameters, this index tells which one is + * the actual value to change. + */ + private int setArgumentIndex; + + /** + * Type of the property. + */ + private transient Class<? extends T> type; + + /* Special serialization to handle method references */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + SerializerHelper.writeClass(out, type); + out.writeObject(instance); + out.writeObject(setArgs); + out.writeObject(getArgs); + if (setMethod != null) { + out.writeObject(setMethod.getName()); + SerializerHelper + .writeClassArray(out, setMethod.getParameterTypes()); + } else { + out.writeObject(null); + out.writeObject(null); + } + if (getMethod != null) { + out.writeObject(getMethod.getName()); + SerializerHelper + .writeClassArray(out, getMethod.getParameterTypes()); + } else { + out.writeObject(null); + out.writeObject(null); + } + }; + + /* Special serialization to handle method references */ + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + try { + @SuppressWarnings("unchecked") + // business assumption; type parameters not checked at runtime + Class<T> class1 = (Class<T>) SerializerHelper.readClass(in); + type = class1; + instance = in.readObject(); + setArgs = (Object[]) in.readObject(); + getArgs = (Object[]) in.readObject(); + String name = (String) in.readObject(); + Class<?>[] paramTypes = SerializerHelper.readClassArray(in); + if (name != null) { + setMethod = instance.getClass().getMethod(name, paramTypes); + } else { + setMethod = null; + } + + name = (String) in.readObject(); + paramTypes = SerializerHelper.readClassArray(in); + if (name != null) { + getMethod = instance.getClass().getMethod(name, paramTypes); + } else { + getMethod = null; + } + } catch (SecurityException e) { + getLogger().log(Level.SEVERE, "Internal deserialization error", e); + } catch (NoSuchMethodException e) { + getLogger().log(Level.SEVERE, "Internal deserialization error", e); + } + }; + + /** + * <p> + * Creates a new instance of <code>MethodProperty</code> from a named bean + * property. This constructor takes an object and the name of a bean + * property and initializes itself with the accessor methods for the + * property. + * </p> + * <p> + * The getter method of a <code>MethodProperty</code> instantiated with this + * constructor will be called with no arguments, and the setter method with + * only the new value as the sole argument. + * </p> + * + * <p> + * If the setter method is unavailable, the resulting + * <code>MethodProperty</code> will be read-only, otherwise it will be + * read-write. + * </p> + * + * <p> + * Method names are constructed from the bean property by adding + * get/is/are/set prefix and capitalising the first character in the name of + * the given bean property. + * </p> + * + * @param instance + * the object that includes the property. + * @param beanPropertyName + * the name of the property to bind to. + */ + @SuppressWarnings("unchecked") + public MethodProperty(Object instance, String beanPropertyName) { + + final Class<?> beanClass = instance.getClass(); + + // Assure that the first letter is upper cased (it is a common + // mistake to write firstName, not FirstName). + if (Character.isLowerCase(beanPropertyName.charAt(0))) { + final char[] buf = beanPropertyName.toCharArray(); + buf[0] = Character.toUpperCase(buf[0]); + beanPropertyName = new String(buf); + } + + // Find the get method + getMethod = null; + try { + getMethod = initGetterMethod(beanPropertyName, beanClass); + } catch (final java.lang.NoSuchMethodException ignored) { + throw new MethodException(this, "Bean property " + beanPropertyName + + " can not be found"); + } + + // In case the get method is found, resolve the type + Class<?> returnType = getMethod.getReturnType(); + + // Finds the set method + setMethod = null; + try { + setMethod = beanClass.getMethod("set" + beanPropertyName, + new Class[] { returnType }); + } catch (final java.lang.NoSuchMethodException skipped) { + } + + // Gets the return type from get method + if (returnType.isPrimitive()) { + type = (Class<T>) convertPrimitiveType(returnType); + if (type.isPrimitive()) { + throw new MethodException(this, "Bean property " + + beanPropertyName + + " getter return type must not be void"); + } + } else { + type = (Class<T>) returnType; + } + + setArguments(new Object[] {}, new Object[] { null }, 0); + this.instance = instance; + } + + /** + * <p> + * Creates a new instance of <code>MethodProperty</code> from named getter + * and setter methods. The getter method of a <code>MethodProperty</code> + * instantiated with this constructor will be called with no arguments, and + * the setter method with only the new value as the sole argument. + * </p> + * + * <p> + * If the setter method is <code>null</code>, the resulting + * <code>MethodProperty</code> will be read-only, otherwise it will be + * read-write. + * </p> + * + * @param type + * the type of the property. + * @param instance + * the object that includes the property. + * @param getMethodName + * the name of the getter method. + * @param setMethodName + * the name of the setter method. + * + */ + public MethodProperty(Class<? extends T> type, Object instance, + String getMethodName, String setMethodName) { + this(type, instance, getMethodName, setMethodName, new Object[] {}, + new Object[] { null }, 0); + } + + /** + * <p> + * Creates a new instance of <code>MethodProperty</code> with the getter and + * setter methods. The getter method of a <code>MethodProperty</code> + * instantiated with this constructor will be called with no arguments, and + * the setter method with only the new value as the sole argument. + * </p> + * + * <p> + * If the setter method is <code>null</code>, the resulting + * <code>MethodProperty</code> will be read-only, otherwise it will be + * read-write. + * </p> + * + * @param type + * the type of the property. + * @param instance + * the object that includes the property. + * @param getMethod + * the getter method. + * @param setMethod + * the setter method. + */ + public MethodProperty(Class<? extends T> type, Object instance, + Method getMethod, Method setMethod) { + this(type, instance, getMethod, setMethod, new Object[] {}, + new Object[] { null }, 0); + } + + /** + * <p> + * Creates a new instance of <code>MethodProperty</code> from named getter + * and setter methods and argument lists. The getter method of a + * <code>MethodProperty</code> instantiated with this constructor will be + * called with the getArgs as arguments. The setArgs will be used as the + * arguments for the setter method, though the argument indexed by the + * setArgumentIndex will be replaced with the argument passed to the + * {@link #setValue(Object newValue)} method. + * </p> + * + * <p> + * For example, if the <code>setArgs</code> contains <code>A</code>, + * <code>B</code> and <code>C</code>, and <code>setArgumentIndex = + * 1</code>, the call <code>methodProperty.setValue(X)</code> would result + * in the setter method to be called with the parameter set of + * <code>{A, X, C}</code> + * </p> + * + * @param type + * the type of the property. + * @param instance + * the object that includes the property. + * @param getMethodName + * the name of the getter method. + * @param setMethodName + * the name of the setter method. + * @param getArgs + * the fixed argument list to be passed to the getter method. + * @param setArgs + * the fixed argument list to be passed to the setter method. + * @param setArgumentIndex + * the index of the argument in <code>setArgs</code> to be + * replaced with <code>newValue</code> when + * {@link #setValue(Object newValue)} is called. + */ + @SuppressWarnings("unchecked") + public MethodProperty(Class<? extends T> type, Object instance, + String getMethodName, String setMethodName, Object[] getArgs, + Object[] setArgs, int setArgumentIndex) { + + // Check the setargs and setargs index + if (setMethodName != null && setArgs == null) { + throw new IndexOutOfBoundsException("The setArgs can not be null"); + } + if (setMethodName != null + && (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length)) { + throw new IndexOutOfBoundsException( + "The setArgumentIndex must be >= 0 and < setArgs.length"); + } + + // Set type + this.type = type; + + // Find set and get -methods + final Method[] m = instance.getClass().getMethods(); + + // Finds get method + boolean found = false; + for (int i = 0; i < m.length; i++) { + + // Tests the name of the get Method + if (!m[i].getName().equals(getMethodName)) { + + // name does not match, try next method + continue; + } + + // Tests return type + if (!type.equals(m[i].getReturnType())) { + continue; + } + + // Tests the parameter types + final Class<?>[] c = m[i].getParameterTypes(); + if (c.length != getArgs.length) { + + // not the right amount of parameters, try next method + continue; + } + int j = 0; + while (j < c.length) { + if (getArgs[j] != null + && !c[j].isAssignableFrom(getArgs[j].getClass())) { + + // parameter type does not match, try next method + break; + } + j++; + } + if (j == c.length) { + + // all paramteters matched + if (found == true) { + throw new MethodException(this, + "Could not uniquely identify " + getMethodName + + "-method"); + } else { + found = true; + getMethod = m[i]; + } + } + } + if (found != true) { + throw new MethodException(this, "Could not find " + getMethodName + + "-method"); + } + + // Finds set method + if (setMethodName != null) { + + // Finds setMethod + found = false; + for (int i = 0; i < m.length; i++) { + + // Checks name + if (!m[i].getName().equals(setMethodName)) { + + // name does not match, try next method + continue; + } + + // Checks parameter compatibility + final Class<?>[] c = m[i].getParameterTypes(); + if (c.length != setArgs.length) { + + // not the right amount of parameters, try next method + continue; + } + int j = 0; + while (j < c.length) { + if (setArgs[j] != null + && !c[j].isAssignableFrom(setArgs[j].getClass())) { + + // parameter type does not match, try next method + break; + } else if (j == setArgumentIndex && !c[j].equals(type)) { + + // Property type is not the same as setArg type + break; + } + j++; + } + if (j == c.length) { + + // all parameters match + if (found == true) { + throw new MethodException(this, + "Could not identify unique " + setMethodName + + "-method"); + } else { + found = true; + setMethod = m[i]; + } + } + } + if (found != true) { + throw new MethodException(this, "Could not identify " + + setMethodName + "-method"); + } + } + + // Gets the return type from get method + this.type = (Class<T>) convertPrimitiveType(type); + + setArguments(getArgs, setArgs, setArgumentIndex); + this.instance = instance; + } + + /** + * <p> + * Creates a new instance of <code>MethodProperty</code> from the getter and + * setter methods, and argument lists. + * </p> + * <p> + * This constructor behaves exactly like + * {@link #MethodProperty(Class type, Object instance, String getMethodName, String setMethodName, Object [] getArgs, Object [] setArgs, int setArgumentIndex)} + * except that instead of names of the getter and setter methods this + * constructor is given the actual methods themselves. + * </p> + * + * @param type + * the type of the property. + * @param instance + * the object that includes the property. + * @param getMethod + * the getter method. + * @param setMethod + * the setter method. + * @param getArgs + * the fixed argument list to be passed to the getter method. + * @param setArgs + * the fixed argument list to be passed to the setter method. + * @param setArgumentIndex + * the index of the argument in <code>setArgs</code> to be + * replaced with <code>newValue</code> when + * {@link #setValue(Object newValue)} is called. + */ + @SuppressWarnings("unchecked") + // cannot use "Class<? extends T>" because of automatic primitive type + // conversions + public MethodProperty(Class<?> type, Object instance, Method getMethod, + Method setMethod, Object[] getArgs, Object[] setArgs, + int setArgumentIndex) { + + if (getMethod == null) { + throw new MethodException(this, + "Property GET-method cannot not be null: " + type); + } + + if (setMethod != null) { + if (setArgs == null) { + throw new IndexOutOfBoundsException( + "The setArgs can not be null"); + } + if (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length) { + throw new IndexOutOfBoundsException( + "The setArgumentIndex must be >= 0 and < setArgs.length"); + } + } + + // Gets the return type from get method + Class<? extends T> convertedType = (Class<? extends T>) convertPrimitiveType(type); + + this.getMethod = getMethod; + this.setMethod = setMethod; + setArguments(getArgs, setArgs, setArgumentIndex); + this.instance = instance; + this.type = convertedType; + } + + /** + * Find a getter method for a property (getXyz(), isXyz() or areXyz()). + * + * @param propertyName + * name of the property + * @param beanClass + * class in which to look for the getter methods + * @return Method + * @throws NoSuchMethodException + * if no getter found + */ + static Method initGetterMethod(String propertyName, final Class<?> beanClass) + throws NoSuchMethodException { + propertyName = propertyName.substring(0, 1).toUpperCase() + + propertyName.substring(1); + + Method getMethod = null; + try { + getMethod = beanClass.getMethod("get" + propertyName, + new Class[] {}); + } catch (final java.lang.NoSuchMethodException ignored) { + try { + getMethod = beanClass.getMethod("is" + propertyName, + new Class[] {}); + } catch (final java.lang.NoSuchMethodException ignoredAsWell) { + getMethod = beanClass.getMethod("are" + propertyName, + new Class[] {}); + } + } + return getMethod; + } + + static Class<?> convertPrimitiveType(Class<?> type) { + // Gets the return type from get method + if (type.isPrimitive()) { + if (type.equals(Boolean.TYPE)) { + type = Boolean.class; + } else if (type.equals(Integer.TYPE)) { + type = Integer.class; + } else if (type.equals(Float.TYPE)) { + type = Float.class; + } else if (type.equals(Double.TYPE)) { + type = Double.class; + } else if (type.equals(Byte.TYPE)) { + type = Byte.class; + } else if (type.equals(Character.TYPE)) { + type = Character.class; + } else if (type.equals(Short.TYPE)) { + type = Short.class; + } else if (type.equals(Long.TYPE)) { + type = Long.class; + } + } + return type; + } + + /** + * Returns the type of the Property. The methods <code>getValue</code> and + * <code>setValue</code> must be compatible with this type: one must be able + * to safely cast the value returned from <code>getValue</code> to the given + * type and pass any variable assignable to this type as an argument to + * <code>setValue</code>. + * + * @return type of the Property + */ + @Override + public final Class<? extends T> getType() { + return type; + } + + /** + * Tests if the object is in read-only mode. In read-only mode calls to + * <code>setValue</code> will throw <code>ReadOnlyException</code> and will + * not modify the value of the Property. + * + * @return <code>true</code> if the object is in read-only mode, + * <code>false</code> if it's not + */ + @Override + public boolean isReadOnly() { + return super.isReadOnly() || (setMethod == null); + } + + /** + * Gets the value stored in the Property. The value is resolved by calling + * the specified getter method with the argument specified at instantiation. + * + * @return the value of the Property + */ + @Override + public T getValue() { + try { + return (T) getMethod.invoke(instance, getArgs); + } catch (final Throwable e) { + throw new MethodException(this, e); + } + } + + /** + * <p> + * Sets the setter method and getter method argument lists. + * </p> + * + * @param getArgs + * the fixed argument list to be passed to the getter method. + * @param setArgs + * the fixed argument list to be passed to the setter method. + * @param setArgumentIndex + * the index of the argument in <code>setArgs</code> to be + * replaced with <code>newValue</code> when + * {@link #setValue(Object newValue)} is called. + */ + public void setArguments(Object[] getArgs, Object[] setArgs, + int setArgumentIndex) { + this.getArgs = new Object[getArgs.length]; + for (int i = 0; i < getArgs.length; i++) { + this.getArgs[i] = getArgs[i]; + } + this.setArgs = new Object[setArgs.length]; + for (int i = 0; i < setArgs.length; i++) { + this.setArgs[i] = setArgs[i]; + } + this.setArgumentIndex = setArgumentIndex; + } + + /** + * Sets the value of the property. + * + * Note that since Vaadin 7, no conversions are performed and the value must + * be of the correct type. + * + * @param newValue + * the New value of the property. + * @throws <code>Property.ReadOnlyException</code> if the object is in + * read-only mode. + * @see #invokeSetMethod(Object) + */ + @Override + @SuppressWarnings("unchecked") + public void setValue(Object newValue) throws Property.ReadOnlyException { + + // Checks the mode + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Checks the type of the value + if (newValue != null && !type.isAssignableFrom(newValue.getClass())) { + throw new IllegalArgumentException( + "Invalid value type for ObjectProperty."); + } + + invokeSetMethod((T) newValue); + fireValueChange(); + } + + /** + * Internal method to actually call the setter method of the wrapped + * property. + * + * @param value + */ + protected void invokeSetMethod(T value) { + + try { + // Construct a temporary argument array only if needed + if (setArgs.length == 1) { + setMethod.invoke(instance, new Object[] { value }); + } else { + + // Sets the value to argument array + final Object[] args = new Object[setArgs.length]; + for (int i = 0; i < setArgs.length; i++) { + args[i] = (i == setArgumentIndex) ? value : setArgs[i]; + } + setMethod.invoke(instance, args); + } + } catch (final InvocationTargetException e) { + final Throwable targetException = e.getTargetException(); + throw new MethodException(this, targetException); + } catch (final Exception e) { + throw new MethodException(this, e); + } + } + + /** + * <code>Exception</code> object that signals that there were problems + * calling or finding the specified getter or setter methods of the + * property. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("rawtypes") + // Exceptions cannot be parameterized, ever. + public static class MethodException extends RuntimeException { + + /** + * The method property from which the exception originates from + */ + private final Property property; + + /** + * Cause of the method exception + */ + private Throwable cause; + + /** + * Constructs a new <code>MethodException</code> with the specified + * detail message. + * + * @param property + * the property. + * @param msg + * the detail message. + */ + public MethodException(Property property, String msg) { + super(msg); + this.property = property; + } + + /** + * Constructs a new <code>MethodException</code> from another exception. + * + * @param property + * the property. + * @param cause + * the cause of the exception. + */ + public MethodException(Property property, Throwable cause) { + this.property = property; + this.cause = cause; + } + + /** + * @see java.lang.Throwable#getCause() + */ + @Override + public Throwable getCause() { + return cause; + } + + /** + * Gets the method property this exception originates from. + * + * @return MethodProperty or null if not a valid MethodProperty + */ + public MethodProperty getMethodProperty() { + return (property instanceof MethodProperty) ? (MethodProperty) property + : null; + } + + /** + * Gets the method property this exception originates from. + * + * @return Property from which the exception originates + */ + public Property getProperty() { + return property; + } + } + + /** + * Sends a value change event to all registered listeners. + * + * Public for backwards compatibility, visibility may be reduced in future + * versions. + */ + @Override + public void fireValueChange() { + super.fireValueChange(); + } + + private static final Logger getLogger() { + return Logger.getLogger(MethodProperty.class.getName()); + } +} diff --git a/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java new file mode 100644 index 0000000000..a2a76ec6cf --- /dev/null +++ b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java @@ -0,0 +1,134 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Property; +import com.vaadin.util.SerializerHelper; + +/** + * Property descriptor that is able to create simple {@link MethodProperty} + * instances for a bean, using given accessors. + * + * @param <BT> + * bean type + * + * @since 6.6 + */ +public class MethodPropertyDescriptor<BT> implements + VaadinPropertyDescriptor<BT> { + + private final String name; + private Class<?> propertyType; + private transient Method readMethod; + private transient Method writeMethod; + + /** + * Creates a property descriptor that can create MethodProperty instances to + * access the underlying bean property. + * + * @param name + * of the property + * @param propertyType + * type (class) of the property + * @param readMethod + * getter {@link Method} for the property + * @param writeMethod + * setter {@link Method} for the property or null if read-only + * property + */ + public MethodPropertyDescriptor(String name, Class<?> propertyType, + Method readMethod, Method writeMethod) { + this.name = name; + this.propertyType = propertyType; + this.readMethod = readMethod; + this.writeMethod = writeMethod; + } + + /* Special serialization to handle method references */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + SerializerHelper.writeClass(out, propertyType); + + if (writeMethod != null) { + out.writeObject(writeMethod.getName()); + SerializerHelper.writeClass(out, writeMethod.getDeclaringClass()); + SerializerHelper.writeClassArray(out, + writeMethod.getParameterTypes()); + } else { + out.writeObject(null); + out.writeObject(null); + out.writeObject(null); + } + + if (readMethod != null) { + out.writeObject(readMethod.getName()); + SerializerHelper.writeClass(out, readMethod.getDeclaringClass()); + SerializerHelper.writeClassArray(out, + readMethod.getParameterTypes()); + } else { + out.writeObject(null); + out.writeObject(null); + out.writeObject(null); + } + } + + /* Special serialization to handle method references */ + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + try { + @SuppressWarnings("unchecked") + // business assumption; type parameters not checked at runtime + Class<BT> class1 = (Class<BT>) SerializerHelper.readClass(in); + propertyType = class1; + + String name = (String) in.readObject(); + Class<?> writeMethodClass = SerializerHelper.readClass(in); + Class<?>[] paramTypes = SerializerHelper.readClassArray(in); + if (name != null) { + writeMethod = writeMethodClass.getMethod(name, paramTypes); + } else { + writeMethod = null; + } + + name = (String) in.readObject(); + Class<?> readMethodClass = SerializerHelper.readClass(in); + paramTypes = SerializerHelper.readClassArray(in); + if (name != null) { + readMethod = readMethodClass.getMethod(name, paramTypes); + } else { + readMethod = null; + } + } catch (SecurityException e) { + getLogger().log(Level.SEVERE, "Internal deserialization error", e); + } catch (NoSuchMethodException e) { + getLogger().log(Level.SEVERE, "Internal deserialization error", e); + } + }; + + @Override + public String getName() { + return name; + } + + @Override + public Class<?> getPropertyType() { + return propertyType; + } + + @Override + public Property<?> createProperty(Object bean) { + return new MethodProperty<Object>(propertyType, bean, readMethod, + writeMethod); + } + + private static final Logger getLogger() { + return Logger.getLogger(MethodPropertyDescriptor.class.getName()); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/NestedMethodProperty.java b/server/src/com/vaadin/data/util/NestedMethodProperty.java new file mode 100644 index 0000000000..9bff38456d --- /dev/null +++ b/server/src/com/vaadin/data/util/NestedMethodProperty.java @@ -0,0 +1,257 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.vaadin.data.Property; +import com.vaadin.data.util.MethodProperty.MethodException; + +/** + * Nested accessor based property for a bean. + * + * The property is specified in the dotted notation, e.g. "address.street", and + * can contain multiple levels of nesting. + * + * When accessing the property value, all intermediate getters must return + * non-null values. + * + * @see MethodProperty + * + * @since 6.6 + */ +public class NestedMethodProperty<T> extends AbstractProperty<T> { + + // needed for de-serialization + private String propertyName; + + // chain of getter methods + private transient List<Method> getMethods; + /** + * The setter method. + */ + private transient Method setMethod; + + /** + * Bean instance used as a starting point for accessing the property value. + */ + private Object instance; + + private Class<? extends T> type; + + /* Special serialization to handle method references */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + // getMethods and setMethod are reconstructed on read based on + // propertyName + } + + /* Special serialization to handle method references */ + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + + initialize(instance.getClass(), propertyName); + } + + /** + * Constructs a nested method property for a given object instance. The + * property name is a dot separated string pointing to a nested property, + * e.g. "manager.address.street". + * + * @param instance + * top-level bean to which the property applies + * @param propertyName + * dot separated nested property name + * @throws IllegalArgumentException + * if the property name is invalid + */ + public NestedMethodProperty(Object instance, String propertyName) { + this.instance = instance; + initialize(instance.getClass(), propertyName); + } + + /** + * For internal use to deduce property type etc. without a bean instance. + * Calling {@link #setValue(Object)} or {@link #getValue()} on properties + * constructed this way is not supported. + * + * @param instanceClass + * class of the top-level bean + * @param propertyName + */ + NestedMethodProperty(Class<?> instanceClass, String propertyName) { + instance = null; + initialize(instanceClass, propertyName); + } + + /** + * Initializes most of the internal fields based on the top-level bean + * instance and property name (dot-separated string). + * + * @param beanClass + * class of the top-level bean to which the property applies + * @param propertyName + * dot separated nested property name + * @throws IllegalArgumentException + * if the property name is invalid + */ + private void initialize(Class<?> beanClass, String propertyName) + throws IllegalArgumentException { + + List<Method> getMethods = new ArrayList<Method>(); + + String lastSimplePropertyName = propertyName; + Class<?> lastClass = beanClass; + + // first top-level property, then go deeper in a loop + Class<?> propertyClass = beanClass; + String[] simplePropertyNames = propertyName.split("\\."); + if (propertyName.endsWith(".") || 0 == simplePropertyNames.length) { + throw new IllegalArgumentException("Invalid property name '" + + propertyName + "'"); + } + for (int i = 0; i < simplePropertyNames.length; i++) { + String simplePropertyName = simplePropertyNames[i].trim(); + if (simplePropertyName.length() > 0) { + lastSimplePropertyName = simplePropertyName; + lastClass = propertyClass; + try { + Method getter = MethodProperty.initGetterMethod( + simplePropertyName, propertyClass); + propertyClass = getter.getReturnType(); + getMethods.add(getter); + } catch (final java.lang.NoSuchMethodException e) { + throw new IllegalArgumentException("Bean property '" + + simplePropertyName + "' not found", e); + } + } else { + throw new IllegalArgumentException( + "Empty or invalid bean property identifier in '" + + propertyName + "'"); + } + } + + // In case the get method is found, resolve the type + Method lastGetMethod = getMethods.get(getMethods.size() - 1); + Class<?> type = lastGetMethod.getReturnType(); + + // Finds the set method + Method setMethod = null; + try { + // Assure that the first letter is upper cased (it is a common + // mistake to write firstName, not FirstName). + if (Character.isLowerCase(lastSimplePropertyName.charAt(0))) { + final char[] buf = lastSimplePropertyName.toCharArray(); + buf[0] = Character.toUpperCase(buf[0]); + lastSimplePropertyName = new String(buf); + } + + setMethod = lastClass.getMethod("set" + lastSimplePropertyName, + new Class[] { type }); + } catch (final NoSuchMethodException skipped) { + } + + this.type = (Class<? extends T>) MethodProperty + .convertPrimitiveType(type); + this.propertyName = propertyName; + this.getMethods = getMethods; + this.setMethod = setMethod; + } + + @Override + public Class<? extends T> getType() { + return type; + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || (null == setMethod); + } + + /** + * Gets the value stored in the Property. The value is resolved by calling + * the specified getter method with the argument specified at instantiation. + * + * @return the value of the Property + */ + @Override + public T getValue() { + try { + Object object = instance; + for (Method m : getMethods) { + object = m.invoke(object); + } + return (T) object; + } catch (final Throwable e) { + throw new MethodException(this, e); + } + } + + /** + * Sets the value of the property. The new value must be assignable to the + * type of this property. + * + * @param newValue + * the New value of the property. + * @throws <code>Property.ReadOnlyException</code> if the object is in + * read-only mode. + * @see #invokeSetMethod(Object) + */ + @Override + public void setValue(Object newValue) throws ReadOnlyException { + // Checks the mode + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Checks the type of the value + if (newValue != null && !type.isAssignableFrom(newValue.getClass())) { + throw new IllegalArgumentException( + "Invalid value type for NestedMethodProperty."); + } + + invokeSetMethod((T) newValue); + fireValueChange(); + } + + /** + * Internal method to actually call the setter method of the wrapped + * property. + * + * @param value + */ + protected void invokeSetMethod(T value) { + try { + Object object = instance; + for (int i = 0; i < getMethods.size() - 1; i++) { + object = getMethods.get(i).invoke(object); + } + setMethod.invoke(object, new Object[] { value }); + } catch (final InvocationTargetException e) { + throw new MethodException(this, e.getTargetException()); + } catch (final Exception e) { + throw new MethodException(this, e); + } + } + + /** + * Returns an unmodifiable list of getter methods to call in sequence to get + * the property value. + * + * This API may change in future versions. + * + * @return unmodifiable list of getter methods corresponding to each segment + * of the property name + */ + protected List<Method> getGetMethods() { + return Collections.unmodifiableList(getMethods); + } + +} diff --git a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java new file mode 100644 index 0000000000..b67b425d1d --- /dev/null +++ b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java @@ -0,0 +1,60 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import com.vaadin.data.Property; + +/** + * Property descriptor that is able to create nested property instances for a + * bean. + * + * The property is specified in the dotted notation, e.g. "address.street", and + * can contain multiple levels of nesting. + * + * @param <BT> + * bean type + * + * @since 6.6 + */ +public class NestedPropertyDescriptor<BT> implements + VaadinPropertyDescriptor<BT> { + + private final String name; + private final Class<?> propertyType; + + /** + * Creates a property descriptor that can create MethodProperty instances to + * access the underlying bean property. + * + * @param name + * of the property in a dotted path format, e.g. "address.street" + * @param beanType + * type (class) of the top-level bean + * @throws IllegalArgumentException + * if the property name is invalid + */ + public NestedPropertyDescriptor(String name, Class<BT> beanType) + throws IllegalArgumentException { + this.name = name; + NestedMethodProperty<?> property = new NestedMethodProperty<Object>( + beanType, name); + this.propertyType = property.getType(); + } + + @Override + public String getName() { + return name; + } + + @Override + public Class<?> getPropertyType() { + return propertyType; + } + + @Override + public Property<?> createProperty(BT bean) { + return new NestedMethodProperty<Object>(bean, name); + } + +} diff --git a/server/src/com/vaadin/data/util/ObjectProperty.java b/server/src/com/vaadin/data/util/ObjectProperty.java new file mode 100644 index 0000000000..cb85b44c2a --- /dev/null +++ b/server/src/com/vaadin/data/util/ObjectProperty.java @@ -0,0 +1,141 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import com.vaadin.data.Property; + +/** + * A simple data object containing one typed value. This class is a + * straightforward implementation of the the {@link com.vaadin.data.Property} + * interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ObjectProperty<T> extends AbstractProperty<T> { + + /** + * The value contained by the Property. + */ + private T value; + + /** + * Data type of the Property's value. + */ + private final Class<T> type; + + /** + * Creates a new instance of ObjectProperty with the given value. The type + * of the property is automatically initialized to be the type of the given + * value. + * + * @param value + * the Initial value of the Property. + */ + @SuppressWarnings("unchecked") + // the cast is safe, because an object of type T has class Class<T> + public ObjectProperty(T value) { + this(value, (Class<T>) value.getClass()); + } + + /** + * Creates a new instance of ObjectProperty with the given value and type. + * + * Since Vaadin 7, only values of the correct type are accepted, and no + * automatic conversions are performed. + * + * @param value + * the Initial value of the Property. + * @param type + * the type of the value. The value must be assignable to given + * type. + */ + public ObjectProperty(T value, Class<T> type) { + + // Set the values + this.type = type; + setValue(value); + } + + /** + * Creates a new instance of ObjectProperty with the given value, type and + * read-only mode status. + * + * Since Vaadin 7, only the correct type of values is accepted, see + * {@link #ObjectProperty(Object, Class)}. + * + * @param value + * the Initial value of the property. + * @param type + * the type of the value. <code>value</code> must be assignable + * to this type. + * @param readOnly + * Sets the read-only mode. + */ + public ObjectProperty(T value, Class<T> type, boolean readOnly) { + this(value, type); + setReadOnly(readOnly); + } + + /** + * Returns the type of the ObjectProperty. The methods <code>getValue</code> + * and <code>setValue</code> must be compatible with this type: one must be + * able to safely cast the value returned from <code>getValue</code> to the + * given type and pass any variable assignable to this type as an argument + * to <code>setValue</code>. + * + * @return type of the Property + */ + @Override + public final Class<T> getType() { + return type; + } + + /** + * Gets the value stored in the Property. + * + * @return the value stored in the Property + */ + @Override + public T getValue() { + return value; + } + + /** + * Sets the value of the property. + * + * Note that since Vaadin 7, no conversions are performed and the value must + * be of the correct type. + * + * @param newValue + * the New value of the property. + * @throws <code>Property.ReadOnlyException</code> if the object is in + * read-only mode + */ + @Override + @SuppressWarnings("unchecked") + public void setValue(Object newValue) throws Property.ReadOnlyException { + + // Checks the mode + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Checks the type of the value + if (newValue != null && !type.isAssignableFrom(newValue.getClass())) { + throw new IllegalArgumentException("Invalid value type " + + newValue.getClass().getName() + + " for ObjectProperty of type " + type.getName() + "."); + } + + // the cast is safe after an isAssignableFrom check + this.value = (T) newValue; + + fireValueChange(); + } +} diff --git a/server/src/com/vaadin/data/util/PropertyFormatter.java b/server/src/com/vaadin/data/util/PropertyFormatter.java new file mode 100644 index 0000000000..3d65726309 --- /dev/null +++ b/server/src/com/vaadin/data/util/PropertyFormatter.java @@ -0,0 +1,245 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import com.vaadin.data.Property; +import com.vaadin.data.util.converter.Converter; + +/** + * Formatting proxy for a {@link Property}. + * + * <p> + * This class can be used to implement formatting for any type of Property + * datasources. The idea is to connect this as proxy between UI component and + * the original datasource. + * </p> + * + * <p> + * For example <code> + * <pre>textfield.setPropertyDataSource(new PropertyFormatter(property) { + public String format(Object value) { + return ((Double) value).toString() + "000000000"; + } + + public Object parse(String formattedValue) throws Exception { + return Double.parseDouble(formattedValue); + } + + });</pre></code> adds formatter for Double-typed property that extends + * standard "1.0" notation with more zeroes. + * </p> + * + * @param T + * type of the underlying property (a PropertyFormatter is always a + * Property<String>) + * + * @deprecated Since 7.0 replaced by {@link Converter} + * @author Vaadin Ltd. + * @since 5.3.0 + */ +@SuppressWarnings("serial") +@Deprecated +public abstract class PropertyFormatter<T> extends AbstractProperty<String> + implements Property.Viewer, Property.ValueChangeListener, + Property.ReadOnlyStatusChangeListener { + + /** Datasource that stores the actual value. */ + Property<T> dataSource; + + /** + * Construct a new {@code PropertyFormatter} that is not connected to any + * data source. Call {@link #setPropertyDataSource(Property)} later on to + * attach it to a property. + * + */ + protected PropertyFormatter() { + } + + /** + * Construct a new formatter that is connected to given data source. Calls + * {@link #format(Object)} which can be a problem if the formatter has not + * yet been initialized. + * + * @param propertyDataSource + * to connect this property to. + */ + public PropertyFormatter(Property<T> propertyDataSource) { + + setPropertyDataSource(propertyDataSource); + } + + /** + * Gets the current data source of the formatter, if any. + * + * @return the current data source as a Property, or <code>null</code> if + * none defined. + */ + @Override + public Property<T> getPropertyDataSource() { + return dataSource; + } + + /** + * Sets the specified Property as the data source for the formatter. + * + * + * <p> + * Remember that new data sources getValue() must return objects that are + * compatible with parse() and format() methods. + * </p> + * + * @param newDataSource + * the new data source Property. + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + + boolean readOnly = false; + String prevValue = null; + + if (dataSource != null) { + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource) + .removeListener(this); + } + if (dataSource instanceof Property.ReadOnlyStatusChangeListener) { + ((Property.ReadOnlyStatusChangeNotifier) dataSource) + .removeListener(this); + } + readOnly = isReadOnly(); + prevValue = getValue(); + } + + dataSource = newDataSource; + + if (dataSource != null) { + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + if (dataSource instanceof Property.ReadOnlyStatusChangeListener) { + ((Property.ReadOnlyStatusChangeNotifier) dataSource) + .addListener(this); + } + } + + if (isReadOnly() != readOnly) { + fireReadOnlyStatusChange(); + } + String newVal = getValue(); + if ((prevValue == null && newVal != null) + || (prevValue != null && !prevValue.equals(newVal))) { + fireValueChange(); + } + } + + /* Documented in the interface */ + @Override + public Class<String> getType() { + return String.class; + } + + /** + * Get the formatted value. + * + * @return If the datasource returns null, this is null. Otherwise this is + * String given by format(). + */ + @Override + public String getValue() { + T value = dataSource == null ? null : dataSource.getValue(); + if (value == null) { + return null; + } + return format(value); + } + + /** Reflects the read-only status of the datasource. */ + @Override + public boolean isReadOnly() { + return dataSource == null ? false : dataSource.isReadOnly(); + } + + /** + * This method must be implemented to format the values received from + * DataSource. + * + * @param value + * Value object got from the datasource. This is guaranteed to be + * non-null and of the type compatible with getType() of the + * datasource. + * @return + */ + abstract public String format(T value); + + /** + * Parse string and convert it to format compatible with datasource. + * + * The method is required to assure that parse(format(x)) equals x. + * + * @param formattedValue + * This is guaranteed to be non-null string. + * @return Non-null value compatible with datasource. + * @throws Exception + * Any type of exception can be thrown to indicate that the + * conversion was not succesful. + */ + abstract public T parse(String formattedValue) throws Exception; + + /** + * Sets the Property's read-only mode to the specified status. + * + * @param newStatus + * the new read-only status of the Property. + */ + @Override + public void setReadOnly(boolean newStatus) { + if (dataSource != null) { + dataSource.setReadOnly(newStatus); + } + } + + @Override + public void setValue(Object newValue) throws ReadOnlyException { + if (dataSource == null) { + return; + } + if (newValue == null) { + if (dataSource.getValue() != null) { + dataSource.setValue(null); + fireValueChange(); + } + } else { + try { + dataSource.setValue(parse(newValue.toString())); + if (!newValue.equals(getValue())) { + fireValueChange(); + } + } catch (Exception e) { + throw new IllegalArgumentException("Could not parse value", e); + } + } + } + + /** + * Listens for changes in the datasource. + * + * This should not be called directly. + */ + @Override + public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) { + fireValueChange(); + } + + /** + * Listens for changes in the datasource. + * + * This should not be called directly. + */ + @Override + public void readOnlyStatusChange( + com.vaadin.data.Property.ReadOnlyStatusChangeEvent event) { + fireReadOnlyStatusChange(); + } + +} diff --git a/server/src/com/vaadin/data/util/PropertysetItem.java b/server/src/com/vaadin/data/util/PropertysetItem.java new file mode 100644 index 0000000000..22f2da75b2 --- /dev/null +++ b/server/src/com/vaadin/data/util/PropertysetItem.java @@ -0,0 +1,340 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Class for handling a set of identified Properties. The elements contained in + * a </code>MapItem</code> can be referenced using locally unique identifiers. + * The class supports listeners who are interested in changes to the Property + * set managed by the class. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class PropertysetItem implements Item, Item.PropertySetChangeNotifier, + Cloneable { + + /* Private representation of the item */ + + /** + * Mapping from property id to property. + */ + private HashMap<Object, Property<?>> map = new HashMap<Object, Property<?>>(); + + /** + * List of all property ids to maintain the order. + */ + private LinkedList<Object> list = new LinkedList<Object>(); + + /** + * List of property set modification listeners. + */ + private LinkedList<Item.PropertySetChangeListener> propertySetChangeListeners = null; + + /* Item methods */ + + /** + * Gets the Property corresponding to the given Property ID stored in the + * Item. If the Item does not contain the Property, <code>null</code> is + * returned. + * + * @param id + * the identifier of the Property to get. + * @return the Property with the given ID or <code>null</code> + */ + @Override + public Property<?> getItemProperty(Object id) { + return map.get(id); + } + + /** + * Gets the collection of IDs of all Properties stored in the Item. + * + * @return unmodifiable collection containing IDs of the Properties stored + * the Item + */ + @Override + public Collection<?> getItemPropertyIds() { + return Collections.unmodifiableCollection(list); + } + + /* Item.Managed methods */ + + /** + * Removes the Property identified by ID from the Item. This functionality + * is optional. If the method is not implemented, the method always returns + * <code>false</code>. + * + * @param id + * the ID of the Property to be removed. + * @return <code>true</code> if the operation succeeded <code>false</code> + * if not + */ + @Override + public boolean removeItemProperty(Object id) { + + // Cant remove missing properties + if (map.remove(id) == null) { + return false; + } + list.remove(id); + + // Send change events + fireItemPropertySetChange(); + + return true; + } + + /** + * Tries to add a new Property into the Item. + * + * @param id + * the ID of the new Property. + * @param property + * the Property to be added and associated with the id. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean addItemProperty(Object id, Property property) { + + // Null ids are not accepted + if (id == null) { + throw new NullPointerException("Item property id can not be null"); + } + + // Cant add a property twice + if (map.containsKey(id)) { + return false; + } + + // Put the property to map + map.put(id, property); + list.add(id); + + // Send event + fireItemPropertySetChange(); + + return true; + } + + /** + * Gets the <code>String</code> representation of the contents of the Item. + * The format of the string is a space separated catenation of the + * <code>String</code> representations of the Properties contained by the + * Item. + * + * @return <code>String</code> representation of the Item contents + */ + @Override + public String toString() { + String retValue = ""; + + for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) { + final Object propertyId = i.next(); + retValue += getItemProperty(propertyId).getValue(); + if (i.hasNext()) { + retValue += " "; + } + } + + return retValue; + } + + /* Notifiers */ + + /** + * An <code>event</code> object specifying an Item whose Property set has + * changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + private static class PropertySetChangeEvent extends EventObject implements + Item.PropertySetChangeEvent { + + private PropertySetChangeEvent(Item source) { + super(source); + } + + /** + * Gets the Item whose Property set has changed. + * + * @return source object of the event as an <code>Item</code> + */ + @Override + public Item getItem() { + return (Item) getSource(); + } + } + + /** + * Registers a new property set change listener for this Item. + * + * @param listener + * the new Listener to be registered. + */ + @Override + public void addListener(Item.PropertySetChangeListener listener) { + if (propertySetChangeListeners == null) { + propertySetChangeListeners = new LinkedList<PropertySetChangeListener>(); + } + propertySetChangeListeners.add(listener); + } + + /** + * Removes a previously registered property set change listener. + * + * @param listener + * the Listener to be removed. + */ + @Override + public void removeListener(Item.PropertySetChangeListener listener) { + if (propertySetChangeListeners != null) { + propertySetChangeListeners.remove(listener); + } + } + + /** + * Sends a Property set change event to all interested listeners. + */ + private void fireItemPropertySetChange() { + if (propertySetChangeListeners != null) { + final Object[] l = propertySetChangeListeners.toArray(); + final Item.PropertySetChangeEvent event = new PropertysetItem.PropertySetChangeEvent( + this); + for (int i = 0; i < l.length; i++) { + ((Item.PropertySetChangeListener) l[i]) + .itemPropertySetChange(event); + } + } + } + + public Collection<?> getListeners(Class<?> eventType) { + if (Item.PropertySetChangeEvent.class.isAssignableFrom(eventType)) { + if (propertySetChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertySetChangeListeners); + } + } + + return Collections.EMPTY_LIST; + } + + /** + * Creates and returns a copy of this object. + * <p> + * The method <code>clone</code> performs a shallow copy of the + * <code>PropertysetItem</code>. + * </p> + * <p> + * Note : All arrays are considered to implement the interface Cloneable. + * Otherwise, this method creates a new instance of the class of this object + * and initializes all its fields with exactly the contents of the + * corresponding fields of this object, as if by assignment, the contents of + * the fields are not themselves cloned. Thus, this method performs a + * "shallow copy" of this object, not a "deep copy" operation. + * </p> + * + * @throws CloneNotSupportedException + * if the object's class does not support the Cloneable + * interface. + * + * @see java.lang.Object#clone() + */ + @Override + public Object clone() throws CloneNotSupportedException { + + final PropertysetItem npsi = new PropertysetItem(); + + npsi.list = list != null ? (LinkedList<Object>) list.clone() : null; + npsi.propertySetChangeListeners = propertySetChangeListeners != null ? (LinkedList<PropertySetChangeListener>) propertySetChangeListeners + .clone() : null; + npsi.map = (HashMap<Object, Property<?>>) map.clone(); + + return npsi; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof PropertysetItem)) { + return false; + } + + final PropertysetItem other = (PropertysetItem) obj; + + if (other.list != list) { + if (other.list == null) { + return false; + } + if (!other.list.equals(list)) { + return false; + } + } + if (other.map != map) { + if (other.map == null) { + return false; + } + if (!other.map.equals(map)) { + return false; + } + } + if (other.propertySetChangeListeners != propertySetChangeListeners) { + boolean thisEmpty = (propertySetChangeListeners == null || propertySetChangeListeners + .isEmpty()); + boolean otherEmpty = (other.propertySetChangeListeners == null || other.propertySetChangeListeners + .isEmpty()); + if (thisEmpty && otherEmpty) { + return true; + } + if (otherEmpty) { + return false; + } + if (!other.propertySetChangeListeners + .equals(propertySetChangeListeners)) { + return false; + } + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + return (list == null ? 0 : list.hashCode()) + ^ (map == null ? 0 : map.hashCode()) + ^ ((propertySetChangeListeners == null || propertySetChangeListeners + .isEmpty()) ? 0 : propertySetChangeListeners.hashCode()); + } +} diff --git a/server/src/com/vaadin/data/util/QueryContainer.java b/server/src/com/vaadin/data/util/QueryContainer.java new file mode 100644 index 0000000000..dc7c883a7e --- /dev/null +++ b/server/src/com/vaadin/data/util/QueryContainer.java @@ -0,0 +1,675 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * <p> + * The <code>QueryContainer</code> is the specialized form of Container which is + * Ordered and Indexed. This is used to represent the contents of relational + * database tables accessed through the JDBC Connection in the Vaadin Table. + * This creates Items based on the queryStatement provided to the container. + * </p> + * + * <p> + * The <code>QueryContainer</code> can be visualized as a representation of a + * relational database table.Each Item in the container represents the row + * fetched by the query.All cells in a column have same data type and the data + * type information is retrieved from the metadata of the resultset. + * </p> + * + * <p> + * Note : If data in the tables gets modified, Container will not get reflected + * with the updates, we have to explicity invoke QueryContainer.refresh method. + * {@link com.vaadin.data.util.QueryContainer#refresh() refresh()} + * </p> + * + * @see com.vaadin.data.Container + * + * @author Vaadin Ltd. + * @version + * @since 4.0 + * + * @deprecated will be removed in the future, use the SQLContainer add-on + */ + +@Deprecated +@SuppressWarnings("serial") +public class QueryContainer implements Container, Container.Ordered, + Container.Indexed { + + // default ResultSet type + public static final int DEFAULT_RESULTSET_TYPE = ResultSet.TYPE_SCROLL_INSENSITIVE; + + // default ResultSet concurrency + public static final int DEFAULT_RESULTSET_CONCURRENCY = ResultSet.CONCUR_READ_ONLY; + + private int resultSetType = DEFAULT_RESULTSET_TYPE; + + private int resultSetConcurrency = DEFAULT_RESULTSET_CONCURRENCY; + + private final String queryStatement; + + private final Connection connection; + + private ResultSet result; + + private Collection<String> propertyIds; + + private final HashMap<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>(); + + private int size = -1; + + private Statement statement; + + /** + * Constructs new <code>QueryContainer</code> with the specified + * <code>queryStatement</code>. + * + * @param queryStatement + * Database query + * @param connection + * Connection object + * @param resultSetType + * @param resultSetConcurrency + * @throws SQLException + * when database operation fails + */ + public QueryContainer(String queryStatement, Connection connection, + int resultSetType, int resultSetConcurrency) throws SQLException { + this.queryStatement = queryStatement; + this.connection = connection; + this.resultSetType = resultSetType; + this.resultSetConcurrency = resultSetConcurrency; + init(); + } + + /** + * Constructs new <code>QueryContainer</code> with the specified + * queryStatement using the default resultset type and default resultset + * concurrency. + * + * @param queryStatement + * Database query + * @param connection + * Connection object + * @see QueryContainer#DEFAULT_RESULTSET_TYPE + * @see QueryContainer#DEFAULT_RESULTSET_CONCURRENCY + * @throws SQLException + * when database operation fails + */ + public QueryContainer(String queryStatement, Connection connection) + throws SQLException { + this(queryStatement, connection, DEFAULT_RESULTSET_TYPE, + DEFAULT_RESULTSET_CONCURRENCY); + } + + /** + * Fills the Container with the items and properties. Invoked by the + * constructor. + * + * @throws SQLException + * when parameter initialization fails. + * @see QueryContainer#QueryContainer(String, Connection, int, int). + */ + private void init() throws SQLException { + refresh(); + ResultSetMetaData metadata; + metadata = result.getMetaData(); + final int count = metadata.getColumnCount(); + final ArrayList<String> list = new ArrayList<String>(count); + for (int i = 1; i <= count; i++) { + final String columnName = metadata.getColumnName(i); + list.add(columnName); + final Property<?> p = getContainerProperty(new Integer(1), + columnName); + propertyTypes.put(columnName, + p == null ? Object.class : p.getType()); + } + propertyIds = Collections.unmodifiableCollection(list); + } + + /** + * <p> + * Restores items in the container. This method will update the latest data + * to the container. + * </p> + * Note: This method should be used to update the container with the latest + * items. + * + * @throws SQLException + * when database operation fails + * + */ + + public void refresh() throws SQLException { + close(); + statement = connection.createStatement(resultSetType, + resultSetConcurrency); + result = statement.executeQuery(queryStatement); + result.last(); + size = result.getRow(); + } + + /** + * Releases and nullifies the <code>statement</code>. + * + * @throws SQLException + * when database operation fails + */ + + public void close() throws SQLException { + if (statement != null) { + statement.close(); + } + statement = null; + } + + /** + * Gets the Item with the given Item ID from the Container. + * + * @param id + * ID of the Item to retrieve + * @return Item Id. + */ + + @Override + public Item getItem(Object id) { + return new Row(id); + } + + /** + * Gets the collection of propertyId from the Container. + * + * @return Collection of Property ID. + */ + + @Override + public Collection<String> getContainerPropertyIds() { + return propertyIds; + } + + /** + * Gets an collection of all the item IDs in the container. + * + * @return collection of Item IDs + */ + @Override + public Collection<?> getItemIds() { + final Collection<Integer> c = new ArrayList<Integer>(size); + for (int i = 1; i <= size; i++) { + c.add(new Integer(i)); + } + return c; + } + + /** + * Gets the property identified by the given itemId and propertyId from the + * container. If the container does not contain the property + * <code>null</code> is returned. + * + * @param itemId + * ID of the Item which contains the Property + * @param propertyId + * ID of the Property to retrieve + * + * @return Property with the given ID if exists; <code>null</code> + * otherwise. + */ + + @Override + public synchronized Property<?> getContainerProperty(Object itemId, + Object propertyId) { + if (!(itemId instanceof Integer && propertyId instanceof String)) { + return null; + } + Object value; + try { + result.absolute(((Integer) itemId).intValue()); + value = result.getObject((String) propertyId); + } catch (final Exception e) { + return null; + } + + // Handle also null values from the database + return new ObjectProperty<Object>(value != null ? value + : new String("")); + } + + /** + * Gets the data type of all properties identified by the given type ID. + * + * @param id + * ID identifying the Properties + * + * @return data type of the Properties + */ + + @Override + public Class<?> getType(Object id) { + return propertyTypes.get(id); + } + + /** + * Gets the number of items in the container. + * + * @return the number of items in the container. + */ + @Override + public int size() { + return size; + } + + /** + * Tests if the list contains the specified Item. + * + * @param id + * ID the of Item to be tested. + * @return <code>true</code> if given id is in the container; + * <code>false</code> otherwise. + */ + @Override + public boolean containsId(Object id) { + if (!(id instanceof Integer)) { + return false; + } + final int i = ((Integer) id).intValue(); + if (i < 1) { + return false; + } + if (i > size) { + return false; + } + return true; + } + + /** + * Creates new Item with the given ID into the Container. + * + * @param itemId + * ID of the Item to be created. + * + * @return Created new Item, or <code>null</code> if it fails. + * + * @throws UnsupportedOperationException + * if the addItem method is not supported. + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Creates a new Item into the Container, and assign it an ID. + * + * @return ID of the newly created Item, or <code>null</code> if it fails. + * @throws UnsupportedOperationException + * if the addItem method is not supported. + */ + @Override + public Object addItem() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removes the Item identified by ItemId from the Container. + * + * @param itemId + * ID of the Item to remove. + * @return <code>true</code> if the operation succeeded; <code>false</code> + * otherwise. + * @throws UnsupportedOperationException + * if the removeItem method is not supported. + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Adds new Property to all Items in the Container. + * + * @param propertyId + * ID of the Property + * @param type + * Data type of the new Property + * @param defaultValue + * The value all created Properties are initialized to. + * @return <code>true</code> if the operation succeeded; <code>false</code> + * otherwise. + * @throws UnsupportedOperationException + * if the addContainerProperty method is not supported. + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removes a Property specified by the given Property ID from the Container. + * + * @param propertyId + * ID of the Property to remove + * @return <code>true</code> if the operation succeeded; <code>false</code> + * otherwise. + * @throws UnsupportedOperationException + * if the removeContainerProperty method is not supported. + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removes all Items from the Container. + * + * @return <code>true</code> if the operation succeeded; <code>false</code> + * otherwise. + * @throws UnsupportedOperationException + * if the removeAllItems method is not supported. + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Adds new item after the given item. + * + * @param previousItemId + * Id of the previous item in ordered container. + * @param newItemId + * Id of the new item to be added. + * @return Returns new item or <code>null</code> if the operation fails. + * @throws UnsupportedOperationException + * if the addItemAfter method is not supported. + */ + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Adds new item after the given item. + * + * @param previousItemId + * Id of the previous item in ordered container. + * @return Returns item id created new item or <code>null</code> if the + * operation fails. + * @throws UnsupportedOperationException + * if the addItemAfter method is not supported. + */ + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Returns id of first item in the Container. + * + * @return ID of the first Item in the list. + */ + @Override + public Object firstItemId() { + if (size < 1) { + return null; + } + return new Integer(1); + } + + /** + * Returns <code>true</code> if given id is first id at first index. + * + * @param id + * ID of an Item in the Container. + */ + @Override + public boolean isFirstId(Object id) { + return size > 0 && (id instanceof Integer) + && ((Integer) id).intValue() == 1; + } + + /** + * Returns <code>true</code> if given id is last id at last index. + * + * @param id + * ID of an Item in the Container + * + */ + @Override + public boolean isLastId(Object id) { + return size > 0 && (id instanceof Integer) + && ((Integer) id).intValue() == size; + } + + /** + * Returns id of last item in the Container. + * + * @return ID of the last Item. + */ + @Override + public Object lastItemId() { + if (size < 1) { + return null; + } + return new Integer(size); + } + + /** + * Returns id of next item in container at next index. + * + * @param id + * ID of an Item in the Container. + * @return ID of the next Item or null. + */ + @Override + public Object nextItemId(Object id) { + if (size < 1 || !(id instanceof Integer)) { + return null; + } + final int i = ((Integer) id).intValue(); + if (i >= size) { + return null; + } + return new Integer(i + 1); + } + + /** + * Returns id of previous item in container at previous index. + * + * @param id + * ID of an Item in the Container. + * @return ID of the previous Item or null. + */ + @Override + public Object prevItemId(Object id) { + if (size < 1 || !(id instanceof Integer)) { + return null; + } + final int i = ((Integer) id).intValue(); + if (i <= 1) { + return null; + } + return new Integer(i - 1); + } + + /** + * The <code>Row</code> class implements methods of Item. + * + * @author Vaadin Ltd. + * @version + * @since 4.0 + */ + class Row implements Item { + + Object id; + + private Row(Object rowId) { + id = rowId; + } + + /** + * Adds the item property. + * + * @param id + * ID of the new Property. + * @param property + * Property to be added and associated with ID. + * @return <code>true</code> if the operation succeeded; + * <code>false</code> otherwise. + * @throws UnsupportedOperationException + * if the addItemProperty method is not supported. + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Gets the property corresponding to the given property ID stored in + * the Item. + * + * @param propertyId + * identifier of the Property to get + * @return the Property with the given ID or <code>null</code> + */ + @Override + public Property<?> getItemProperty(Object propertyId) { + return getContainerProperty(id, propertyId); + } + + /** + * Gets the collection of property IDs stored in the Item. + * + * @return unmodifiable collection containing IDs of the Properties + * stored the Item. + */ + @Override + public Collection<String> getItemPropertyIds() { + return propertyIds; + } + + /** + * Removes given item property. + * + * @param id + * ID of the Property to be removed. + * @return <code>true</code> if the item property is removed; + * <code>false</code> otherwise. + * @throws UnsupportedOperationException + * if the removeItemProperty is not supported. + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + } + + /** + * Closes the statement. + * + * @see #close() + */ + @Override + public void finalize() { + try { + close(); + } catch (final SQLException ignored) { + + } + } + + /** + * Adds the given item at the position of given index. + * + * @param index + * Index to add the new item. + * @param newItemId + * Id of the new item to be added. + * @return new item or <code>null</code> if the operation fails. + * @throws UnsupportedOperationException + * if the addItemAt is not supported. + */ + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Adds item at the position of provided index in the container. + * + * @param index + * Index to add the new item. + * @return item id created new item or <code>null</code> if the operation + * fails. + * + * @throws UnsupportedOperationException + * if the addItemAt is not supported. + */ + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Gets the Index id in the container. + * + * @param index + * Index Id. + * @return ID in the given index. + */ + @Override + public Object getIdByIndex(int index) { + if (size < 1 || index < 0 || index >= size) { + return null; + } + return new Integer(index + 1); + } + + /** + * Gets the index of the Item corresponding to id in the container. + * + * @param id + * ID of an Item in the Container + * @return index of the Item, or -1 if the Container does not include the + * Item + */ + + @Override + public int indexOfId(Object id) { + if (size < 1 || !(id instanceof Integer)) { + return -1; + } + final int i = ((Integer) id).intValue(); + if (i >= size || i < 1) { + return -1; + } + return i - 1; + } + +} diff --git a/server/src/com/vaadin/data/util/TextFileProperty.java b/server/src/com/vaadin/data/util/TextFileProperty.java new file mode 100644 index 0000000000..598b721a9c --- /dev/null +++ b/server/src/com/vaadin/data/util/TextFileProperty.java @@ -0,0 +1,144 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; + +/** + * Property implementation for wrapping a text file. + * + * Supports reading and writing of a File from/to String. + * + * {@link ValueChangeListener}s are supported, but only fire when + * setValue(Object) is explicitly called. {@link ReadOnlyStatusChangeListener}s + * are supported but only fire when setReadOnly(boolean) is explicitly called. + * + */ +@SuppressWarnings("serial") +public class TextFileProperty extends AbstractProperty<String> { + + private File file; + private Charset charset = null; + + /** + * Wrap given file with property interface. + * + * Setting the file to null works, but getValue() will return null. + * + * @param file + * File to be wrapped. + */ + public TextFileProperty(File file) { + this.file = file; + } + + /** + * Wrap the given file with the property interface and specify character + * set. + * + * Setting the file to null works, but getValue() will return null. + * + * @param file + * File to be wrapped. + * @param charset + * Charset to be used for reading and writing the file. + */ + public TextFileProperty(File file, Charset charset) { + this.file = file; + this.charset = charset; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#getType() + */ + @Override + public Class<String> getType() { + return String.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#getValue() + */ + @Override + public String getValue() { + if (file == null) { + return null; + } + try { + FileInputStream fis = new FileInputStream(file); + InputStreamReader isr = charset == null ? new InputStreamReader(fis) + : new InputStreamReader(fis, charset); + BufferedReader r = new BufferedReader(isr); + StringBuilder b = new StringBuilder(); + char buf[] = new char[8 * 1024]; + int len; + while ((len = r.read(buf)) != -1) { + b.append(buf, 0, len); + } + r.close(); + isr.close(); + fis.close(); + return b.toString(); + } catch (FileNotFoundException e) { + return null; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#isReadOnly() + */ + @Override + public boolean isReadOnly() { + return file == null || super.isReadOnly() || !file.canWrite(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#setValue(java.lang.Object) + */ + @Override + public void setValue(Object newValue) throws ReadOnlyException { + if (isReadOnly()) { + throw new ReadOnlyException(); + } + if (file == null) { + return; + } + + try { + FileOutputStream fos = new FileOutputStream(file); + OutputStreamWriter osw = charset == null ? new OutputStreamWriter( + fos) : new OutputStreamWriter(fos, charset); + BufferedWriter w = new BufferedWriter(osw); + w.append(newValue.toString()); + w.flush(); + w.close(); + osw.close(); + fos.close(); + fireValueChange(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java b/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java new file mode 100644 index 0000000000..d042bfaac2 --- /dev/null +++ b/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java @@ -0,0 +1,114 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeNotifier; + +/** + * Wrapper class that helps implement two-phase commit for a non-transactional + * property. + * + * When accessing the property through the wrapper, getting and setting the + * property value take place immediately. However, the wrapper keeps track of + * the old value of the property so that it can be set for the property in case + * of a roll-back. This can result in the underlying property value changing + * multiple times (first based on modifications made by the application, then + * back upon roll-back). + * + * Value change events on the {@link TransactionalPropertyWrapper} are only + * fired at the end of a successful transaction, whereas listeners attached to + * the underlying property may receive multiple value change events. + * + * @see com.vaadin.data.Property.Transactional + * + * @author Vaadin Ltd + * @version @version@ + * @since 7.0 + * + * @param <T> + */ +public class TransactionalPropertyWrapper<T> extends AbstractProperty<T> + implements ValueChangeNotifier, Property.Transactional<T> { + + private Property<T> wrappedProperty; + private boolean inTransaction = false; + private boolean valueChangePending; + private T valueBeforeTransaction; + + public TransactionalPropertyWrapper(Property<T> wrappedProperty) { + this.wrappedProperty = wrappedProperty; + if (wrappedProperty instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) wrappedProperty) + .addListener(new ValueChangeListener() { + + @Override + public void valueChange(ValueChangeEvent event) { + fireValueChange(); + } + }); + } + } + + @Override + public Class getType() { + return wrappedProperty.getType(); + } + + @Override + public T getValue() { + return wrappedProperty.getValue(); + } + + @Override + public void setValue(Object newValue) throws ReadOnlyException { + // Causes a value change to be sent to this listener which in turn fires + // a new value change event for this property + wrappedProperty.setValue(newValue); + } + + @Override + public void startTransaction() { + inTransaction = true; + valueBeforeTransaction = getValue(); + } + + @Override + public void commit() { + endTransaction(); + } + + @Override + public void rollback() { + try { + wrappedProperty.setValue(valueBeforeTransaction); + } finally { + valueChangePending = false; + endTransaction(); + } + } + + protected void endTransaction() { + inTransaction = false; + valueBeforeTransaction = null; + if (valueChangePending) { + fireValueChange(); + } + } + + @Override + protected void fireValueChange() { + if (inTransaction) { + valueChangePending = true; + } else { + super.fireValueChange(); + } + } + + public Property<T> getWrappedProperty() { + return wrappedProperty; + } + +} diff --git a/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java b/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java new file mode 100644 index 0000000000..ee1e525540 --- /dev/null +++ b/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java @@ -0,0 +1,43 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util; + +import java.io.Serializable; + +import com.vaadin.data.Property; + +/** + * Property descriptor that can create a property instance for a bean. + * + * Used by {@link BeanItem} and {@link AbstractBeanContainer} to keep track of + * the set of properties of items. + * + * @param <BT> + * bean type + * + * @since 6.6 + */ +public interface VaadinPropertyDescriptor<BT> extends Serializable { + /** + * Returns the name of the property. + * + * @return + */ + public String getName(); + + /** + * Returns the type of the property. + * + * @return Class<?> + */ + public Class<?> getPropertyType(); + + /** + * Creates a new {@link Property} instance for this property for a bean. + * + * @param bean + * @return + */ + public Property<?> createProperty(BT bean); +} diff --git a/server/src/com/vaadin/data/util/converter/Converter.java b/server/src/com/vaadin/data/util/converter/Converter.java new file mode 100644 index 0000000000..b8c15e8cdc --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/Converter.java @@ -0,0 +1,159 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.io.Serializable; +import java.util.Locale; + +/** + * Interface that implements conversion between a model and a presentation type. + * <p> + * Typically {@link #convertToPresentation(Object, Locale)} and + * {@link #convertToModel(Object, Locale)} should be symmetric so that chaining + * these together returns the original result for all input but this is not a + * requirement. + * </p> + * <p> + * Converters must not have any side effects (never update UI from inside a + * converter). + * </p> + * <p> + * All Converters must be stateless and thread safe. + * </p> + * <p> + * If conversion of a value fails, a {@link ConversionException} is thrown. + * </p> + * + * @param <MODEL> + * The model type. Must be compatible with what + * {@link #getModelType()} returns. + * @param <PRESENTATION> + * The presentation type. Must be compatible with what + * {@link #getPresentationType()} returns. + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ +public interface Converter<PRESENTATION, MODEL> extends Serializable { + + /** + * Converts the given value from target type to source type. + * <p> + * A converter can optionally use locale to do the conversion. + * </p> + * A converter should in most cases be symmetric so chaining + * {@link #convertToPresentation(Object, Locale)} and + * {@link #convertToModel(Object, Locale)} should return the original value. + * + * @param value + * The value to convert, compatible with the target type. Can be + * null + * @param locale + * The locale to use for conversion. Can be null. + * @return The converted value compatible with the source type + * @throws ConversionException + * If the value could not be converted + */ + public MODEL convertToModel(PRESENTATION value, Locale locale) + throws ConversionException; + + /** + * Converts the given value from source type to target type. + * <p> + * A converter can optionally use locale to do the conversion. + * </p> + * A converter should in most cases be symmetric so chaining + * {@link #convertToPresentation(Object, Locale)} and + * {@link #convertToModel(Object, Locale)} should return the original value. + * + * @param value + * The value to convert, compatible with the target type. Can be + * null + * @param locale + * The locale to use for conversion. Can be null. + * @return The converted value compatible with the source type + * @throws ConversionException + * If the value could not be converted + */ + public PRESENTATION convertToPresentation(MODEL value, Locale locale) + throws ConversionException; + + /** + * The source type of the converter. + * + * Values of this type can be passed to + * {@link #convertToPresentation(Object, Locale)}. + * + * @return The source type + */ + public Class<MODEL> getModelType(); + + /** + * The target type of the converter. + * + * Values of this type can be passed to + * {@link #convertToModel(Object, Locale)}. + * + * @return The target type + */ + public Class<PRESENTATION> getPresentationType(); + + /** + * An exception that signals that the value passed to + * {@link Converter#convertToPresentation(Object, Locale)} or + * {@link Converter#convertToModel(Object, Locale)} could not be converted. + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ + public static class ConversionException extends RuntimeException { + + /** + * Constructs a new <code>ConversionException</code> without a detail + * message. + */ + public ConversionException() { + } + + /** + * Constructs a new <code>ConversionException</code> with the specified + * detail message. + * + * @param msg + * the detail message + */ + public ConversionException(String msg) { + super(msg); + } + + /** + * Constructs a new {@code ConversionException} with the specified + * cause. + * + * @param cause + * The cause of the the exception + */ + public ConversionException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new <code>ConversionException</code> with the specified + * detail message and cause. + * + * @param message + * the detail message + * @param cause + * The cause of the the exception + */ + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/server/src/com/vaadin/data/util/converter/ConverterFactory.java b/server/src/com/vaadin/data/util/converter/ConverterFactory.java new file mode 100644 index 0000000000..ed4ab41ac0 --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/ConverterFactory.java @@ -0,0 +1,23 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.io.Serializable; + +/** + * Factory interface for providing Converters based on a presentation type and a + * model type. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + * + */ +public interface ConverterFactory extends Serializable { + public <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> createConverter( + Class<PRESENTATION> presentationType, Class<MODEL> modelType); + +} diff --git a/server/src/com/vaadin/data/util/converter/ConverterUtil.java b/server/src/com/vaadin/data/util/converter/ConverterUtil.java new file mode 100644 index 0000000000..7011496ed7 --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/ConverterUtil.java @@ -0,0 +1,168 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.converter; + +import java.io.Serializable; +import java.util.Locale; + +import com.vaadin.Application; + +public class ConverterUtil implements Serializable { + + /** + * Finds a converter that can convert from the given presentation type to + * the given model type and back. Uses the given application to find a + * {@link ConverterFactory} or, if application is null, uses the + * {@link Application#getCurrent()}. + * + * @param <PRESENTATIONTYPE> + * The presentation type + * @param <MODELTYPE> + * The model type + * @param presentationType + * The presentation type + * @param modelType + * The model type + * @param application + * The application to use to find a ConverterFactory or null to + * use the current application + * @return A Converter capable of converting between the given types or null + * if no converter was found + */ + public static <PRESENTATIONTYPE, MODELTYPE> Converter<PRESENTATIONTYPE, MODELTYPE> getConverter( + Class<PRESENTATIONTYPE> presentationType, + Class<MODELTYPE> modelType, Application application) { + Converter<PRESENTATIONTYPE, MODELTYPE> converter = null; + if (application == null) { + application = Application.getCurrent(); + } + + if (application != null) { + ConverterFactory factory = application.getConverterFactory(); + converter = factory.createConverter(presentationType, modelType); + } + return converter; + + } + + /** + * Convert the given value from the data source type to the UI type. + * + * @param modelValue + * The model value to convert + * @param presentationType + * The type of the presentation value + * @param converter + * The converter to (try to) use + * @param locale + * The locale to use for conversion + * @param <PRESENTATIONTYPE> + * Presentation type + * + * @return The converted value, compatible with the presentation type, or + * the original value if its type is compatible and no converter is + * set. + * @throws Converter.ConversionException + * if there is no converter and the type is not compatible with + * the model type. + */ + @SuppressWarnings("unchecked") + public static <PRESENTATIONTYPE, MODELTYPE> PRESENTATIONTYPE convertFromModel( + MODELTYPE modelValue, + Class<? extends PRESENTATIONTYPE> presentationType, + Converter<PRESENTATIONTYPE, MODELTYPE> converter, Locale locale) + throws Converter.ConversionException { + if (converter != null) { + return converter.convertToPresentation(modelValue, locale); + } + if (modelValue == null) { + return null; + } + + if (presentationType.isAssignableFrom(modelValue.getClass())) { + return (PRESENTATIONTYPE) modelValue; + } else { + throw new Converter.ConversionException( + "Unable to convert value of type " + + modelValue.getClass().getName() + + " to presentation type " + + presentationType + + ". No converter is set and the types are not compatible."); + } + } + + /** + * @param <MODELTYPE> + * @param <PRESENTATIONTYPE> + * @param presentationValue + * @param modelType + * @param converter + * @param locale + * @return + * @throws Converter.ConversionException + */ + public static <MODELTYPE, PRESENTATIONTYPE> MODELTYPE convertToModel( + PRESENTATIONTYPE presentationValue, Class<MODELTYPE> modelType, + Converter<PRESENTATIONTYPE, MODELTYPE> converter, Locale locale) + throws Converter.ConversionException { + if (converter != null) { + /* + * If there is a converter, always use it. It must convert or throw + * an exception. + */ + return converter.convertToModel(presentationValue, locale); + } + + if (presentationValue == null) { + // Null should always be passed through the converter but if there + // is no converter we can safely return null + return null; + } + + if (modelType == null) { + // No model type, return original value + return (MODELTYPE) presentationValue; + } else if (modelType.isAssignableFrom(presentationValue.getClass())) { + // presentation type directly compatible with model type + return modelType.cast(presentationValue); + } else { + throw new Converter.ConversionException( + "Unable to convert value of type " + + presentationValue.getClass().getName() + + " to model type " + + modelType + + ". No converter is set and the types are not compatible."); + } + + } + + /** + * Checks if the given converter can handle conversion between the given + * presentation and model type + * + * @param converter + * The converter to check + * @param presentationType + * The presentation type + * @param modelType + * The model type + * @return true if the converter supports conversion between the given + * presentation and model type, false otherwise + */ + public static boolean canConverterHandle(Converter<?, ?> converter, + Class<?> presentationType, Class<?> modelType) { + if (converter == null) { + return false; + } + + if (!modelType.isAssignableFrom(converter.getModelType())) { + return false; + } + if (!presentationType.isAssignableFrom(converter.getPresentationType())) { + return false; + } + + return true; + } +} diff --git a/server/src/com/vaadin/data/util/converter/DateToLongConverter.java b/server/src/com/vaadin/data/util/converter/DateToLongConverter.java new file mode 100644 index 0000000000..aeba38aa1f --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/DateToLongConverter.java @@ -0,0 +1,72 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.util.Date; +import java.util.Locale; + +/** + * A converter that converts from {@link Long} to {@link Date} and back. + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class DateToLongConverter implements Converter<Date, Long> { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, + * java.util.Locale) + */ + @Override + public Long convertToModel(Date value, Locale locale) { + if (value == null) { + return null; + } + + return value.getTime(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang + * .Object, java.util.Locale) + */ + @Override + public Date convertToPresentation(Long value, Locale locale) { + if (value == null) { + return null; + } + + return new Date(value); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getModelType() + */ + @Override + public Class<Long> getModelType() { + return Long.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class<Date> getPresentationType() { + return Date.class; + } + +} diff --git a/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java new file mode 100644 index 0000000000..afb95d81ed --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java @@ -0,0 +1,101 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.util.Date; +import java.util.logging.Logger; + +import com.vaadin.Application; + +/** + * Default implementation of {@link ConverterFactory}. Provides converters for + * standard types like {@link String}, {@link Double} and {@link Date}. </p> + * <p> + * Custom converters can be provided by extending this class and using + * {@link Application#setConverterFactory(ConverterFactory)}. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class DefaultConverterFactory implements ConverterFactory { + + private final static Logger log = Logger + .getLogger(DefaultConverterFactory.class.getName()); + + @Override + public <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> createConverter( + Class<PRESENTATION> presentationType, Class<MODEL> modelType) { + Converter<PRESENTATION, MODEL> converter = findConverter( + presentationType, modelType); + if (converter != null) { + log.finest(getClass().getName() + " created a " + + converter.getClass()); + return converter; + } + + // Try to find a reverse converter + Converter<MODEL, PRESENTATION> reverseConverter = findConverter( + modelType, presentationType); + if (reverseConverter != null) { + log.finest(getClass().getName() + " created a reverse " + + reverseConverter.getClass()); + return new ReverseConverter<PRESENTATION, MODEL>(reverseConverter); + } + + log.finest(getClass().getName() + " could not find a converter for " + + presentationType.getName() + " to " + modelType.getName() + + " conversion"); + return null; + + } + + protected <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> findConverter( + Class<PRESENTATION> presentationType, Class<MODEL> modelType) { + if (presentationType == String.class) { + // TextField converters and more + Converter<PRESENTATION, MODEL> converter = (Converter<PRESENTATION, MODEL>) createStringConverter(modelType); + if (converter != null) { + return converter; + } + } else if (presentationType == Date.class) { + // DateField converters and more + Converter<PRESENTATION, MODEL> converter = (Converter<PRESENTATION, MODEL>) createDateConverter(modelType); + if (converter != null) { + return converter; + } + } + + return null; + + } + + protected Converter<Date, ?> createDateConverter(Class<?> sourceType) { + if (Long.class.isAssignableFrom(sourceType)) { + return new DateToLongConverter(); + } else { + return null; + } + } + + protected Converter<String, ?> createStringConverter(Class<?> sourceType) { + if (Double.class.isAssignableFrom(sourceType)) { + return new StringToDoubleConverter(); + } else if (Integer.class.isAssignableFrom(sourceType)) { + return new StringToIntegerConverter(); + } else if (Boolean.class.isAssignableFrom(sourceType)) { + return new StringToBooleanConverter(); + } else if (Number.class.isAssignableFrom(sourceType)) { + return new StringToNumberConverter(); + } else if (Date.class.isAssignableFrom(sourceType)) { + return new StringToDateConverter(); + } else { + return null; + } + } + +} diff --git a/server/src/com/vaadin/data/util/converter/ReverseConverter.java b/server/src/com/vaadin/data/util/converter/ReverseConverter.java new file mode 100644 index 0000000000..fa1bb5daf1 --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/ReverseConverter.java @@ -0,0 +1,84 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.util.Locale; + +/** + * A converter that wraps another {@link Converter} and reverses source and + * target types. + * + * @param <MODEL> + * The source type + * @param <PRESENTATION> + * The target type + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class ReverseConverter<PRESENTATION, MODEL> implements + Converter<PRESENTATION, MODEL> { + + private Converter<MODEL, PRESENTATION> realConverter; + + /** + * Creates a converter from source to target based on a converter that + * converts from target to source. + * + * @param converter + * The converter to use in a reverse fashion + */ + public ReverseConverter(Converter<MODEL, PRESENTATION> converter) { + this.realConverter = converter; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#convertToModel(java + * .lang.Object, java.util.Locale) + */ + @Override + public MODEL convertToModel(PRESENTATION value, Locale locale) + throws com.vaadin.data.util.converter.Converter.ConversionException { + return realConverter.convertToPresentation(value, locale); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang + * .Object, java.util.Locale) + */ + @Override + public PRESENTATION convertToPresentation(MODEL value, Locale locale) + throws com.vaadin.data.util.converter.Converter.ConversionException { + return realConverter.convertToModel(value, locale); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getSourceType() + */ + @Override + public Class<MODEL> getModelType() { + return realConverter.getPresentationType(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getTargetType() + */ + @Override + public Class<PRESENTATION> getPresentationType() { + return realConverter.getModelType(); + } + +} diff --git a/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java new file mode 100644 index 0000000000..999f575dc4 --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java @@ -0,0 +1,108 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.util.Locale; + +/** + * A converter that converts from {@link String} to {@link Boolean} and back. + * The String representation is given by Boolean.toString(). + * <p> + * Leading and trailing white spaces are ignored when converting from a String. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToBooleanConverter implements Converter<String, Boolean> { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, + * java.util.Locale) + */ + @Override + public Boolean convertToModel(String value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + // Remove leading and trailing white space + value = value.trim(); + + if (getTrueString().equals(value)) { + return true; + } else if (getFalseString().equals(value)) { + return false; + } else { + throw new ConversionException("Cannot convert " + value + " to " + + getModelType().getName()); + } + } + + /** + * Gets the string representation for true. Default is "true". + * + * @return the string representation for true + */ + protected String getTrueString() { + return Boolean.TRUE.toString(); + } + + /** + * Gets the string representation for false. Default is "false". + * + * @return the string representation for false + */ + protected String getFalseString() { + return Boolean.FALSE.toString(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang + * .Object, java.util.Locale) + */ + @Override + public String convertToPresentation(Boolean value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + if (value) { + return getTrueString(); + } else { + return getFalseString(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getModelType() + */ + @Override + public Class<Boolean> getModelType() { + return Boolean.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class<String> getPresentationType() { + return String.class; + } + +} diff --git a/server/src/com/vaadin/data/util/converter/StringToDateConverter.java b/server/src/com/vaadin/data/util/converter/StringToDateConverter.java new file mode 100644 index 0000000000..487b02b2aa --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/StringToDateConverter.java @@ -0,0 +1,112 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.text.DateFormat; +import java.text.ParsePosition; +import java.util.Date; +import java.util.Locale; + +/** + * A converter that converts from {@link Date} to {@link String} and back. Uses + * the given locale and {@link DateFormat} for formatting and parsing. + * <p> + * Leading and trailing white spaces are ignored when converting from a String. + * </p> + * <p> + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToDateConverter implements Converter<String, Date> { + + /** + * Returns the format used by {@link #convertToPresentation(Date, Locale)} + * and {@link #convertToModel(String, Locale)}. + * + * @param locale + * The locale to use + * @return A DateFormat instance + */ + protected DateFormat getFormat(Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + + DateFormat f = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, + DateFormat.MEDIUM, locale); + f.setLenient(false); + return f; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, + * java.util.Locale) + */ + @Override + public Date convertToModel(String value, Locale locale) + throws com.vaadin.data.util.converter.Converter.ConversionException { + if (value == null) { + return null; + } + + // Remove leading and trailing white space + value = value.trim(); + + ParsePosition parsePosition = new ParsePosition(0); + Date parsedValue = getFormat(locale).parse(value, parsePosition); + if (parsePosition.getIndex() != value.length()) { + throw new ConversionException("Could not convert '" + value + + "' to " + getModelType().getName()); + } + + return parsedValue; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang + * .Object, java.util.Locale) + */ + @Override + public String convertToPresentation(Date value, Locale locale) + throws com.vaadin.data.util.converter.Converter.ConversionException { + if (value == null) { + return null; + } + + return getFormat(locale).format(value); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getModelType() + */ + @Override + public Class<Date> getModelType() { + return Date.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class<String> getPresentationType() { + return String.class; + } + +} diff --git a/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java b/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java new file mode 100644 index 0000000000..251f91855b --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java @@ -0,0 +1,107 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Locale; + +/** + * A converter that converts from {@link String} to {@link Double} and back. + * Uses the given locale and a {@link NumberFormat} instance for formatting and + * parsing. + * <p> + * Leading and trailing white spaces are ignored when converting from a String. + * </p> + * <p> + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToDoubleConverter implements Converter<String, Double> { + + /** + * Returns the format used by {@link #convertToPresentation(Double, Locale)} + * and {@link #convertToModel(String, Locale)}. + * + * @param locale + * The locale to use + * @return A NumberFormat instance + */ + protected NumberFormat getFormat(Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + + return NumberFormat.getNumberInstance(locale); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, + * java.util.Locale) + */ + @Override + public Double convertToModel(String value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + // Remove leading and trailing white space + value = value.trim(); + + ParsePosition parsePosition = new ParsePosition(0); + Number parsedValue = getFormat(locale).parse(value, parsePosition); + if (parsePosition.getIndex() != value.length()) { + throw new ConversionException("Could not convert '" + value + + "' to " + getModelType().getName()); + } + return parsedValue.doubleValue(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang + * .Object, java.util.Locale) + */ + @Override + public String convertToPresentation(Double value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + return getFormat(locale).format(value); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getModelType() + */ + @Override + public Class<Double> getModelType() { + return Double.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class<String> getPresentationType() { + return String.class; + } +} diff --git a/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java b/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java new file mode 100644 index 0000000000..950f01c6ab --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java @@ -0,0 +1,88 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Locale; + +/** + * A converter that converts from {@link String} to {@link Integer} and back. + * Uses the given locale and a {@link NumberFormat} instance for formatting and + * parsing. + * <p> + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToIntegerConverter implements Converter<String, Integer> { + + /** + * Returns the format used by + * {@link #convertToPresentation(Integer, Locale)} and + * {@link #convertToModel(String, Locale)}. + * + * @param locale + * The locale to use + * @return A NumberFormat instance + */ + protected NumberFormat getFormat(Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + return NumberFormat.getIntegerInstance(locale); + } + + @Override + public Integer convertToModel(String value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + // Remove leading and trailing white space + value = value.trim(); + + // Parse and detect errors. If the full string was not used, it is + // an error. + ParsePosition parsePosition = new ParsePosition(0); + Number parsedValue = getFormat(locale).parse(value, parsePosition); + if (parsePosition.getIndex() != value.length()) { + throw new ConversionException("Could not convert '" + value + + "' to " + getModelType().getName()); + } + + if (parsedValue == null) { + // Convert "" to null + return null; + } + return parsedValue.intValue(); + } + + @Override + public String convertToPresentation(Integer value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + return getFormat(locale).format(value); + } + + @Override + public Class<Integer> getModelType() { + return Integer.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + +} diff --git a/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java new file mode 100644 index 0000000000..42699a326a --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java @@ -0,0 +1,111 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.util.converter; + +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Locale; + +/** + * A converter that converts from {@link Number} to {@link String} and back. + * Uses the given locale and {@link NumberFormat} for formatting and parsing. + * <p> + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + * </p> + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToNumberConverter implements Converter<String, Number> { + + /** + * Returns the format used by {@link #convertToPresentation(Number, Locale)} + * and {@link #convertToModel(String, Locale)}. + * + * @param locale + * The locale to use + * @return A NumberFormat instance + */ + protected NumberFormat getFormat(Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + + return NumberFormat.getNumberInstance(locale); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, + * java.util.Locale) + */ + @Override + public Number convertToModel(String value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + // Remove leading and trailing white space + value = value.trim(); + + // Parse and detect errors. If the full string was not used, it is + // an error. + ParsePosition parsePosition = new ParsePosition(0); + Number parsedValue = getFormat(locale).parse(value, parsePosition); + if (parsePosition.getIndex() != value.length()) { + throw new ConversionException("Could not convert '" + value + + "' to " + getModelType().getName()); + } + + if (parsedValue == null) { + // Convert "" to null + return null; + } + return parsedValue; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang + * .Object, java.util.Locale) + */ + @Override + public String convertToPresentation(Number value, Locale locale) + throws ConversionException { + if (value == null) { + return null; + } + + return getFormat(locale).format(value); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getModelType() + */ + @Override + public Class<Number> getModelType() { + return Number.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class<String> getPresentationType() { + return String.class; + } + +} diff --git a/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java b/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java new file mode 100644 index 0000000000..482b10120c --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java @@ -0,0 +1,76 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.data.Container.Filter; + +/** + * Abstract base class for filters that are composed of multiple sub-filters. + * + * The method {@link #appliesToProperty(Object)} is provided to help + * implementing {@link Filter} for in-memory filters. + * + * @since 6.6 + */ +public abstract class AbstractJunctionFilter implements Filter { + + protected final Collection<Filter> filters; + + public AbstractJunctionFilter(Filter... filters) { + this.filters = Collections.unmodifiableCollection(Arrays + .asList(filters)); + } + + /** + * Returns an unmodifiable collection of the sub-filters of this composite + * filter. + * + * @return + */ + public Collection<Filter> getFilters() { + return filters; + } + + /** + * Returns true if a change in the named property may affect the filtering + * result. If some of the sub-filters are not in-memory filters, true is + * returned. + * + * By default, all sub-filters are iterated to check if any of them applies. + * If there are no sub-filters, false is returned - override in subclasses + * to change this behavior. + */ + @Override + public boolean appliesToProperty(Object propertyId) { + for (Filter filter : getFilters()) { + if (filter.appliesToProperty(propertyId)) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + AbstractJunctionFilter other = (AbstractJunctionFilter) obj; + // contents comparison with equals() + return Arrays.equals(filters.toArray(), other.filters.toArray()); + } + + @Override + public int hashCode() { + int hash = getFilters().size(); + for (Filter filter : filters) { + hash = (hash << 1) ^ filter.hashCode(); + } + return hash; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/filter/And.java b/server/src/com/vaadin/data/util/filter/And.java new file mode 100644 index 0000000000..ca6c35aba7 --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/And.java @@ -0,0 +1,44 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +/** + * A compound {@link Filter} that accepts an item if all of its filters accept + * the item. + * + * If no filters are given, the filter should accept all items. + * + * This filter also directly supports in-memory filtering when all sub-filters + * do so. + * + * @see Or + * + * @since 6.6 + */ +public final class And extends AbstractJunctionFilter { + + /** + * + * @param filters + * filters of which the And filter will be composed + */ + public And(Filter... filters) { + super(filters); + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedFilterException { + for (Filter filter : getFilters()) { + if (!filter.passesFilter(itemId, item)) { + return false; + } + } + return true; + } + +} diff --git a/server/src/com/vaadin/data/util/filter/Between.java b/server/src/com/vaadin/data/util/filter/Between.java new file mode 100644 index 0000000000..b00a74d13d --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/Between.java @@ -0,0 +1,74 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +public class Between implements Filter { + + private final Object propertyId; + private final Comparable startValue; + private final Comparable endValue; + + public Between(Object propertyId, Comparable startValue, Comparable endValue) { + this.propertyId = propertyId; + this.startValue = startValue; + this.endValue = endValue; + } + + public Object getPropertyId() { + return propertyId; + } + + public Comparable<?> getStartValue() { + return startValue; + } + + public Comparable<?> getEndValue() { + return endValue; + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + Object value = item.getItemProperty(getPropertyId()).getValue(); + if (value instanceof Comparable) { + Comparable cval = (Comparable) value; + return cval.compareTo(getStartValue()) >= 0 + && cval.compareTo(getEndValue()) <= 0; + } + return false; + } + + @Override + public boolean appliesToProperty(Object propertyId) { + return getPropertyId() != null && getPropertyId().equals(propertyId); + } + + @Override + public int hashCode() { + return getPropertyId().hashCode() + getStartValue().hashCode() + + getEndValue().hashCode(); + } + + @Override + public boolean equals(Object obj) { + // Only objects of the same class can be equal + if (!getClass().equals(obj.getClass())) { + return false; + } + final Between o = (Between) obj; + + // Checks the properties one by one + boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId() + .equals(o.getPropertyId()) : null == o.getPropertyId(); + boolean startValueEqual = (null != getStartValue()) ? getStartValue() + .equals(o.getStartValue()) : null == o.getStartValue(); + boolean endValueEqual = (null != getEndValue()) ? getEndValue().equals( + o.getEndValue()) : null == o.getEndValue(); + return propertyIdEqual && startValueEqual && endValueEqual; + + } +} diff --git a/server/src/com/vaadin/data/util/filter/Compare.java b/server/src/com/vaadin/data/util/filter/Compare.java new file mode 100644 index 0000000000..4091f5b922 --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/Compare.java @@ -0,0 +1,327 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Simple container filter comparing an item property value against a given + * constant value. Use the nested classes {@link Equal}, {@link Greater}, + * {@link Less}, {@link GreaterOrEqual} and {@link LessOrEqual} instead of this + * class directly. + * + * This filter also directly supports in-memory filtering. + * + * The reference and actual values must implement {@link Comparable} and the + * class of the actual property value must be assignable from the class of the + * reference value. + * + * @since 6.6 + */ +public abstract class Compare implements Filter { + + public enum Operation { + EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL + }; + + private final Object propertyId; + private final Operation operation; + private final Object value; + + /** + * A {@link Compare} filter that accepts items for which the identified + * property value is equal to <code>value</code>. + * + * For in-memory filters, equals() is used for the comparison. For other + * containers, the comparison implementation is container dependent and may + * use e.g. database comparison operations. + * + * @since 6.6 + */ + public static final class Equal extends Compare { + /** + * Construct a filter that accepts items for which the identified + * property value is equal to <code>value</code>. + * + * For in-memory filters, equals() is used for the comparison. For other + * containers, the comparison implementation is container dependent and + * may use e.g. database comparison operations. + * + * @param propertyId + * the identifier of the property whose value to compare + * against value, not null + * @param value + * the value to compare against - null values may or may not + * be supported depending on the container + */ + public Equal(Object propertyId, Object value) { + super(propertyId, value, Operation.EQUAL); + } + } + + /** + * A {@link Compare} filter that accepts items for which the identified + * property value is greater than <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} and + * {@link Comparable#compareTo(Object)} is used for the comparison. For + * other containers, the comparison implementation is container dependent + * and may use e.g. database comparison operations. + * + * @since 6.6 + */ + public static final class Greater extends Compare { + /** + * Construct a filter that accepts items for which the identified + * property value is greater than <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} + * and {@link Comparable#compareTo(Object)} is used for the comparison. + * For other containers, the comparison implementation is container + * dependent and may use e.g. database comparison operations. + * + * @param propertyId + * the identifier of the property whose value to compare + * against value, not null + * @param value + * the value to compare against - null values may or may not + * be supported depending on the container + */ + public Greater(Object propertyId, Object value) { + super(propertyId, value, Operation.GREATER); + } + } + + /** + * A {@link Compare} filter that accepts items for which the identified + * property value is less than <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} and + * {@link Comparable#compareTo(Object)} is used for the comparison. For + * other containers, the comparison implementation is container dependent + * and may use e.g. database comparison operations. + * + * @since 6.6 + */ + public static final class Less extends Compare { + /** + * Construct a filter that accepts items for which the identified + * property value is less than <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} + * and {@link Comparable#compareTo(Object)} is used for the comparison. + * For other containers, the comparison implementation is container + * dependent and may use e.g. database comparison operations. + * + * @param propertyId + * the identifier of the property whose value to compare + * against value, not null + * @param value + * the value to compare against - null values may or may not + * be supported depending on the container + */ + public Less(Object propertyId, Object value) { + super(propertyId, value, Operation.LESS); + } + } + + /** + * A {@link Compare} filter that accepts items for which the identified + * property value is greater than or equal to <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} and + * {@link Comparable#compareTo(Object)} is used for the comparison. For + * other containers, the comparison implementation is container dependent + * and may use e.g. database comparison operations. + * + * @since 6.6 + */ + public static final class GreaterOrEqual extends Compare { + /** + * Construct a filter that accepts items for which the identified + * property value is greater than or equal to <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} + * and {@link Comparable#compareTo(Object)} is used for the comparison. + * For other containers, the comparison implementation is container + * dependent and may use e.g. database comparison operations. + * + * @param propertyId + * the identifier of the property whose value to compare + * against value, not null + * @param value + * the value to compare against - null values may or may not + * be supported depending on the container + */ + public GreaterOrEqual(Object propertyId, Object value) { + super(propertyId, value, Operation.GREATER_OR_EQUAL); + } + } + + /** + * A {@link Compare} filter that accepts items for which the identified + * property value is less than or equal to <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} and + * {@link Comparable#compareTo(Object)} is used for the comparison. For + * other containers, the comparison implementation is container dependent + * and may use e.g. database comparison operations. + * + * @since 6.6 + */ + public static final class LessOrEqual extends Compare { + /** + * Construct a filter that accepts items for which the identified + * property value is less than or equal to <code>value</code>. + * + * For in-memory filters, the values must implement {@link Comparable} + * and {@link Comparable#compareTo(Object)} is used for the comparison. + * For other containers, the comparison implementation is container + * dependent and may use e.g. database comparison operations. + * + * @param propertyId + * the identifier of the property whose value to compare + * against value, not null + * @param value + * the value to compare against - null values may or may not + * be supported depending on the container + */ + public LessOrEqual(Object propertyId, Object value) { + super(propertyId, value, Operation.LESS_OR_EQUAL); + } + } + + /** + * Constructor for a {@link Compare} filter that compares the value of an + * item property with the given constant <code>value</code>. + * + * This constructor is intended to be used by the nested static classes only + * ({@link Equal}, {@link Greater}, {@link Less}, {@link GreaterOrEqual}, + * {@link LessOrEqual}). + * + * For in-memory filtering, comparisons except EQUAL require that the values + * implement {@link Comparable} and {@link Comparable#compareTo(Object)} is + * used for the comparison. The equality comparison is performed using + * {@link Object#equals(Object)}. + * + * For other containers, the comparison implementation is container + * dependent and may use e.g. database comparison operations. Therefore, the + * behavior of comparisons might differ in some cases between in-memory and + * other containers. + * + * @param propertyId + * the identifier of the property whose value to compare against + * value, not null + * @param value + * the value to compare against - null values may or may not be + * supported depending on the container + * @param operation + * the comparison {@link Operation} to use + */ + Compare(Object propertyId, Object value, Operation operation) { + this.propertyId = propertyId; + this.value = value; + this.operation = operation; + } + + @Override + public boolean passesFilter(Object itemId, Item item) { + final Property<?> p = item.getItemProperty(getPropertyId()); + if (null == p) { + return false; + } + Object value = p.getValue(); + switch (getOperation()) { + case EQUAL: + return (null == this.value) ? (null == value) : this.value + .equals(value); + case GREATER: + return compareValue(value) > 0; + case LESS: + return compareValue(value) < 0; + case GREATER_OR_EQUAL: + return compareValue(value) >= 0; + case LESS_OR_EQUAL: + return compareValue(value) <= 0; + } + // all cases should have been processed above + return false; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected int compareValue(Object value1) { + if (null == value) { + return null == value1 ? 0 : -1; + } else if (null == value1) { + return 1; + } else if (getValue() instanceof Comparable + && value1.getClass().isAssignableFrom(getValue().getClass())) { + return -((Comparable) getValue()).compareTo(value1); + } + throw new IllegalArgumentException("Could not compare the arguments: " + + value1 + ", " + getValue()); + } + + @Override + public boolean appliesToProperty(Object propertyId) { + return getPropertyId().equals(propertyId); + } + + @Override + public boolean equals(Object obj) { + + // Only objects of the same class can be equal + if (!getClass().equals(obj.getClass())) { + return false; + } + final Compare o = (Compare) obj; + + // Checks the properties one by one + if (getPropertyId() != o.getPropertyId() && null != o.getPropertyId() + && !o.getPropertyId().equals(getPropertyId())) { + return false; + } + if (getOperation() != o.getOperation()) { + return false; + } + return (null == getValue()) ? null == o.getValue() : getValue().equals( + o.getValue()); + } + + @Override + public int hashCode() { + return (null != getPropertyId() ? getPropertyId().hashCode() : 0) + ^ (null != getValue() ? getValue().hashCode() : 0); + } + + /** + * Returns the property id of the property to compare against the fixed + * value. + * + * @return property id (not null) + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * Returns the comparison operation. + * + * @return {@link Operation} + */ + public Operation getOperation() { + return operation; + } + + /** + * Returns the value to compare the property against. + * + * @return comparison reference value + */ + public Object getValue() { + return value; + } +} diff --git a/server/src/com/vaadin/data/util/filter/IsNull.java b/server/src/com/vaadin/data/util/filter/IsNull.java new file mode 100644 index 0000000000..3faf4153ee --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/IsNull.java @@ -0,0 +1,79 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Simple container filter checking whether an item property value is null. + * + * This filter also directly supports in-memory filtering. + * + * @since 6.6 + */ +public final class IsNull implements Filter { + + private final Object propertyId; + + /** + * Constructor for a filter that compares the value of an item property with + * null. + * + * For in-memory filtering, a simple == check is performed. For other + * containers, the comparison implementation is container dependent but + * should correspond to the in-memory null check. + * + * @param propertyId + * the identifier (not null) of the property whose value to check + */ + public IsNull(Object propertyId) { + this.propertyId = propertyId; + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + final Property<?> p = item.getItemProperty(getPropertyId()); + if (null == p) { + return false; + } + return null == p.getValue(); + } + + @Override + public boolean appliesToProperty(Object propertyId) { + return getPropertyId().equals(propertyId); + } + + @Override + public boolean equals(Object obj) { + // Only objects of the same class can be equal + if (!getClass().equals(obj.getClass())) { + return false; + } + final IsNull o = (IsNull) obj; + + // Checks the properties one by one + return (null != getPropertyId()) ? getPropertyId().equals( + o.getPropertyId()) : null == o.getPropertyId(); + } + + @Override + public int hashCode() { + return (null != getPropertyId() ? getPropertyId().hashCode() : 0); + } + + /** + * Returns the property id of the property tested by the filter, not null + * for valid filters. + * + * @return property id (not null) + */ + public Object getPropertyId() { + return propertyId; + } + +} diff --git a/server/src/com/vaadin/data/util/filter/Like.java b/server/src/com/vaadin/data/util/filter/Like.java new file mode 100644 index 0000000000..3dcc48e809 --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/Like.java @@ -0,0 +1,83 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +public class Like implements Filter { + private final Object propertyId; + private final String value; + private boolean caseSensitive; + + public Like(String propertyId, String value) { + this(propertyId, value, true); + } + + public Like(String propertyId, String value, boolean caseSensitive) { + this.propertyId = propertyId; + this.value = value; + setCaseSensitive(caseSensitive); + } + + public Object getPropertyId() { + return propertyId; + } + + public String getValue() { + return value; + } + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + if (!item.getItemProperty(getPropertyId()).getType() + .isAssignableFrom(String.class)) { + // We can only handle strings + return false; + } + String colValue = (String) item.getItemProperty(getPropertyId()) + .getValue(); + + String pattern = getValue().replace("%", ".*"); + if (isCaseSensitive()) { + return colValue.matches(pattern); + } + return colValue.toUpperCase().matches(pattern.toUpperCase()); + } + + @Override + public boolean appliesToProperty(Object propertyId) { + return getPropertyId() != null && getPropertyId().equals(propertyId); + } + + @Override + public int hashCode() { + return getPropertyId().hashCode() + getValue().hashCode(); + } + + @Override + public boolean equals(Object obj) { + // Only objects of the same class can be equal + if (!getClass().equals(obj.getClass())) { + return false; + } + final Like o = (Like) obj; + + // Checks the properties one by one + boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId() + .equals(o.getPropertyId()) : null == o.getPropertyId(); + boolean valueEqual = (null != getValue()) ? getValue().equals( + o.getValue()) : null == o.getValue(); + return propertyIdEqual && valueEqual; + } +} diff --git a/server/src/com/vaadin/data/util/filter/Not.java b/server/src/com/vaadin/data/util/filter/Not.java new file mode 100644 index 0000000000..bbfc9ca86a --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/Not.java @@ -0,0 +1,70 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +/** + * Negating filter that accepts the items rejected by another filter. + * + * This filter directly supports in-memory filtering when the negated filter + * does so. + * + * @since 6.6 + */ +public final class Not implements Filter { + private final Filter filter; + + /** + * Constructs a filter that negates a filter. + * + * @param filter + * {@link Filter} to negate, not-null + */ + public Not(Filter filter) { + this.filter = filter; + } + + /** + * Returns the negated filter. + * + * @return Filter + */ + public Filter getFilter() { + return filter; + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + return !filter.passesFilter(itemId, item); + } + + /** + * Returns true if a change in the named property may affect the filtering + * result. Return value is the same as {@link #appliesToProperty(Object)} + * for the negated filter. + * + * @return boolean + */ + @Override + public boolean appliesToProperty(Object propertyId) { + return filter.appliesToProperty(propertyId); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + return filter.equals(((Not) obj).getFilter()); + } + + @Override + public int hashCode() { + return filter.hashCode(); + } + +} diff --git a/server/src/com/vaadin/data/util/filter/Or.java b/server/src/com/vaadin/data/util/filter/Or.java new file mode 100644 index 0000000000..b60074f7e3 --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/Or.java @@ -0,0 +1,63 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +/** + * A compound {@link Filter} that accepts an item if any of its filters accept + * the item. + * + * If no filters are given, the filter should reject all items. + * + * This filter also directly supports in-memory filtering when all sub-filters + * do so. + * + * @see And + * + * @since 6.6 + */ +public final class Or extends AbstractJunctionFilter { + + /** + * + * @param filters + * filters of which the Or filter will be composed + */ + public Or(Filter... filters) { + super(filters); + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedFilterException { + for (Filter filter : getFilters()) { + if (filter.passesFilter(itemId, item)) { + return true; + } + } + return false; + } + + /** + * Returns true if a change in the named property may affect the filtering + * result. If some of the sub-filters are not in-memory filters, true is + * returned. + * + * By default, all sub-filters are iterated to check if any of them applies. + * If there are no sub-filters, true is returned as an empty Or rejects all + * items. + */ + @Override + public boolean appliesToProperty(Object propertyId) { + if (getFilters().isEmpty()) { + // empty Or filters out everything + return true; + } else { + return super.appliesToProperty(propertyId); + } + } + +} diff --git a/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java b/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java new file mode 100644 index 0000000000..f98b2c02b4 --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java @@ -0,0 +1,152 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Simple string filter for matching items that start with or contain a + * specified string. The matching can be case-sensitive or case-insensitive. + * + * This filter also directly supports in-memory filtering. When performing + * in-memory filtering, values of other types are converted using toString(), + * but other (lazy container) implementations do not need to perform such + * conversions and might not support values of different types. + * + * Note that this filter is modeled after the pre-6.6 filtering mechanisms, and + * might not be very efficient e.g. for database filtering. + * + * TODO this might still change + * + * @since 6.6 + */ +public final class SimpleStringFilter implements Filter { + + final Object propertyId; + final String filterString; + final boolean ignoreCase; + final boolean onlyMatchPrefix; + + public SimpleStringFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + this.propertyId = propertyId; + this.filterString = ignoreCase ? filterString.toLowerCase() + : filterString; + this.ignoreCase = ignoreCase; + this.onlyMatchPrefix = onlyMatchPrefix; + } + + @Override + public boolean passesFilter(Object itemId, Item item) { + final Property<?> p = item.getItemProperty(propertyId); + if (p == null) { + return false; + } + Object propertyValue = p.getValue(); + if (propertyValue == null) { + return false; + } + final String value = ignoreCase ? propertyValue.toString() + .toLowerCase() : propertyValue.toString(); + if (onlyMatchPrefix) { + if (!value.startsWith(filterString)) { + return false; + } + } else { + if (!value.contains(filterString)) { + return false; + } + } + return true; + } + + @Override + public boolean appliesToProperty(Object propertyId) { + return this.propertyId.equals(propertyId); + } + + @Override + public boolean equals(Object obj) { + + // Only ones of the objects of the same class can be equal + if (!(obj instanceof SimpleStringFilter)) { + return false; + } + final SimpleStringFilter o = (SimpleStringFilter) obj; + + // Checks the properties one by one + if (propertyId != o.propertyId && o.propertyId != null + && !o.propertyId.equals(propertyId)) { + return false; + } + if (filterString != o.filterString && o.filterString != null + && !o.filterString.equals(filterString)) { + return false; + } + if (ignoreCase != o.ignoreCase) { + return false; + } + if (onlyMatchPrefix != o.onlyMatchPrefix) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return (propertyId != null ? propertyId.hashCode() : 0) + ^ (filterString != null ? filterString.hashCode() : 0); + } + + /** + * Returns the property identifier to which this filter applies. + * + * @return property id + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * Returns the filter string. + * + * Note: this method is intended only for implementations of lazy string + * filters and may change in the future. + * + * @return filter string given to the constructor + */ + public String getFilterString() { + return filterString; + } + + /** + * Returns whether the filter is case-insensitive or case-sensitive. + * + * Note: this method is intended only for implementations of lazy string + * filters and may change in the future. + * + * @return true if performing case-insensitive filtering, false for + * case-sensitive + */ + public boolean isIgnoreCase() { + return ignoreCase; + } + + /** + * Returns true if the filter only applies to the beginning of the value + * string, false for any location in the value. + * + * Note: this method is intended only for implementations of lazy string + * filters and may change in the future. + * + * @return true if checking for matches at the beginning of the value only, + * false if matching any part of value + */ + public boolean isOnlyMatchPrefix() { + return onlyMatchPrefix; + } +} diff --git a/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java b/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java new file mode 100644 index 0000000000..c09cc474e9 --- /dev/null +++ b/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java @@ -0,0 +1,35 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.filter; + +import java.io.Serializable; + +/** + * Exception for cases where a container does not support a specific type of + * filters. + * + * If possible, this should be thrown already when adding a filter to a + * container. If a problem is not detected at that point, an + * {@link UnsupportedOperationException} can be throws when attempting to + * perform filtering. + * + * @since 6.6 + */ +public class UnsupportedFilterException extends RuntimeException implements + Serializable { + public UnsupportedFilterException() { + } + + public UnsupportedFilterException(String message) { + super(message); + } + + public UnsupportedFilterException(Exception cause) { + super(cause); + } + + public UnsupportedFilterException(String message, Exception cause) { + super(message, cause); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/package.html b/server/src/com/vaadin/data/util/package.html new file mode 100644 index 0000000000..07e3acde9e --- /dev/null +++ b/server/src/com/vaadin/data/util/package.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<p>Provides implementations of Property, Item and Container +interfaces, and utilities for the data layer.</p> + +<p>Various Property, Item and Container implementations are provided +in this package. Each implementation can have its own sets of +constraints on the data it encapsulates and on how the implementation +can be used. See the class javadocs for more information.</p> + +</body> +</html> diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java new file mode 100644 index 0000000000..788966048d --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java @@ -0,0 +1,92 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.data.util.sqlcontainer.query.FreeformQuery; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; + +/** + * CacheFlushNotifier is a simple static notification mechanism to inform other + * SQLContainers that the contents of their caches may have become stale. + */ +class CacheFlushNotifier implements Serializable { + /* + * SQLContainer instance reference list and dead reference queue. Used for + * the cache flush notification feature. + */ + private static List<WeakReference<SQLContainer>> allInstances = new ArrayList<WeakReference<SQLContainer>>(); + private static ReferenceQueue<SQLContainer> deadInstances = new ReferenceQueue<SQLContainer>(); + + /** + * Adds the given SQLContainer to the cache flush notification receiver list + * + * @param c + * Container to add + */ + public static void addInstance(SQLContainer c) { + removeDeadReferences(); + if (c != null) { + allInstances.add(new WeakReference<SQLContainer>(c, deadInstances)); + } + } + + /** + * Removes dead references from instance list + */ + private static void removeDeadReferences() { + java.lang.ref.Reference<? extends SQLContainer> dead = deadInstances + .poll(); + while (dead != null) { + allInstances.remove(dead); + dead = deadInstances.poll(); + } + } + + /** + * Iterates through the instances and notifies containers which are + * connected to the same table or are using the same query string. + * + * @param c + * SQLContainer that issued the cache flush notification + */ + public static void notifyOfCacheFlush(SQLContainer c) { + removeDeadReferences(); + for (WeakReference<SQLContainer> wr : allInstances) { + if (wr.get() != null) { + SQLContainer wrc = wr.get(); + if (wrc == null) { + continue; + } + /* + * If the reference points to the container sending the + * notification, do nothing. + */ + if (wrc.equals(c)) { + continue; + } + /* Compare QueryDelegate types and tableName/queryString */ + QueryDelegate wrQd = wrc.getQueryDelegate(); + QueryDelegate qd = c.getQueryDelegate(); + if (wrQd instanceof TableQuery + && qd instanceof TableQuery + && ((TableQuery) wrQd).getTableName().equals( + ((TableQuery) qd).getTableName())) { + wrc.refresh(); + } else if (wrQd instanceof FreeformQuery + && qd instanceof FreeformQuery + && ((FreeformQuery) wrQd).getQueryString().equals( + ((FreeformQuery) qd).getQueryString())) { + wrc.refresh(); + } + } + } + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java new file mode 100644 index 0000000000..839fceb3c2 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * CacheMap extends LinkedHashMap, adding the possibility to adjust maximum + * number of items. In SQLContainer this is used for RowItem -cache. Cache size + * will be two times the page length parameter of the container. + */ +class CacheMap<K, V> extends LinkedHashMap<K, V> { + private static final long serialVersionUID = 679999766473555231L; + private int cacheLimit = SQLContainer.CACHE_RATIO + * SQLContainer.DEFAULT_PAGE_LENGTH; + + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + return size() > cacheLimit; + } + + void setCacheLimit(int limit) { + cacheLimit = limit > 0 ? limit : SQLContainer.DEFAULT_PAGE_LENGTH; + } + + int getCacheLimit() { + return cacheLimit; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java new file mode 100644 index 0000000000..168bce1880 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java @@ -0,0 +1,248 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; + +import com.vaadin.data.Property; + +/** + * ColumnProperty represents the value of one column in a RowItem. In addition + * to the value, ColumnProperty also contains some basic column attributes such + * as nullability status, read-only status and data type. + * + * Note that depending on the QueryDelegate in use this does not necessarily map + * into an actual column in a database table. + */ +final public class ColumnProperty implements Property { + private static final long serialVersionUID = -3694463129581802457L; + + private RowItem owner; + + private String propertyId; + + private boolean readOnly; + private boolean allowReadOnlyChange = true; + private boolean nullable = true; + + private Object value; + private Object changedValue; + private Class<?> type; + + private boolean modified; + + private boolean versionColumn; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private ColumnProperty() { + } + + public ColumnProperty(String propertyId, boolean readOnly, + boolean allowReadOnlyChange, boolean nullable, Object value, + Class<?> type) { + if (propertyId == null) { + throw new IllegalArgumentException("Properties must be named."); + } + if (type == null) { + throw new IllegalArgumentException("Property type must be set."); + } + this.propertyId = propertyId; + this.type = type; + this.value = value; + + this.allowReadOnlyChange = allowReadOnlyChange; + this.nullable = nullable; + this.readOnly = readOnly; + } + + @Override + public Object getValue() { + if (isModified()) { + return changedValue; + } + return value; + } + + @Override + public void setValue(Object newValue) throws ReadOnlyException { + if (newValue == null && !nullable) { + throw new NotNullableException( + "Null values are not allowed for this property."); + } + if (readOnly) { + throw new ReadOnlyException( + "Cannot set value for read-only property."); + } + + /* Check if this property is a date property. */ + boolean isDateProperty = Time.class.equals(getType()) + || Date.class.equals(getType()) + || Timestamp.class.equals(getType()); + + if (newValue != null) { + /* Handle SQL dates, times and Timestamps given as java.util.Date */ + if (isDateProperty) { + /* + * Try to get the millisecond value from the new value of this + * property. Possible type to convert from is java.util.Date. + */ + long millis = 0; + if (newValue instanceof java.util.Date) { + millis = ((java.util.Date) newValue).getTime(); + /* + * Create the new object based on the millisecond value, + * according to the type of this property. + */ + if (Time.class.equals(getType())) { + newValue = new Time(millis); + } else if (Date.class.equals(getType())) { + newValue = new Date(millis); + } else if (Timestamp.class.equals(getType())) { + newValue = new Timestamp(millis); + } + } + } + + if (!getType().isAssignableFrom(newValue.getClass())) { + throw new IllegalArgumentException( + "Illegal value type for ColumnProperty"); + } + + /* + * If the value to be set is the same that has already been set, do + * not set it again. + */ + if (isValueAlreadySet(newValue)) { + return; + } + } + + /* Set the new value and notify container of the change. */ + changedValue = newValue; + modified = true; + owner.getContainer().itemChangeNotification(owner); + } + + private boolean isValueAlreadySet(Object newValue) { + Object referenceValue = isModified() ? changedValue : value; + + return (isNullable() && newValue == null && referenceValue == null) + || newValue.equals(referenceValue); + } + + @Override + public Class<?> getType() { + return type; + } + + @Override + public boolean isReadOnly() { + return readOnly; + } + + public boolean isReadOnlyChangeAllowed() { + return allowReadOnlyChange; + } + + @Override + public void setReadOnly(boolean newStatus) { + if (allowReadOnlyChange) { + readOnly = newStatus; + } + } + + public String getPropertyId() { + return propertyId; + } + + /** + * Returns the value of the Property in human readable textual format. + * + * @see java.lang.Object#toString() + * @deprecated get the string representation from the value + */ + @Deprecated + @Override + public String toString() { + throw new UnsupportedOperationException( + "Use ColumnProperty.getValue() instead of ColumnProperty.toString()"); + } + + public void setOwner(RowItem owner) { + if (owner == null) { + throw new IllegalArgumentException("Owner can not be set to null."); + } + if (this.owner != null) { + throw new IllegalStateException( + "ColumnProperties can only be bound once."); + } + this.owner = owner; + } + + public boolean isModified() { + return modified; + } + + public boolean isVersionColumn() { + return versionColumn; + } + + public void setVersionColumn(boolean versionColumn) { + this.versionColumn = versionColumn; + } + + public boolean isNullable() { + return nullable; + } + + /** + * An exception that signals that a <code>null</code> value was passed to + * the <code>setValue</code> method, but the value of this property can not + * be set to <code>null</code>. + */ + @SuppressWarnings("serial") + public class NotNullableException extends RuntimeException { + + /** + * Constructs a new <code>NotNullableException</code> without a detail + * message. + */ + public NotNullableException() { + } + + /** + * Constructs a new <code>NotNullableException</code> with the specified + * detail message. + * + * @param msg + * the detail message + */ + public NotNullableException(String msg) { + super(msg); + } + + /** + * Constructs a new <code>NotNullableException</code> from another + * exception. + * + * @param cause + * The cause of the failure + */ + public NotNullableException(Throwable cause) { + super(cause); + } + } + + public void commit() { + if (isModified()) { + modified = false; + value = changedValue; + } + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java new file mode 100644 index 0000000000..adfd439ac8 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import com.vaadin.data.util.sqlcontainer.query.TableQuery; + +/** + * An OptimisticLockException is thrown when trying to update or delete a row + * that has been changed since last read from the database. + * + * OptimisticLockException is a runtime exception because optimistic locking is + * turned off by default, and as such will never be thrown in a default + * configuration. In order to turn on optimistic locking, you need to specify + * the version column in your TableQuery instance. + * + * @see TableQuery#setVersionColumn(String) + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public class OptimisticLockException extends RuntimeException { + + private final RowId rowId; + + public OptimisticLockException(RowId rowId) { + super(); + this.rowId = rowId; + } + + public OptimisticLockException(String msg, RowId rowId) { + super(msg); + this.rowId = rowId; + } + + public RowId getRowId() { + return rowId; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java new file mode 100644 index 0000000000..c73ffce63a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +public class ReadOnlyRowId extends RowId { + private static final long serialVersionUID = -2626764781642012467L; + private final Integer rowNum; + + public ReadOnlyRowId(int rowNum) { + super(); + this.rowNum = rowNum; + } + + @Override + public int hashCode() { + return rowNum.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ReadOnlyRowId)) { + return false; + } + return rowNum.equals(((ReadOnlyRowId) obj).rowNum); + } + + public int getRowNum() { + return rowNum; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/Reference.java b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java new file mode 100644 index 0000000000..dea1aa87c0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java @@ -0,0 +1,56 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; + +/** + * The reference class represents a simple [usually foreign key] reference to + * another SQLContainer. Actual foreign key reference in the database is not + * required, but it is recommended to make sure that certain constraints are + * followed. + */ +@SuppressWarnings("serial") +class Reference implements Serializable { + + /** + * The SQLContainer that this reference points to. + */ + private SQLContainer referencedContainer; + + /** + * The column ID/name in the referencing SQLContainer that contains the key + * used for the reference. + */ + private String referencingColumn; + + /** + * The column ID/name in the referenced SQLContainer that contains the key + * used for the reference. + */ + private String referencedColumn; + + /** + * Constructs a new reference to be used within the SQLContainer to + * reference another SQLContainer. + */ + Reference(SQLContainer referencedContainer, String referencingColumn, + String referencedColumn) { + this.referencedContainer = referencedContainer; + this.referencingColumn = referencingColumn; + this.referencedColumn = referencedColumn; + } + + SQLContainer getReferencedContainer() { + return referencedContainer; + } + + String getReferencingColumn() { + return referencingColumn; + } + + String getReferencedColumn() { + return referencedColumn; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowId.java b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java new file mode 100644 index 0000000000..925325134a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java @@ -0,0 +1,81 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; + +/** + * RowId represents identifiers of a single database result set row. + * + * The data structure of a RowId is an Object array which contains the values of + * the primary key columns of the identified row. This allows easy equals() + * -comparison of RowItems. + */ +public class RowId implements Serializable { + private static final long serialVersionUID = -3161778404698901258L; + protected Object[] id; + + /** + * Prevent instantiation without required parameters. + */ + protected RowId() { + } + + public RowId(Object[] id) { + if (id == null) { + throw new IllegalArgumentException("id parameter must not be null!"); + } + this.id = id; + } + + public Object[] getId() { + return id; + } + + @Override + public int hashCode() { + int result = 31; + if (id != null) { + for (Object o : id) { + if (o != null) { + result += o.hashCode(); + } + } + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof RowId)) { + return false; + } + Object[] compId = ((RowId) obj).getId(); + if (id == null && compId == null) { + return true; + } + if (id.length != compId.length) { + return false; + } + for (int i = 0; i < id.length; i++) { + if ((id[i] == null && compId[i] != null) + || (id[i] != null && !id[i].equals(compId[i]))) { + return false; + } + } + return true; + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + for (int i = 0; i < id.length; i++) { + s.append(id[i]); + if (i < id.length - 1) { + s.append("/"); + } + } + return s.toString(); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java new file mode 100644 index 0000000000..d613a06b63 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java @@ -0,0 +1,133 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * RowItem represents one row of a result set obtained from a QueryDelegate. + * + * Note that depending on the QueryDelegate in use this does not necessarily map + * into an actual row in a database table. + */ +public final class RowItem implements Item { + private static final long serialVersionUID = -6228966439127951408L; + private SQLContainer container; + private RowId id; + private Collection<ColumnProperty> properties; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private RowItem() { + } + + public RowItem(SQLContainer container, RowId id, + Collection<ColumnProperty> properties) { + if (container == null) { + throw new IllegalArgumentException("Container cannot be null."); + } + if (id == null) { + throw new IllegalArgumentException("Row ID cannot be null."); + } + this.container = container; + this.properties = properties; + /* Set this RowItem as owner to the properties */ + if (properties != null) { + for (ColumnProperty p : properties) { + p.setOwner(this); + } + } + this.id = id; + } + + @Override + public Property<?> getItemProperty(Object id) { + if (id instanceof String && id != null) { + for (ColumnProperty cp : properties) { + if (id.equals(cp.getPropertyId())) { + return cp; + } + } + } + return null; + } + + @Override + public Collection<?> getItemPropertyIds() { + Collection<String> ids = new ArrayList<String>(properties.size()); + for (ColumnProperty cp : properties) { + ids.add(cp.getPropertyId()); + } + return Collections.unmodifiableCollection(ids); + } + + /** + * Adding properties is not supported. Properties are generated by + * SQLContainer. + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removing properties is not supported. Properties are generated by + * SQLContainer. + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + public RowId getId() { + return id; + } + + public SQLContainer getContainer() { + return container; + } + + public boolean isModified() { + if (properties != null) { + for (ColumnProperty p : properties) { + if (p.isModified()) { + return true; + } + } + } + return false; + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + s.append("ID:"); + s.append(getId().toString()); + for (Object propId : getItemPropertyIds()) { + s.append("|"); + s.append(propId.toString()); + s.append(":"); + Object value = getItemProperty(propId).getValue(); + s.append((null != value) ? value.toString() : null); + } + return s.toString(); + } + + public void commit() { + if (properties != null) { + for (ColumnProperty p : properties) { + p.commit(); + } + } + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java new file mode 100644 index 0000000000..5827390723 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java @@ -0,0 +1,1716 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.EventObject; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.UnsupportedFilterException; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator; + +public class SQLContainer implements Container, Container.Filterable, + Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier { + + /** Query delegate */ + private QueryDelegate delegate; + /** Auto commit mode, default = false */ + private boolean autoCommit = false; + + /** Page length = number of items contained in one page */ + private int pageLength = DEFAULT_PAGE_LENGTH; + public static final int DEFAULT_PAGE_LENGTH = 100; + + /** Number of items to cache = CACHE_RATIO x pageLength */ + public static final int CACHE_RATIO = 2; + + /** Item and index caches */ + private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>(); + private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>(); + + /** Container properties = column names, data types and statuses */ + private final List<String> propertyIds = new ArrayList<String>(); + private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>(); + private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>(); + private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>(); + + /** Filters (WHERE) and sorters (ORDER BY) */ + private final List<Filter> filters = new ArrayList<Filter>(); + private final List<OrderBy> sorters = new ArrayList<OrderBy>(); + + /** + * Total number of items available in the data source using the current + * query, filters and sorters. + */ + private int size; + + /** + * Size updating logic. Do not update size from data source if it has been + * updated in the last sizeValidMilliSeconds milliseconds. + */ + private final int sizeValidMilliSeconds = 10000; + private boolean sizeDirty = true; + private Date sizeUpdated = new Date(); + + /** Starting row number of the currently fetched page */ + private int currentOffset; + + /** ItemSetChangeListeners */ + private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners; + + /** Temporary storage for modified items and items to be removed and added */ + private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>(); + private final List<RowItem> addedItems = new ArrayList<RowItem>(); + private final List<RowItem> modifiedItems = new ArrayList<RowItem>(); + + /** List of references to other SQLContainers */ + private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>(); + + /** Cache flush notification system enabled. Disabled by default. */ + private boolean notificationsEnabled; + + /** + * Prevent instantiation without a QueryDelegate. + */ + @SuppressWarnings("unused") + private SQLContainer() { + } + + /** + * Creates and initializes SQLContainer using the given QueryDelegate + * + * @param delegate + * QueryDelegate implementation + * @throws SQLException + */ + public SQLContainer(QueryDelegate delegate) throws SQLException { + if (delegate == null) { + throw new IllegalArgumentException( + "QueryDelegate must not be null."); + } + this.delegate = delegate; + getPropertyIds(); + cachedItems.setCacheLimit(CACHE_RATIO * getPageLength()); + } + + /**************************************/ + /** Methods from interface Container **/ + /**************************************/ + + /** + * Note! If auto commit mode is enabled, this method will still return the + * temporary row ID assigned for the item. Implement + * QueryDelegate.RowIdChangeListener to receive the actual Row ID value + * after the addition has been committed. + * + * {@inheritDoc} + */ + + @Override + public Object addItem() throws UnsupportedOperationException { + Object emptyKey[] = new Object[delegate.getPrimaryKeyColumns().size()]; + RowId itemId = new TemporaryRowId(emptyKey); + // Create new empty column properties for the row item. + List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>(); + for (String propertyId : propertyIds) { + /* Default settings for new item properties. */ + itemProperties + .add(new ColumnProperty(propertyId, propertyReadOnly + .get(propertyId), + !propertyReadOnly.get(propertyId), propertyNullable + .get(propertyId), null, getType(propertyId))); + } + RowItem newRowItem = new RowItem(this, itemId, itemProperties); + + if (autoCommit) { + /* Add and commit instantly */ + try { + if (delegate instanceof TableQuery) { + itemId = ((TableQuery) delegate) + .storeRowImmediately(newRowItem); + } else { + delegate.beginTransaction(); + delegate.storeRow(newRowItem); + delegate.commit(); + } + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + getLogger().log(Level.FINER, "Row added to DB..."); + return itemId; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to add row to DB. Rolling back.", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + getLogger().log(Level.SEVERE, + "Failed to roll back row addition", e); + } + return null; + } + } else { + addedItems.add(newRowItem); + fireContentsChange(); + return itemId; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#containsId(java.lang.Object) + */ + + @Override + public boolean containsId(Object itemId) { + if (itemId == null) { + return false; + } + + if (cachedItems.containsKey(itemId)) { + return true; + } else { + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + return itemPassesFilters(item); + } + } + } + if (removedItems.containsKey(itemId)) { + return false; + } + + if (itemId instanceof ReadOnlyRowId) { + int rowNum = ((ReadOnlyRowId) itemId).getRowNum(); + return rowNum >= 0 && rowNum < size; + } + + if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) { + try { + return delegate.containsRowWithKey(((RowId) itemId).getId()); + } catch (Exception e) { + /* Query failed, just return false. */ + getLogger().log(Level.WARNING, "containsId query failed", e); + } + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + Item item = getItem(itemId); + if (item == null) { + return null; + } + return item.getItemProperty(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + + @Override + public Collection<?> getContainerPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ + + @Override + public Item getItem(Object itemId) { + if (!cachedItems.containsKey(itemId)) { + int index = indexOfId(itemId); + if (index >= size) { + // The index is in the added items + int offset = index - size; + RowItem item = addedItems.get(offset); + if (itemPassesFilters(item)) { + return item; + } else { + return null; + } + } else { + // load the item into cache + updateOffsetAndCache(index); + } + } + return cachedItems.get(itemId); + } + + /** + * Bypasses in-memory filtering to return items that are cached in memory. + * <em>NOTE</em>: This does not bypass database-level filtering. + * + * @param itemId + * the id of the item to retrieve. + * @return the item represented by itemId. + */ + public Item getItemUnfiltered(Object itemId) { + if (!cachedItems.containsKey(itemId)) { + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + return item; + } + } + } + return cachedItems.get(itemId); + } + + /** + * NOTE! Do not use this method if in any way avoidable. This method doesn't + * (and cannot) use lazy loading, which means that all rows in the database + * will be loaded into memory. + * + * {@inheritDoc} + */ + + @Override + public Collection<?> getItemIds() { + updateCount(); + ArrayList<RowId> ids = new ArrayList<RowId>(); + ResultSet rs = null; + try { + // Load ALL rows :( + delegate.beginTransaction(); + rs = delegate.getResults(0, 0); + List<String> pKeys = delegate.getPrimaryKeyColumns(); + while (rs.next()) { + RowId id = null; + if (pKeys.isEmpty()) { + /* Create a read only itemId */ + id = new ReadOnlyRowId(rs.getRow()); + } else { + /* Generate itemId for the row based on primary key(s) */ + Object[] itemId = new Object[pKeys.size()]; + for (int i = 0; i < pKeys.size(); i++) { + itemId[i] = rs.getObject(pKeys.get(i)); + } + id = new RowId(itemId); + } + if (id != null && !removedItems.containsKey(id)) { + ids.add(id); + } + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "getItemIds() failed, rolling back.", e); + try { + delegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back state", e1); + } + try { + rs.getStatement().close(); + rs.close(); + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Closing session failed", e1); + } + throw new RuntimeException("Failed to fetch item indexes.", e); + } + for (RowItem item : getFilteredAddedItems()) { + ids.add(item.getId()); + } + return Collections.unmodifiableCollection(ids); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + + @Override + public Class<?> getType(Object propertyId) { + if (!propertyIds.contains(propertyId)) { + return null; + } + return propertyTypes.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#size() + */ + + @Override + public int size() { + updateCount(); + return size + sizeOfAddedItems() - removedItems.size(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + if (!containsId(itemId)) { + return false; + } + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + addedItems.remove(item); + fireContentsChange(); + return true; + } + } + + if (autoCommit) { + /* Remove and commit instantly. */ + Item i = getItem(itemId); + if (i == null) { + return false; + } + try { + delegate.beginTransaction(); + boolean success = delegate.removeRow((RowItem) i); + delegate.commit(); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + if (success) { + getLogger().log(Level.FINER, "Row removed from DB..."); + } + return success; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to remove row, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, + "Failed to rollback row removal", ee); + } + return false; + } catch (OptimisticLockException e) { + getLogger().log(Level.WARNING, + "Failed to remove row, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, + "Failed to rollback row removal", ee); + } + throw e; + } + } else { + removedItems.put((RowId) itemId, (RowItem) getItem(itemId)); + cachedItems.remove(itemId); + refresh(); + return true; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + if (autoCommit) { + /* Remove and commit instantly. */ + try { + delegate.beginTransaction(); + boolean success = true; + for (Object id : getItemIds()) { + if (!delegate.removeRow((RowItem) getItem(id))) { + success = false; + } + } + if (success) { + delegate.commit(); + getLogger().log(Level.FINER, "All rows removed from DB..."); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } else { + delegate.rollback(); + } + return success; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "removeAllItems() failed, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Failed to roll back", ee); + } + return false; + } catch (OptimisticLockException e) { + getLogger().log(Level.WARNING, + "removeAllItems() failed, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Failed to roll back", ee); + } + throw e; + } + } else { + for (Object id : getItemIds()) { + removedItems.put((RowId) id, (RowItem) getItem(id)); + cachedItems.remove(id); + } + refresh(); + return true; + } + } + + /*************************************************/ + /** Methods from interface Container.Filterable **/ + /*************************************************/ + + /** + * {@inheritDoc} + */ + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + // filter.setCaseSensitive(!ignoreCase); + + filters.add(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + + @Override + public void removeContainerFilter(Filter filter) { + filters.remove(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + if (propertyId == null || !propertyIds.contains(propertyId)) { + return; + } + + /* Generate Filter -object */ + String likeStr = onlyMatchPrefix ? filterString + "%" : "%" + + filterString + "%"; + Like like = new Like(propertyId.toString(), likeStr); + like.setCaseSensitive(!ignoreCase); + filters.add(like); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void removeContainerFilters(Object propertyId) { + ArrayList<Filter> toRemove = new ArrayList<Filter>(); + for (Filter f : filters) { + if (f.appliesToProperty(propertyId)) { + toRemove.add(f); + } + } + filters.removeAll(toRemove); + refresh(); + } + + /** + * {@inheritDoc} + */ + + @Override + public void removeAllContainerFilters() { + filters.clear(); + refresh(); + } + + /**********************************************/ + /** Methods from interface Container.Indexed **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object) + */ + + @Override + public int indexOfId(Object itemId) { + // First check if the id is in the added items + for (int ix = 0; ix < addedItems.size(); ix++) { + RowItem item = addedItems.get(ix); + if (item.getId().equals(itemId)) { + if (itemPassesFilters(item)) { + updateCount(); + return size + ix; + } else { + return -1; + } + } + } + + if (!containsId(itemId)) { + return -1; + } + if (cachedItems.isEmpty()) { + getPage(); + } + int size = size(); + boolean wrappedAround = false; + while (!wrappedAround) { + for (Integer i : itemIndexes.keySet()) { + if (itemIndexes.get(i).equals(itemId)) { + return i; + } + } + // load in the next page. + int nextIndex = (currentOffset / (pageLength * CACHE_RATIO) + 1) + * (pageLength * CACHE_RATIO); + if (nextIndex >= size) { + // Container wrapped around, start from index 0. + wrappedAround = true; + nextIndex = 0; + } + updateOffsetAndCache(nextIndex); + } + return -1; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#getIdByIndex(int) + */ + + @Override + public Object getIdByIndex(int index) { + if (index < 0 || index > size() - 1) { + return null; + } + if (index < size) { + if (itemIndexes.keySet().contains(index)) { + return itemIndexes.get(index); + } + updateOffsetAndCache(index); + return itemIndexes.get(index); + } else { + // The index is in the added items + int offset = index - size; + return addedItems.get(offset).getId(); + } + } + + /**********************************************/ + /** Methods from interface Container.Ordered **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) + */ + + @Override + public Object nextItemId(Object itemId) { + return getIdByIndex(indexOfId(itemId) + 1); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ + + @Override + public Object prevItemId(Object itemId) { + return getIdByIndex(indexOfId(itemId) - 1); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ + + @Override + public Object firstItemId() { + updateCount(); + if (size == 0) { + if (addedItems.isEmpty()) { + return null; + } else { + int ix = -1; + do { + ix++; + } while (!itemPassesFilters(addedItems.get(ix)) + && ix < addedItems.size()); + if (ix < addedItems.size()) { + return addedItems.get(ix).getId(); + } + } + } + if (!itemIndexes.containsKey(0)) { + updateOffsetAndCache(0); + } + return itemIndexes.get(0); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#lastItemId() + */ + + @Override + public Object lastItemId() { + if (addedItems.isEmpty()) { + int lastIx = size() - 1; + if (!itemIndexes.containsKey(lastIx)) { + updateOffsetAndCache(size - 1); + } + return itemIndexes.get(lastIx); + } else { + int ix = addedItems.size(); + do { + ix--; + } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0); + if (ix >= 0) { + return addedItems.get(ix).getId(); + } else { + return null; + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object) + */ + + @Override + public boolean isFirstId(Object itemId) { + return firstItemId().equals(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ + + @Override + public boolean isLastId(Object itemId) { + return lastItemId().equals(itemId); + } + + /***********************************************/ + /** Methods from interface Container.Sortable **/ + /***********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sorters.clear(); + if (propertyId == null || propertyId.length == 0) { + refresh(); + return; + } + /* Generate OrderBy -objects */ + boolean asc = true; + for (int i = 0; i < propertyId.length; i++) { + /* Check that the property id is valid */ + if (propertyId[i] instanceof String + && propertyIds.contains(propertyId[i])) { + try { + asc = ascending[i]; + } catch (Exception e) { + getLogger().log(Level.WARNING, "", e); + } + sorters.add(new OrderBy((String) propertyId[i], asc)); + } + } + refresh(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getContainerPropertyIds(); + } + + /**************************************/ + /** Methods specific to SQLContainer **/ + /**************************************/ + + /** + * Refreshes the container - clears all caches and resets size and offset. + * Does NOT remove sorting or filtering rules! + */ + public void refresh() { + sizeDirty = true; + currentOffset = 0; + cachedItems.clear(); + itemIndexes.clear(); + fireContentsChange(); + } + + /** + * Returns modify state of the container. + * + * @return true if contents of this container have been modified + */ + public boolean isModified() { + return !removedItems.isEmpty() || !addedItems.isEmpty() + || !modifiedItems.isEmpty(); + } + + /** + * Set auto commit mode enabled or disabled. Auto commit mode means that all + * changes made to items of this container will be immediately written to + * the underlying data source. + * + * @param autoCommitEnabled + * true to enable auto commit mode + */ + public void setAutoCommit(boolean autoCommitEnabled) { + autoCommit = autoCommitEnabled; + } + + /** + * Returns status of the auto commit mode. + * + * @return true if auto commit mode is enabled + */ + public boolean isAutoCommit() { + return autoCommit; + } + + /** + * Returns the currently set page length. + * + * @return current page length + */ + public int getPageLength() { + return pageLength; + } + + /** + * Sets the page length used in lazy fetching of items from the data source. + * Also resets the cache size to match the new page length. + * + * As a side effect the container will be refreshed. + * + * @param pageLength + * new page length + */ + public void setPageLength(int pageLength) { + setPageLengthInternal(pageLength); + refresh(); + } + + /** + * Sets the page length internally, without refreshing the container. + * + * @param pageLength + * the new page length + */ + private void setPageLengthInternal(int pageLength) { + this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH; + cachedItems.setCacheLimit(CACHE_RATIO * getPageLength()); + } + + /** + * Adds the given OrderBy to this container and refreshes the container + * contents with the new sorting rules. + * + * Note that orderBy.getColumn() must return a column name that exists in + * this container. + * + * @param orderBy + * OrderBy to be added to the container sorting rules + */ + public void addOrderBy(OrderBy orderBy) { + if (orderBy == null) { + return; + } + if (!propertyIds.contains(orderBy.getColumn())) { + throw new IllegalArgumentException( + "The column given for sorting does not exist in this container."); + } + sorters.add(orderBy); + refresh(); + } + + /** + * Commits all the changes, additions and removals made to the items of this + * container. + * + * @throws UnsupportedOperationException + * @throws SQLException + */ + public void commit() throws UnsupportedOperationException, SQLException { + try { + getLogger().log(Level.FINER, + "Commiting changes through delegate..."); + delegate.beginTransaction(); + /* Perform buffered deletions */ + for (RowItem item : removedItems.values()) { + if (!delegate.removeRow(item)) { + throw new SQLException("Removal failed for row with ID: " + + item.getId()); + } + } + /* Perform buffered modifications */ + for (RowItem item : modifiedItems) { + if (delegate.storeRow(item) > 0) { + /* + * Also reset the modified state in the item in case it is + * reused e.g. in a form. + */ + item.commit(); + } else { + delegate.rollback(); + refresh(); + throw new ConcurrentModificationException( + "Item with the ID '" + item.getId() + + "' has been externally modified."); + } + } + /* Perform buffered additions */ + for (RowItem item : addedItems) { + delegate.storeRow(item); + } + delegate.commit(); + removedItems.clear(); + addedItems.clear(); + modifiedItems.clear(); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } catch (SQLException e) { + delegate.rollback(); + throw e; + } catch (OptimisticLockException e) { + delegate.rollback(); + throw e; + } + } + + /** + * Rolls back all the changes, additions and removals made to the items of + * this container. + * + * @throws UnsupportedOperationException + * @throws SQLException + */ + public void rollback() throws UnsupportedOperationException, SQLException { + getLogger().log(Level.FINE, "Rolling back changes..."); + removedItems.clear(); + addedItems.clear(); + modifiedItems.clear(); + refresh(); + } + + /** + * Notifies this container that a property in the given item has been + * modified. The change will be buffered or made instantaneously depending + * on auto commit mode. + * + * @param changedItem + * item that has a modified property + */ + void itemChangeNotification(RowItem changedItem) { + if (autoCommit) { + try { + delegate.beginTransaction(); + if (delegate.storeRow(changedItem) == 0) { + delegate.rollback(); + refresh(); + throw new ConcurrentModificationException( + "Item with the ID '" + changedItem.getId() + + "' has been externally modified."); + } + delegate.commit(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + getLogger().log(Level.FINER, "Row updated to DB..."); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "itemChangeNotification failed, rolling back...", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Rollback failed", e); + } + throw new RuntimeException(e); + } + } else { + if (!(changedItem.getId() instanceof TemporaryRowId) + && !modifiedItems.contains(changedItem)) { + modifiedItems.add(changedItem); + } + } + } + + /** + * Determines a new offset for updating the row cache. The offset is + * calculated from the given index, and will be fixed to match the start of + * a page, based on the value of pageLength. + * + * @param index + * Index of the item that was requested, but not found in cache + */ + private void updateOffsetAndCache(int index) { + if (itemIndexes.containsKey(index)) { + return; + } + currentOffset = (index / (pageLength * CACHE_RATIO)) + * (pageLength * CACHE_RATIO); + if (currentOffset < 0) { + currentOffset = 0; + } + getPage(); + } + + /** + * Fetches new count of rows from the data source, if needed. + */ + private void updateCount() { + if (!sizeDirty + && new Date().getTime() < sizeUpdated.getTime() + + sizeValidMilliSeconds) { + return; + } + try { + try { + delegate.setFilters(filters); + } catch (UnsupportedOperationException e) { + getLogger().log(Level.FINE, + "The query delegate doesn't support filtering", e); + } + try { + delegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + getLogger().log(Level.FINE, + "The query delegate doesn't support filtering", e); + } + int newSize = delegate.getCount(); + if (newSize != size) { + size = newSize; + refresh(); + } + sizeUpdated = new Date(); + sizeDirty = false; + getLogger().log(Level.FINER, + "Updated row count. New count is: " + size); + } catch (SQLException e) { + throw new RuntimeException("Failed to update item set size.", e); + } + } + + /** + * Fetches property id's (column names and their types) from the data + * source. + * + * @throws SQLException + */ + private void getPropertyIds() throws SQLException { + propertyIds.clear(); + propertyTypes.clear(); + delegate.setFilters(null); + delegate.setOrderBy(null); + ResultSet rs = null; + ResultSetMetaData rsmd = null; + try { + delegate.beginTransaction(); + rs = delegate.getResults(0, 1); + boolean resultExists = rs.next(); + rsmd = rs.getMetaData(); + Class<?> type = null; + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) { + continue; + } + String colName = rsmd.getColumnLabel(i); + /* + * Make sure not to add the same colName twice. This can easily + * happen if the SQL query joins many tables with an ID column. + */ + if (!propertyIds.contains(colName)) { + propertyIds.add(colName); + } + /* Try to determine the column's JDBC class by all means. */ + if (resultExists && rs.getObject(i) != null) { + type = rs.getObject(i).getClass(); + } else { + try { + type = Class.forName(rsmd.getColumnClassName(i)); + } catch (Exception e) { + getLogger().log(Level.WARNING, "Class not found", e); + /* On failure revert to Object and hope for the best. */ + type = Object.class; + } + } + /* + * Determine read only and nullability status of the column. A + * column is read only if it is reported as either read only or + * auto increment by the database, and also it is set as the + * version column in a TableQuery delegate. + */ + boolean readOnly = rsmd.isAutoIncrement(i) + || rsmd.isReadOnly(i); + if (delegate instanceof TableQuery + && rsmd.getColumnLabel(i).equals( + ((TableQuery) delegate).getVersionColumn())) { + readOnly = true; + } + propertyReadOnly.put(colName, readOnly); + propertyNullable.put(colName, + rsmd.isNullable(i) == ResultSetMetaData.columnNullable); + propertyTypes.put(colName, type); + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + getLogger().log(Level.FINER, "Property IDs fetched."); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to fetch property ids, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back", e1); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + } + rs.close(); + } + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Failed to close session", e1); + } + throw e; + } + } + + /** + * Fetches a page from the data source based on the values of pageLenght and + * currentOffset. Also updates the set of primary keys, used in + * identification of RowItems. + */ + private void getPage() { + updateCount(); + ResultSet rs = null; + ResultSetMetaData rsmd = null; + cachedItems.clear(); + itemIndexes.clear(); + try { + try { + delegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + /* The query delegate doesn't support sorting. */ + /* No need to do anything. */ + getLogger().log(Level.FINE, + "The query delegate doesn't support sorting", e); + } + delegate.beginTransaction(); + rs = delegate.getResults(currentOffset, pageLength * CACHE_RATIO); + rsmd = rs.getMetaData(); + List<String> pKeys = delegate.getPrimaryKeyColumns(); + // } + /* Create new items and column properties */ + ColumnProperty cp = null; + int rowCount = currentOffset; + if (!delegate.implementationRespectsPagingLimits()) { + rowCount = currentOffset = 0; + setPageLengthInternal(size); + } + while (rs.next()) { + List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>(); + /* Generate row itemId based on primary key(s) */ + Object[] itemId = new Object[pKeys.size()]; + for (int i = 0; i < pKeys.size(); i++) { + itemId[i] = rs.getObject(pKeys.get(i)); + } + RowId id = null; + if (pKeys.isEmpty()) { + id = new ReadOnlyRowId(rs.getRow()); + } else { + id = new RowId(itemId); + } + List<String> propertiesToAdd = new ArrayList<String>( + propertyIds); + if (!removedItems.containsKey(id)) { + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) { + continue; + } + String colName = rsmd.getColumnLabel(i); + Object value = rs.getObject(i); + Class<?> type = value != null ? value.getClass() + : Object.class; + if (value == null) { + for (String propName : propertyTypes.keySet()) { + if (propName.equals(rsmd.getColumnLabel(i))) { + type = propertyTypes.get(propName); + break; + } + } + } + /* + * In case there are more than one column with the same + * name, add only the first one. This can easily happen + * if you join many tables where each table has an ID + * column. + */ + if (propertiesToAdd.contains(colName)) { + cp = new ColumnProperty(colName, + propertyReadOnly.get(colName), + !propertyReadOnly.get(colName), + propertyNullable.get(colName), value, type); + itemProperties.add(cp); + propertiesToAdd.remove(colName); + } + } + /* Cache item */ + itemIndexes.put(rowCount, id); + + // if an item with the id is contained in the modified + // cache, then use this record and add it to the cached + // items. Otherwise create a new item + int modifiedIndex = indexInModifiedCache(id); + if (modifiedIndex != -1) { + cachedItems.put(id, modifiedItems.get(modifiedIndex)); + } else { + cachedItems.put(id, new RowItem(this, id, + itemProperties)); + } + + rowCount++; + } + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + getLogger().log( + Level.FINER, + "Fetched " + pageLength * CACHE_RATIO + + " rows starting from " + currentOffset); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to fetch rows, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back", e1); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + rs.close(); + } + } + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Failed to close session", e1); + } + throw new RuntimeException("Failed to fetch page.", e); + } + } + + /** + * Returns the index of the item with the given itemId for the modified + * cache. + * + * @param itemId + * @return the index of the item with the itemId in the modified cache. Or + * -1 if not found. + */ + private int indexInModifiedCache(Object itemId) { + for (int ix = 0; ix < modifiedItems.size(); ix++) { + RowItem item = modifiedItems.get(ix); + if (item.getId().equals(itemId)) { + return ix; + } + } + return -1; + } + + private int sizeOfAddedItems() { + return getFilteredAddedItems().size(); + } + + private List<RowItem> getFilteredAddedItems() { + ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems); + if (filters != null && !filters.isEmpty()) { + for (RowItem item : addedItems) { + if (!itemPassesFilters(item)) { + filtered.remove(item); + } + } + } + return filtered; + } + + private boolean itemPassesFilters(RowItem item) { + for (Filter filter : filters) { + if (!filter.passesFilter(item.getId(), item)) { + return false; + } + } + return true; + } + + /** + * Checks is the given column identifier valid to be used with SQLContainer. + * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle + * is used. This is due to the way the SELECT queries are constructed in + * order to implement paging in these databases. + * + * @param identifier + * Column identifier + * @return true if the identifier is valid + */ + private boolean isColumnIdentifierValid(String identifier) { + if (identifier.equalsIgnoreCase("rownum") + && delegate instanceof TableQuery) { + TableQuery tq = (TableQuery) delegate; + if (tq.getSqlGenerator() instanceof MSSQLGenerator + || tq.getSqlGenerator() instanceof OracleGenerator) { + return false; + } + } + return true; + } + + /** + * Returns the QueryDelegate set for this SQLContainer. + * + * @return current querydelegate + */ + protected QueryDelegate getQueryDelegate() { + return delegate; + } + + /************************************/ + /** UNSUPPORTED CONTAINER FEATURES **/ + /************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object) + */ + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object) + */ + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int) + */ + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /******************************************/ + /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/ + /******************************************/ + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ + + @Override + public void addListener(Container.ItemSetChangeListener listener) { + if (itemSetChangeListeners == null) { + itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>(); + } + itemSetChangeListeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ + + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + if (itemSetChangeListeners != null) { + itemSetChangeListeners.remove(listener); + } + } + + protected void fireContentsChange() { + if (itemSetChangeListeners != null) { + final Object[] l = itemSetChangeListeners.toArray(); + final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent( + this); + for (int i = 0; i < l.length; i++) { + ((Container.ItemSetChangeListener) l[i]) + .containerItemSetChange(event); + } + } + } + + /** + * Simple ItemSetChangeEvent implementation. + */ + @SuppressWarnings("serial") + public static class ItemSetChangeEvent extends EventObject implements + Container.ItemSetChangeEvent { + + private ItemSetChangeEvent(SQLContainer source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + /**************************************************/ + /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/ + /**************************************************/ + + /** + * Adds a RowIdChangeListener to the QueryDelegate + * + * @param listener + */ + public void addListener(RowIdChangeListener listener) { + if (delegate instanceof QueryDelegate.RowIdChangeNotifier) { + ((QueryDelegate.RowIdChangeNotifier) delegate) + .addListener(listener); + } + } + + /** + * Removes a RowIdChangeListener from the QueryDelegate + * + * @param listener + */ + public void removeListener(RowIdChangeListener listener) { + if (delegate instanceof QueryDelegate.RowIdChangeNotifier) { + ((QueryDelegate.RowIdChangeNotifier) delegate) + .removeListener(listener); + } + } + + /** + * Calling this will enable this SQLContainer to send and receive cache + * flush notifications for its lifetime. + */ + public void enableCacheFlushNotifications() { + if (!notificationsEnabled) { + notificationsEnabled = true; + CacheFlushNotifier.addInstance(this); + } + } + + /******************************************/ + /** Referencing mechanism implementation **/ + /******************************************/ + + /** + * Adds a new reference to the given SQLContainer. In addition to the + * container you must provide the column (property) names used for the + * reference in both this and the referenced SQLContainer. + * + * Note that multiple references pointing to the same SQLContainer are not + * supported. + * + * @param refdCont + * Target SQLContainer of the new reference + * @param refingCol + * Column (property) name in this container storing the (foreign + * key) reference + * @param refdCol + * Column (property) name in the referenced container storing the + * referenced key + */ + public void addReference(SQLContainer refdCont, String refingCol, + String refdCol) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + if (!getContainerPropertyIds().contains(refingCol)) { + throw new IllegalArgumentException( + "Given referencing column name is invalid." + + " Please ensure that this container" + + " contains a property ID named: " + refingCol); + } + if (!refdCont.getContainerPropertyIds().contains(refdCol)) { + throw new IllegalArgumentException( + "Given referenced column name is invalid." + + " Please ensure that the referenced container" + + " contains a property ID named: " + refdCol); + } + if (references.keySet().contains(refdCont)) { + throw new IllegalArgumentException( + "An SQLContainer instance can only be referenced once."); + } + references.put(refdCont, new Reference(refdCont, refingCol, refdCol)); + } + + /** + * Removes the reference pointing to the given SQLContainer. + * + * @param refdCont + * Target SQLContainer of the reference + * @return true if successful, false if the reference did not exist + */ + public boolean removeReference(SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + return references.remove(refdCont) == null ? false : true; + } + + /** + * Sets the referenced item. The referencing column of the item in this + * container is updated accordingly. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdItemId + * Item Id of the reference target (from referenced container) + * @param refdCont + * Target SQLContainer of the reference + * @return true if the referenced item was successfully set, false on + * failure + */ + public boolean setReferencedItem(Object itemId, Object refdItemId, + SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + Reference r = references.get(refdCont); + if (r == null) { + throw new IllegalArgumentException( + "Reference to the given SQLContainer not defined."); + } + try { + getContainerProperty(itemId, r.getReferencingColumn()).setValue( + refdCont.getContainerProperty(refdItemId, + r.getReferencedColumn())); + return true; + } catch (Exception e) { + getLogger() + .log(Level.WARNING, "Setting referenced item failed.", e); + return false; + } + } + + /** + * Fetches the Item Id of the referenced item from the target SQLContainer. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdCont + * Target SQLContainer of the reference + * @return Item Id of the referenced item, or null if not found + */ + public Object getReferencedItemId(Object itemId, SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + Reference r = references.get(refdCont); + if (r == null) { + throw new IllegalArgumentException( + "Reference to the given SQLContainer not defined."); + } + Object refKey = getContainerProperty(itemId, r.getReferencingColumn()) + .getValue(); + + refdCont.removeAllContainerFilters(); + refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey)); + Object toReturn = refdCont.firstItemId(); + refdCont.removeAllContainerFilters(); + return toReturn; + } + + /** + * Fetches the referenced item from the target SQLContainer. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdCont + * Target SQLContainer of the reference + * @return The referenced item, or null if not found + */ + public Item getReferencedItem(Object itemId, SQLContainer refdCont) { + return refdCont.getItem(getReferencedItemId(itemId, refdCont)); + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + if (notificationsEnabled) { + /* + * Register instance with CacheFlushNotifier after de-serialization + * if notifications are enabled + */ + CacheFlushNotifier.addInstance(this); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(SQLContainer.class.getName()); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java new file mode 100644 index 0000000000..4a48dbf499 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java @@ -0,0 +1,36 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; + +public class SQLUtil implements Serializable { + /** + * Escapes different special characters in strings that are passed to SQL. + * Replaces the following: + * + * <list> <li>' is replaced with ''</li> <li>\x00 is removed</li> <li>\ is + * replaced with \\</li> <li>" is replaced with \"</li> <li> + * \x1a is removed</li> </list> + * + * Also note! The escaping done here may or may not be enough to prevent any + * and all SQL injections so it is recommended to check user input before + * giving it to the SQLContainer/TableQuery. + * + * @param constant + * @return \\\'\' + */ + public static String escapeSQL(String constant) { + if (constant == null) { + return null; + } + String fixedConstant = constant; + fixedConstant = fixedConstant.replaceAll("\\\\x00", ""); + fixedConstant = fixedConstant.replaceAll("\\\\x1a", ""); + fixedConstant = fixedConstant.replaceAll("'", "''"); + fixedConstant = fixedConstant.replaceAll("\\\\", "\\\\\\\\"); + fixedConstant = fixedConstant.replaceAll("\\\"", "\\\\\""); + return fixedConstant; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java new file mode 100644 index 0000000000..b4bca75a2a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java @@ -0,0 +1,32 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +public class TemporaryRowId extends RowId { + private static final long serialVersionUID = -641983830469018329L; + + public TemporaryRowId(Object[] id) { + super(id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof TemporaryRowId)) { + return false; + } + Object[] compId = ((TemporaryRowId) obj).getId(); + return id.equals(compId); + } + + @Override + public String toString() { + return "Temporary row id"; + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java new file mode 100644 index 0000000000..9aa4f7c4be --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java @@ -0,0 +1,72 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +public class J2EEConnectionPool implements JDBCConnectionPool { + + private String dataSourceJndiName; + + private DataSource dataSource = null; + + public J2EEConnectionPool(DataSource dataSource) { + this.dataSource = dataSource; + } + + public J2EEConnectionPool(String dataSourceJndiName) { + this.dataSourceJndiName = dataSourceJndiName; + } + + @Override + public Connection reserveConnection() throws SQLException { + Connection conn = getDataSource().getConnection(); + conn.setAutoCommit(false); + + return conn; + } + + private DataSource getDataSource() throws SQLException { + if (dataSource == null) { + dataSource = lookupDataSource(); + } + return dataSource; + } + + private DataSource lookupDataSource() throws SQLException { + try { + InitialContext ic = new InitialContext(); + return (DataSource) ic.lookup(dataSourceJndiName); + } catch (NamingException e) { + throw new SQLException( + "NamingException - Cannot connect to the database. Cause: " + + e.getMessage()); + } + } + + @Override + public void releaseConnection(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + Logger.getLogger(J2EEConnectionPool.class.getName()).log( + Level.FINE, "Could not release SQL connection", e); + } + } + } + + @Override + public void destroy() { + dataSource = null; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java new file mode 100644 index 0000000000..cf12461588 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Interface for implementing connection pools to be used with SQLContainer. + */ +public interface JDBCConnectionPool extends Serializable { + /** + * Retrieves a connection. + * + * @return a usable connection to the database + * @throws SQLException + */ + public Connection reserveConnection() throws SQLException; + + /** + * Releases a connection that was retrieved earlier. + * + * Note that depending on implementation, the transaction possibly open in + * the connection may or may not be rolled back. + * + * @param conn + * Connection to be released + */ + public void releaseConnection(Connection conn); + + /** + * Destroys the connection pool: close() is called an all the connections in + * the pool, whether available or reserved. + * + * This method was added to fix PostgreSQL -related issues with connections + * that were left hanging 'idle'. + */ + public void destroy(); +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java new file mode 100644 index 0000000000..21760014b9 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java @@ -0,0 +1,168 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Set; + +/** + * Simple implementation of the JDBCConnectionPool interface. Handles loading + * the JDBC driver, setting up the connections and ensuring they are still + * usable upon release. + */ +@SuppressWarnings("serial") +public class SimpleJDBCConnectionPool implements JDBCConnectionPool { + + private int initialConnections = 5; + private int maxConnections = 20; + + private String driverName; + private String connectionUri; + private String userName; + private String password; + + private transient Set<Connection> availableConnections; + private transient Set<Connection> reservedConnections; + + private boolean initialized; + + public SimpleJDBCConnectionPool(String driverName, String connectionUri, + String userName, String password) throws SQLException { + if (driverName == null) { + throw new IllegalArgumentException( + "JDBC driver class name must be given."); + } + if (connectionUri == null) { + throw new IllegalArgumentException( + "Database connection URI must be given."); + } + if (userName == null) { + throw new IllegalArgumentException( + "Database username must be given."); + } + if (password == null) { + throw new IllegalArgumentException( + "Database password must be given."); + } + this.driverName = driverName; + this.connectionUri = connectionUri; + this.userName = userName; + this.password = password; + + /* Initialize JDBC driver */ + try { + Class.forName(driverName).newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Specified JDBC Driver: " + driverName + + " - initialization failed.", ex); + } + } + + public SimpleJDBCConnectionPool(String driverName, String connectionUri, + String userName, String password, int initialConnections, + int maxConnections) throws SQLException { + this(driverName, connectionUri, userName, password); + this.initialConnections = initialConnections; + this.maxConnections = maxConnections; + } + + private void initializeConnections() throws SQLException { + availableConnections = new HashSet<Connection>(initialConnections); + reservedConnections = new HashSet<Connection>(initialConnections); + for (int i = 0; i < initialConnections; i++) { + availableConnections.add(createConnection()); + } + initialized = true; + } + + @Override + public synchronized Connection reserveConnection() throws SQLException { + if (!initialized) { + initializeConnections(); + } + if (availableConnections.isEmpty()) { + if (reservedConnections.size() < maxConnections) { + availableConnections.add(createConnection()); + } else { + throw new SQLException("Connection limit has been reached."); + } + } + + Connection c = availableConnections.iterator().next(); + availableConnections.remove(c); + reservedConnections.add(c); + + return c; + } + + @Override + public synchronized void releaseConnection(Connection conn) { + if (conn == null || !initialized) { + return; + } + /* Try to roll back if necessary */ + try { + if (!conn.getAutoCommit()) { + conn.rollback(); + } + } catch (SQLException e) { + /* Roll back failed, close and discard connection */ + try { + conn.close(); + } catch (SQLException e1) { + /* Nothing needs to be done */ + } + reservedConnections.remove(conn); + return; + } + reservedConnections.remove(conn); + availableConnections.add(conn); + } + + private Connection createConnection() throws SQLException { + Connection c = DriverManager.getConnection(connectionUri, userName, + password); + c.setAutoCommit(false); + if (driverName.toLowerCase().contains("mysql")) { + try { + Statement s = c.createStatement(); + s.execute("SET SESSION sql_mode = 'ANSI'"); + s.close(); + } catch (Exception e) { + // Failed to set ansi mode; continue + } + } + return c; + } + + @Override + public void destroy() { + for (Connection c : availableConnections) { + try { + c.close(); + } catch (SQLException e) { + // No need to do anything + } + } + for (Connection c : reservedConnections) { + try { + c.close(); + } catch (SQLException e) { + // No need to do anything + } + } + + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + initialized = false; + out.defaultWriteObject(); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java new file mode 100644 index 0000000000..ec986fab95 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java @@ -0,0 +1,507 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLContainer; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class FreeformQuery implements QueryDelegate { + + FreeformQueryDelegate delegate = null; + private String queryString; + private List<String> primaryKeyColumns; + private JDBCConnectionPool connectionPool; + private transient Connection activeConnection = null; + + /** + * Prevent no-parameters instantiation of FreeformQuery + */ + @SuppressWarnings("unused") + private FreeformQuery() { + } + + /** + * Creates a new freeform query delegate to be used with the + * {@link SQLContainer}. + * + * @param queryString + * The actual query to perform. + * @param primaryKeyColumns + * The primary key columns. Read-only mode is forced if this + * parameter is null or empty. + * @param connectionPool + * the JDBCConnectionPool to use to open connections to the SQL + * database. + * @deprecated @see + * {@link FreeformQuery#FreeformQuery(String, JDBCConnectionPool, String...)} + */ + @Deprecated + public FreeformQuery(String queryString, List<String> primaryKeyColumns, + JDBCConnectionPool connectionPool) { + if (primaryKeyColumns == null) { + primaryKeyColumns = new ArrayList<String>(); + } + if (primaryKeyColumns.contains("")) { + throw new IllegalArgumentException( + "The primary key columns contain an empty string!"); + } else if (queryString == null || "".equals(queryString)) { + throw new IllegalArgumentException( + "The query string may not be empty or null!"); + } else if (connectionPool == null) { + throw new IllegalArgumentException( + "The connectionPool may not be null!"); + } + this.queryString = queryString; + this.primaryKeyColumns = Collections + .unmodifiableList(primaryKeyColumns); + this.connectionPool = connectionPool; + } + + /** + * Creates a new freeform query delegate to be used with the + * {@link SQLContainer}. + * + * @param queryString + * The actual query to perform. + * @param connectionPool + * the JDBCConnectionPool to use to open connections to the SQL + * database. + * @param primaryKeyColumns + * The primary key columns. Read-only mode is forced if none are + * provided. (optional) + */ + public FreeformQuery(String queryString, JDBCConnectionPool connectionPool, + String... primaryKeyColumns) { + this(queryString, Arrays.asList(primaryKeyColumns), connectionPool); + } + + /** + * This implementation of getCount() actually fetches all records from the + * database, which might be a performance issue. Override this method with a + * SELECT COUNT(*) ... query if this is too slow for your needs. + * + * {@inheritDoc} + */ + @Override + public int getCount() throws SQLException { + // First try the delegate + int count = countByDelegate(); + if (count < 0) { + // Couldn't use the delegate, use the bad way. + Connection conn = getConnection(); + Statement statement = conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY); + + ResultSet rs = statement.executeQuery(queryString); + if (rs.last()) { + count = rs.getRow(); + } else { + count = 0; + } + rs.close(); + statement.close(); + releaseConnection(conn); + } + return count; + } + + @SuppressWarnings("deprecation") + private int countByDelegate() throws SQLException { + int count = -1; + if (delegate == null) { + return count; + } + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getCountStatement(); + Connection c = getConnection(); + PreparedStatement pstmt = c.prepareStatement(sh + .getQueryString()); + sh.setParameterValuesToStatement(pstmt); + ResultSet rs = pstmt.executeQuery(); + rs.next(); + count = rs.getInt(1); + rs.close(); + pstmt.clearParameters(); + pstmt.close(); + releaseConnection(c); + return count; + } catch (UnsupportedOperationException e) { + // Count statement generation not supported + } + } + /* Try using regular statement */ + try { + String countQuery = delegate.getCountQuery(); + if (countQuery != null) { + Connection conn = getConnection(); + Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery(countQuery); + rs.next(); + count = rs.getInt(1); + rs.close(); + statement.close(); + releaseConnection(conn); + return count; + } + } catch (UnsupportedOperationException e) { + // Count query generation not supported + } + return count; + } + + private Connection getConnection() throws SQLException { + if (activeConnection != null) { + return activeConnection; + } + return connectionPool.reserveConnection(); + } + + /** + * Fetches the results for the query. This implementation always fetches the + * entire record set, ignoring the offset and page length parameters. In + * order to support lazy loading of records, you must supply a + * FreeformQueryDelegate that implements the + * FreeformQueryDelegate.getQueryString(int,int) method. + * + * @throws SQLException + * + * @see FreeformQueryDelegate#getQueryString(int, int) + */ + @Override + @SuppressWarnings("deprecation") + public ResultSet getResults(int offset, int pagelength) throws SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction!"); + } + String query = queryString; + if (delegate != null) { + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getQueryStatement(offset, pagelength); + PreparedStatement pstmt = activeConnection + .prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + return pstmt.executeQuery(); + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + query = delegate.getQueryString(offset, pagelength); + } catch (UnsupportedOperationException e) { + // This is fine, we'll just use the default queryString. + } + } + Statement statement = activeConnection.createStatement(); + ResultSet rs = statement.executeQuery(query); + return rs; + } + + @Override + @SuppressWarnings("deprecation") + public boolean implementationRespectsPagingLimits() { + if (delegate == null) { + return false; + } + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getCountStatement(); + if (sh != null && sh.getQueryString() != null + && sh.getQueryString().length() > 0) { + return true; + } + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + String queryString = delegate.getQueryString(0, 50); + return queryString != null && queryString.length() > 0; + } catch (UnsupportedOperationException e) { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setFilters(java + * .util.List) + */ + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setFilters(filters); + } else if (filters != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setOrderBy(java + * .util.List) + */ + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setOrderBy(orderBys); + } else if (orderBys != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .data.util.sqlcontainer.RowItem) + */ + @Override + public int storeRow(RowItem row) throws SQLException { + if (activeConnection == null) { + throw new IllegalStateException("No transaction is active!"); + } else if (primaryKeyColumns.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot store items fetched with a read-only freeform query!"); + } + if (delegate != null) { + return delegate.storeRow(activeConnection, row); + } else { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin + * .data.util.sqlcontainer.RowItem) + */ + @Override + public boolean removeRow(RowItem row) throws SQLException { + if (activeConnection == null) { + throw new IllegalStateException("No transaction is active!"); + } else if (primaryKeyColumns.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot remove items fetched with a read-only freeform query!"); + } + if (delegate != null) { + return delegate.removeRow(activeConnection, row); + } else { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#beginTransaction() + */ + @Override + public synchronized void beginTransaction() + throws UnsupportedOperationException, SQLException { + if (activeConnection != null) { + throw new IllegalStateException("A transaction is already active!"); + } + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#commit() + */ + @Override + public synchronized void commit() throws UnsupportedOperationException, + SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction"); + } + if (!activeConnection.getAutoCommit()) { + activeConnection.commit(); + } + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#rollback() + */ + @Override + public synchronized void rollback() throws UnsupportedOperationException, + SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction"); + } + activeConnection.rollback(); + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns + * () + */ + @Override + public List<String> getPrimaryKeyColumns() { + return primaryKeyColumns; + } + + public String getQueryString() { + return queryString; + } + + public FreeformQueryDelegate getDelegate() { + return delegate; + } + + public void setDelegate(FreeformQueryDelegate delegate) { + this.delegate = delegate; + } + + /** + * This implementation of the containsRowWithKey method rewrites existing + * WHERE clauses in the query string. The logic is, however, not very + * complex and some times can do the Wrong Thing<sup>TM</sup>. For the + * situations where this logic is not enough, you can implement the + * getContainsRowQueryString method in FreeformQueryDelegate and this will + * be used instead of the logic. + * + * @see FreeformQueryDelegate#getContainsRowQueryString(Object...) + * + */ + @Override + @SuppressWarnings("deprecation") + public boolean containsRowWithKey(Object... keys) throws SQLException { + String query = null; + boolean contains = false; + if (delegate != null) { + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getContainsRowQueryStatement(keys); + Connection c = getConnection(); + PreparedStatement pstmt = c.prepareStatement(sh + .getQueryString()); + sh.setParameterValuesToStatement(pstmt); + ResultSet rs = pstmt.executeQuery(); + contains = rs.next(); + rs.close(); + pstmt.clearParameters(); + pstmt.close(); + releaseConnection(c); + return contains; + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + query = delegate.getContainsRowQueryString(keys); + } catch (UnsupportedOperationException e) { + query = modifyWhereClause(keys); + } + } else { + query = modifyWhereClause(keys); + } + Connection conn = getConnection(); + try { + Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery(query); + contains = rs.next(); + rs.close(); + statement.close(); + } finally { + releaseConnection(conn); + } + return contains; + } + + /** + * Releases the connection if it is not part of an active transaction. + * + * @param conn + * the connection to release + */ + private void releaseConnection(Connection conn) { + if (conn != activeConnection) { + connectionPool.releaseConnection(conn); + } + } + + private String modifyWhereClause(Object... keys) { + // Build the where rules for the provided keys + StringBuffer where = new StringBuffer(); + for (int ix = 0; ix < primaryKeyColumns.size(); ix++) { + where.append(QueryBuilder.quote(primaryKeyColumns.get(ix))); + if (keys[ix] == null) { + where.append(" IS NULL"); + } else { + where.append(" = '").append(keys[ix]).append("'"); + } + if (ix < primaryKeyColumns.size() - 1) { + where.append(" AND "); + } + } + // Is there already a WHERE clause in the query string? + int index = queryString.toLowerCase().indexOf("where "); + if (index > -1) { + // Rewrite the where clause + return queryString.substring(0, index) + "WHERE " + where + " AND " + + queryString.substring(index + 6); + } + // Append a where clause + return queryString + " WHERE " + where; + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + try { + rollback(); + } catch (SQLException ignored) { + } + out.defaultWriteObject(); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java new file mode 100644 index 0000000000..433d742be8 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; + +public interface FreeformQueryDelegate extends Serializable { + /** + * Should return the SQL query string to be performed. This method is + * responsible for gluing together the select query from the filters and the + * order by conditions if these are supported. + * + * @param offset + * the first record (row) to fetch. + * @param pagelength + * the number of records (rows) to fetch. 0 means all records + * starting from offset. + * @deprecated Implement {@link FreeformStatementDelegate} instead of + * {@link FreeformQueryDelegate} + */ + @Deprecated + public String getQueryString(int offset, int limit) + throws UnsupportedOperationException; + + /** + * Generates and executes a query to determine the current row count from + * the DB. Row count will be fetched using filters that are currently set to + * the QueryDelegate. + * + * @return row count + * @throws SQLException + * @deprecated Implement {@link FreeformStatementDelegate} instead of + * {@link FreeformQueryDelegate} + */ + @Deprecated + public String getCountQuery() throws UnsupportedOperationException; + + /** + * Sets the filters to apply when performing the SQL query. These are + * translated into a WHERE clause. Default filtering mode will be used. + * + * @param filters + * The filters to apply. + * @throws UnsupportedOperationException + * if the implementation doesn't support filtering. + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException; + + /** + * Sets the order in which to retrieve rows from the database. The result + * can be ordered by zero or more columns and each column can be in + * ascending or descending order. These are translated into an ORDER BY + * clause in the SQL query. + * + * @param orderBys + * A list of the OrderBy conditions. + * @throws UnsupportedOperationException + * if the implementation doesn't support ordering. + */ + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException; + + /** + * Stores a row in the database. The implementation of this interface + * decides how to identify whether to store a new row or update an existing + * one. + * + * @param conn + * the JDBC connection to use + * @param row + * RowItem to be stored or updated. + * @throws UnsupportedOperationException + * if the implementation is read only. + * @throws SQLException + */ + public int storeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Removes the given RowItem from the database. + * + * @param conn + * the JDBC connection to use + * @param row + * RowItem to be removed + * @return true on success + * @throws UnsupportedOperationException + * @throws SQLException + */ + public boolean removeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Generates an SQL Query string that allows the user of the FreeformQuery + * class to customize the query string used by the + * FreeformQuery.containsRowWithKeys() method. This is useful for cases when + * the logic in the containsRowWithKeys method is not enough to support more + * complex free form queries. + * + * @param keys + * the values of the primary keys + * @throws UnsupportedOperationException + * to use the default logic in FreeformQuery + * @deprecated Implement {@link FreeformStatementDelegate} instead of + * {@link FreeformQueryDelegate} + */ + @Deprecated + public String getContainsRowQueryString(Object... keys) + throws UnsupportedOperationException; +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java new file mode 100644 index 0000000000..95521c5019 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +/** + * FreeformStatementDelegate is an extension to FreeformQueryDelegate that + * provides definitions for methods that produce StatementHelper objects instead + * of basic query strings. This allows the FreeformQuery query delegate to use + * PreparedStatements instead of regular Statement when accessing the database. + * + * Due to the injection protection and other benefits of prepared statements, it + * is advisable to implement this interface instead of the FreeformQueryDelegate + * whenever possible. + */ +public interface FreeformStatementDelegate extends FreeformQueryDelegate { + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement. This + * method is responsible for gluing together the select query from the + * filters and the order by conditions if these are supported. + * + * @param offset + * the first record (row) to fetch. + * @param pagelength + * the number of records (rows) to fetch. 0 means all records + * starting from offset. + */ + public StatementHelper getQueryStatement(int offset, int limit) + throws UnsupportedOperationException; + + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement that + * will fetch the row count from the DB. Row count should be fetched using + * filters that are currently set to the QueryDelegate. + */ + public StatementHelper getCountStatement() + throws UnsupportedOperationException; + + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement used + * by the FreeformQuery.containsRowWithKeys() method. This is useful for + * cases when the default logic in said method is not enough to support more + * complex free form queries. + * + * @param keys + * the values of the primary keys + * @throws UnsupportedOperationException + * to use the default logic in FreeformQuery + */ + public StatementHelper getContainsRowQueryStatement(Object... keys) + throws UnsupportedOperationException; +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java new file mode 100644 index 0000000000..8ebe10067e --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.Serializable; + +/** + * OrderBy represents a sorting rule to be applied to a query made by the + * SQLContainer's QueryDelegate. + * + * The sorting rule is simple and contains only the affected column's name and + * the direction of the sort. + */ +public class OrderBy implements Serializable { + private String column; + private boolean isAscending; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private OrderBy() { + } + + public OrderBy(String column, boolean isAscending) { + setColumn(column); + setAscending(isAscending); + } + + public void setColumn(String column) { + this.column = column; + } + + public String getColumn() { + return column; + } + + public void setAscending(boolean isAscending) { + this.isAscending = isAscending; + } + + public boolean isAscending() { + return isAscending; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java new file mode 100644 index 0000000000..6e4396fad1 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java @@ -0,0 +1,211 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; + +public interface QueryDelegate extends Serializable { + /** + * Generates and executes a query to determine the current row count from + * the DB. Row count will be fetched using filters that are currently set to + * the QueryDelegate. + * + * @return row count + * @throws SQLException + */ + public int getCount() throws SQLException; + + /** + * Executes a paged SQL query and returns the ResultSet. The query is + * defined through implementations of this QueryDelegate interface. + * + * @param offset + * the first item of the page to load + * @param pagelength + * the length of the page to load + * @return a ResultSet containing the rows of the page + * @throws SQLException + * if the database access fails. + */ + public ResultSet getResults(int offset, int pagelength) throws SQLException; + + /** + * Allows the SQLContainer implementation to check whether the QueryDelegate + * implementation implements paging in the getResults method. + * + * @see QueryDelegate#getResults(int, int) + * + * @return true if the delegate implements paging + */ + public boolean implementationRespectsPagingLimits(); + + /** + * Sets the filters to apply when performing the SQL query. These are + * translated into a WHERE clause. Default filtering mode will be used. + * + * @param filters + * The filters to apply. + * @throws UnsupportedOperationException + * if the implementation doesn't support filtering. + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException; + + /** + * Sets the order in which to retrieve rows from the database. The result + * can be ordered by zero or more columns and each column can be in + * ascending or descending order. These are translated into an ORDER BY + * clause in the SQL query. + * + * @param orderBys + * A list of the OrderBy conditions. + * @throws UnsupportedOperationException + * if the implementation doesn't support ordering. + */ + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException; + + /** + * Stores a row in the database. The implementation of this interface + * decides how to identify whether to store a new row or update an existing + * one. + * + * @param columnToValueMap + * A map containing the values for all columns to be stored or + * updated. + * @return the number of affected rows in the database table + * @throws UnsupportedOperationException + * if the implementation is read only. + */ + public int storeRow(RowItem row) throws UnsupportedOperationException, + SQLException; + + /** + * Removes the given RowItem from the database. + * + * @param row + * RowItem to be removed + * @return true on success + * @throws UnsupportedOperationException + * @throws SQLException + */ + public boolean removeRow(RowItem row) throws UnsupportedOperationException, + SQLException; + + /** + * Starts a new database transaction. Used when storing multiple changes. + * + * Note that if a transaction is already open, it will be rolled back when a + * new transaction is started. + * + * @throws SQLException + * if the database access fails. + */ + public void beginTransaction() throws SQLException; + + /** + * Commits a transaction. If a transaction is not open nothing should + * happen. + * + * @throws SQLException + * if the database access fails. + */ + public void commit() throws SQLException; + + /** + * Rolls a transaction back. If a transaction is not open nothing should + * happen. + * + * @throws SQLException + * if the database access fails. + */ + public void rollback() throws SQLException; + + /** + * Returns a list of primary key column names. The list is either fetched + * from the database (TableQuery) or given as an argument depending on + * implementation. + * + * @return + */ + public List<String> getPrimaryKeyColumns(); + + /** + * Performs a query to find out whether the SQL table contains a row with + * the given set of primary keys. + * + * @param keys + * the primary keys + * @return true if the SQL table contains a row with the provided keys + * @throws SQLException + */ + public boolean containsRowWithKey(Object... keys) throws SQLException; + + /************************/ + /** ROWID CHANGE EVENT **/ + /************************/ + + /** + * An <code>Event</code> object specifying the old and new RowId of an added + * item after the addition has been successfully committed. + */ + public interface RowIdChangeEvent extends Serializable { + /** + * Gets the old (temporary) RowId of the added row that raised this + * event. + * + * @return old RowId + */ + public RowId getOldRowId(); + + /** + * Gets the new, possibly database assigned RowId of the added row that + * raised this event. + * + * @return new RowId + */ + public RowId getNewRowId(); + } + + /** RowId change listener interface. */ + public interface RowIdChangeListener extends Serializable { + /** + * Lets the listener know that a RowId has been changed. + * + * @param event + */ + public void rowIdChange(QueryDelegate.RowIdChangeEvent event); + } + + /** + * The interface for adding and removing <code>RowIdChangeEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>RowIdChangeEvent</code> when it performs a + * database commit that may change the RowId. + */ + public interface RowIdChangeNotifier extends Serializable { + /** + * Adds a RowIdChangeListener for the object. + * + * @param listener + * listener to be added + */ + public void addListener(QueryDelegate.RowIdChangeListener listener); + + /** + * Removes the specified RowIdChangeListener from the object. + * + * @param listener + * listener to be removed + */ + public void removeListener(QueryDelegate.RowIdChangeListener listener); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java new file mode 100644 index 0000000000..d0606704f7 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java @@ -0,0 +1,715 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.sqlcontainer.ColumnProperty; +import com.vaadin.data.util.sqlcontainer.OptimisticLockException; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLUtil; +import com.vaadin.data.util.sqlcontainer.TemporaryRowId; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +@SuppressWarnings("serial") +public class TableQuery implements QueryDelegate, + QueryDelegate.RowIdChangeNotifier { + + /** Table name, primary key column name(s) and version column name */ + private String tableName; + private List<String> primaryKeyColumns; + private String versionColumn; + + /** Currently set Filters and OrderBys */ + private List<Filter> filters; + private List<OrderBy> orderBys; + + /** SQLGenerator instance to use for generating queries */ + private SQLGenerator sqlGenerator; + + /** Fields related to Connection and Transaction handling */ + private JDBCConnectionPool connectionPool; + private transient Connection activeConnection; + private boolean transactionOpen; + + /** Row ID change listeners */ + private LinkedList<RowIdChangeListener> rowIdChangeListeners; + /** Row ID change events, stored until commit() is called */ + private final List<RowIdChangeEvent> bufferedEvents = new ArrayList<RowIdChangeEvent>(); + + /** Set to true to output generated SQL Queries to System.out */ + private boolean debug = false; + + /** Prevent no-parameters instantiation of TableQuery */ + @SuppressWarnings("unused") + private TableQuery() { + } + + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. All parameters must be non-null. + * + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + */ + public TableQuery(String tableName, JDBCConnectionPool connectionPool, + SQLGenerator sqlGenerator) { + if (tableName == null || tableName.trim().length() < 1 + || connectionPool == null || sqlGenerator == null) { + throw new IllegalArgumentException( + "All parameters must be non-null and a table name must be given."); + } + this.tableName = tableName; + this.sqlGenerator = sqlGenerator; + this.connectionPool = connectionPool; + fetchMetaData(); + } + + /** + * Creates a new TableQuery using the given connection pool and table name + * to fetch the data from. All parameters must be non-null. The default SQL + * generator will be used for queries. + * + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + */ + public TableQuery(String tableName, JDBCConnectionPool connectionPool) { + this(tableName, connectionPool, new DefaultSQLGenerator()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getCount() + */ + @Override + public int getCount() throws SQLException { + getLogger().log(Level.FINE, "Fetching count..."); + StatementHelper sh = sqlGenerator.generateSelectQuery(tableName, + filters, null, 0, 0, "COUNT(*)"); + boolean shouldCloseTransaction = false; + if (!transactionOpen) { + shouldCloseTransaction = true; + beginTransaction(); + } + ResultSet r = executeQuery(sh); + r.next(); + int count = r.getInt(1); + r.getStatement().close(); + r.close(); + if (shouldCloseTransaction) { + commit(); + } + return count; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getResults(int, + * int) + */ + @Override + public ResultSet getResults(int offset, int pagelength) throws SQLException { + StatementHelper sh; + /* + * If no ordering is explicitly set, results will be ordered by the + * first primary key column. + */ + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy(primaryKeyColumns.get(0), true)); + sh = sqlGenerator.generateSelectQuery(tableName, filters, ob, + offset, pagelength, null); + } else { + sh = sqlGenerator.generateSelectQuery(tableName, filters, orderBys, + offset, pagelength, null); + } + return executeQuery(sh); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate# + * implementationRespectsPagingLimits() + */ + @Override + public boolean implementationRespectsPagingLimits() { + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + @Override + public int storeRow(RowItem row) throws UnsupportedOperationException, + SQLException { + if (row == null) { + throw new IllegalArgumentException("Row argument must be non-null."); + } + StatementHelper sh; + int result = 0; + if (row.getId() instanceof TemporaryRowId) { + setVersionColumnFlagInProperty(row); + sh = sqlGenerator.generateInsertQuery(tableName, row); + result = executeUpdateReturnKeys(sh, row); + } else { + setVersionColumnFlagInProperty(row); + sh = sqlGenerator.generateUpdateQuery(tableName, row); + result = executeUpdate(sh); + } + if (versionColumn != null && result == 0) { + throw new OptimisticLockException( + "Someone else changed the row that was being updated.", + row.getId()); + } + return result; + } + + private void setVersionColumnFlagInProperty(RowItem row) { + ColumnProperty versionProperty = (ColumnProperty) row + .getItemProperty(versionColumn); + if (versionProperty != null) { + versionProperty.setVersionColumn(true); + } + } + + /** + * Inserts the given row in the database table immediately. Begins and + * commits the transaction needed. This method was added specifically to + * solve the problem of returning the final RowId immediately on the + * SQLContainer.addItem() call when auto commit mode is enabled in the + * SQLContainer. + * + * @param row + * RowItem to add to the database + * @return Final RowId of the added row + * @throws SQLException + */ + public RowId storeRowImmediately(RowItem row) throws SQLException { + beginTransaction(); + /* Set version column, if one is provided */ + setVersionColumnFlagInProperty(row); + /* Generate query */ + StatementHelper sh = sqlGenerator.generateInsertQuery(tableName, row); + PreparedStatement pstmt = activeConnection.prepareStatement( + sh.getQueryString(), primaryKeyColumns.toArray(new String[0])); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + int result = pstmt.executeUpdate(); + if (result > 0) { + /* + * If affected rows exist, we'll get the new RowId, commit the + * transaction and return the new RowId. + */ + ResultSet generatedKeys = pstmt.getGeneratedKeys(); + RowId newId = getNewRowId(row, generatedKeys); + generatedKeys.close(); + pstmt.clearParameters(); + pstmt.close(); + commit(); + return newId; + } else { + pstmt.clearParameters(); + pstmt.close(); + /* On failure return null */ + return null; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util + * .List) + */ + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (filters == null) { + this.filters = null; + return; + } + this.filters = Collections.unmodifiableList(filters); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setOrderBy(java.util + * .List) + */ + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (orderBys == null) { + this.orderBys = null; + return; + } + this.orderBys = Collections.unmodifiableList(orderBys); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#beginTransaction() + */ + @Override + public void beginTransaction() throws UnsupportedOperationException, + SQLException { + if (transactionOpen && activeConnection != null) { + throw new IllegalStateException(); + } + + getLogger().log(Level.FINE, "DB -> begin transaction"); + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + transactionOpen = true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit() + */ + @Override + public void commit() throws UnsupportedOperationException, SQLException { + if (transactionOpen && activeConnection != null) { + getLogger().log(Level.FINE, "DB -> commit"); + activeConnection.commit(); + connectionPool.releaseConnection(activeConnection); + } else { + throw new SQLException("No active transaction"); + } + transactionOpen = false; + + /* Handle firing row ID change events */ + RowIdChangeEvent[] unFiredEvents = bufferedEvents + .toArray(new RowIdChangeEvent[] {}); + bufferedEvents.clear(); + if (rowIdChangeListeners != null && !rowIdChangeListeners.isEmpty()) { + for (RowIdChangeListener r : rowIdChangeListeners) { + for (RowIdChangeEvent e : unFiredEvents) { + r.rowIdChange(e); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#rollback() + */ + @Override + public void rollback() throws UnsupportedOperationException, SQLException { + if (transactionOpen && activeConnection != null) { + getLogger().log(Level.FINE, "DB -> rollback"); + activeConnection.rollback(); + connectionPool.releaseConnection(activeConnection); + } else { + throw new SQLException("No active transaction"); + } + transactionOpen = false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns() + */ + @Override + public List<String> getPrimaryKeyColumns() { + return Collections.unmodifiableList(primaryKeyColumns); + } + + public String getVersionColumn() { + return versionColumn; + } + + public void setVersionColumn(String column) { + versionColumn = column; + } + + public String getTableName() { + return tableName; + } + + public SQLGenerator getSqlGenerator() { + return sqlGenerator; + } + + /** + * Executes the given query string using either the active connection if a + * transaction is already open, or a new connection from this query's + * connection pool. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @return ResultSet of the query + * @throws SQLException + */ + private ResultSet executeQuery(StatementHelper sh) throws SQLException { + Connection c = null; + if (transactionOpen && activeConnection != null) { + c = activeConnection; + } else { + throw new SQLException("No active transaction!"); + } + PreparedStatement pstmt = c.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + return pstmt.executeQuery(); + } + + /** + * Executes the given update query string using either the active connection + * if a transaction is already open, or a new connection from this query's + * connection pool. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @return Number of affected rows + * @throws SQLException + */ + private int executeUpdate(StatementHelper sh) throws SQLException { + Connection c = null; + PreparedStatement pstmt = null; + try { + if (transactionOpen && activeConnection != null) { + c = activeConnection; + } else { + c = connectionPool.reserveConnection(); + } + pstmt = c.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + int retval = pstmt.executeUpdate(); + return retval; + } finally { + if (pstmt != null) { + pstmt.clearParameters(); + pstmt.close(); + } + if (!transactionOpen) { + connectionPool.releaseConnection(c); + } + } + } + + /** + * Executes the given update query string using either the active connection + * if a transaction is already open, or a new connection from this query's + * connection pool. + * + * Additionally adds a new RowIdChangeEvent to the event buffer. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @param row + * the row item to update + * @return Number of affected rows + * @throws SQLException + */ + private int executeUpdateReturnKeys(StatementHelper sh, RowItem row) + throws SQLException { + Connection c = null; + PreparedStatement pstmt = null; + ResultSet genKeys = null; + try { + if (transactionOpen && activeConnection != null) { + c = activeConnection; + } else { + c = connectionPool.reserveConnection(); + } + pstmt = c.prepareStatement(sh.getQueryString(), + primaryKeyColumns.toArray(new String[0])); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + int result = pstmt.executeUpdate(); + genKeys = pstmt.getGeneratedKeys(); + RowId newId = getNewRowId(row, genKeys); + bufferedEvents.add(new RowIdChangeEvent(row.getId(), newId)); + return result; + } finally { + if (genKeys != null) { + genKeys.close(); + } + if (pstmt != null) { + pstmt.clearParameters(); + pstmt.close(); + } + if (!transactionOpen) { + connectionPool.releaseConnection(c); + } + } + } + + /** + * Fetches name(s) of primary key column(s) from DB metadata. + * + * Also tries to get the escape string to be used in search strings. + */ + private void fetchMetaData() { + Connection c = null; + try { + c = connectionPool.reserveConnection(); + DatabaseMetaData dbmd = c.getMetaData(); + if (dbmd != null) { + tableName = SQLUtil.escapeSQL(tableName); + ResultSet tables = dbmd.getTables(null, null, tableName, null); + if (!tables.next()) { + tables = dbmd.getTables(null, null, + tableName.toUpperCase(), null); + if (!tables.next()) { + throw new IllegalArgumentException( + "Table with the name \"" + + tableName + + "\" was not found. Check your database contents."); + } else { + tableName = tableName.toUpperCase(); + } + } + tables.close(); + ResultSet rs = dbmd.getPrimaryKeys(null, null, tableName); + List<String> names = new ArrayList<String>(); + while (rs.next()) { + names.add(rs.getString("COLUMN_NAME")); + } + rs.close(); + if (!names.isEmpty()) { + primaryKeyColumns = names; + } + if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { + throw new IllegalArgumentException( + "Primary key constraints have not been defined for the table \"" + + tableName + + "\". Use FreeFormQuery to access this table."); + } + for (String colName : primaryKeyColumns) { + if (colName.equalsIgnoreCase("rownum")) { + if (getSqlGenerator() instanceof MSSQLGenerator + || getSqlGenerator() instanceof MSSQLGenerator) { + throw new IllegalArgumentException( + "When using Oracle or MSSQL, a primary key column" + + " named \'rownum\' is not allowed!"); + } + } + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(c); + } + } + + private RowId getNewRowId(RowItem row, ResultSet genKeys) { + try { + /* Fetch primary key values and generate a map out of them. */ + Map<String, Object> values = new HashMap<String, Object>(); + ResultSetMetaData rsmd = genKeys.getMetaData(); + int colCount = rsmd.getColumnCount(); + if (genKeys.next()) { + for (int i = 1; i <= colCount; i++) { + values.put(rsmd.getColumnName(i), genKeys.getObject(i)); + } + } + /* Generate new RowId */ + List<Object> newRowId = new ArrayList<Object>(); + if (values.size() == 1) { + if (primaryKeyColumns.size() == 1) { + newRowId.add(values.get(values.keySet().iterator().next())); + } else { + for (String s : primaryKeyColumns) { + if (!((ColumnProperty) row.getItemProperty(s)) + .isReadOnlyChangeAllowed()) { + newRowId.add(values.get(values.keySet().iterator() + .next())); + } else { + newRowId.add(values.get(s)); + } + } + } + } else { + for (String s : primaryKeyColumns) { + newRowId.add(values.get(s)); + } + } + return new RowId(newRowId.toArray()); + } catch (Exception e) { + getLogger().log(Level.FINE, + "Failed to fetch key values on insert: " + e.getMessage()); + return null; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + @Override + public boolean removeRow(RowItem row) throws UnsupportedOperationException, + SQLException { + getLogger().log(Level.FINE, + "Removing row with id: " + row.getId().getId()[0].toString()); + if (executeUpdate(sqlGenerator.generateDeleteQuery(getTableName(), + primaryKeyColumns, versionColumn, row)) == 1) { + return true; + } + if (versionColumn != null) { + throw new OptimisticLockException( + "Someone else changed the row that was being deleted.", + row.getId()); + } + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#containsRowWithKey( + * java.lang.Object[]) + */ + @Override + public boolean containsRowWithKey(Object... keys) throws SQLException { + ArrayList<Filter> filtersAndKeys = new ArrayList<Filter>(); + if (filters != null) { + filtersAndKeys.addAll(filters); + } + int ix = 0; + for (String colName : primaryKeyColumns) { + filtersAndKeys.add(new Equal(colName, keys[ix])); + ix++; + } + StatementHelper sh = sqlGenerator.generateSelectQuery(tableName, + filtersAndKeys, orderBys, 0, 0, "*"); + + boolean shouldCloseTransaction = false; + if (!transactionOpen) { + shouldCloseTransaction = true; + beginTransaction(); + } + ResultSet rs = null; + try { + rs = executeQuery(sh); + boolean contains = rs.next(); + return contains; + } finally { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + } + rs.close(); + } + if (shouldCloseTransaction) { + commit(); + } + } + } + + /** + * Custom writeObject to call rollback() if object is serialized. + */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + try { + rollback(); + } catch (SQLException ignored) { + } + out.defaultWriteObject(); + } + + /** + * Simple RowIdChangeEvent implementation. + */ + public class RowIdChangeEvent extends EventObject implements + QueryDelegate.RowIdChangeEvent { + private final RowId oldId; + private final RowId newId; + + private RowIdChangeEvent(RowId oldId, RowId newId) { + super(oldId); + this.oldId = oldId; + this.newId = newId; + } + + @Override + public RowId getNewRowId() { + return newId; + } + + @Override + public RowId getOldRowId() { + return oldId; + } + } + + /** + * Adds RowIdChangeListener to this query + */ + @Override + public void addListener(RowIdChangeListener listener) { + if (rowIdChangeListeners == null) { + rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>(); + } + rowIdChangeListeners.add(listener); + } + + /** + * Removes the given RowIdChangeListener from this query + */ + @Override + public void removeListener(RowIdChangeListener listener) { + if (rowIdChangeListeners != null) { + rowIdChangeListeners.remove(listener); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(TableQuery.class.getName()); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java new file mode 100644 index 0000000000..6485330541 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java @@ -0,0 +1,367 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.ColumnProperty; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLUtil; +import com.vaadin.data.util.sqlcontainer.TemporaryRowId; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator; + +/** + * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +@SuppressWarnings("serial") +public class DefaultSQLGenerator implements SQLGenerator { + + private Class<? extends StatementHelper> statementHelperClass = null; + + public DefaultSQLGenerator() { + + } + + /** + * Create a new DefaultSqlGenerator instance that uses the given + * implementation of {@link StatementHelper} + * + * @param statementHelper + */ + public DefaultSQLGenerator( + Class<? extends StatementHelper> statementHelperClazz) { + this(); + statementHelperClass = statementHelperClazz; + } + + /** + * Construct a DefaultSQLGenerator with the specified identifiers for start + * and end of quoted strings. The identifiers may be different depending on + * the database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public DefaultSQLGenerator(String quoteStart, String quoteEnd) { + QueryBuilder.setStringDecorator(new StringDecorator(quoteStart, + quoteEnd)); + } + + /** + * Same as {@link #DefaultSQLGenerator(String, String)} but with support for + * custom {@link StatementHelper} implementation. + * + * @param quoteStart + * @param quoteEnd + * @param statementHelperClazz + */ + public DefaultSQLGenerator(String quoteStart, String quoteEnd, + Class<? extends StatementHelper> statementHelperClazz) { + this(quoteStart, quoteEnd); + statementHelperClass = statementHelperClazz; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, java.util.List, + * int, int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("SELECT " + toSelect + " FROM ").append( + SQLUtil.escapeSQL(tableName)); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + if (pagelength != 0) { + generateLimits(query, offset, pagelength); + } + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateUpdateQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateUpdateQuery(String tableName, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException("Updated item must be given."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("UPDATE ").append(tableName).append(" SET"); + + /* Generate column<->value and rowidentifiers map */ + Map<String, Object> columnToValueMap = generateColumnToValueMap(item); + Map<String, Object> rowIdentifiers = generateRowIdentifiers(item); + /* Generate columns and values to update */ + boolean first = true; + for (String column : columnToValueMap.keySet()) { + if (first) { + query.append(" " + QueryBuilder.quote(column) + " = ?"); + } else { + query.append(", " + QueryBuilder.quote(column) + " = ?"); + } + sh.addParameterValue(columnToValueMap.get(column), item + .getItemProperty(column).getType()); + first = false; + } + /* Generate identifiers for the row to be updated */ + first = true; + for (String column : rowIdentifiers.keySet()) { + if (first) { + query.append(" WHERE " + QueryBuilder.quote(column) + " = ?"); + } else { + query.append(" AND " + QueryBuilder.quote(column) + " = ?"); + } + sh.addParameterValue(rowIdentifiers.get(column), item + .getItemProperty(column).getType()); + first = false; + } + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateInsertQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateInsertQuery(String tableName, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException("New item must be given."); + } + if (!(item.getId() instanceof TemporaryRowId)) { + throw new IllegalArgumentException( + "Cannot generate an insert query for item already in database."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("INSERT INTO ").append(tableName).append(" ("); + + /* Generate column<->value map */ + Map<String, Object> columnToValueMap = generateColumnToValueMap(item); + /* Generate column names for insert query */ + boolean first = true; + for (String column : columnToValueMap.keySet()) { + if (!first) { + query.append(", "); + } + query.append(QueryBuilder.quote(column)); + first = false; + } + + /* Generate values for insert query */ + query.append(") VALUES ("); + first = true; + for (String column : columnToValueMap.keySet()) { + if (!first) { + query.append(", "); + } + query.append("?"); + sh.addParameterValue(columnToValueMap.get(column), item + .getItemProperty(column).getType()); + first = false; + } + query.append(")"); + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateDeleteQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateDeleteQuery(String tableName, + List<String> primaryKeyColumns, String versionColumn, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException( + "Item to be deleted must be given."); + } + if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { + throw new IllegalArgumentException( + "Valid keyColumnNames must be provided."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("DELETE FROM ").append(tableName).append(" WHERE "); + int count = 1; + for (String keyColName : primaryKeyColumns) { + if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator) + && keyColName.equalsIgnoreCase("rownum")) { + count++; + continue; + } + if (count > 1) { + query.append(" AND "); + } + if (item.getItemProperty(keyColName).getValue() != null) { + query.append(QueryBuilder.quote(keyColName) + " = ?"); + sh.addParameterValue(item.getItemProperty(keyColName) + .getValue(), item.getItemProperty(keyColName).getType()); + } + count++; + } + if (versionColumn != null) { + query.append(String.format(" AND %s = ?", + QueryBuilder.quote(versionColumn))); + sh.addParameterValue( + item.getItemProperty(versionColumn).getValue(), item + .getItemProperty(versionColumn).getType()); + } + + sh.setQueryString(query.toString()); + return sh; + } + + /** + * Generates sorting rules as an ORDER BY -clause + * + * @param sb + * StringBuffer to which the clause is appended. + * @param o + * OrderBy object to be added into the sb. + * @param firstOrderBy + * If true, this is the first OrderBy. + * @return + */ + protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o, + boolean firstOrderBy) { + if (firstOrderBy) { + sb.append(" ORDER BY "); + } else { + sb.append(", "); + } + sb.append(QueryBuilder.quote(o.getColumn())); + if (o.isAscending()) { + sb.append(" ASC"); + } else { + sb.append(" DESC"); + } + return sb; + } + + /** + * Generates the LIMIT and OFFSET clause. + * + * @param sb + * StringBuffer to which the clause is appended. + * @param offset + * Value for offset. + * @param pagelength + * Value for pagelength. + * @return StringBuffer with LIMIT and OFFSET clause added. + */ + protected StringBuffer generateLimits(StringBuffer sb, int offset, + int pagelength) { + sb.append(" LIMIT ").append(pagelength).append(" OFFSET ") + .append(offset); + return sb; + } + + protected Map<String, Object> generateColumnToValueMap(RowItem item) { + Map<String, Object> columnToValueMap = new HashMap<String, Object>(); + for (Object id : item.getItemPropertyIds()) { + ColumnProperty cp = (ColumnProperty) item.getItemProperty(id); + /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */ + if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator) + && cp.getPropertyId().equalsIgnoreCase("rownum")) { + continue; + } + Object value = cp.getValue() == null ? null : cp.getValue(); + /* Only include properties whose read-only status can be altered */ + if (cp.isReadOnlyChangeAllowed() && !cp.isVersionColumn()) { + columnToValueMap.put(cp.getPropertyId(), value); + } + } + return columnToValueMap; + } + + protected Map<String, Object> generateRowIdentifiers(RowItem item) { + Map<String, Object> rowIdentifiers = new HashMap<String, Object>(); + for (Object id : item.getItemPropertyIds()) { + ColumnProperty cp = (ColumnProperty) item.getItemProperty(id); + /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */ + if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator) + && cp.getPropertyId().equalsIgnoreCase("rownum")) { + continue; + } + Object value = cp.getValue() == null ? null : cp.getValue(); + if (!cp.isReadOnlyChangeAllowed() || cp.isVersionColumn()) { + rowIdentifiers.put(cp.getPropertyId(), value); + } + } + return rowIdentifiers; + } + + /** + * Returns the statement helper for the generator. Override this to handle + * platform specific data types. + * + * @see http://dev.vaadin.com/ticket/9148 + * @return a new instance of the statement helper + */ + protected StatementHelper getStatementHelper() { + if (statementHelperClass == null) { + return new StatementHelper(); + } + + try { + return statementHelperClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException( + "Unable to instantiate custom StatementHelper", e); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Unable to instantiate custom StatementHelper", e); + } + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java new file mode 100644 index 0000000000..13ef1d0090 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java @@ -0,0 +1,101 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class MSSQLGenerator extends DefaultSQLGenerator { + + public MSSQLGenerator() { + + } + + /** + * Construct a MSSQLGenerator with the specified identifiers for start and + * end of quoted strings. The identifiers may be different depending on the + * database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public MSSQLGenerator(String quoteStart, String quoteEnd) { + super(quoteStart, quoteEnd); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, + * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int, + * int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + /* Adjust offset and page length parameters to match "row numbers" */ + offset = pagelength > 1 ? ++offset : offset; + pagelength = pagelength > 1 ? --pagelength : pagelength; + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + + /* Row count request is handled here */ + if ("COUNT(*)".equalsIgnoreCase(toSelect)) { + query.append(String.format( + "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s", + QueryBuilder.quote("rowcount"), tableName)); + if (filters != null && !filters.isEmpty()) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS t"); + sh.setQueryString(query.toString()); + return sh; + } + + /* SELECT without row number constraints */ + if (offset == 0 && pagelength == 0) { + query.append("SELECT ").append(toSelect).append(" FROM ") + .append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + sh.setQueryString(query.toString()); + return sh; + } + + /* Remaining SELECT cases are handled here */ + query.append("SELECT * FROM (SELECT row_number() OVER ("); + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + query.append(") AS rownum, " + toSelect + " FROM ").append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS a WHERE a.rownum BETWEEN ").append(offset) + .append(" AND ").append(Integer.toString(offset + pagelength)); + sh.setQueryString(query.toString()); + return sh; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java new file mode 100644 index 0000000000..43a562d3a8 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java @@ -0,0 +1,112 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class OracleGenerator extends DefaultSQLGenerator { + + public OracleGenerator() { + + } + + public OracleGenerator(Class<? extends StatementHelper> statementHelperClazz) { + super(statementHelperClazz); + } + + /** + * Construct an OracleSQLGenerator with the specified identifiers for start + * and end of quoted strings. The identifiers may be different depending on + * the database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public OracleGenerator(String quoteStart, String quoteEnd) { + super(quoteStart, quoteEnd); + } + + public OracleGenerator(String quoteStart, String quoteEnd, + Class<? extends StatementHelper> statementHelperClazz) { + super(quoteStart, quoteEnd, statementHelperClazz); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, + * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int, + * int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + /* Adjust offset and page length parameters to match "row numbers" */ + offset = pagelength > 1 ? ++offset : offset; + pagelength = pagelength > 1 ? --pagelength : pagelength; + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + + /* Row count request is handled here */ + if ("COUNT(*)".equalsIgnoreCase(toSelect)) { + query.append(String.format( + "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s", + QueryBuilder.quote("rowcount"), tableName)); + if (filters != null && !filters.isEmpty()) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(")"); + sh.setQueryString(query.toString()); + return sh; + } + + /* SELECT without row number constraints */ + if (offset == 0 && pagelength == 0) { + query.append("SELECT ").append(toSelect).append(" FROM ") + .append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + sh.setQueryString(query.toString()); + return sh; + } + + /* Remaining SELECT cases are handled here */ + query.append(String + .format("SELECT * FROM (SELECT x.*, ROWNUM AS %s FROM (SELECT %s FROM %s", + QueryBuilder.quote("rownum"), toSelect, tableName)); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + query.append(String.format(") x) WHERE %s BETWEEN %d AND %d", + QueryBuilder.quote("rownum"), offset, offset + pagelength)); + sh.setQueryString(query.toString()); + return sh; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java new file mode 100644 index 0000000000..dde7077eee --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java @@ -0,0 +1,88 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.io.Serializable; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; + +/** + * The SQLGenerator interface is meant to be implemented for each different SQL + * syntax that is to be supported. By default there are implementations for + * HSQLDB, MySQL, PostgreSQL, MSSQL and Oracle syntaxes. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public interface SQLGenerator extends Serializable { + /** + * Generates a SELECT query with the provided parameters. Uses default + * filtering mode (INCLUSIVE). + * + * @param tableName + * Name of the table queried + * @param filters + * The filters, converted into a WHERE clause + * @param orderBys + * The the ordering conditions, converted into an ORDER BY clause + * @param offset + * The offset of the first row to be included + * @param pagelength + * The number of rows to be returned when the query executes + * @param toSelect + * String containing what to select, e.g. "*", "COUNT(*)" + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect); + + /** + * Generates an UPDATE query with the provided parameters. + * + * @param tableName + * Name of the table queried + * @param item + * RowItem containing the updated values update. + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateUpdateQuery(String tableName, RowItem item); + + /** + * Generates an INSERT query for inserting a new row with the provided + * values. + * + * @param tableName + * Name of the table queried + * @param item + * New RowItem to be inserted into the database. + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateInsertQuery(String tableName, RowItem item); + + /** + * Generates a DELETE query for deleting data related to the given RowItem + * from the database. + * + * @param tableName + * Name of the table queried + * @param primaryKeyColumns + * the names of the columns holding the primary key. Usually just + * one column, but might be several. + * @param versionColumn + * the column containing the version number of the row, null if + * versioning (optimistic locking) not enabled. + * @param item + * Item to be deleted from the database + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateDeleteQuery(String tableName, + List<String> primaryKeyColumns, String versionColumn, RowItem item); +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java new file mode 100644 index 0000000000..b012ce7685 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java @@ -0,0 +1,163 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * StatementHelper is a simple helper class that assists TableQuery and the + * query generators in filling a PreparedStatement. The actual statement is + * generated by the query generator methods, but the resulting statement and all + * the parameter values are stored in an instance of StatementHelper. + * + * This class will also fill the values with correct setters into the + * PreparedStatement on request. + */ +public class StatementHelper implements Serializable { + + private String queryString; + + private List<Object> parameters = new ArrayList<Object>(); + private Map<Integer, Class<?>> dataTypes = new HashMap<Integer, Class<?>>(); + + public StatementHelper() { + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + public String getQueryString() { + return queryString; + } + + public void addParameterValue(Object parameter) { + if (parameter != null) { + parameters.add(parameter); + dataTypes.put(parameters.size() - 1, parameter.getClass()); + } else { + throw new IllegalArgumentException( + "You cannot add null parameters using addParamaters(Object). " + + "Use addParameters(Object,Class) instead"); + } + } + + public void addParameterValue(Object parameter, Class<?> type) { + parameters.add(parameter); + dataTypes.put(parameters.size() - 1, type); + } + + public void setParameterValuesToStatement(PreparedStatement pstmt) + throws SQLException { + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) == null) { + handleNullValue(i, pstmt); + } else { + pstmt.setObject(i + 1, parameters.get(i)); + } + } + + /* + * The following list contains the data types supported by + * PreparedStatement but not supported by SQLContainer: + * + * [The list is provided as PreparedStatement method signatures] + * + * setNCharacterStream(int parameterIndex, Reader value) + * + * setNClob(int parameterIndex, NClob value) + * + * setNString(int parameterIndex, String value) + * + * setRef(int parameterIndex, Ref x) + * + * setRowId(int parameterIndex, RowId x) + * + * setSQLXML(int parameterIndex, SQLXML xmlObject) + * + * setBytes(int parameterIndex, byte[] x) + * + * setCharacterStream(int parameterIndex, Reader reader) + * + * setClob(int parameterIndex, Clob x) + * + * setURL(int parameterIndex, URL x) + * + * setArray(int parameterIndex, Array x) + * + * setAsciiStream(int parameterIndex, InputStream x) + * + * setBinaryStream(int parameterIndex, InputStream x) + * + * setBlob(int parameterIndex, Blob x) + */ + } + + private void handleNullValue(int i, PreparedStatement pstmt) + throws SQLException { + if (BigDecimal.class.equals(dataTypes.get(i))) { + pstmt.setBigDecimal(i + 1, null); + } else if (Boolean.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.BOOLEAN); + } else if (Byte.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.SMALLINT); + } else if (Date.class.equals(dataTypes.get(i))) { + pstmt.setDate(i + 1, null); + } else if (Double.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.DOUBLE); + } else if (Float.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.FLOAT); + } else if (Integer.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.INTEGER); + } else if (Long.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.BIGINT); + } else if (Short.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.SMALLINT); + } else if (String.class.equals(dataTypes.get(i))) { + pstmt.setString(i + 1, null); + } else if (Time.class.equals(dataTypes.get(i))) { + pstmt.setTime(i + 1, null); + } else if (Timestamp.class.equals(dataTypes.get(i))) { + pstmt.setTimestamp(i + 1, null); + } else { + + if (handleUnrecognizedTypeNullValue(i, pstmt, dataTypes)) { + return; + } + + throw new SQLException("Data type not supported by SQLContainer: " + + parameters.get(i).getClass().toString()); + } + } + + /** + * Handle unrecognized null values. Override this to handle null values for + * platform specific data types that are not handled by the default + * implementation of the {@link StatementHelper}. + * + * @param i + * @param pstmt + * @param dataTypes2 + * + * @return true if handled, false otherwise + * + * @see {@link http://dev.vaadin.com/ticket/9148} + */ + protected boolean handleUnrecognizedTypeNullValue(int i, + PreparedStatement pstmt, Map<Integer, Class<?>> dataTypes) + throws SQLException { + return false; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java new file mode 100644 index 0000000000..251a543a8a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java @@ -0,0 +1,23 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.And; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class AndTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof And; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder.getJoinedFilterString( + ((And) filter).getFilters(), "AND", sh)); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java new file mode 100644 index 0000000000..4fcaf759ea --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Between; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class BetweenTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Between; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Between between = (Between) filter; + sh.addParameterValue(between.getStartValue()); + sh.addParameterValue(between.getEndValue()); + return QueryBuilder.quote(between.getPropertyId()) + " BETWEEN ? AND ?"; + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java new file mode 100644 index 0000000000..4293e1d630 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class CompareTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Compare; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Compare compare = (Compare) filter; + sh.addParameterValue(compare.getValue()); + String prop = QueryBuilder.quote(compare.getPropertyId()); + switch (compare.getOperation()) { + case EQUAL: + return prop + " = ?"; + case GREATER: + return prop + " > ?"; + case GREATER_OR_EQUAL: + return prop + " >= ?"; + case LESS: + return prop + " < ?"; + case LESS_OR_EQUAL: + return prop + " <= ?"; + default: + return ""; + } + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java new file mode 100644 index 0000000000..84af9d5c97 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java @@ -0,0 +1,16 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public interface FilterTranslator extends Serializable { + public boolean translatesFilter(Filter filter); + + public String getWhereStringForFilter(Filter filter, StatementHelper sh); + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java new file mode 100644 index 0000000000..a2a6cd2c09 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java @@ -0,0 +1,22 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class IsNullTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof IsNull; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + IsNull in = (IsNull) filter; + return QueryBuilder.quote(in.getPropertyId()) + " IS NULL"; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java new file mode 100644 index 0000000000..25a85caec0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class LikeTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Like; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Like like = (Like) filter; + if (like.isCaseSensitive()) { + sh.addParameterValue(like.getValue()); + return QueryBuilder.quote(like.getPropertyId()) + " LIKE ?"; + } else { + sh.addParameterValue(like.getValue().toUpperCase()); + return "UPPER(" + QueryBuilder.quote(like.getPropertyId()) + + ") LIKE ?"; + } + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java new file mode 100644 index 0000000000..5dfbe240e7 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java @@ -0,0 +1,29 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.filter.Not; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class NotTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Not; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Not not = (Not) filter; + if (not.getFilter() instanceof IsNull) { + IsNull in = (IsNull) not.getFilter(); + return QueryBuilder.quote(in.getPropertyId()) + " IS NOT NULL"; + } + return "NOT " + + QueryBuilder.getWhereStringForFilter(not.getFilter(), sh); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java new file mode 100644 index 0000000000..2f0ed814e0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java @@ -0,0 +1,23 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Or; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class OrTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Or; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder.getJoinedFilterString( + ((Or) filter).getFilters(), "OR", sh)); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java new file mode 100644 index 0000000000..24be8963e0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java @@ -0,0 +1,98 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class QueryBuilder implements Serializable { + + private static ArrayList<FilterTranslator> filterTranslators = new ArrayList<FilterTranslator>(); + private static StringDecorator stringDecorator = new StringDecorator("\"", + "\""); + + static { + /* Register all default filter translators */ + addFilterTranslator(new AndTranslator()); + addFilterTranslator(new OrTranslator()); + addFilterTranslator(new LikeTranslator()); + addFilterTranslator(new BetweenTranslator()); + addFilterTranslator(new CompareTranslator()); + addFilterTranslator(new NotTranslator()); + addFilterTranslator(new IsNullTranslator()); + addFilterTranslator(new SimpleStringTranslator()); + } + + public synchronized static void addFilterTranslator( + FilterTranslator translator) { + filterTranslators.add(translator); + } + + /** + * Allows specification of a custom ColumnQuoter instance that handles + * quoting of column names for the current DB dialect. + * + * @param decorator + * the ColumnQuoter instance to use. + */ + public static void setStringDecorator(StringDecorator decorator) { + stringDecorator = decorator; + } + + public static String quote(Object str) { + return stringDecorator.quote(str); + } + + public static String group(String str) { + return stringDecorator.group(str); + } + + /** + * Constructs and returns a string representing the filter that can be used + * in a WHERE clause. + * + * @param filter + * the filter to translate + * @param sh + * the statement helper to update with the value(s) of the filter + * @return a string representing the filter. + */ + public synchronized static String getWhereStringForFilter(Filter filter, + StatementHelper sh) { + for (FilterTranslator ft : filterTranslators) { + if (ft.translatesFilter(filter)) { + return ft.getWhereStringForFilter(filter, sh); + } + } + return ""; + } + + public static String getJoinedFilterString(Collection<Filter> filters, + String joinString, StatementHelper sh) { + StringBuilder result = new StringBuilder(); + for (Filter f : filters) { + result.append(getWhereStringForFilter(f, sh)); + result.append(" ").append(joinString).append(" "); + } + // Remove the last instance of joinString + result.delete(result.length() - joinString.length() - 2, + result.length()); + return result.toString(); + } + + public static String getWhereStringForFilters(List<Filter> filters, + StatementHelper sh) { + if (filters == null || filters.isEmpty()) { + return ""; + } + StringBuilder where = new StringBuilder(" WHERE "); + where.append(getJoinedFilterString(filters, "AND", sh)); + return where.toString(); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java new file mode 100644 index 0000000000..f108003535 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class SimpleStringTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof SimpleStringFilter; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + SimpleStringFilter ssf = (SimpleStringFilter) filter; + // Create a Like filter based on the SimpleStringFilter and execute the + // LikeTranslator + String likeStr = ssf.isOnlyMatchPrefix() ? ssf.getFilterString() + "%" + : "%" + ssf.getFilterString() + "%"; + Like like = new Like(ssf.getPropertyId().toString(), likeStr); + like.setCaseSensitive(!ssf.isIgnoreCase()); + return new LikeTranslator().getWhereStringForFilter(like, sh); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java new file mode 100644 index 0000000000..8d2eabb5bc --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java @@ -0,0 +1,58 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; + +/** + * The StringDecorator knows how to produce a quoted string using the specified + * quote start and quote end characters. It also handles grouping of a string + * (surrounding it in parenthesis). + * + * Extend this class if you need to support special characters for grouping + * (parenthesis). + * + * @author Vaadin Ltd + */ +public class StringDecorator implements Serializable { + + private final String quoteStart; + private final String quoteEnd; + + /** + * Constructs a StringDecorator that uses the quoteStart and quoteEnd + * characters to create quoted strings. + * + * @param quoteStart + * the character denoting the start of a quote. + * @param quoteEnd + * the character denoting the end of a quote. + */ + public StringDecorator(String quoteStart, String quoteEnd) { + this.quoteStart = quoteStart; + this.quoteEnd = quoteEnd; + } + + /** + * Surround a string with quote characters. + * + * @param str + * the string to quote + * @return the quoted string + */ + public String quote(Object str) { + return quoteStart + str + quoteEnd; + } + + /** + * Groups a string by surrounding it in parenthesis + * + * @param str + * the string to group + * @return the grouped string + */ + public String group(String str) { + return "(" + str + ")"; + } +} diff --git a/server/src/com/vaadin/data/validator/AbstractStringValidator.java b/server/src/com/vaadin/data/validator/AbstractStringValidator.java new file mode 100644 index 0000000000..5267cc7b7b --- /dev/null +++ b/server/src/com/vaadin/data/validator/AbstractStringValidator.java @@ -0,0 +1,42 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * Validator base class for validating strings. + * <p> + * To include the value that failed validation in the exception message you can + * use "{0}" in the error message. This will be replaced with the failed value + * (converted to string using {@link #toString()}) or "null" if the value is + * null. + * </p> + * + * @author Vaadin Ltd. + * @version @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public abstract class AbstractStringValidator extends AbstractValidator<String> { + + /** + * Constructs a validator for strings. + * + * <p> + * Null and empty string values are always accepted. To reject empty values, + * set the field being validated as required. + * </p> + * + * @param errorMessage + * the message to be included in an {@link InvalidValueException} + * (with "{0}" replaced by the value that failed validation). + * */ + public AbstractStringValidator(String errorMessage) { + super(errorMessage); + } + + @Override + public Class<String> getType() { + return String.class; + } +} diff --git a/server/src/com/vaadin/data/validator/AbstractValidator.java b/server/src/com/vaadin/data/validator/AbstractValidator.java new file mode 100644 index 0000000000..8febe5338a --- /dev/null +++ b/server/src/com/vaadin/data/validator/AbstractValidator.java @@ -0,0 +1,139 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +import com.vaadin.data.Validator; + +/** + * Abstract {@link com.vaadin.data.Validator Validator} implementation that + * provides a basic Validator implementation except the + * {@link #isValidValue(Object)} method. + * <p> + * To include the value that failed validation in the exception message you can + * use "{0}" in the error message. This will be replaced with the failed value + * (converted to string using {@link #toString()}) or "null" if the value is + * null. + * </p> + * <p> + * The default implementation of AbstractValidator does not support HTML in + * error messages. To enable HTML support, override + * {@link InvalidValueException#getHtmlMessage()} and throw such exceptions from + * {@link #validate(Object)}. + * </p> + * <p> + * Since Vaadin 7, subclasses can either implement {@link #validate(Object)} + * directly or implement {@link #isValidValue(Object)} when migrating legacy + * applications. To check validity, {@link #validate(Object)} should be used. + * </p> + * + * @param <T> + * The type + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +public abstract class AbstractValidator<T> implements Validator { + + /** + * Error message that is included in an {@link InvalidValueException} if + * such is thrown. + */ + private String errorMessage; + + /** + * Constructs a validator with the given error message. + * + * @param errorMessage + * the message to be included in an {@link InvalidValueException} + * (with "{0}" replaced by the value that failed validation). + */ + public AbstractValidator(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * Since Vaadin 7, subclasses of AbstractValidator should override + * {@link #isValidValue(Object)} or {@link #validate(Object)} instead of + * {@link #isValid(Object)}. {@link #validate(Object)} should normally be + * used to check values. + * + * @param value + * @return true if the value is valid + */ + public boolean isValid(Object value) { + try { + validate(value); + return true; + } catch (InvalidValueException e) { + return false; + } + } + + /** + * Internally check the validity of a value. This method can be used to + * perform validation in subclasses if customization of the error message is + * not needed. Otherwise, subclasses should override + * {@link #validate(Object)} and the return value of this method is ignored. + * + * This method should not be called from outside the validator class itself. + * + * @param value + * @return + */ + protected abstract boolean isValidValue(T value); + + @Override + public void validate(Object value) throws InvalidValueException { + // isValidType ensures that value can safely be cast to TYPE + if (!isValidType(value) || !isValidValue((T) value)) { + String message = getErrorMessage().replace("{0}", + String.valueOf(value)); + throw new InvalidValueException(message); + } + } + + /** + * Checks the type of the value to validate to ensure it conforms with + * getType. Enables sub classes to handle the specific type instead of + * Object. + * + * @param value + * The value to check + * @return true if the value can safely be cast to the type specified by + * {@link #getType()} + */ + protected boolean isValidType(Object value) { + if (value == null) { + return true; + } + + return getType().isAssignableFrom(value.getClass()); + } + + /** + * Returns the message to be included in the exception in case the value + * does not validate. + * + * @return the error message provided in the constructor or using + * {@link #setErrorMessage(String)}. + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * Sets the message to be included in the exception in case the value does + * not validate. The exception message is typically shown to the end user. + * + * @param errorMessage + * the error message. "{0}" is automatically replaced by the + * value that did not validate. + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public abstract Class<T> getType(); +} diff --git a/server/src/com/vaadin/data/validator/BeanValidator.java b/server/src/com/vaadin/data/validator/BeanValidator.java new file mode 100644 index 0000000000..816ff79b83 --- /dev/null +++ b/server/src/com/vaadin/data/validator/BeanValidator.java @@ -0,0 +1,176 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.validator; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.MessageInterpolator.Context; +import javax.validation.Validation; +import javax.validation.ValidatorFactory; +import javax.validation.metadata.ConstraintDescriptor; + +import com.vaadin.data.Validator; + +/** + * Vaadin {@link Validator} using the JSR-303 (javax.validation) + * annotation-based bean validation. + * + * The annotations of the fields of the beans are used to determine the + * validation to perform. + * + * Note that a JSR-303 implementation (e.g. Hibernate Validator or Apache Bean + * Validation - formerly agimatec validation) must be present on the project + * classpath when using bean validation. + * + * @since 7.0 + * + * @author Petri Hakala + * @author Henri Sara + */ +public class BeanValidator implements Validator { + + private static final long serialVersionUID = 1L; + private static ValidatorFactory factory; + + private transient javax.validation.Validator javaxBeanValidator; + private String propertyName; + private Class<?> beanClass; + private Locale locale; + + /** + * Simple implementation of a message interpolator context that returns + * fixed values. + */ + protected static class SimpleContext implements Context, Serializable { + + private final Object value; + private final ConstraintDescriptor<?> descriptor; + + /** + * Create a simple immutable message interpolator context. + * + * @param value + * value being validated + * @param descriptor + * ConstraintDescriptor corresponding to the constraint being + * validated + */ + public SimpleContext(Object value, ConstraintDescriptor<?> descriptor) { + this.value = value; + this.descriptor = descriptor; + } + + @Override + public ConstraintDescriptor<?> getConstraintDescriptor() { + return descriptor; + } + + @Override + public Object getValidatedValue() { + return value; + } + + } + + /** + * Creates a Vaadin {@link Validator} utilizing JSR-303 bean validation. + * + * @param beanClass + * bean class based on which the validation should be performed + * @param propertyName + * property to validate + */ + public BeanValidator(Class<?> beanClass, String propertyName) { + this.beanClass = beanClass; + this.propertyName = propertyName; + locale = Locale.getDefault(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Validator#validate(java.lang.Object) + */ + @Override + public void validate(final Object value) throws InvalidValueException { + Set<?> violations = getJavaxBeanValidator().validateValue(beanClass, + propertyName, value); + if (violations.size() > 0) { + List<String> exceptions = new ArrayList<String>(); + for (Object v : violations) { + final ConstraintViolation<?> violation = (ConstraintViolation<?>) v; + String msg = getJavaxBeanValidatorFactory() + .getMessageInterpolator().interpolate( + violation.getMessageTemplate(), + new SimpleContext(value, violation + .getConstraintDescriptor()), locale); + exceptions.add(msg); + } + StringBuilder b = new StringBuilder(); + for (int i = 0; i < exceptions.size(); i++) { + if (i != 0) { + b.append("<br/>"); + } + b.append(exceptions.get(i)); + } + throw new InvalidValueException(b.toString()); + } + } + + /** + * Sets the locale used for validation error messages. + * + * Revalidation is not automatically triggered by setting the locale. + * + * @param locale + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Gets the locale used for validation error messages. + * + * @return locale used for validation + */ + public Locale getLocale() { + return locale; + } + + /** + * Returns the underlying JSR-303 bean validator factory used. A factory is + * created using {@link Validation} if necessary. + * + * @return {@link ValidatorFactory} to use + */ + protected static ValidatorFactory getJavaxBeanValidatorFactory() { + if (factory == null) { + factory = Validation.buildDefaultValidatorFactory(); + } + + return factory; + } + + /** + * Returns a shared Validator instance to use. An instance is created using + * the validator factory if necessary and thereafter reused by the + * {@link BeanValidator} instance. + * + * @return the JSR-303 {@link javax.validation.Validator} to use + */ + protected javax.validation.Validator getJavaxBeanValidator() { + if (javaxBeanValidator == null) { + javaxBeanValidator = getJavaxBeanValidatorFactory().getValidator(); + } + + return javaxBeanValidator; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/validator/CompositeValidator.java b/server/src/com/vaadin/data/validator/CompositeValidator.java new file mode 100644 index 0000000000..cad31c9d4d --- /dev/null +++ b/server/src/com/vaadin/data/validator/CompositeValidator.java @@ -0,0 +1,259 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.validator; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.data.Validator; + +/** + * The <code>CompositeValidator</code> allows you to chain (compose) many + * validators to validate one field. The contained validators may be required to + * all validate the value to validate or it may be enough that one contained + * validator validates the value. This behaviour is controlled by the modes + * <code>AND</code> and <code>OR</code>. + * + * @author Vaadin Ltd. + * @version @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CompositeValidator implements Validator { + + public enum CombinationMode { + /** + * The validators are combined with <code>AND</code> clause: validity of + * the composite implies validity of the all validators it is composed + * of must be valid. + */ + AND, + /** + * The validators are combined with <code>OR</code> clause: validity of + * the composite implies that some of validators it is composed of must + * be valid. + */ + OR; + } + + /** + * @deprecated from 7.0, use {@link CombinationMode#AND} instead   + */ + @Deprecated + public static final CombinationMode MODE_AND = CombinationMode.AND; + /** + * @deprecated from 7.0, use {@link CombinationMode#OR} instead   + */ + @Deprecated + public static final CombinationMode MODE_OR = CombinationMode.OR; + + private String errorMessage; + + /** + * Operation mode. + */ + private CombinationMode mode = CombinationMode.AND; + + /** + * List of contained validators. + */ + private final List<Validator> validators = new LinkedList<Validator>(); + + /** + * Construct a composite validator in <code>AND</code> mode without error + * message. + */ + public CompositeValidator() { + this(CombinationMode.AND, ""); + } + + /** + * Constructs a composite validator in given mode. + * + * @param mode + * @param errorMessage + */ + public CompositeValidator(CombinationMode mode, String errorMessage) { + setErrorMessage(errorMessage); + setMode(mode); + } + + /** + * Validates the given value. + * <p> + * The value is valid, if: + * <ul> + * <li><code>MODE_AND</code>: All of the sub-validators are valid + * <li><code>MODE_OR</code>: Any of the sub-validators are valid + * </ul> + * + * If the value is invalid, validation error is thrown. If the error message + * is set (non-null), it is used. If the error message has not been set, the + * first error occurred is thrown. + * </p> + * + * @param value + * the value to check. + * @throws Validator.InvalidValueException + * if the value is not valid. + */ + @Override + public void validate(Object value) throws Validator.InvalidValueException { + switch (mode) { + case AND: + for (Validator validator : validators) { + validator.validate(value); + } + return; + + case OR: + Validator.InvalidValueException first = null; + for (Validator v : validators) { + try { + v.validate(value); + return; + } catch (final Validator.InvalidValueException e) { + if (first == null) { + first = e; + } + } + } + if (first == null) { + return; + } + final String em = getErrorMessage(); + if (em != null) { + throw new Validator.InvalidValueException(em); + } else { + throw first; + } + } + } + + /** + * Gets the mode of the validator. + * + * @return Operation mode of the validator: {@link CombinationMode#AND} or + * {@link CombinationMode#OR}. + */ + public final CombinationMode getMode() { + return mode; + } + + /** + * Sets the mode of the validator. The valid modes are: + * <ul> + * <li>{@link CombinationMode#AND} (default) + * <li>{@link CombinationMode#OR} + * </ul> + * + * @param mode + * the mode to set. + */ + public void setMode(CombinationMode mode) { + if (mode == null) { + throw new IllegalArgumentException( + "The validator can't be set to null"); + } + this.mode = mode; + } + + /** + * Gets the error message for the composite validator. If the error message + * is null, original error messages of the sub-validators are used instead. + */ + public String getErrorMessage() { + if (errorMessage != null) { + return errorMessage; + } + + // TODO Return composite error message + + return null; + } + + /** + * Adds validator to the interface. + * + * @param validator + * the Validator object which performs validation checks on this + * set of data field values. + */ + public void addValidator(Validator validator) { + if (validator == null) { + return; + } + validators.add(validator); + } + + /** + * Removes a validator from the composite. + * + * @param validator + * the Validator object which performs validation checks on this + * set of data field values. + */ + public void removeValidator(Validator validator) { + validators.remove(validator); + } + + /** + * Gets sub-validators by class. + * + * <p> + * If the component contains directly or recursively (it contains another + * composite containing the validator) validators compatible with given type + * they are returned. This only applies to <code>AND</code> mode composite + * validators. + * </p> + * + * <p> + * If the validator is in <code>OR</code> mode or does not contain any + * validators of given type null is returned. + * </p> + * + * @param validatorType + * The type of validators to return + * + * @return Collection<Validator> of validators compatible with given type + * that must apply or null if none found. + */ + public Collection<Validator> getSubValidators(Class validatorType) { + if (mode != CombinationMode.AND) { + return null; + } + + final HashSet<Validator> found = new HashSet<Validator>(); + for (Validator v : validators) { + if (validatorType.isAssignableFrom(v.getClass())) { + found.add(v); + } + if (v instanceof CompositeValidator + && ((CompositeValidator) v).getMode() == MODE_AND) { + final Collection<Validator> c = ((CompositeValidator) v) + .getSubValidators(validatorType); + if (c != null) { + found.addAll(c); + } + } + } + + return found.isEmpty() ? null : found; + } + + /** + * Sets the message to be included in the exception in case the value does + * not validate. The exception message is typically shown to the end user. + * + * @param errorMessage + * the error message. + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + +} diff --git a/server/src/com/vaadin/data/validator/DateRangeValidator.java b/server/src/com/vaadin/data/validator/DateRangeValidator.java new file mode 100644 index 0000000000..24f3d3ce10 --- /dev/null +++ b/server/src/com/vaadin/data/validator/DateRangeValidator.java @@ -0,0 +1,51 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +import java.util.Date; + +import com.vaadin.ui.DateField.Resolution; + +/** + * Validator for validating that a Date is inside a given range. + * + * <p> + * Note that the comparison is done directly on the Date object so take care + * that the hours/minutes/seconds/milliseconds of the min/max values are + * properly set. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ +public class DateRangeValidator extends RangeValidator<Date> { + + /** + * Creates a validator for checking that an Date is within a given range. + * <p> + * By default the range is inclusive i.e. both minValue and maxValue are + * valid values. Use {@link #setMinValueIncluded(boolean)} or + * {@link #setMaxValueIncluded(boolean)} to change it. + * </p> + * <p> + * Note that the comparison is done directly on the Date object so take care + * that the hours/minutes/seconds/milliseconds of the min/max values are + * properly set. + * </p> + * + * @param errorMessage + * the message to display in case the value does not validate. + * @param minValue + * The minimum value to accept or null for no limit + * @param maxValue + * The maximum value to accept or null for no limit + */ + public DateRangeValidator(String errorMessage, Date minValue, + Date maxValue, Resolution resolution) { + super(errorMessage, Date.class, minValue, maxValue); + } + +} diff --git a/server/src/com/vaadin/data/validator/DoubleRangeValidator.java b/server/src/com/vaadin/data/validator/DoubleRangeValidator.java new file mode 100644 index 0000000000..05ae2f827e --- /dev/null +++ b/server/src/com/vaadin/data/validator/DoubleRangeValidator.java @@ -0,0 +1,37 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * Validator for validating that a {@link Double} is inside a given range. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ +@SuppressWarnings("serial") +public class DoubleRangeValidator extends RangeValidator<Double> { + + /** + * Creates a validator for checking that an Double is within a given range. + * + * By default the range is inclusive i.e. both minValue and maxValue are + * valid values. Use {@link #setMinValueIncluded(boolean)} or + * {@link #setMaxValueIncluded(boolean)} to change it. + * + * + * @param errorMessage + * the message to display in case the value does not validate. + * @param minValue + * The minimum value to accept or null for no limit + * @param maxValue + * The maximum value to accept or null for no limit + */ + public DoubleRangeValidator(String errorMessage, Double minValue, + Double maxValue) { + super(errorMessage, Double.class, minValue, maxValue); + } + +} diff --git a/server/src/com/vaadin/data/validator/DoubleValidator.java b/server/src/com/vaadin/data/validator/DoubleValidator.java new file mode 100644 index 0000000000..18f1909add --- /dev/null +++ b/server/src/com/vaadin/data/validator/DoubleValidator.java @@ -0,0 +1,58 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * String validator for a double precision floating point number. See + * {@link com.vaadin.data.validator.AbstractStringValidator} for more + * information. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + * @deprecated in Vaadin 7.0. Use an Double converter on the field instead. + */ +@Deprecated +@SuppressWarnings("serial") +public class DoubleValidator extends AbstractStringValidator { + + /** + * Creates a validator for checking that a string can be parsed as an + * double. + * + * @param errorMessage + * the message to display in case the value does not validate. + * @deprecated in Vaadin 7.0. Use a Double converter on the field instead + * and/or use a {@link DoubleRangeValidator} for validating that + * the value is inside a given range. + */ + @Deprecated + public DoubleValidator(String errorMessage) { + super(errorMessage); + } + + @Override + protected boolean isValidValue(String value) { + try { + Double.parseDouble(value); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void validate(Object value) throws InvalidValueException { + if (value != null && value instanceof Double) { + // Allow Doubles to pass through the validator for easier + // migration. Otherwise a TextField connected to an double property + // with a DoubleValidator will fail. + return; + } + + super.validate(value); + } + +} diff --git a/server/src/com/vaadin/data/validator/EmailValidator.java b/server/src/com/vaadin/data/validator/EmailValidator.java new file mode 100644 index 0000000000..c76d7e13dc --- /dev/null +++ b/server/src/com/vaadin/data/validator/EmailValidator.java @@ -0,0 +1,35 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * String validator for e-mail addresses. The e-mail address syntax is not + * complete according to RFC 822 but handles the vast majority of valid e-mail + * addresses correctly. + * + * See {@link com.vaadin.data.validator.AbstractStringValidator} for more + * information. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public class EmailValidator extends RegexpValidator { + + /** + * Creates a validator for checking that a string is a syntactically valid + * e-mail address. + * + * @param errorMessage + * the message to display in case the value does not validate. + */ + public EmailValidator(String errorMessage) { + super( + "^([a-zA-Z0-9_\\.\\-+])+@(([a-zA-Z0-9-])+\\.)+([a-zA-Z0-9]{2,4})+$", + true, errorMessage); + } + +} diff --git a/server/src/com/vaadin/data/validator/IntegerRangeValidator.java b/server/src/com/vaadin/data/validator/IntegerRangeValidator.java new file mode 100644 index 0000000000..c171dd97d8 --- /dev/null +++ b/server/src/com/vaadin/data/validator/IntegerRangeValidator.java @@ -0,0 +1,37 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * Validator for validating that an {@link Integer} is inside a given range. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public class IntegerRangeValidator extends RangeValidator<Integer> { + + /** + * Creates a validator for checking that an Integer is within a given range. + * + * By default the range is inclusive i.e. both minValue and maxValue are + * valid values. Use {@link #setMinValueIncluded(boolean)} or + * {@link #setMaxValueIncluded(boolean)} to change it. + * + * + * @param errorMessage + * the message to display in case the value does not validate. + * @param minValue + * The minimum value to accept or null for no limit + * @param maxValue + * The maximum value to accept or null for no limit + */ + public IntegerRangeValidator(String errorMessage, Integer minValue, + Integer maxValue) { + super(errorMessage, Integer.class, minValue, maxValue); + } + +} diff --git a/server/src/com/vaadin/data/validator/IntegerValidator.java b/server/src/com/vaadin/data/validator/IntegerValidator.java new file mode 100644 index 0000000000..88ae9f3f0b --- /dev/null +++ b/server/src/com/vaadin/data/validator/IntegerValidator.java @@ -0,0 +1,58 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * String validator for integers. See + * {@link com.vaadin.data.validator.AbstractStringValidator} for more + * information. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + * @deprecated in Vaadin 7.0. Use an Integer converter on the field instead. + */ +@SuppressWarnings("serial") +@Deprecated +public class IntegerValidator extends AbstractStringValidator { + + /** + * Creates a validator for checking that a string can be parsed as an + * integer. + * + * @param errorMessage + * the message to display in case the value does not validate. + * @deprecated in Vaadin 7.0. Use an Integer converter on the field instead + * and/or use an {@link IntegerRangeValidator} for validating + * that the value is inside a given range. + */ + @Deprecated + public IntegerValidator(String errorMessage) { + super(errorMessage); + + } + + @Override + protected boolean isValidValue(String value) { + try { + Integer.parseInt(value); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void validate(Object value) throws InvalidValueException { + if (value != null && value instanceof Integer) { + // Allow Integers to pass through the validator for easier + // migration. Otherwise a TextField connected to an integer property + // with an IntegerValidator will fail. + return; + } + + super.validate(value); + } +} diff --git a/server/src/com/vaadin/data/validator/NullValidator.java b/server/src/com/vaadin/data/validator/NullValidator.java new file mode 100644 index 0000000000..551d88c776 --- /dev/null +++ b/server/src/com/vaadin/data/validator/NullValidator.java @@ -0,0 +1,92 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.validator; + +import com.vaadin.data.Validator; + +/** + * This validator is used for validating properties that do or do not allow null + * values. By default, nulls are not allowed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class NullValidator implements Validator { + + private boolean onlyNullAllowed; + + private String errorMessage; + + /** + * Creates a new NullValidator. + * + * @param errorMessage + * the error message to display on invalidation. + * @param onlyNullAllowed + * Are only nulls allowed? + */ + public NullValidator(String errorMessage, boolean onlyNullAllowed) { + setErrorMessage(errorMessage); + setNullAllowed(onlyNullAllowed); + } + + /** + * Validates the data given in value. + * + * @param value + * the value to validate. + * @throws Validator.InvalidValueException + * if the value was invalid. + */ + @Override + public void validate(Object value) throws Validator.InvalidValueException { + if ((onlyNullAllowed && value != null) + || (!onlyNullAllowed && value == null)) { + throw new Validator.InvalidValueException(errorMessage); + } + } + + /** + * Returns <code>true</code> if nulls are allowed otherwise + * <code>false</code>. + */ + public final boolean isNullAllowed() { + return onlyNullAllowed; + } + + /** + * Sets if nulls (and only nulls) are to be allowed. + * + * @param onlyNullAllowed + * If true, only nulls are allowed. If false only non-nulls are + * allowed. Do we allow nulls? + */ + public void setNullAllowed(boolean onlyNullAllowed) { + this.onlyNullAllowed = onlyNullAllowed; + } + + /** + * Gets the error message that is displayed in case the value is invalid. + * + * @return the Error Message. + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * Sets the error message to be displayed on invalid value. + * + * @param errorMessage + * the Error Message to set. + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + +} diff --git a/server/src/com/vaadin/data/validator/RangeValidator.java b/server/src/com/vaadin/data/validator/RangeValidator.java new file mode 100644 index 0000000000..433271274f --- /dev/null +++ b/server/src/com/vaadin/data/validator/RangeValidator.java @@ -0,0 +1,186 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +/** + * An base implementation for validating any objects that implement + * {@link Comparable}. + * + * Verifies that the value is of the given type and within the (optionally) + * given limits. Typically you want to use a sub class of this like + * {@link IntegerRangeValidator}, {@link DoubleRangeValidator} or + * {@link DateRangeValidator} in applications. + * <p> + * Note that {@link RangeValidator} always accept null values. Make a field + * required to ensure that no empty values are accepted or override + * {@link #isValidValue(Comparable)}. + * </p> + * + * @param <T> + * The type of Number to validate. Must implement Comparable so that + * minimum and maximum checks work. + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ +public class RangeValidator<T extends Comparable> extends AbstractValidator<T> { + + private T minValue = null; + private boolean minValueIncluded = true; + private T maxValue = null; + private boolean maxValueIncluded = true; + private Class<T> type; + + /** + * Creates a new range validator of the given type. + * + * @param errorMessage + * The error message to use if validation fails + * @param type + * The type of object the validator can validate. + * @param minValue + * The minimum value that should be accepted or null for no limit + * @param maxValue + * The maximum value that should be accepted or null for no limit + */ + public RangeValidator(String errorMessage, Class<T> type, T minValue, + T maxValue) { + super(errorMessage); + this.type = type; + this.minValue = minValue; + this.maxValue = maxValue; + } + + /** + * Checks if the minimum value is part of the accepted range + * + * @return true if the minimum value is part of the range, false otherwise + */ + public boolean isMinValueIncluded() { + return minValueIncluded; + } + + /** + * Sets if the minimum value is part of the accepted range + * + * @param minValueIncluded + * true if the minimum value should be part of the range, false + * otherwise + */ + public void setMinValueIncluded(boolean minValueIncluded) { + this.minValueIncluded = minValueIncluded; + } + + /** + * Checks if the maximum value is part of the accepted range + * + * @return true if the maximum value is part of the range, false otherwise + */ + public boolean isMaxValueIncluded() { + return maxValueIncluded; + } + + /** + * Sets if the maximum value is part of the accepted range + * + * @param maxValueIncluded + * true if the maximum value should be part of the range, false + * otherwise + */ + public void setMaxValueIncluded(boolean maxValueIncluded) { + this.maxValueIncluded = maxValueIncluded; + } + + /** + * Gets the minimum value of the range + * + * @return the minimum value + */ + public T getMinValue() { + return minValue; + } + + /** + * Sets the minimum value of the range. Use + * {@link #setMinValueIncluded(boolean)} to control whether this value is + * part of the range or not. + * + * @param minValue + * the minimum value + */ + public void setMinValue(T minValue) { + this.minValue = minValue; + } + + /** + * Gets the maximum value of the range + * + * @return the maximum value + */ + public T getMaxValue() { + return maxValue; + } + + /** + * Sets the maximum value of the range. Use + * {@link #setMaxValueIncluded(boolean)} to control whether this value is + * part of the range or not. + * + * @param maxValue + * the maximum value + */ + public void setMaxValue(T maxValue) { + this.maxValue = maxValue; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object + * ) + */ + @Override + protected boolean isValidValue(T value) { + if (value == null) { + return true; + } + + if (getMinValue() != null) { + // Ensure that the min limit is ok + int result = value.compareTo(getMinValue()); + if (result < 0) { + // value less than min value + return false; + } else if (result == 0 && !isMinValueIncluded()) { + // values equal and min value not included + return false; + } + } + if (getMaxValue() != null) { + // Ensure that the Max limit is ok + int result = value.compareTo(getMaxValue()); + if (result > 0) { + // value greater than max value + return false; + } else if (result == 0 && !isMaxValueIncluded()) { + // values equal and max value not included + return false; + } + } + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.validator.AbstractValidator#getType() + */ + @Override + public Class<T> getType() { + return type; + } + +} diff --git a/server/src/com/vaadin/data/validator/RegexpValidator.java b/server/src/com/vaadin/data/validator/RegexpValidator.java new file mode 100644 index 0000000000..8143d54c97 --- /dev/null +++ b/server/src/com/vaadin/data/validator/RegexpValidator.java @@ -0,0 +1,97 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.validator; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * String validator comparing the string against a Java regular expression. Both + * complete matches and substring matches are supported. + * + * <p> + * For the Java regular expression syntax, see + * {@link java.util.regex.Pattern#sum} + * </p> + * <p> + * See {@link com.vaadin.data.validator.AbstractStringValidator} for more + * information. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public class RegexpValidator extends AbstractStringValidator { + + private Pattern pattern; + private boolean complete; + private transient Matcher matcher = null; + + /** + * Creates a validator for checking that the regular expression matches the + * complete string to validate. + * + * @param regexp + * a Java regular expression + * @param errorMessage + * the message to display in case the value does not validate. + */ + public RegexpValidator(String regexp, String errorMessage) { + this(regexp, true, errorMessage); + } + + /** + * Creates a validator for checking that the regular expression matches the + * string to validate. + * + * @param regexp + * a Java regular expression + * @param complete + * true to use check for a complete match, false to look for a + * matching substring + * @param errorMessage + * the message to display in case the value does not validate. + */ + public RegexpValidator(String regexp, boolean complete, String errorMessage) { + super(errorMessage); + pattern = Pattern.compile(regexp); + this.complete = complete; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object + * ) + */ + @Override + protected boolean isValidValue(String value) { + if (complete) { + return getMatcher(value).matches(); + } else { + return getMatcher(value).find(); + } + } + + /** + * Get a new or reused matcher for the pattern + * + * @param value + * the string to find matches in + * @return Matcher for the string + */ + private Matcher getMatcher(String value) { + if (matcher == null) { + matcher = pattern.matcher(value); + } else { + matcher.reset(value); + } + return matcher; + } + +} diff --git a/server/src/com/vaadin/data/validator/StringLengthValidator.java b/server/src/com/vaadin/data/validator/StringLengthValidator.java new file mode 100644 index 0000000000..54b2d28f58 --- /dev/null +++ b/server/src/com/vaadin/data/validator/StringLengthValidator.java @@ -0,0 +1,139 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.data.validator; + +/** + * This <code>StringLengthValidator</code> is used to validate the length of + * strings. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class StringLengthValidator extends AbstractStringValidator { + + private Integer minLength = null; + + private Integer maxLength = null; + + private boolean allowNull = true; + + /** + * Creates a new StringLengthValidator with a given error message. + * + * @param errorMessage + * the message to display in case the value does not validate. + */ + public StringLengthValidator(String errorMessage) { + super(errorMessage); + } + + /** + * Creates a new StringLengthValidator with a given error message and + * minimum and maximum length limits. + * + * @param errorMessage + * the message to display in case the value does not validate. + * @param minLength + * the minimum permissible length of the string or null for no + * limit. A negative value for no limit is also supported for + * backwards compatibility. + * @param maxLength + * the maximum permissible length of the string or null for no + * limit. A negative value for no limit is also supported for + * backwards compatibility. + * @param allowNull + * Are null strings permissible? This can be handled better by + * setting a field as required or not. + */ + public StringLengthValidator(String errorMessage, Integer minLength, + Integer maxLength, boolean allowNull) { + this(errorMessage); + setMinLength(minLength); + setMaxLength(maxLength); + setNullAllowed(allowNull); + } + + /** + * Checks if the given value is valid. + * + * @param value + * the value to validate. + * @return <code>true</code> for valid value, otherwise <code>false</code>. + */ + @Override + protected boolean isValidValue(String value) { + if (value == null) { + return allowNull; + } + final int len = value.length(); + if ((minLength != null && minLength > -1 && len < minLength) + || (maxLength != null && maxLength > -1 && len > maxLength)) { + return false; + } + return true; + } + + /** + * Returns <code>true</code> if null strings are allowed. + * + * @return <code>true</code> if allows null string, otherwise + * <code>false</code>. + */ + @Deprecated + public final boolean isNullAllowed() { + return allowNull; + } + + /** + * Gets the maximum permissible length of the string. + * + * @return the maximum length of the string or null if there is no limit + */ + public Integer getMaxLength() { + return maxLength; + } + + /** + * Gets the minimum permissible length of the string. + * + * @return the minimum length of the string or null if there is no limit + */ + public Integer getMinLength() { + return minLength; + } + + /** + * Sets whether null-strings are to be allowed. This can be better handled + * by setting a field as required or not. + */ + @Deprecated + public void setNullAllowed(boolean allowNull) { + this.allowNull = allowNull; + } + + /** + * Sets the maximum permissible length of the string. + * + * @param maxLength + * the maximum length to accept or null for no limit + */ + public void setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + } + + /** + * Sets the minimum permissible length. + * + * @param minLength + * the minimum length to accept or null for no limit + */ + public void setMinLength(Integer minLength) { + this.minLength = minLength; + } + +} diff --git a/server/src/com/vaadin/data/validator/package.html b/server/src/com/vaadin/data/validator/package.html new file mode 100644 index 0000000000..c991bfc82a --- /dev/null +++ b/server/src/com/vaadin/data/validator/package.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides various {@link com.vaadin.data.Validator} +implementations.</p> + +<p>{@link com.vaadin.data.validator.AbstractValidator +AbstractValidator} provides an abstract implementation of the {@link +com.vaadin.data.Validator} interface and can be extended for custom +validation needs. {@link +com.vaadin.data.validator.AbstractStringValidator +AbstractStringValidator} can also be extended if the value is a String.</p> + + +</body> +</html> diff --git a/server/src/com/vaadin/event/Action.java b/server/src/com/vaadin/event/Action.java new file mode 100644 index 0000000000..6c218c25dc --- /dev/null +++ b/server/src/com/vaadin/event/Action.java @@ -0,0 +1,195 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.io.Serializable; + +import com.vaadin.terminal.Resource; + +/** + * Implements the action framework. This class contains subinterfaces for action + * handling and listing, and for action handler registrations and + * unregistration. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Action implements Serializable { + + /** + * Action title. + */ + private String caption; + + /** + * Action icon. + */ + private Resource icon = null; + + /** + * Constructs a new action with the given caption. + * + * @param caption + * the caption for the new action. + */ + public Action(String caption) { + this.caption = caption; + } + + /** + * Constructs a new action with the given caption string and icon. + * + * @param caption + * the caption for the new action. + * @param icon + * the icon for the new action. + */ + public Action(String caption, Resource icon) { + this.caption = caption; + this.icon = icon; + } + + /** + * Returns the action's caption. + * + * @return the action's caption as a <code>String</code>. + */ + public String getCaption() { + return caption; + } + + /** + * Returns the action's icon. + * + * @return the action's Icon. + */ + public Resource getIcon() { + return icon; + } + + /** + * An Action that implements this interface can be added to an + * Action.Notifier (or NotifierProxy) via the <code>addAction()</code> + * -method, which in many cases is easier than implementing the + * Action.Handler interface.<br/> + * + */ + public interface Listener extends Serializable { + public void handleAction(Object sender, Object target); + } + + /** + * Action.Containers implementing this support an easier way of adding + * single Actions than the more involved Action.Handler. The added actions + * must be Action.Listeners, thus handling the action themselves. + * + */ + public interface Notifier extends Container { + public <T extends Action & Action.Listener> void addAction(T action); + + public <T extends Action & Action.Listener> void removeAction(T action); + } + + public interface ShortcutNotifier extends Serializable { + public void addShortcutListener(ShortcutListener shortcut); + + public void removeShortcutListener(ShortcutListener shortcut); + } + + /** + * Interface implemented by classes who wish to handle actions. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Handler extends Serializable { + + /** + * Gets the list of actions applicable to this handler. + * + * @param target + * the target handler to list actions for. For item + * containers this is the item id. + * @param sender + * the party that would be sending the actions. Most of this + * is the action container. + * @return the list of Action + */ + public Action[] getActions(Object target, Object sender); + + /** + * Handles an action for the given target. The handler method may just + * discard the action if it's not suitable. + * + * @param action + * the action to be handled. + * @param sender + * the sender of the action. This is most often the action + * container. + * @param target + * the target of the action. For item containers this is the + * item id. + */ + public void handleAction(Action action, Object sender, Object target); + } + + /** + * Interface implemented by all components where actions can be registered. + * This means that the components lets others to register as action handlers + * to it. When the component receives an action targeting its contents it + * should loop all action handlers registered to it and let them handle the + * action. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Container extends Serializable { + + /** + * Registers a new action handler for this container + * + * @param actionHandler + * the new handler to be added. + */ + public void addActionHandler(Action.Handler actionHandler); + + /** + * Removes a previously registered action handler for the contents of + * this container. + * + * @param actionHandler + * the handler to be removed. + */ + public void removeActionHandler(Action.Handler actionHandler); + } + + /** + * Sets the caption. + * + * @param caption + * the caption to set. + */ + public void setCaption(String caption) { + this.caption = caption; + } + + /** + * Sets the icon. + * + * @param icon + * the icon to set. + */ + public void setIcon(Resource icon) { + this.icon = icon; + } + +} diff --git a/server/src/com/vaadin/event/ActionManager.java b/server/src/com/vaadin/event/ActionManager.java new file mode 100644 index 0000000000..64fdeea69b --- /dev/null +++ b/server/src/com/vaadin/event/ActionManager.java @@ -0,0 +1,249 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.util.HashSet; +import java.util.Map; + +import com.vaadin.event.Action.Container; +import com.vaadin.event.Action.Handler; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.ui.Component; + +/** + * Javadoc TODO + * + * Notes: + * <p> + * Empties the keymapper for each repaint to avoid leaks; can cause problems in + * the future if the client assumes key don't change. (if lazyloading, one must + * not cache results) + * </p> + * + * + */ +public class ActionManager implements Action.Container, Action.Handler, + Action.Notifier { + + private static final long serialVersionUID = 1641868163608066491L; + + /** List of action handlers */ + protected HashSet<Action> ownActions = null; + + /** List of action handlers */ + protected HashSet<Handler> actionHandlers = null; + + /** Action mapper */ + protected KeyMapper<Action> actionMapper = null; + + protected Component viewer; + + private boolean clientHasActions = false; + + public ActionManager() { + + } + + public <T extends Component & Container & VariableOwner> ActionManager( + T viewer) { + this.viewer = viewer; + } + + private void requestRepaint() { + if (viewer != null) { + viewer.requestRepaint(); + } + } + + public <T extends Component & Container & VariableOwner> void setViewer( + T viewer) { + if (viewer == this.viewer) { + return; + } + if (this.viewer != null) { + ((Container) this.viewer).removeActionHandler(this); + } + requestRepaint(); // this goes to the old viewer + if (viewer != null) { + viewer.addActionHandler(this); + } + this.viewer = viewer; + requestRepaint(); // this goes to the new viewer + } + + @Override + public <T extends Action & Action.Listener> void addAction(T action) { + if (ownActions == null) { + ownActions = new HashSet<Action>(); + } + if (ownActions.add(action)) { + requestRepaint(); + } + } + + @Override + public <T extends Action & Action.Listener> void removeAction(T action) { + if (ownActions != null) { + if (ownActions.remove(action)) { + requestRepaint(); + } + } + } + + @Override + public void addActionHandler(Handler actionHandler) { + if (actionHandler == this) { + // don't add the actionHandler to itself + return; + } + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new HashSet<Handler>(); + } + + if (actionHandlers.add(actionHandler)) { + requestRepaint(); + } + } + } + + @Override + public void removeActionHandler(Action.Handler actionHandler) { + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + if (actionHandlers.remove(actionHandler)) { + requestRepaint(); + } + if (actionHandlers.isEmpty()) { + actionHandlers = null; + } + + } + } + + public void removeAllActionHandlers() { + if (actionHandlers != null) { + actionHandlers = null; + requestRepaint(); + } + } + + public void paintActions(Object actionTarget, PaintTarget paintTarget) + throws PaintException { + + actionMapper = null; + + HashSet<Action> actions = new HashSet<Action>(); + if (actionHandlers != null) { + for (Action.Handler handler : actionHandlers) { + Action[] as = handler.getActions(actionTarget, viewer); + if (as != null) { + for (Action action : as) { + actions.add(action); + } + } + } + } + if (ownActions != null) { + actions.addAll(ownActions); + } + + /* + * Must repaint whenever there are actions OR if all actions have been + * removed but still exist on client side + */ + if (!actions.isEmpty() || clientHasActions) { + actionMapper = new KeyMapper<Action>(); + + paintTarget.addVariable((VariableOwner) viewer, "action", ""); + paintTarget.startTag("actions"); + + for (final Action a : actions) { + paintTarget.startTag("action"); + final String akey = actionMapper.key(a); + paintTarget.addAttribute("key", akey); + if (a.getCaption() != null) { + paintTarget.addAttribute("caption", a.getCaption()); + } + if (a.getIcon() != null) { + paintTarget.addAttribute("icon", a.getIcon()); + } + if (a instanceof ShortcutAction) { + final ShortcutAction sa = (ShortcutAction) a; + paintTarget.addAttribute("kc", sa.getKeyCode()); + final int[] modifiers = sa.getModifiers(); + if (modifiers != null) { + final String[] smodifiers = new String[modifiers.length]; + for (int i = 0; i < modifiers.length; i++) { + smodifiers[i] = String.valueOf(modifiers[i]); + } + paintTarget.addAttribute("mk", smodifiers); + } + } + paintTarget.endTag("action"); + } + + paintTarget.endTag("actions"); + } + + /* + * Update flag for next repaint so we know if we need to paint empty + * actions or not (must send actions is client had actions before and + * all actions were removed). + */ + clientHasActions = !actions.isEmpty(); + } + + public void handleActions(Map<String, Object> variables, Container sender) { + if (variables.containsKey("action") && actionMapper != null) { + final String key = (String) variables.get("action"); + final Action action = actionMapper.get(key); + final Object target = variables.get("actiontarget"); + if (action != null) { + handleAction(action, sender, target); + } + } + } + + @Override + public Action[] getActions(Object target, Object sender) { + HashSet<Action> actions = new HashSet<Action>(); + if (ownActions != null) { + for (Action a : ownActions) { + actions.add(a); + } + } + if (actionHandlers != null) { + for (Action.Handler h : actionHandlers) { + Action[] as = h.getActions(target, sender); + if (as != null) { + for (Action a : as) { + actions.add(a); + } + } + } + } + return actions.toArray(new Action[actions.size()]); + } + + @Override + public void handleAction(Action action, Object sender, Object target) { + if (actionHandlers != null) { + Handler[] array = actionHandlers.toArray(new Handler[actionHandlers + .size()]); + for (Handler handler : array) { + handler.handleAction(action, sender, target); + } + } + if (ownActions != null && ownActions.contains(action) + && action instanceof Action.Listener) { + ((Action.Listener) action).handleAction(sender, target); + } + } + +} diff --git a/server/src/com/vaadin/event/ComponentEventListener.java b/server/src/com/vaadin/event/ComponentEventListener.java new file mode 100644 index 0000000000..21fe8683f6 --- /dev/null +++ b/server/src/com/vaadin/event/ComponentEventListener.java @@ -0,0 +1,11 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.io.Serializable; +import java.util.EventListener; + +public interface ComponentEventListener extends EventListener, Serializable { + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/DataBoundTransferable.java b/server/src/com/vaadin/event/DataBoundTransferable.java new file mode 100644 index 0000000000..6f742e68d3 --- /dev/null +++ b/server/src/com/vaadin/event/DataBoundTransferable.java @@ -0,0 +1,66 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.ui.Component; + +/** + * Parent class for {@link Transferable} implementations that have a Vaadin + * container as a data source. The transfer is associated with an item + * (identified by its Id) and optionally also a property identifier (e.g. a + * table column identifier when transferring a single table cell). + * + * The component must implement the interface + * {@link com.vaadin.data.Container.Viewer}. + * + * In most cases, receivers of data transfers should depend on this class + * instead of its concrete subclasses. + * + * @since 6.3 + */ +public abstract class DataBoundTransferable extends TransferableImpl { + + public DataBoundTransferable(Component sourceComponent, + Map<String, Object> rawVariables) { + super(sourceComponent, rawVariables); + } + + /** + * Returns the identifier of the item being transferred. + * + * @return item identifier + */ + public abstract Object getItemId(); + + /** + * Returns the optional property identifier that the transfer concerns. + * + * This can be e.g. the table column from which a drag operation originated. + * + * @return property identifier + */ + public abstract Object getPropertyId(); + + /** + * Returns the container data source from which the transfer occurs. + * + * {@link com.vaadin.data.Container.Viewer#getContainerDataSource()} is used + * to obtain the underlying container of the source component. + * + * @return Container + */ + public Container getSourceContainer() { + Component sourceComponent = getSourceComponent(); + if (sourceComponent instanceof Container.Viewer) { + return ((Container.Viewer) sourceComponent) + .getContainerDataSource(); + } else { + // this should not happen + return null; + } + } +} diff --git a/server/src/com/vaadin/event/EventRouter.java b/server/src/com/vaadin/event/EventRouter.java new file mode 100644 index 0000000000..90c080b860 --- /dev/null +++ b/server/src/com/vaadin/event/EventRouter.java @@ -0,0 +1,201 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventObject; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * <code>EventRouter</code> class implementing the inheritable event listening + * model. For more information on the event model see the + * {@link com.vaadin.event package documentation}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class EventRouter implements MethodEventSource { + + /** + * List of registered listeners. + */ + private LinkedHashSet<ListenerMethod> listenerList = null; + + /* + * Registers a new listener with the specified activation method to listen + * events generated by this component. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public void addListener(Class<?> eventType, Object object, Method method) { + if (listenerList == null) { + listenerList = new LinkedHashSet<ListenerMethod>(); + } + listenerList.add(new ListenerMethod(eventType, object, method)); + } + + /* + * Registers a new listener with the specified named activation method to + * listen events generated by this component. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public void addListener(Class<?> eventType, Object object, String methodName) { + if (listenerList == null) { + listenerList = new LinkedHashSet<ListenerMethod>(); + } + listenerList.add(new ListenerMethod(eventType, object, methodName)); + } + + /* + * Removes all registered listeners matching the given parameters. Don't add + * a JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Class<?> eventType, Object target) { + if (listenerList != null) { + final Iterator<ListenerMethod> i = listenerList.iterator(); + while (i.hasNext()) { + final ListenerMethod lm = i.next(); + if (lm.matches(eventType, target)) { + i.remove(); + return; + } + } + } + } + + /* + * Removes the event listener methods matching the given given paramaters. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void removeListener(Class<?> eventType, Object target, Method method) { + if (listenerList != null) { + final Iterator<ListenerMethod> i = listenerList.iterator(); + while (i.hasNext()) { + final ListenerMethod lm = i.next(); + if (lm.matches(eventType, target, method)) { + i.remove(); + return; + } + } + } + } + + /* + * Removes the event listener method matching the given given parameters. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void removeListener(Class<?> eventType, Object target, + String methodName) { + + // Find the correct method + final Method[] methods = target.getClass().getMethods(); + Method method = null; + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName)) { + method = methods[i]; + } + } + if (method == null) { + throw new IllegalArgumentException(); + } + + // Remove the listeners + if (listenerList != null) { + final Iterator<ListenerMethod> i = listenerList.iterator(); + while (i.hasNext()) { + final ListenerMethod lm = i.next(); + if (lm.matches(eventType, target, method)) { + i.remove(); + return; + } + } + } + + } + + /** + * Removes all listeners from event router. + */ + public void removeAllListeners() { + listenerList = null; + } + + /** + * Sends an event to all registered listeners. The listeners will decide if + * the activation method should be called or not. + * + * @param event + * the Event to be sent to all listeners. + */ + public void fireEvent(EventObject event) { + // It is not necessary to send any events if there are no listeners + if (listenerList != null) { + + // Make a copy of the listener list to allow listeners to be added + // inside listener methods. Fixes #3605. + + // Send the event to all listeners. The listeners themselves + // will filter out unwanted events. + final Object[] listeners = listenerList.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((ListenerMethod) listeners[i]).receiveEvent(event); + } + + } + } + + /** + * Checks if the given Event type is listened by a listener registered to + * this router. + * + * @param eventType + * the event type to be checked + * @return true if a listener is registered for the given event type + */ + public boolean hasListeners(Class<?> eventType) { + if (listenerList != null) { + for (ListenerMethod lm : listenerList) { + if (lm.isType(eventType)) { + return true; + } + } + } + return false; + } + + /** + * Returns all listeners that match or extend the given event type. + * + * @param eventType + * The type of event to return listeners for. + * @return A collection with all registered listeners. Empty if no listeners + * are found. + */ + public Collection<?> getListeners(Class<?> eventType) { + List<Object> listeners = new ArrayList<Object>(); + if (listenerList != null) { + for (ListenerMethod lm : listenerList) { + if (lm.isOrExtendsType(eventType)) { + listeners.add(lm.getTarget()); + } + } + } + return listeners; + } +} diff --git a/server/src/com/vaadin/event/FieldEvents.java b/server/src/com/vaadin/event/FieldEvents.java new file mode 100644 index 0000000000..8f101c1913 --- /dev/null +++ b/server/src/com/vaadin/event/FieldEvents.java @@ -0,0 +1,275 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.vaadin.shared.EventId; +import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.Component; +import com.vaadin.ui.Component.Event; +import com.vaadin.ui.Field; +import com.vaadin.ui.Field.ValueChangeEvent; +import com.vaadin.ui.TextField; + +/** + * Interface that serves as a wrapper for {@link Field} related events. + */ +public interface FieldEvents { + + /** + * The interface for adding and removing <code>FocusEvent</code> listeners. + * By implementing this interface a class explicitly announces that it will + * generate a <code>FocusEvent</code> when it receives keyboard focus. + * <p> + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + * + * @since 6.2 + * @see FocusListener + * @see FocusEvent + */ + public interface FocusNotifier extends Serializable { + /** + * Adds a <code>FocusListener</code> to the Component which gets fired + * when a <code>Field</code> receives keyboard focus. + * + * @param listener + * @see FocusListener + * @since 6.2 + */ + public void addListener(FocusListener listener); + + /** + * Removes a <code>FocusListener</code> from the Component. + * + * @param listener + * @see FocusListener + * @since 6.2 + */ + public void removeListener(FocusListener listener); + } + + /** + * The interface for adding and removing <code>BlurEvent</code> listeners. + * By implementing this interface a class explicitly announces that it will + * generate a <code>BlurEvent</code> when it loses keyboard focus. + * <p> + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + * + * @since 6.2 + * @see BlurListener + * @see BlurEvent + */ + public interface BlurNotifier extends Serializable { + /** + * Adds a <code>BlurListener</code> to the Component which gets fired + * when a <code>Field</code> loses keyboard focus. + * + * @param listener + * @see BlurListener + * @since 6.2 + */ + public void addListener(BlurListener listener); + + /** + * Removes a <code>BlurListener</code> from the Component. + * + * @param listener + * @see BlurListener + * @since 6.2 + */ + public void removeListener(BlurListener listener); + } + + /** + * <code>FocusEvent</code> class for holding additional event information. + * Fired when a <code>Field</code> receives keyboard focus. + * + * @since 6.2 + */ + @SuppressWarnings("serial") + public class FocusEvent extends Component.Event { + + /** + * Identifier for event that can be used in {@link EventRouter} + */ + public static final String EVENT_ID = EventId.FOCUS; + + public FocusEvent(Component source) { + super(source); + } + } + + /** + * <code>FocusListener</code> interface for listening for + * <code>FocusEvent</code> fired by a <code>Field</code>. + * + * @see FocusEvent + * @since 6.2 + */ + public interface FocusListener extends ComponentEventListener { + + public static final Method focusMethod = ReflectTools.findMethod( + FocusListener.class, "focus", FocusEvent.class); + + /** + * Component has been focused + * + * @param event + * Component focus event. + */ + public void focus(FocusEvent event); + } + + /** + * <code>BlurEvent</code> class for holding additional event information. + * Fired when a <code>Field</code> loses keyboard focus. + * + * @since 6.2 + */ + @SuppressWarnings("serial") + public class BlurEvent extends Component.Event { + + /** + * Identifier for event that can be used in {@link EventRouter} + */ + public static final String EVENT_ID = EventId.BLUR; + + public BlurEvent(Component source) { + super(source); + } + } + + /** + * <code>BlurListener</code> interface for listening for + * <code>BlurEvent</code> fired by a <code>Field</code>. + * + * @see BlurEvent + * @since 6.2 + */ + public interface BlurListener extends ComponentEventListener { + + public static final Method blurMethod = ReflectTools.findMethod( + BlurListener.class, "blur", BlurEvent.class); + + /** + * Component has been blurred + * + * @param event + * Component blur event. + */ + public void blur(BlurEvent event); + } + + /** + * TextChangeEvents are fired when the user is editing the text content of a + * field. Most commonly text change events are triggered by typing text with + * keyboard, but e.g. pasting content from clip board to a text field also + * triggers an event. + * <p> + * TextChangeEvents differ from {@link ValueChangeEvent}s so that they are + * triggered repeatedly while the end user is filling the field. + * ValueChangeEvents are not fired until the user for example hits enter or + * focuses another field. Also note the difference that TextChangeEvents are + * only fired if the change is triggered from the user, while + * ValueChangeEvents are also fired if the field value is set by the + * application code. + * <p> + * The {@link TextChangeNotifier}s implementation may decide when exactly + * TextChangeEvents are fired. TextChangeEvents are not necessary fire for + * example on each key press, but buffered with a small delay. The + * {@link TextField} component supports different modes for triggering + * TextChangeEvents. + * + * @see TextChangeListener + * @see TextChangeNotifier + * @see TextField#setTextChangeEventMode(com.vaadin.ui.TextField.TextChangeEventMode) + * @since 6.5 + */ + public static abstract class TextChangeEvent extends Component.Event { + public TextChangeEvent(Component source) { + super(source); + } + + /** + * @return the text content of the field after the + * {@link TextChangeEvent} + */ + public abstract String getText(); + + /** + * @return the cursor position during after the {@link TextChangeEvent} + */ + public abstract int getCursorPosition(); + } + + /** + * A listener for {@link TextChangeEvent}s. + * + * @since 6.5 + */ + public interface TextChangeListener extends ComponentEventListener { + + public static String EVENT_ID = "ie"; + public static Method EVENT_METHOD = ReflectTools.findMethod( + TextChangeListener.class, "textChange", TextChangeEvent.class); + + /** + * This method is called repeatedly while the text is edited by a user. + * + * @param event + * the event providing details of the text change + */ + public void textChange(TextChangeEvent event); + } + + /** + * An interface implemented by a {@link Field} supporting + * {@link TextChangeEvent}s. An example a {@link TextField} supports + * {@link TextChangeListener}s. + */ + public interface TextChangeNotifier extends Serializable { + public void addListener(TextChangeListener listener); + + public void removeListener(TextChangeListener listener); + } + + public static abstract class FocusAndBlurServerRpcImpl implements + FocusAndBlurServerRpc { + + private Component component; + + public FocusAndBlurServerRpcImpl(Component component) { + this.component = component; + } + + protected abstract void fireEvent(Event event); + + @Override + public void blur() { + fireEvent(new BlurEvent(component)); + } + + @Override + public void focus() { + fireEvent(new FocusEvent(component)); + } + }; + +} diff --git a/server/src/com/vaadin/event/ItemClickEvent.java b/server/src/com/vaadin/event/ItemClickEvent.java new file mode 100644 index 0000000000..0aa0e106c5 --- /dev/null +++ b/server/src/com/vaadin/event/ItemClickEvent.java @@ -0,0 +1,121 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.ui.Component; + +/** + * + * Click event fired by a {@link Component} implementing + * {@link com.vaadin.data.Container} interface. ItemClickEvents happens on an + * {@link Item} rendered somehow on terminal. Event may also contain a specific + * {@link Property} on which the click event happened. + * + * @since 5.3 + * + */ +@SuppressWarnings("serial") +public class ItemClickEvent extends ClickEvent implements Serializable { + private Item item; + private Object itemId; + private Object propertyId; + + public ItemClickEvent(Component source, Item item, Object itemId, + Object propertyId, MouseEventDetails details) { + super(source, details); + this.item = item; + this.itemId = itemId; + this.propertyId = propertyId; + } + + /** + * Gets the item on which the click event occurred. + * + * @return item which was clicked + */ + public Item getItem() { + return item; + } + + /** + * Gets a possible identifier in source for clicked Item + * + * @return + */ + public Object getItemId() { + return itemId; + } + + /** + * Returns property on which click event occurred. Returns null if source + * cannot be resolved at property leve. For example if clicked a cell in + * table, the "column id" is returned. + * + * @return a property id of clicked property or null if click didn't occur + * on any distinct property. + */ + public Object getPropertyId() { + return propertyId; + } + + public static final Method ITEM_CLICK_METHOD; + + static { + try { + ITEM_CLICK_METHOD = ItemClickListener.class.getDeclaredMethod( + "itemClick", new Class[] { ItemClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + public interface ItemClickListener extends Serializable { + public void itemClick(ItemClickEvent event); + } + + /** + * The interface for adding and removing <code>ItemClickEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate an <code>ItemClickEvent</code> when one of its + * items is clicked. + * <p> + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + * + * @since 6.5 + * @see ItemClickListener + * @see ItemClickEvent + */ + public interface ItemClickNotifier extends Serializable { + /** + * Register a listener to handle {@link ItemClickEvent}s. + * + * @param listener + * ItemClickListener to be registered + */ + public void addListener(ItemClickListener listener); + + /** + * Removes an ItemClickListener. + * + * @param listener + * ItemClickListener to be removed + */ + public void removeListener(ItemClickListener listener); + } + +} diff --git a/server/src/com/vaadin/event/LayoutEvents.java b/server/src/com/vaadin/event/LayoutEvents.java new file mode 100644 index 0000000000..602440ea07 --- /dev/null +++ b/server/src/com/vaadin/event/LayoutEvents.java @@ -0,0 +1,138 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; + +public interface LayoutEvents { + + public interface LayoutClickListener extends ComponentEventListener { + + public static final Method clickMethod = ReflectTools.findMethod( + LayoutClickListener.class, "layoutClick", + LayoutClickEvent.class); + + /** + * Layout has been clicked + * + * @param event + * Component click event. + */ + public void layoutClick(LayoutClickEvent event); + } + + /** + * The interface for adding and removing <code>LayoutClickEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>LayoutClickEvent</code> when a component + * inside it is clicked and a <code>LayoutClickListener</code> is + * registered. + * <p> + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * <code>addListener</code> and <code>removeListener</code> methods. That + * way the caller of these methods has no real way of finding out if the + * class really will send the events, or if it just defines the methods to + * be able to implement an interface. + * </p> + * + * @since 6.5.2 + * @see LayoutClickListener + * @see LayoutClickEvent + */ + public interface LayoutClickNotifier extends Serializable { + /** + * Add a click listener to the layout. The listener is called whenever + * the user clicks inside the layout. An event is also triggered when + * the click targets a component inside a nested layout or Panel, + * provided the targeted component does not prevent the click event from + * propagating. A caption is not considered part of a component. + * + * The child component that was clicked is included in the + * {@link LayoutClickEvent}. + * + * Use {@link #removeListener(LayoutClickListener)} to remove the + * listener. + * + * @param listener + * The listener to add + */ + public void addListener(LayoutClickListener listener); + + /** + * Removes an LayoutClickListener. + * + * @param listener + * LayoutClickListener to be removed + */ + public void removeListener(LayoutClickListener listener); + } + + /** + * An event fired when the layout has been clicked. The event contains + * information about the target layout (component) and the child component + * that was clicked. If no child component was found it is set to null. + */ + public static class LayoutClickEvent extends ClickEvent { + + private final Component clickedComponent; + private final Component childComponent; + + public LayoutClickEvent(Component source, + MouseEventDetails mouseEventDetails, + Component clickedComponent, Component childComponent) { + super(source, mouseEventDetails); + this.clickedComponent = clickedComponent; + this.childComponent = childComponent; + } + + /** + * Returns the component that was clicked, which is somewhere inside the + * parent layout on which the listener was registered. + * + * For the direct child component of the layout, see + * {@link #getChildComponent()}. + * + * @return clicked {@link Component}, null if none found + */ + public Component getClickedComponent() { + return clickedComponent; + } + + /** + * Returns the direct child component of the layout which contains the + * clicked component. + * + * For the clicked component inside that child component of the layout, + * see {@link #getClickedComponent()}. + * + * @return direct child {@link Component} of the layout which contains + * the clicked Component, null if none found + */ + public Component getChildComponent() { + return childComponent; + } + + public static LayoutClickEvent createEvent(ComponentContainer layout, + MouseEventDetails mouseDetails, Connector clickedConnector) { + Component clickedComponent = (Component) clickedConnector; + Component childComponent = clickedComponent; + while (childComponent != null + && childComponent.getParent() != layout) { + childComponent = childComponent.getParent(); + } + + return new LayoutClickEvent(layout, mouseDetails, clickedComponent, + childComponent); + } + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/ListenerMethod.java b/server/src/com/vaadin/event/ListenerMethod.java new file mode 100644 index 0000000000..f7dc8a7f13 --- /dev/null +++ b/server/src/com/vaadin/event/ListenerMethod.java @@ -0,0 +1,663 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.EventListener; +import java.util.EventObject; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * <p> + * One registered event listener. This class contains the listener object + * reference, listened event type, the trigger method to call when the event + * fires, and the optional argument list to pass to the method and the index of + * the argument to replace with the event object. + * </p> + * + * <p> + * This Class provides several constructors that allow omission of the optional + * arguments, and giving the listener method directly, or having the constructor + * to reflect it using merely the name of the method. + * </p> + * + * <p> + * It should be pointed out that the method + * {@link #receiveEvent(EventObject event)} is the one that filters out the + * events that do not match with the given event type and thus do not result in + * calling of the trigger method. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ListenerMethod implements EventListener, Serializable { + + /** + * Type of the event that should trigger this listener. Also the subclasses + * of this class are accepted to trigger the listener. + */ + private final Class<?> eventType; + + /** + * The object containing the trigger method. + */ + private final Object target; + + /** + * The trigger method to call when an event passing the given criteria + * fires. + */ + private transient Method method; + + /** + * Optional argument set to pass to the trigger method. + */ + private Object[] arguments; + + /** + * Optional index to <code>arguments</code> that point out which one should + * be replaced with the triggering event object and thus be passed to the + * trigger method. + */ + private int eventArgumentIndex; + + /* Special serialization to handle method references */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + try { + out.defaultWriteObject(); + String name = method.getName(); + Class<?>[] paramTypes = method.getParameterTypes(); + out.writeObject(name); + out.writeObject(paramTypes); + } catch (NotSerializableException e) { + getLogger().warning( + "Error in serialization of the application: Class " + + target.getClass().getName() + + " must implement serialization."); + throw e; + } + + }; + + /* Special serialization to handle method references */ + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + try { + String name = (String) in.readObject(); + Class<?>[] paramTypes = (Class<?>[]) in.readObject(); + // We can not use getMethod directly as we want to support anonymous + // inner classes + method = findHighestMethod(target.getClass(), name, paramTypes); + } catch (SecurityException e) { + getLogger().log(Level.SEVERE, "Internal deserialization error", e); + } + }; + + private static Method findHighestMethod(Class<?> cls, String method, + Class<?>[] paramTypes) { + Class<?>[] ifaces = cls.getInterfaces(); + for (int i = 0; i < ifaces.length; i++) { + Method ifaceMethod = findHighestMethod(ifaces[i], method, + paramTypes); + if (ifaceMethod != null) { + return ifaceMethod; + } + } + if (cls.getSuperclass() != null) { + Method parentMethod = findHighestMethod(cls.getSuperclass(), + method, paramTypes); + if (parentMethod != null) { + return parentMethod; + } + } + Method[] methods = cls.getMethods(); + for (int i = 0; i < methods.length; i++) { + // we ignore parameter types for now - you need to add this + if (methods[i].getName().equals(method)) { + return methods[i]; + } + } + return null; + } + + /** + * <p> + * Constructs a new event listener from a trigger method, it's arguments and + * the argument index specifying which one is replaced with the event object + * when the trigger method is called. + * </p> + * + * <p> + * This constructor gets the trigger method as a parameter so it does not + * need to reflect to find it out. + * </p> + * + * @param eventType + * the event type that is listener listens to. All events of this + * kind (or its subclasses) result in calling the trigger method. + * @param target + * the object instance that contains the trigger method + * @param method + * the trigger method + * @param arguments + * the arguments to be passed to the trigger method + * @param eventArgumentIndex + * An index to the argument list. This index points out the + * argument that is replaced with the event object before the + * argument set is passed to the trigger method. If the + * eventArgumentIndex is negative, the triggering event object + * will not be passed to the trigger method, though it is still + * called. + * @throws java.lang.IllegalArgumentException + * if <code>method</code> is not a member of <code>target</code> + * . + */ + public ListenerMethod(Class<?> eventType, Object target, Method method, + Object[] arguments, int eventArgumentIndex) + throws java.lang.IllegalArgumentException { + + // Checks that the object is of correct type + if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) { + throw new java.lang.IllegalArgumentException("The method " + + method.getName() + + " cannot be used for the given target: " + + target.getClass().getName()); + } + + // Checks that the event argument is null + if (eventArgumentIndex >= 0 && arguments[eventArgumentIndex] != null) { + throw new java.lang.IllegalArgumentException("argument[" + + eventArgumentIndex + "] must be null"); + } + + // Checks the event type is supported by the method + if (eventArgumentIndex >= 0 + && !method.getParameterTypes()[eventArgumentIndex] + .isAssignableFrom(eventType)) { + throw new java.lang.IllegalArgumentException("The method " + + method.getName() + + " does not accept the given eventType: " + + eventType.getName()); + } + + this.eventType = eventType; + this.target = target; + this.method = method; + this.arguments = arguments; + this.eventArgumentIndex = eventArgumentIndex; + } + + /** + * <p> + * Constructs a new event listener from a trigger method name, it's + * arguments and the argument index specifying which one is replaced with + * the event object. The actual trigger method is reflected from + * <code>object</code>, and <code>java.lang.IllegalArgumentException</code> + * is thrown unless exactly one match is found. + * </p> + * + * @param eventType + * the event type that is listener listens to. All events of this + * kind (or its subclasses) result in calling the trigger method. + * @param target + * the object instance that contains the trigger method. + * @param methodName + * the name of the trigger method. If the object does not contain + * the method or it contains more than one matching methods + * <code>java.lang.IllegalArgumentException</code> is thrown. + * @param arguments + * the arguments to be passed to the trigger method. + * @param eventArgumentIndex + * An index to the argument list. This index points out the + * argument that is replaced with the event object before the + * argument set is passed to the trigger method. If the + * eventArgumentIndex is negative, the triggering event object + * will not be passed to the trigger method, though it is still + * called. + * @throws java.lang.IllegalArgumentException + * unless exactly one match <code>methodName</code> is found in + * <code>target</code>. + */ + public ListenerMethod(Class<?> eventType, Object target, String methodName, + Object[] arguments, int eventArgumentIndex) + throws java.lang.IllegalArgumentException { + + // Finds the correct method + final Method[] methods = target.getClass().getMethods(); + Method method = null; + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName)) { + method = methods[i]; + } + } + if (method == null) { + throw new IllegalArgumentException("Method " + methodName + + " not found in class " + target.getClass().getName()); + } + + // Checks that the event argument is null + if (eventArgumentIndex >= 0 && arguments[eventArgumentIndex] != null) { + throw new java.lang.IllegalArgumentException("argument[" + + eventArgumentIndex + "] must be null"); + } + + // Checks the event type is supported by the method + if (eventArgumentIndex >= 0 + && !method.getParameterTypes()[eventArgumentIndex] + .isAssignableFrom(eventType)) { + throw new java.lang.IllegalArgumentException("The method " + + method.getName() + + " does not accept the given eventType: " + + eventType.getName()); + } + + this.eventType = eventType; + this.target = target; + this.method = method; + this.arguments = arguments; + this.eventArgumentIndex = eventArgumentIndex; + } + + /** + * <p> + * Constructs a new event listener from the trigger method and it's + * arguments. Since the the index to the replaced parameter is not specified + * the event triggering this listener will not be passed to the trigger + * method. + * </p> + * + * <p> + * This constructor gets the trigger method as a parameter so it does not + * need to reflect to find it out. + * </p> + * + * @param eventType + * the event type that is listener listens to. All events of this + * kind (or its subclasses) result in calling the trigger method. + * @param target + * the object instance that contains the trigger method. + * @param method + * the trigger method. + * @param arguments + * the arguments to be passed to the trigger method. + * @throws java.lang.IllegalArgumentException + * if <code>method</code> is not a member of <code>target</code> + * . + */ + public ListenerMethod(Class<?> eventType, Object target, Method method, + Object[] arguments) throws java.lang.IllegalArgumentException { + + // Check that the object is of correct type + if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) { + throw new java.lang.IllegalArgumentException("The method " + + method.getName() + + " cannot be used for the given target: " + + target.getClass().getName()); + } + + this.eventType = eventType; + this.target = target; + this.method = method; + this.arguments = arguments; + eventArgumentIndex = -1; + } + + /** + * <p> + * Constructs a new event listener from a trigger method name and it's + * arguments. Since the the index to the replaced parameter is not specified + * the event triggering this listener will not be passed to the trigger + * method. + * </p> + * + * <p> + * The actual trigger method is reflected from <code>target</code>, and + * <code>java.lang.IllegalArgumentException</code> is thrown unless exactly + * one match is found. + * </p> + * + * @param eventType + * the event type that is listener listens to. All events of this + * kind (or its subclasses) result in calling the trigger method. + * @param target + * the object instance that contains the trigger method. + * @param methodName + * the name of the trigger method. If the object does not contain + * the method or it contains more than one matching methods + * <code>java.lang.IllegalArgumentException</code> is thrown. + * @param arguments + * the arguments to be passed to the trigger method. + * @throws java.lang.IllegalArgumentException + * unless exactly one match <code>methodName</code> is found in + * <code>object</code>. + */ + public ListenerMethod(Class<?> eventType, Object target, String methodName, + Object[] arguments) throws java.lang.IllegalArgumentException { + + // Find the correct method + final Method[] methods = target.getClass().getMethods(); + Method method = null; + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName)) { + method = methods[i]; + } + } + if (method == null) { + throw new IllegalArgumentException("Method " + methodName + + " not found in class " + target.getClass().getName()); + } + + this.eventType = eventType; + this.target = target; + this.method = method; + this.arguments = arguments; + eventArgumentIndex = -1; + } + + /** + * <p> + * Constructs a new event listener from a trigger method. Since the argument + * list is unspecified no parameters are passed to the trigger method when + * the listener is triggered. + * </p> + * + * <p> + * This constructor gets the trigger method as a parameter so it does not + * need to reflect to find it out. + * </p> + * + * @param eventType + * the event type that is listener listens to. All events of this + * kind (or its subclasses) result in calling the trigger method. + * @param target + * the object instance that contains the trigger method. + * @param method + * the trigger method. + * @throws java.lang.IllegalArgumentException + * if <code>method</code> is not a member of <code>object</code> + * . + */ + public ListenerMethod(Class<?> eventType, Object target, Method method) + throws java.lang.IllegalArgumentException { + + // Checks that the object is of correct type + if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) { + throw new java.lang.IllegalArgumentException("The method " + + method.getName() + + " cannot be used for the given target: " + + target.getClass().getName()); + } + + this.eventType = eventType; + this.target = target; + this.method = method; + eventArgumentIndex = -1; + + final Class<?>[] params = method.getParameterTypes(); + + if (params.length == 0) { + arguments = new Object[0]; + } else if (params.length == 1 && params[0].isAssignableFrom(eventType)) { + arguments = new Object[] { null }; + eventArgumentIndex = 0; + } else { + throw new IllegalArgumentException( + "Method requires unknown parameters"); + } + } + + /** + * <p> + * Constructs a new event listener from a trigger method name. Since the + * argument list is unspecified no parameters are passed to the trigger + * method when the listener is triggered. + * </p> + * + * <p> + * The actual trigger method is reflected from <code>object</code>, and + * <code>java.lang.IllegalArgumentException</code> is thrown unless exactly + * one match is found. + * </p> + * + * @param eventType + * the event type that is listener listens to. All events of this + * kind (or its subclasses) result in calling the trigger method. + * @param target + * the object instance that contains the trigger method. + * @param methodName + * the name of the trigger method. If the object does not contain + * the method or it contains more than one matching methods + * <code>java.lang.IllegalArgumentException</code> is thrown. + * @throws java.lang.IllegalArgumentException + * unless exactly one match <code>methodName</code> is found in + * <code>target</code>. + */ + public ListenerMethod(Class<?> eventType, Object target, String methodName) + throws java.lang.IllegalArgumentException { + + // Finds the correct method + final Method[] methods = target.getClass().getMethods(); + Method method = null; + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName)) { + method = methods[i]; + } + } + if (method == null) { + throw new IllegalArgumentException("Method " + methodName + + " not found in class " + target.getClass().getName()); + } + + this.eventType = eventType; + this.target = target; + this.method = method; + eventArgumentIndex = -1; + + final Class<?>[] params = method.getParameterTypes(); + + if (params.length == 0) { + arguments = new Object[0]; + } else if (params.length == 1 && params[0].isAssignableFrom(eventType)) { + arguments = new Object[] { null }; + eventArgumentIndex = 0; + } else { + throw new IllegalArgumentException( + "Method requires unknown parameters"); + } + } + + /** + * Receives one event from the <code>EventRouter</code> and calls the + * trigger method if it matches with the criteria defined for the listener. + * Only the events of the same or subclass of the specified event class + * result in the trigger method to be called. + * + * @param event + * the fired event. Unless the trigger method's argument list and + * the index to the to be replaced argument is specified, this + * event will not be passed to the trigger method. + */ + public void receiveEvent(EventObject event) { + // Only send events supported by the method + if (eventType.isAssignableFrom(event.getClass())) { + try { + if (eventArgumentIndex >= 0) { + if (eventArgumentIndex == 0 && arguments.length == 1) { + method.invoke(target, new Object[] { event }); + } else { + final Object[] arg = new Object[arguments.length]; + for (int i = 0; i < arg.length; i++) { + arg[i] = arguments[i]; + } + arg[eventArgumentIndex] = event; + method.invoke(target, arg); + } + } else { + method.invoke(target, arguments); + } + + } catch (final java.lang.IllegalAccessException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error - please report", e); + } catch (final java.lang.reflect.InvocationTargetException e) { + // An exception was thrown by the invocation target. Throw it + // forwards. + throw new MethodException("Invocation of method " + + method.getName() + " in " + + target.getClass().getName() + " failed.", + e.getTargetException()); + } + } + } + + /** + * Checks if the given object and event match with the ones stored in this + * listener. + * + * @param target + * the object to be matched against the object stored by this + * listener. + * @param eventType + * the type to be tested for equality against the type stored by + * this listener. + * @return <code>true</code> if <code>target</code> is the same object as + * the one stored in this object and <code>eventType</code> equals + * the event type stored in this object. * + */ + public boolean matches(Class<?> eventType, Object target) { + return (this.target == target) && (eventType.equals(this.eventType)); + } + + /** + * Checks if the given object, event and method match with the ones stored + * in this listener. + * + * @param target + * the object to be matched against the object stored by this + * listener. + * @param eventType + * the type to be tested for equality against the type stored by + * this listener. + * @param method + * the method to be tested for equality against the method stored + * by this listener. + * @return <code>true</code> if <code>target</code> is the same object as + * the one stored in this object, <code>eventType</code> equals with + * the event type stored in this object and <code>method</code> + * equals with the method stored in this object + */ + public boolean matches(Class<?> eventType, Object target, Method method) { + return (this.target == target) + && (eventType.equals(this.eventType) && method + .equals(this.method)); + } + + @Override + public int hashCode() { + int hash = 7; + + hash = 31 * hash + eventArgumentIndex; + hash = 31 * hash + (eventType == null ? 0 : eventType.hashCode()); + hash = 31 * hash + (target == null ? 0 : target.hashCode()); + hash = 31 * hash + (method == null ? 0 : method.hashCode()); + + return hash; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + // return false if obj is a subclass (do not use instanceof check) + if ((obj == null) || (obj.getClass() != getClass())) { + return false; + } + + // obj is of same class, test it further + ListenerMethod t = (ListenerMethod) obj; + + return eventArgumentIndex == t.eventArgumentIndex + && (eventType == t.eventType || (eventType != null && eventType + .equals(t.eventType))) + && (target == t.target || (target != null && target + .equals(t.target))) + && (method == t.method || (method != null && method + .equals(t.method))) + && (arguments == t.arguments || (Arrays.equals(arguments, + t.arguments))); + } + + /** + * Exception that wraps an exception thrown by an invoked method. When + * <code>ListenerMethod</code> invokes the target method, it may throw + * arbitrary exception. The original exception is wrapped into + * MethodException instance and rethrown by the <code>ListenerMethod</code>. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class MethodException extends RuntimeException implements + Serializable { + + private MethodException(String message, Throwable cause) { + super(message, cause); + } + + } + + /** + * Compares the type of this ListenerMethod to the given type + * + * @param eventType + * The type to compare with + * @return true if this type of this ListenerMethod matches the given type, + * false otherwise + */ + public boolean isType(Class<?> eventType) { + return this.eventType == eventType; + } + + /** + * Compares the type of this ListenerMethod to the given type + * + * @param eventType + * The type to compare with + * @return true if this event type can be assigned to the given type, false + * otherwise + */ + public boolean isOrExtendsType(Class<?> eventType) { + return eventType.isAssignableFrom(this.eventType); + } + + /** + * Returns the target object which contains the trigger method. + * + * @return The target object + */ + public Object getTarget() { + return target; + } + + private static final Logger getLogger() { + return Logger.getLogger(ListenerMethod.class.getName()); + } + +} diff --git a/server/src/com/vaadin/event/MethodEventSource.java b/server/src/com/vaadin/event/MethodEventSource.java new file mode 100644 index 0000000000..fb2e7b029b --- /dev/null +++ b/server/src/com/vaadin/event/MethodEventSource.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.io.Serializable; +import java.lang.reflect.Method; + +/** + * <p> + * Interface for classes supporting registration of methods as event receivers. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface MethodEventSource extends Serializable { + + /** + * <p> + * Registers a new event listener with the specified activation method to + * listen events generated by this component. If the activation method does + * not have any arguments the event object will not be passed to it when + * it's called. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param object + * the object instance who owns the activation method. + * @param method + * the activation method. + * @throws java.lang.IllegalArgumentException + * unless <code>method</code> has exactly one match in + * <code>object</code> + */ + public void addListener(Class<?> eventType, Object object, Method method); + + /** + * <p> + * Registers a new listener with the specified activation method to listen + * events generated by this component. If the activation method does not + * have any arguments the event object will not be passed to it when it's + * called. + * </p> + * + * <p> + * This version of <code>addListener</code> gets the name of the activation + * method as a parameter. The actual method is reflected from + * <code>object</code>, and unless exactly one match is found, + * <code>java.lang.IllegalArgumentException</code> is thrown. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param object + * the object instance who owns the activation method. + * @param methodName + * the name of the activation method. + * @throws java.lang.IllegalArgumentException + * unless <code>method</code> has exactly one match in + * <code>object</code> + */ + public void addListener(Class<?> eventType, Object object, String methodName); + + /** + * Removes all registered listeners matching the given parameters. Since + * this method receives the event type and the listener object as + * parameters, it will unregister all <code>object</code>'s methods that are + * registered to listen to events of type <code>eventType</code> generated + * by this component. + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + */ + public void removeListener(Class<?> eventType, Object target); + + /** + * Removes one registered listener method. The given method owned by the + * given object will no longer be called when the specified events are + * generated by this component. + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type eventType with one or more methods. + * @param method + * the method owned by the target that's registered to listen to + * events of type eventType. + */ + public void removeListener(Class<?> eventType, Object target, Method method); + + /** + * <p> + * Removes one registered listener method. The given method owned by the + * given object will no longer be called when the specified events are + * generated by this component. + * </p> + * + * <p> + * This version of <code>removeListener</code> gets the name of the + * activation method as a parameter. The actual method is reflected from the + * target, and unless exactly one match is found, + * <code>java.lang.IllegalArgumentException</code> is thrown. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + * @param methodName + * the name of the method owned by <code>target</code> that's + * registered to listen to events of type <code>eventType</code>. + */ + public void removeListener(Class<?> eventType, Object target, + String methodName); +} diff --git a/server/src/com/vaadin/event/MouseEvents.java b/server/src/com/vaadin/event/MouseEvents.java new file mode 100644 index 0000000000..fafd44be89 --- /dev/null +++ b/server/src/com/vaadin/event/MouseEvents.java @@ -0,0 +1,234 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.lang.reflect.Method; + +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.Component; + +/** + * Interface that serves as a wrapper for mouse related events. + * + * @author Vaadin Ltd. + * @see ClickListener + * @version + * @VERSION@ + * @since 6.2 + */ +public interface MouseEvents { + + /** + * Class for holding information about a mouse click event. A + * {@link ClickEvent} is fired when the user clicks on a + * <code>Component</code>. + * + * The information available for click events are terminal dependent. + * Correct values for all event details cannot be guaranteed. + * + * @author Vaadin Ltd. + * @see ClickListener + * @version + * @VERSION@ + * @since 6.2 + */ + public class ClickEvent extends Component.Event { + public static final int BUTTON_LEFT = MouseEventDetails.BUTTON_LEFT; + public static final int BUTTON_MIDDLE = MouseEventDetails.BUTTON_MIDDLE; + public static final int BUTTON_RIGHT = MouseEventDetails.BUTTON_RIGHT; + + private MouseEventDetails details; + + public ClickEvent(Component source, MouseEventDetails mouseEventDetails) { + super(source); + details = mouseEventDetails; + } + + /** + * Returns an identifier describing which mouse button the user pushed. + * Compare with {@link #BUTTON_LEFT},{@link #BUTTON_MIDDLE}, + * {@link #BUTTON_RIGHT} to find out which butten it is. + * + * @return one of {@link #BUTTON_LEFT}, {@link #BUTTON_MIDDLE}, + * {@link #BUTTON_RIGHT}. + */ + public int getButton() { + return details.getButton(); + } + + /** + * Returns the mouse position (x coordinate) when the click took place. + * The position is relative to the browser client area. + * + * @return The mouse cursor x position + */ + public int getClientX() { + return details.getClientX(); + } + + /** + * Returns the mouse position (y coordinate) when the click took place. + * The position is relative to the browser client area. + * + * @return The mouse cursor y position + */ + public int getClientY() { + return details.getClientY(); + } + + /** + * Returns the relative mouse position (x coordinate) when the click + * took place. The position is relative to the clicked component. + * + * @return The mouse cursor x position relative to the clicked layout + * component or -1 if no x coordinate available + */ + public int getRelativeX() { + return details.getRelativeX(); + } + + /** + * Returns the relative mouse position (y coordinate) when the click + * took place. The position is relative to the clicked component. + * + * @return The mouse cursor y position relative to the clicked layout + * component or -1 if no y coordinate available + */ + public int getRelativeY() { + return details.getRelativeY(); + } + + /** + * Checks if the event is a double click event. + * + * @return true if the event is a double click event, false otherwise + */ + public boolean isDoubleClick() { + return details.isDoubleClick(); + } + + /** + * Checks if the Alt key was down when the mouse event took place. + * + * @return true if Alt was down when the event occured, false otherwise + */ + public boolean isAltKey() { + return details.isAltKey(); + } + + /** + * Checks if the Ctrl key was down when the mouse event took place. + * + * @return true if Ctrl was pressed when the event occured, false + * otherwise + */ + public boolean isCtrlKey() { + return details.isCtrlKey(); + } + + /** + * Checks if the Meta key was down when the mouse event took place. + * + * @return true if Meta was pressed when the event occured, false + * otherwise + */ + public boolean isMetaKey() { + return details.isMetaKey(); + } + + /** + * Checks if the Shift key was down when the mouse event took place. + * + * @return true if Shift was pressed when the event occured, false + * otherwise + */ + public boolean isShiftKey() { + return details.isShiftKey(); + } + + /** + * Returns a human readable string representing which button has been + * pushed. This is meant for debug purposes only and the string returned + * could change. Use {@link #getButton()} to check which button was + * pressed. + * + * @since 6.3 + * @return A string representation of which button was pushed. + */ + public String getButtonName() { + return details.getButtonName(); + } + } + + /** + * Interface for listening for a {@link ClickEvent} fired by a + * {@link Component}. + * + * @see ClickEvent + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.2 + */ + public interface ClickListener extends ComponentEventListener { + + public static final Method clickMethod = ReflectTools.findMethod( + ClickListener.class, "click", ClickEvent.class); + + /** + * Called when a {@link Component} has been clicked. A reference to the + * component is given by {@link ClickEvent#getComponent()}. + * + * @param event + * An event containing information about the click. + */ + public void click(ClickEvent event); + } + + /** + * Class for holding additional event information for DoubleClick events. + * Fired when the user double-clicks on a <code>Component</code>. + * + * @see ClickEvent + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.2 + */ + public class DoubleClickEvent extends Component.Event { + + public DoubleClickEvent(Component source) { + super(source); + } + } + + /** + * Interface for listening for a {@link DoubleClickEvent} fired by a + * {@link Component}. + * + * @see DoubleClickEvent + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.2 + */ + public interface DoubleClickListener extends ComponentEventListener { + + public static final Method doubleClickMethod = ReflectTools.findMethod( + DoubleClickListener.class, "doubleClick", + DoubleClickEvent.class); + + /** + * Called when a {@link Component} has been double clicked. A reference + * to the component is given by {@link DoubleClickEvent#getComponent()}. + * + * @param event + * An event containing information about the double click. + */ + public void doubleClick(DoubleClickEvent event); + } + +} diff --git a/server/src/com/vaadin/event/ShortcutAction.java b/server/src/com/vaadin/event/ShortcutAction.java new file mode 100644 index 0000000000..c42dd731c8 --- /dev/null +++ b/server/src/com/vaadin/event/ShortcutAction.java @@ -0,0 +1,373 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.event; + +import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.vaadin.terminal.Resource; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.Panel; +import com.vaadin.ui.Window; + +/** + * Shortcuts are a special type of {@link Action}s used to create keyboard + * shortcuts. + * <p> + * The ShortcutAction is triggered when the user presses a given key in + * combination with the (optional) given modifier keys. + * </p> + * <p> + * ShortcutActions can be global (by attaching to the {@link Window}), or + * attached to different parts of the UI so that a specific shortcut is only + * valid in part of the UI. For instance, one can attach shortcuts to a specific + * {@link Panel} - look for {@link ComponentContainer}s implementing + * {@link Handler Action.Handler} or {@link Notifier Action.Notifier}. + * </p> + * <p> + * ShortcutActions have a caption that may be used to display the shortcut + * visually. This allows the ShortcutAction to be used as a plain Action while + * still reacting to a keyboard shortcut. Note that this functionality is not + * very well supported yet, but it might still be a good idea to give a caption + * to the shortcut. + * </p> + * + * @author Vaadin Ltd. + * @version + * @since 4.0.1 + */ +@SuppressWarnings("serial") +public class ShortcutAction extends Action { + + private final int keyCode; + + private final int[] modifiers; + + /** + * Creates a shortcut that reacts to the given {@link KeyCode} and + * (optionally) {@link ModifierKey}s. <br/> + * The shortcut might be shown in the UI (e.g context menu), in which case + * the caption will be used. + * + * @param caption + * used when displaying the shortcut visually + * @param kc + * KeyCode that the shortcut reacts to + * @param m + * optional modifier keys + */ + public ShortcutAction(String caption, int kc, int[] m) { + super(caption); + keyCode = kc; + modifiers = m; + } + + /** + * Creates a shortcut that reacts to the given {@link KeyCode} and + * (optionally) {@link ModifierKey}s. <br/> + * The shortcut might be shown in the UI (e.g context menu), in which case + * the caption and icon will be used. + * + * @param caption + * used when displaying the shortcut visually + * @param icon + * used when displaying the shortcut visually + * @param kc + * KeyCode that the shortcut reacts to + * @param m + * optional modifier keys + */ + public ShortcutAction(String caption, Resource icon, int kc, int[] m) { + super(caption, icon); + keyCode = kc; + modifiers = m; + } + + /** + * Used in the caption shorthand notation to indicate the ALT modifier. + */ + public static final char SHORTHAND_CHAR_ALT = '&'; + /** + * Used in the caption shorthand notation to indicate the SHIFT modifier. + */ + public static final char SHORTHAND_CHAR_SHIFT = '_'; + /** + * Used in the caption shorthand notation to indicate the CTRL modifier. + */ + public static final char SHORTHAND_CHAR_CTRL = '^'; + + // regex-quote (escape) the characters + private static final String SHORTHAND_ALT = Pattern.quote(Character + .toString(SHORTHAND_CHAR_ALT)); + private static final String SHORTHAND_SHIFT = Pattern.quote(Character + .toString(SHORTHAND_CHAR_SHIFT)); + private static final String SHORTHAND_CTRL = Pattern.quote(Character + .toString(SHORTHAND_CHAR_CTRL)); + // Used for replacing escaped chars, e.g && with & + private static final Pattern SHORTHAND_ESCAPE = Pattern.compile("(" + + SHORTHAND_ALT + "?)" + SHORTHAND_ALT + "|(" + SHORTHAND_SHIFT + + "?)" + SHORTHAND_SHIFT + "|(" + SHORTHAND_CTRL + "?)" + + SHORTHAND_CTRL); + // Used for removing escaped chars, only leaving real shorthands + private static final Pattern SHORTHAND_REMOVE = Pattern.compile("([" + + SHORTHAND_ALT + "|" + SHORTHAND_SHIFT + "|" + SHORTHAND_CTRL + + "])\\1"); + // Mnemonic char, optionally followed by another, and optionally a third + private static final Pattern SHORTHANDS = Pattern.compile("(" + + SHORTHAND_ALT + "|" + SHORTHAND_SHIFT + "|" + SHORTHAND_CTRL + + ")(?!\\1)(?:(" + SHORTHAND_ALT + "|" + SHORTHAND_SHIFT + "|" + + SHORTHAND_CTRL + ")(?!\\1|\\2))?(?:(" + SHORTHAND_ALT + "|" + + SHORTHAND_SHIFT + "|" + SHORTHAND_CTRL + ")(?!\\1|\\2|\\3))?."); + + /** + * Constructs a ShortcutAction using a shorthand notation to encode the + * keycode and modifiers in the caption. + * <p> + * Insert one or more modifier characters before the character to use as + * keycode. E.g <code>"&Save"</code> will make a shortcut responding to + * ALT-S, <code>"E^xit"</code> will respond to CTRL-X.<br/> + * Multiple modifiers can be used, e.g <code>"&^Delete"</code> will respond + * to CTRL-ALT-D (the order of the modifier characters is not important). + * </p> + * <p> + * The modifier characters will be removed from the caption. The modifier + * character is be escaped by itself: two consecutive characters are turned + * into the original character w/o the special meaning. E.g + * <code>"Save&&&close"</code> will respond to ALT-C, and the caption will + * say "Save&close". + * </p> + * + * @param shorthandCaption + * the caption in modifier shorthand + */ + public ShortcutAction(String shorthandCaption) { + this(shorthandCaption, null); + } + + /** + * Constructs a ShortcutAction using a shorthand notation to encode the + * keycode a in the caption. + * <p> + * This works the same way as {@link #ShortcutAction(String)}, with the + * exception that the modifiers given override those indicated in the + * caption. I.e use any of the modifier characters in the caption to + * indicate the keycode, but the modifier will be the given set.<br/> + * E.g + * <code>new ShortcutAction("Do &stuff", new int[]{ShortcutAction.ModifierKey.CTRL}));</code> + * will respond to CTRL-S. + * </p> + * + * @param shorthandCaption + * @param modifierKeys + */ + public ShortcutAction(String shorthandCaption, int[] modifierKeys) { + // && -> & etc + super(SHORTHAND_ESCAPE.matcher(shorthandCaption).replaceAll("$1$2$3")); + // replace escaped chars with something that won't accidentally match + shorthandCaption = SHORTHAND_REMOVE.matcher(shorthandCaption) + .replaceAll("\u001A"); + Matcher matcher = SHORTHANDS.matcher(shorthandCaption); + if (matcher.find()) { + String match = matcher.group(); + + // KeyCode from last char in match, uppercase + keyCode = Character.toUpperCase(matcher.group().charAt( + match.length() - 1)); + + // Given modifiers override this indicated in the caption + if (modifierKeys != null) { + modifiers = modifierKeys; + } else { + // Read modifiers from caption + int[] mod = new int[match.length() - 1]; + for (int i = 0; i < mod.length; i++) { + int kc = match.charAt(i); + switch (kc) { + case SHORTHAND_CHAR_ALT: + mod[i] = ModifierKey.ALT; + break; + case SHORTHAND_CHAR_CTRL: + mod[i] = ModifierKey.CTRL; + break; + case SHORTHAND_CHAR_SHIFT: + mod[i] = ModifierKey.SHIFT; + break; + } + } + modifiers = mod; + } + + } else { + keyCode = -1; + modifiers = modifierKeys; + } + } + + /** + * Get the {@link KeyCode} that this shortcut reacts to (in combination with + * the {@link ModifierKey}s). + * + * @return keycode for this shortcut + */ + public int getKeyCode() { + return keyCode; + } + + /** + * Get the {@link ModifierKey}s required for the shortcut to react. + * + * @return modifier keys for this shortcut + */ + public int[] getModifiers() { + return modifiers; + } + + /** + * Key codes that can be used for shortcuts + * + */ + public interface KeyCode extends Serializable { + public static final int ENTER = 13; + + public static final int ESCAPE = 27; + + public static final int PAGE_UP = 33; + + public static final int PAGE_DOWN = 34; + + public static final int TAB = 9; + + public static final int ARROW_LEFT = 37; + + public static final int ARROW_UP = 38; + + public static final int ARROW_RIGHT = 39; + + public static final int ARROW_DOWN = 40; + + public static final int BACKSPACE = 8; + + public static final int DELETE = 46; + + public static final int INSERT = 45; + + public static final int END = 35; + + public static final int HOME = 36; + + public static final int F1 = 112; + + public static final int F2 = 113; + + public static final int F3 = 114; + + public static final int F4 = 115; + + public static final int F5 = 116; + + public static final int F6 = 117; + + public static final int F7 = 118; + + public static final int F8 = 119; + + public static final int F9 = 120; + + public static final int F10 = 121; + + public static final int F11 = 122; + + public static final int F12 = 123; + + public static final int A = 65; + + public static final int B = 66; + + public static final int C = 67; + + public static final int D = 68; + + public static final int E = 69; + + public static final int F = 70; + + public static final int G = 71; + + public static final int H = 72; + + public static final int I = 73; + + public static final int J = 74; + + public static final int K = 75; + + public static final int L = 76; + + public static final int M = 77; + + public static final int N = 78; + + public static final int O = 79; + + public static final int P = 80; + + public static final int Q = 81; + + public static final int R = 82; + + public static final int S = 83; + + public static final int T = 84; + + public static final int U = 85; + + public static final int V = 86; + + public static final int W = 87; + + public static final int X = 88; + + public static final int Y = 89; + + public static final int Z = 90; + + public static final int NUM0 = 48; + + public static final int NUM1 = 49; + + public static final int NUM2 = 50; + + public static final int NUM3 = 51; + + public static final int NUM4 = 52; + + public static final int NUM5 = 53; + + public static final int NUM6 = 54; + + public static final int NUM7 = 55; + + public static final int NUM8 = 56; + + public static final int NUM9 = 57; + + public static final int SPACEBAR = 32; + } + + /** + * Modifier key constants + * + */ + public interface ModifierKey extends Serializable { + public static final int SHIFT = 16; + + public static final int CTRL = 17; + + public static final int ALT = 18; + + public static final int META = 91; + } +} diff --git a/server/src/com/vaadin/event/ShortcutListener.java b/server/src/com/vaadin/event/ShortcutListener.java new file mode 100644 index 0000000000..b760cfabe6 --- /dev/null +++ b/server/src/com/vaadin/event/ShortcutListener.java @@ -0,0 +1,33 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import com.vaadin.event.Action.Listener; +import com.vaadin.terminal.Resource; + +public abstract class ShortcutListener extends ShortcutAction implements + Listener { + + private static final long serialVersionUID = 1L; + + public ShortcutListener(String caption, int keyCode, int... modifierKeys) { + super(caption, keyCode, modifierKeys); + } + + public ShortcutListener(String shorthandCaption, int... modifierKeys) { + super(shorthandCaption, modifierKeys); + } + + public ShortcutListener(String caption, Resource icon, int keyCode, + int... modifierKeys) { + super(caption, icon, keyCode, modifierKeys); + } + + public ShortcutListener(String shorthandCaption) { + super(shorthandCaption); + } + + @Override + abstract public void handleAction(Object sender, Object target); +} diff --git a/server/src/com/vaadin/event/Transferable.java b/server/src/com/vaadin/event/Transferable.java new file mode 100644 index 0000000000..838d8ad7e2 --- /dev/null +++ b/server/src/com/vaadin/event/Transferable.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.io.Serializable; +import java.util.Collection; + +import com.vaadin.ui.Component; + +/** + * Transferable wraps the data that is to be imported into another component. + * Currently Transferable is only used for drag and drop. + * + * @since 6.3 + */ +public interface Transferable extends Serializable { + + /** + * Returns the data from Transferable by its data flavor (aka data type). + * Data types can be any string keys, but MIME types like "text/plain" are + * commonly used. + * <p> + * Note, implementations of {@link Transferable} often provide a better + * typed API for accessing data. + * + * @param dataFlavor + * the data flavor to be returned from Transferable + * @return the data stored in the Transferable or null if Transferable + * contains no data for given data flavour + */ + public Object getData(String dataFlavor); + + /** + * Stores data of given data flavor to Transferable. Possibly existing value + * of the same data flavor will be replaced. + * + * @param dataFlavor + * the data flavor + * @param value + * the new value of the data flavor + */ + public void setData(String dataFlavor, Object value); + + /** + * @return a collection of data flavors ( data types ) available in this + * Transferable + */ + public Collection<String> getDataFlavors(); + + /** + * @return the component that created the Transferable or null if the source + * component is unknown + */ + public Component getSourceComponent(); + +} diff --git a/server/src/com/vaadin/event/TransferableImpl.java b/server/src/com/vaadin/event/TransferableImpl.java new file mode 100644 index 0000000000..4c973571f7 --- /dev/null +++ b/server/src/com/vaadin/event/TransferableImpl.java @@ -0,0 +1,47 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.ui.Component; + +/** + * TODO Javadoc! + * + * @since 6.3 + */ +public class TransferableImpl implements Transferable { + private Map<String, Object> rawVariables = new HashMap<String, Object>(); + private Component sourceComponent; + + public TransferableImpl(Component sourceComponent, + Map<String, Object> rawVariables) { + this.sourceComponent = sourceComponent; + this.rawVariables = rawVariables; + } + + @Override + public Component getSourceComponent() { + return sourceComponent; + } + + @Override + public Object getData(String dataFlavor) { + return rawVariables.get(dataFlavor); + } + + @Override + public void setData(String dataFlavor, Object value) { + rawVariables.put(dataFlavor, value); + } + + @Override + public Collection<String> getDataFlavors() { + return rawVariables.keySet(); + } + +} diff --git a/server/src/com/vaadin/event/dd/DragAndDropEvent.java b/server/src/com/vaadin/event/dd/DragAndDropEvent.java new file mode 100644 index 0000000000..b920d43469 --- /dev/null +++ b/server/src/com/vaadin/event/dd/DragAndDropEvent.java @@ -0,0 +1,50 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd; + +import java.io.Serializable; + +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; + +/** + * DragAndDropEvent wraps information related to drag and drop operation. It is + * passed by terminal implementation for + * {@link DropHandler#drop(DragAndDropEvent)} and + * {@link AcceptCriterion#accept(DragAndDropEvent)} methods. + * <p> + * DragAndDropEvent instances contains both the dragged data in + * {@link Transferable} (generated by {@link DragSource} and details about the + * current drop event in {@link TargetDetails} (generated by {@link DropTarget}. + * + * @since 6.3 + * + */ +public class DragAndDropEvent implements Serializable { + private Transferable transferable; + private TargetDetails dropTargetDetails; + + public DragAndDropEvent(Transferable transferable, + TargetDetails dropTargetDetails) { + this.transferable = transferable; + this.dropTargetDetails = dropTargetDetails; + } + + /** + * @return the Transferable instance representing the data dragged in this + * drag and drop event + */ + public Transferable getTransferable() { + return transferable; + } + + /** + * @return the TargetDetails containing drop target related details of drag + * and drop operation + */ + public TargetDetails getTargetDetails() { + return dropTargetDetails; + } + +} diff --git a/server/src/com/vaadin/event/dd/DragSource.java b/server/src/com/vaadin/event/dd/DragSource.java new file mode 100644 index 0000000000..4daf0dcb18 --- /dev/null +++ b/server/src/com/vaadin/event/dd/DragSource.java @@ -0,0 +1,52 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd; + +import java.util.Map; + +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.ui.Component; +import com.vaadin.ui.Tree; + +/** + * DragSource is a {@link Component} that builds a {@link Transferable} for a + * drag and drop operation. + * <p> + * In Vaadin the drag and drop operation practically starts from client side + * component. The client side component initially defines the data that will be + * present in {@link Transferable} object on server side. If the server side + * counterpart of the component implements this interface, terminal + * implementation lets it create the {@link Transferable} instance from the raw + * client side "seed data". This way server side implementation may translate or + * extend the data that will be available for {@link DropHandler}. + * + * @since 6.3 + * + */ +public interface DragSource extends Component { + + /** + * DragSource may convert data added by client side component to meaningful + * values for server side developer or add other data based on it. + * + * <p> + * For example Tree converts item identifiers to generated string keys for + * the client side. Vaadin developer don't and can't know anything about + * these generated keys, only about item identifiers. When tree node is + * dragged client puts that key to {@link Transferable}s client side + * counterpart. In {@link Tree#getTransferable(Map)} the key is converted + * back to item identifier that the server side developer can use. + * <p> + * + * @since 6.3 + * @param rawVariables + * the data that client side initially included in + * {@link Transferable}s client side counterpart. + * @return the {@link Transferable} instance that will be passed to + * {@link DropHandler} (and/or {@link AcceptCriterion}) + */ + public Transferable getTransferable(Map<String, Object> rawVariables); + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/DropHandler.java b/server/src/com/vaadin/event/dd/DropHandler.java new file mode 100644 index 0000000000..7a15ea5b68 --- /dev/null +++ b/server/src/com/vaadin/event/dd/DropHandler.java @@ -0,0 +1,61 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd; + +import java.io.Serializable; + +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.acceptcriteria.AcceptAll; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; + +/** + * DropHandlers contain the actual business logic for drag and drop operations. + * <p> + * The {@link #drop(DragAndDropEvent)} method is used to receive the transferred + * data and the {@link #getAcceptCriterion()} method contains the (possibly + * client side verifiable) criterion whether the dragged data will be handled at + * all. + * + * @since 6.3 + * + */ +public interface DropHandler extends Serializable { + + /** + * Drop method is called when the end user has finished the drag operation + * on a {@link DropTarget} and {@link DragAndDropEvent} has passed + * {@link AcceptCriterion} defined by {@link #getAcceptCriterion()} method. + * The actual business logic of drag and drop operation is implemented into + * this method. + * + * @param event + * the event related to this drop + */ + public void drop(DragAndDropEvent event); + + /** + * Returns the {@link AcceptCriterion} used to evaluate whether the + * {@link Transferable} will be handed over to + * {@link DropHandler#drop(DragAndDropEvent)} method. If client side can't + * verify the {@link AcceptCriterion}, the same criteria may be tested also + * prior to actual drop - during the drag operation. + * <p> + * Based on information from {@link AcceptCriterion} components may display + * some hints for the end user whether the drop will be accepted or not. + * <p> + * Vaadin contains a variety of criteria built in that can be composed to + * more complex criterion. If the build in criteria are not enough, + * developer can use a {@link ServerSideCriterion} or build own custom + * criterion with client side counterpart. + * <p> + * If developer wants to handle everything in the + * {@link #drop(DragAndDropEvent)} method, {@link AcceptAll} instance can be + * returned. + * + * @return the {@link AcceptCriterion} + */ + public AcceptCriterion getAcceptCriterion(); + +} diff --git a/server/src/com/vaadin/event/dd/DropTarget.java b/server/src/com/vaadin/event/dd/DropTarget.java new file mode 100644 index 0000000000..c18aa60b19 --- /dev/null +++ b/server/src/com/vaadin/event/dd/DropTarget.java @@ -0,0 +1,42 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd; + +import java.util.Map; + +import com.vaadin.ui.Component; + +/** + * DropTarget is an interface for components supporting drop operations. A + * component that wants to receive drop events should implement this interface + * and provide a {@link DropHandler} which will handle the actual drop event. + * + * @since 6.3 + */ +public interface DropTarget extends Component { + + /** + * @return the drop hanler that will receive the dragged data or null if + * drops are not currently accepted + */ + public DropHandler getDropHandler(); + + /** + * Called before the {@link DragAndDropEvent} is passed to + * {@link DropHandler}. Implementation may for example translate the drop + * target details provided by the client side (drop target) to meaningful + * server side values. If null is returned the terminal implementation will + * automatically create a {@link TargetDetails} with raw client side data. + * + * @see DragSource#getTransferable(Map) + * + * @param clientVariables + * data passed from the DropTargets client side counterpart. + * @return A DropTargetDetails object with the translated data or null to + * use a default implementation. + */ + public TargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables); + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/TargetDetails.java b/server/src/com/vaadin/event/dd/TargetDetails.java new file mode 100644 index 0000000000..a352fbec60 --- /dev/null +++ b/server/src/com/vaadin/event/dd/TargetDetails.java @@ -0,0 +1,37 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd; + +import java.io.Serializable; + +import com.vaadin.ui.Tree.TreeTargetDetails; + +/** + * TargetDetails wraps drop target related information about + * {@link DragAndDropEvent}. + * <p> + * When a TargetDetails object is used in {@link DropHandler} it is often + * preferable to cast the TargetDetails to an implementation provided by + * DropTarget like {@link TreeTargetDetails}. They often provide a better typed, + * drop target specific API. + * + * @since 6.3 + * + */ +public interface TargetDetails extends Serializable { + + /** + * Gets target data associated with the given string key + * + * @param key + * @return The data associated with the key + */ + public Object getData(String key); + + /** + * @return the drop target on which the {@link DragAndDropEvent} happened. + */ + public DropTarget getTarget(); + +} diff --git a/server/src/com/vaadin/event/dd/TargetDetailsImpl.java b/server/src/com/vaadin/event/dd/TargetDetailsImpl.java new file mode 100644 index 0000000000..4a459777ed --- /dev/null +++ b/server/src/com/vaadin/event/dd/TargetDetailsImpl.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd; + +import java.util.HashMap; +import java.util.Map; + +/** + * A HashMap backed implementation of {@link TargetDetails} for terminal + * implementation and for extension. + * + * @since 6.3 + * + */ +@SuppressWarnings("serial") +public class TargetDetailsImpl implements TargetDetails { + + private HashMap<String, Object> data = new HashMap<String, Object>(); + private DropTarget dropTarget; + + protected TargetDetailsImpl(Map<String, Object> rawDropData) { + data.putAll(rawDropData); + } + + public TargetDetailsImpl(Map<String, Object> rawDropData, + DropTarget dropTarget) { + this(rawDropData); + this.dropTarget = dropTarget; + } + + @Override + public Object getData(String key) { + return data.get(key); + } + + public Object setData(String key, Object value) { + return data.put(key, value); + } + + @Override + public DropTarget getTarget() { + return dropTarget; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java new file mode 100644 index 0000000000..1457ea9df3 --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java @@ -0,0 +1,36 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.dd.DragAndDropEvent; + +/** + * Criterion that accepts all drops anywhere on the component. + * <p> + * Note! Class is singleton, use {@link #get()} method to get the instance. + * + * + * @since 6.3 + * + */ +public final class AcceptAll extends ClientSideCriterion { + + private static final long serialVersionUID = 7406683402153141461L; + private static AcceptCriterion singleton = new AcceptAll(); + + private AcceptAll() { + } + + public static AcceptCriterion get() { + return singleton; + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + return true; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java new file mode 100644 index 0000000000..c0f04d362f --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java @@ -0,0 +1,75 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import java.io.Serializable; + +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Criterion that can be used create policy to accept/discard dragged content + * (presented by {@link Transferable}). + * + * The drag and drop mechanism will verify the criteria returned by + * {@link DropHandler#getAcceptCriterion()} before calling + * {@link DropHandler#drop(DragAndDropEvent)}. + * + * The criteria can be evaluated either on the client (browser - see + * {@link ClientSideCriterion}) or on the server (see + * {@link ServerSideCriterion}). If no constraints are needed, an + * {@link AcceptAll} can be used. + * + * In addition to accepting or rejecting a possible drop, criteria can provide + * additional hints for client side painting. + * + * @see DropHandler + * @see ClientSideCriterion + * @see ServerSideCriterion + * + * @since 6.3 + */ +public interface AcceptCriterion extends Serializable { + + /** + * Returns whether the criteria can be checked on the client or whether a + * server request is needed to check the criteria. + * + * This requirement may depend on the state of the criterion (e.g. logical + * operations between criteria), so this cannot be based on a marker + * interface. + */ + public boolean isClientSideVerifiable(); + + public void paint(PaintTarget target) throws PaintException; + + /** + * This needs to be implemented iff criterion does some lazy server side + * initialization. The UIDL painted in this method will be passed to client + * side drop handler implementation. Implementation can assume that + * {@link #accept(DragAndDropEvent)} is called before this method. + * + * @param target + * @throws PaintException + */ + public void paintResponse(PaintTarget target) throws PaintException; + + /** + * Validates the data in event to be appropriate for the + * {@link DropHandler#drop(DragAndDropEvent)} method. + * <p> + * Note that even if your criterion is validated on client side, you should + * always validate the data on server side too. + * + * @param dragEvent + * @return + */ + public boolean accept(DragAndDropEvent dragEvent); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/And.java b/server/src/com/vaadin/event/dd/acceptcriteria/And.java new file mode 100644 index 0000000000..4122d67160 --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/And.java @@ -0,0 +1,54 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * A compound criterion that accepts the drag if all of its criteria accepts the + * drag. + * + * @see Or + * + * @since 6.3 + * + */ +public class And extends ClientSideCriterion { + + private static final long serialVersionUID = -5242574480825471748L; + protected ClientSideCriterion[] criteria; + + /** + * + * @param criteria + * criteria of which the And criterion will be composed + */ + public And(ClientSideCriterion... criteria) { + this.criteria = criteria; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + for (ClientSideCriterion crit : criteria) { + crit.paint(target); + } + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + for (ClientSideCriterion crit : criteria) { + if (!crit.accept(dragEvent)) { + return false; + } + } + return true; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java b/server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java new file mode 100644 index 0000000000..7d2c42ecb0 --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java @@ -0,0 +1,61 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd.acceptcriteria; + +import java.io.Serializable; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Parent class for criteria that can be completely validated on client side. + * All classes that provide criteria that can be completely validated on client + * side should extend this class. + * + * It is recommended that subclasses of ClientSideCriterion re-validate the + * condition on the server side in + * {@link AcceptCriterion#accept(com.vaadin.event.dd.DragAndDropEvent)} after + * the client side validation has accepted a transfer. + * + * @since 6.3 + */ +public abstract class ClientSideCriterion implements Serializable, + AcceptCriterion { + + /* + * All criteria that extend this must be completely validatable on client + * side. + * + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#isClientSideVerifiable + * () + */ + @Override + public final boolean isClientSideVerifiable() { + return true; + } + + @Override + public void paint(PaintTarget target) throws PaintException { + target.startTag("-ac"); + target.addAttribute("name", getIdentifier()); + paintContent(target); + target.endTag("-ac"); + } + + protected void paintContent(PaintTarget target) throws PaintException { + } + + protected String getIdentifier() { + return getClass().getCanonicalName(); + } + + @Override + public final void paintResponse(PaintTarget target) throws PaintException { + // NOP, nothing to do as this is client side verified criterion + } + +} diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java b/server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java new file mode 100644 index 0000000000..4c52698a4a --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java @@ -0,0 +1,53 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * A Criterion that checks whether {@link Transferable} contains given data + * flavor. The developer might for example accept the incoming data only if it + * contains "Url" or "Text". + * + * @since 6.3 + */ +public class ContainsDataFlavor extends ClientSideCriterion { + + private String dataFlavorId; + + /** + * Constructs a new instance of {@link ContainsDataFlavor}. + * + * @param dataFlawor + * the type of data that will be checked from + * {@link Transferable} + */ + public ContainsDataFlavor(String dataFlawor) { + dataFlavorId = dataFlawor; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + target.addAttribute("p", dataFlavorId); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + return dragEvent.getTransferable().getDataFlavors() + .contains(dataFlavorId); + } + + @Override + protected String getIdentifier() { + // extending classes use client side implementation from this class + return ContainsDataFlavor.class.getCanonicalName(); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/Not.java b/server/src/com/vaadin/event/dd/acceptcriteria/Not.java new file mode 100644 index 0000000000..1ed40a324d --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/Not.java @@ -0,0 +1,39 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Criterion that wraps another criterion and inverts its return value. + * + * @since 6.3 + * + */ +public class Not extends ClientSideCriterion { + + private static final long serialVersionUID = 1131422338558613244L; + private AcceptCriterion acceptCriterion; + + public Not(ClientSideCriterion acceptCriterion) { + this.acceptCriterion = acceptCriterion; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + acceptCriterion.paint(target); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + return !acceptCriterion.accept(dragEvent); + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/Or.java b/server/src/com/vaadin/event/dd/acceptcriteria/Or.java new file mode 100644 index 0000000000..6ad45c54af --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/Or.java @@ -0,0 +1,52 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * A compound criterion that accepts the drag if any of its criterion accepts + * it. + * + * @see And + * + * @since 6.3 + * + */ +public class Or extends ClientSideCriterion { + private static final long serialVersionUID = 1L; + private AcceptCriterion criteria[]; + + /** + * @param criteria + * the criteria of which the Or criteria will be composed + */ + public Or(ClientSideCriterion... criteria) { + this.criteria = criteria; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + for (AcceptCriterion crit : criteria) { + crit.paint(target); + } + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + for (AcceptCriterion crit : criteria) { + if (crit.accept(dragEvent)) { + return true; + } + } + return false; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java b/server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java new file mode 100644 index 0000000000..47f06d434c --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.event.dd.acceptcriteria; + +import java.io.Serializable; + +import com.vaadin.event.Transferable; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Parent class for criteria which are verified on the server side during a drag + * operation to accept/discard dragged content (presented by + * {@link Transferable}). + * <p> + * Subclasses should implement the + * {@link AcceptCriterion#accept(com.vaadin.event.dd.DragAndDropEvent)} method. + * <p> + * As all server side state can be used to make a decision, this is more + * flexible than {@link ClientSideCriterion}. However, this does require + * additional requests from the browser to the server during a drag operation. + * + * @see AcceptCriterion + * @see ClientSideCriterion + * + * @since 6.3 + */ +public abstract class ServerSideCriterion implements Serializable, + AcceptCriterion { + + private static final long serialVersionUID = 2128510128911628902L; + + @Override + public final boolean isClientSideVerifiable() { + return false; + } + + @Override + public void paint(PaintTarget target) throws PaintException { + target.startTag("-ac"); + target.addAttribute("name", getIdentifier()); + paintContent(target); + target.endTag("-ac"); + } + + public void paintContent(PaintTarget target) { + } + + @Override + public void paintResponse(PaintTarget target) throws PaintException { + } + + protected String getIdentifier() { + return ServerSideCriterion.class.getCanonicalName(); + } +} diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java new file mode 100644 index 0000000000..d4fd20c952 --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java @@ -0,0 +1,67 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.event.TransferableImpl; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.ui.Component; + +/** + * Client side criteria that checks if the drag source is one of the given + * components. + * + * @since 6.3 + */ +@SuppressWarnings("serial") +public class SourceIs extends ClientSideCriterion { + + private Component[] components; + + public SourceIs(Component... component) { + components = component; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + int paintedComponents = 0; + for (int i = 0; i < components.length; i++) { + Component c = components[i]; + if (c.getApplication() != null) { + target.addAttribute("component" + paintedComponents++, c); + } else { + Logger.getLogger(SourceIs.class.getName()) + .log(Level.WARNING, + "SourceIs component {0} at index {1} is not attached to the component hierachy and will thus be ignored", + new Object[] { c.getClass().getName(), + Integer.valueOf(i) }); + } + } + target.addAttribute("c", paintedComponents); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + if (dragEvent.getTransferable() instanceof TransferableImpl) { + Component sourceComponent = ((TransferableImpl) dragEvent + .getTransferable()).getSourceComponent(); + for (Component c : components) { + if (c == sourceComponent) { + return true; + } + } + } + + return false; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java new file mode 100644 index 0000000000..a644b858e2 --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java @@ -0,0 +1,51 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.Transferable; +import com.vaadin.event.TransferableImpl; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.ui.Component; +import com.vaadin.ui.Table; +import com.vaadin.ui.Tree; + +/** + * + * A criterion that ensures the drag source is the same as drop target. Eg. + * {@link Tree} or {@link Table} could support only re-ordering of items, but no + * {@link Transferable}s coming outside. + * <p> + * Note! Class is singleton, use {@link #get()} method to get the instance. + * + * @since 6.3 + * + */ +public class SourceIsTarget extends ClientSideCriterion { + + private static final long serialVersionUID = -451399314705532584L; + private static SourceIsTarget instance = new SourceIsTarget(); + + private SourceIsTarget() { + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + if (dragEvent.getTransferable() instanceof TransferableImpl) { + Component sourceComponent = ((TransferableImpl) dragEvent + .getTransferable()).getSourceComponent(); + DropTarget target = dragEvent.getTargetDetails().getTarget(); + return sourceComponent == target; + } + return false; + } + + public static synchronized SourceIsTarget get() { + return instance; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java b/server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java new file mode 100644 index 0000000000..5df8f3f618 --- /dev/null +++ b/server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java @@ -0,0 +1,72 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +/** + * + */ +package com.vaadin.event.dd.acceptcriteria; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Criterion for checking if drop target details contains the specific property + * with the specific value. Currently only String values are supported. + * + * @since 6.3 + * + * TODO add support for other basic data types that we support in UIDL. + * + */ +public class TargetDetailIs extends ClientSideCriterion { + + private static final long serialVersionUID = 763165450054331246L; + private String propertyName; + private Object value; + + /** + * Constructs a criterion which ensures that the value there is a value in + * {@link TargetDetails} that equals the reference value. + * + * @param dataFlavor + * the type of data to be checked + * @param value + * the reference value to which the drop target detail will be + * compared + */ + public TargetDetailIs(String dataFlavor, String value) { + propertyName = dataFlavor; + this.value = value; + } + + public TargetDetailIs(String dataFlavor, Boolean true1) { + propertyName = dataFlavor; + value = true1; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + target.addAttribute("p", propertyName); + if (value instanceof Boolean) { + target.addAttribute("v", ((Boolean) value).booleanValue()); + target.addAttribute("t", "b"); + } else if (value instanceof String) { + target.addAttribute("v", (String) value); + } + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + Object data = dragEvent.getTargetDetails().getData(propertyName); + return value.equals(data); + } + + @Override + protected String getIdentifier() { + // sub classes by default use VDropDetailEquals a client implementation + return TargetDetailIs.class.getCanonicalName(); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/event/package.html b/server/src/com/vaadin/event/package.html new file mode 100644 index 0000000000..2e7e17b892 --- /dev/null +++ b/server/src/com/vaadin/event/package.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides classes and interfaces for the inheritable event +model. The model supports inheritable events and a flexible way of +registering and unregistering event listeners. It's a fundamental building +block of Vaadin, and as it is included in +{@link com.vaadin.ui.AbstractComponent}, all UI components +automatically support it.</p> + +<h2>Package Specification</h2> + +<p>The core of the event model is the inheritable event class +hierarchy, and the {@link com.vaadin.event.EventRouter EventRouter} +which provide a simple, ubiquitous mechanism to transport events to all +interested parties.</p> + +<p>The power of the event inheritance arises from the possibility of +receiving not only the events of the registered type, <i>but also the +ones which are inherited from it</i>. For example, let's assume that there +are the events <code>GeneralEvent</code> and <code>SpecializedEvent</code> +so that the latter inherits the former. Furthermore we have an object +<code>A</code> which registers to receive <code>GeneralEvent</code> type +events from the object <code>B</code>. <code>A</code> would of course +receive all <code>GeneralEvent</code>s generated by <code>B</code>, but in +addition to this, <code>A</code> would also receive all +<code>SpecializedEvent</code>s generated by <code>B</code>. However, if +<code>B</code> generates some other events that do not have +<code>GeneralEvent</code> as an ancestor, <code>A</code> would not receive +them unless it registers to listen for them, too.</p> + +<p>The interface to attaching and detaching listeners to and from an object +works with methods. One specifies the event that should trigger the listener, +the trigger method that should be called when a suitable event occurs and the +object owning the method. From these a new listener is constructed and added +to the event router of the specified component.</p> + +<p>The interface is defined in +{@link com.vaadin.event.MethodEventSource MethodEventSource}, and a +straightforward implementation of it is defined in +{@link com.vaadin.event.EventRouter EventRouter} which also includes +a method to actually fire the events.</p> + +<p>All fired events are passed to all registered listeners, which are of +type {@link com.vaadin.event.ListenerMethod ListenerMethod}. The +listener then checks if the event type matches with the specified event +type and calls the specified trigger method if it does.</p> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> diff --git a/server/src/com/vaadin/external/json/JSONArray.java b/server/src/com/vaadin/external/json/JSONArray.java new file mode 100644 index 0000000000..2307749ffc --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONArray.java @@ -0,0 +1,963 @@ +package com.vaadin.external.json; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having <code>get</code> and <code>opt</code> + * methods for accessing the values by index, and <code>put</code> methods for + * adding or replacing values. The values can be any of these types: + * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>, + * <code>Number</code>, <code>String</code>, or the + * <code>JSONObject.NULL object</code>. + * <p> + * The constructor can convert a JSON text into a Java object. The + * <code>toString</code> method converts to JSON text. + * <p> + * A <code>get</code> method returns a value if one can be found, and throws an + * exception if one cannot be found. An <code>opt</code> method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + * <p> + * The generic <code>get()</code> and <code>opt()</code> methods return an + * object which you can cast or query for type. There are also typed + * <code>get</code> and <code>opt</code> methods that do type checking and type + * coercion for you. + * <p> + * The texts produced by the <code>toString</code> methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + * <ul> + * <li>An extra <code>,</code> <small>(comma)</small> may appear just + * before the closing bracket.</li> + * <li>The <code>null</code> value will be inserted when there is <code>,</code> + * <small>(comma)</small> elision.</li> + * <li>Strings may be quoted with <code>'</code> <small>(single + * quote)</small>.</li> + * <li>Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and + * if they are not the reserved words <code>true</code>, <code>false</code>, or + * <code>null</code>.</li> + * <li>Values can be separated by <code>;</code> <small>(semicolon)</small> as + * well as by <code>,</code> <small>(comma)</small>.</li> + * <li>Numbers may have the <code>0x-</code> <small>(hex)</small> prefix.</li> + * </ul> + * + * @author JSON.org + * @version 2011-08-25 + */ +public class JSONArray implements Serializable { + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private ArrayList myArrayList; + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * + * @param x + * A JSONTokener + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + if (x.nextClean() != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + myArrayList.add(JSONObject.NULL); + } else { + x.back(); + myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + /** + * Construct a JSONArray from a source JSON text. + * + * @param source + * A string that begins with <code>[</code> <small>(left + * bracket)</small> and ends with <code>]</code> + * <small>(right bracket)</small>. + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONArray from a Collection. + * + * @param collection + * A Collection. + */ + public JSONArray(Collection collection) { + myArrayList = new ArrayList(); + if (collection != null) { + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + myArrayList.add(JSONObject.wrap(iter.next())); + } + } + } + + /** + * Construct a JSONArray from an array + * + * @throws JSONException + * If not an array. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + } + + /** + * Get the object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException + * If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object object = opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with an index. The string values "true" + * and "false" are converted to boolean. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException + * If there is no value for the index or if the value is not + * convertible to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = get(index); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONArray[" + index + "] is not a boolean."); + } + + /** + * Get the double value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public double getDouble(int index) throws JSONException { + Object object = get(index); + try { + return object instanceof Number ? ((Number) object).doubleValue() + : Double.parseDouble((String) object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number."); + } + } + + /** + * Get the int value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value is not a number. + */ + public int getInt(int index) throws JSONException { + Object object = get(index); + try { + return object instanceof Number ? ((Number) object).intValue() + : Integer.parseInt((String) object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number."); + } + } + + /** + * Get the JSONArray associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException + * If there is no value for the index. or if the value is not a + * JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONArray[" + index + "] is not a JSONArray."); + } + + /** + * Get the JSONObject associated with an index. + * + * @param index + * subscript + * @return A JSONObject value. + * @throws JSONException + * If there is no value for the index or if the value is not a + * JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONArray[" + index + "] is not a JSONObject."); + } + + /** + * Get the long value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public long getLong(int index) throws JSONException { + Object object = get(index); + try { + return object instanceof Number ? ((Number) object).longValue() + : Long.parseLong((String) object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number."); + } + } + + /** + * Get the string associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException + * If there is no string value for the index. + */ + public String getString(int index) throws JSONException { + Object object = get(index); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONArray[" + index + "] not a string."); + } + + /** + * Determine if the value is null. + * + * @param index + * The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(opt(index)); + } + + /** + * Make a string from the contents of this JSONArray. The + * <code>separator</code> string is inserted between each element. Warning: + * This method assumes that the data structure is acyclical. + * + * @param separator + * A string that will be inserted between the elements. + * @return a string. + * @throws JSONException + * If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = length(); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(separator); + } + sb.append(JSONObject.valueToString(myArrayList.get(i))); + } + return sb.toString(); + } + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return myArrayList.size(); + } + + /** + * Get the optional object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object value, or null if there is no object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= length()) ? null : myArrayList.get(index); + } + + /** + * Get the optional boolean value associated with an index. It returns false + * if there is no value at that index, or if the value is not Boolean.TRUE + * or the String "true". + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return optBoolean(index, false); + } + + /** + * Get the optional boolean value associated with an index. It returns the + * defaultValue if there is no value at that index or if it is not a Boolean + * or the String "true" or "false" (case insensitive). + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return optDouble(index, Double.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + try { + return getDouble(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional int value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return optInt(index, 0); + } + + /** + * Get the optional int value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + try { + return getInt(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional JSONArray associated with an index. + * + * @param index + * subscript + * @return A JSONArray value, or null if the index has no value, or if the + * value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get the optional JSONObject associated with an index. Null is returned if + * the key is not found, or null if the index has no value, or if the value + * is not a JSONObject. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + /** + * Get the optional long value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return optLong(index, 0); + } + + /** + * Get the optional long value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + try { + return getLong(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value is not a + * string and is not null, then it is coverted to a string. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return optString(index, ""); + } + + /** + * Get the optional string associated with an index. The defaultValue is + * returned if the key is not found. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object object = opt(index); + return JSONObject.NULL.equals(object) ? object.toString() + : defaultValue; + } + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value + * A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + put(value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param value + * A Collection value. + * @return this. + */ + public JSONArray put(Collection value) { + put(new JSONArray(value)); + return this; + } + + /** + * Append a double value. This increases the array's length by one. + * + * @param value + * A double value. + * @throws JSONException + * if the value is not finite. + * @return this. + */ + public JSONArray put(double value) throws JSONException { + Double d = new Double(value); + JSONObject.testValidity(d); + put(d); + return this; + } + + /** + * Append an int value. This increases the array's length by one. + * + * @param value + * An int value. + * @return this. + */ + public JSONArray put(int value) { + put(new Integer(value)); + return this; + } + + /** + * Append an long value. This increases the array's length by one. + * + * @param value + * A long value. + * @return this. + */ + public JSONArray put(long value) { + put(new Long(value)); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject which + * is produced from a Map. + * + * @param value + * A Map value. + * @return this. + */ + public JSONArray put(Map value) { + put(new JSONObject(value)); + return this; + } + + /** + * Append an object value. This increases the array's length by one. + * + * @param value + * An object value. The value should be a Boolean, Double, + * Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + */ + public JSONArray put(Object value) { + myArrayList.add(value); + return this; + } + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * A boolean value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + put(index, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param index + * The subscript. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is not finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + put(index, new JSONArray(value)); + return this; + } + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A double value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is not finite. + */ + public JSONArray put(int index, double value) throws JSONException { + put(index, new Double(value)); + return this; + } + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * An int value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + put(index, new Integer(value)); + return this; + } + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A long value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + put(index, new Long(value)); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject that + * is produced from a Map. + * + * @param index + * The subscript. + * @param value + * The Map value. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Map value) throws JSONException { + put(index, new JSONObject(value)); + return this; + } + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Object value) throws JSONException { + JSONObject.testValidity(value); + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < length()) { + myArrayList.set(index, value); + } else { + while (index != length()) { + put(JSONObject.NULL); + } + put(value); + } + return this; + } + + /** + * Remove an index and close the hole. + * + * @param index + * The index of the element to be removed. + * @return The value that was associated with the index, or null if there + * was no value. + */ + public Object remove(int index) { + Object o = opt(index); + myArrayList.remove(index); + return o; + } + + /** + * Produce a JSONObject by combining a JSONArray of names with the values of + * this JSONArray. + * + * @param names + * A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException + * If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.length() == 0 || length() == 0) { + return null; + } + JSONObject jo = new JSONObject(); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), opt(i)); + } + return jo; + } + + /** + * Make a JSON text of this JSONArray. For compactness, no unnecessary + * whitespace is added. If it is not possible to produce a syntactically + * correct JSON text then null will be returned instead. This could occur if + * the array contains an invalid number. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, transmittable representation of the + * array. + */ + @Override + public String toString() { + try { + return '[' + join(",") + ']'; + } catch (Exception e) { + return null; + } + } + + /** + * Make a prettyprinted JSON text of this JSONArray. Warning: This method + * assumes that the data structure is acyclical. + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, transmittable representation of the + * object, beginning with <code>[</code> <small>(left + * bracket)</small> and ending with <code>]</code> + * <small>(right bracket)</small>. + * @throws JSONException + */ + public String toString(int indentFactor) throws JSONException { + return toString(indentFactor, 0); + } + + /** + * Make a prettyprinted JSON text of this JSONArray. Warning: This method + * assumes that the data structure is acyclical. + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indention of the top level. + * @return a printable, displayable, transmittable representation of the + * array. + * @throws JSONException + */ + String toString(int indentFactor, int indent) throws JSONException { + int len = length(); + if (len == 0) { + return "[]"; + } + int i; + StringBuffer sb = new StringBuffer("["); + if (len == 1) { + sb.append(JSONObject.valueToString(myArrayList.get(0), + indentFactor, indent)); + } else { + int newindent = indent + indentFactor; + sb.append('\n'); + for (i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(",\n"); + } + for (int j = 0; j < newindent; j += 1) { + sb.append(' '); + } + sb.append(JSONObject.valueToString(myArrayList.get(i), + indentFactor, newindent)); + } + sb.append('\n'); + for (i = 0; i < indent; i += 1) { + sb.append(' '); + } + } + sb.append(']'); + return sb.toString(); + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. For + * compactness, no whitespace is added. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + try { + boolean b = false; + int len = length(); + + writer.write('['); + + for (int i = 0; i < len; i += 1) { + if (b) { + writer.write(','); + } + Object v = myArrayList.get(i); + if (v instanceof JSONObject) { + ((JSONObject) v).write(writer); + } else if (v instanceof JSONArray) { + ((JSONArray) v).write(writer); + } else { + writer.write(JSONObject.valueToString(v)); + } + b = true; + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/external/json/JSONException.java b/server/src/com/vaadin/external/json/JSONException.java new file mode 100644 index 0000000000..895ffcb457 --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONException.java @@ -0,0 +1,32 @@ +package com.vaadin.external.json; + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * + * @author JSON.org + * @version 2010-12-24 + */ +public class JSONException extends Exception { + private static final long serialVersionUID = 0; + private Throwable cause; + + /** + * Constructs a JSONException with an explanatory message. + * + * @param message + * Detail about the reason for the exception. + */ + public JSONException(String message) { + super(message); + } + + public JSONException(Throwable cause) { + super(cause.getMessage()); + this.cause = cause; + } + + @Override + public Throwable getCause() { + return this.cause; + } +} diff --git a/server/src/com/vaadin/external/json/JSONObject.java b/server/src/com/vaadin/external/json/JSONObject.java new file mode 100644 index 0000000000..ba772933be --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONObject.java @@ -0,0 +1,1693 @@ +package com.vaadin.external.json; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having <code>get</code> and <code>opt</code> methods for accessing the + * values by name, and <code>put</code> methods for adding or replacing values + * by name. The values can be any of these types: <code>Boolean</code>, + * <code>JSONArray</code>, <code>JSONObject</code>, <code>Number</code>, + * <code>String</code>, or the <code>JSONObject.NULL</code> object. A JSONObject + * constructor can be used to convert an external form JSON text into an + * internal form whose values can be retrieved with the <code>get</code> and + * <code>opt</code> methods, or to convert values into a JSON text using the + * <code>put</code> and <code>toString</code> methods. A <code>get</code> method + * returns a value if one can be found, and throws an exception if one cannot be + * found. An <code>opt</code> method returns a default value instead of throwing + * an exception, and so is useful for obtaining optional values. + * <p> + * The generic <code>get()</code> and <code>opt()</code> methods return an + * object, which you can cast or query for type. There are also typed + * <code>get</code> and <code>opt</code> methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they do + * not throw. Instead, they return a specified value, such as null. + * <p> + * The <code>put</code> methods add or replace values in an object. For example, + * + * <pre> + * myString = new JSONObject().put("JSON", "Hello, World!").toString(); + * </pre> + * + * produces the string <code>{"JSON": "Hello, World"}</code>. + * <p> + * The texts produced by the <code>toString</code> methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + * <ul> + * <li>An extra <code>,</code> <small>(comma)</small> may appear just + * before the closing brace.</li> + * <li>Strings may be quoted with <code>'</code> <small>(single + * quote)</small>.</li> + * <li>Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and + * if they are not the reserved words <code>true</code>, <code>false</code>, or + * <code>null</code>.</li> + * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as by + * <code>:</code>.</li> + * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as + * well as by <code>,</code> <small>(comma)</small>.</li> + * <li>Numbers may have the <code>0x-</code> <small>(hex)</small> prefix.</li> + * </ul> + * + * @author JSON.org + * @version 2011-10-16 + */ +public class JSONObject implements Serializable { + + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null implements Serializable { + + /** + * There is only intended to be a single instance of the NULL object, so + * the clone method returns itself. + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object + * An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } + } + + /** + * The map where the JSONObject's properties are kept. + */ + private Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * <code>NULL</code> object than to use Java's <code>null</code> value. + * <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>. + * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>. + */ + public static final Object NULL = new Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + map = new HashMap(); + } + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo + * A JSONObject. + * @param names + * An array of strings. + * @throws JSONException + * @exception JSONException + * If a value is a non-finite number or if a name is + * duplicated. + */ + public JSONObject(JSONObject jo, String[] names) { + this(); + for (int i = 0; i < names.length; i += 1) { + try { + putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x + * A JSONTokener object containing the source string. + * @throws JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. We will also tolerate '=' or '=>'. + + c = x.nextClean(); + if (c == '=') { + if (x.next() != '>') { + x.back(); + } + } else if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + putOnce(key, x.nextValue()); + + // Pairs are separated by ','. We will also tolerate ';'. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param map + * A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + */ + public JSONObject(Map map) { + this.map = new HashMap(); + if (map != null) { + Iterator i = map.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + Object value = e.getValue(); + if (value != null) { + this.map.put(e.getKey(), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with <code>"get"</code> or + * <code>"is"</code> followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + * + * The key is formed by removing the <code>"get"</code> or <code>"is"</code> + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + * + * For example, if an object has a method named <code>"getName"</code>, and + * if the result of calling <code>object.getName()</code> is + * <code>"Larry Fine"</code>, then the JSONObject will contain + * <code>"name": "Larry Fine"</code>. + * + * @param bean + * An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONObject(Object bean) { + this(); + populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object + * An object that has fields that should be used to make a + * JSONObject. + * @param names + * An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String names[]) { + this(); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source + * A string beginning with <code>{</code> <small>(left + * brace)</small> and ending with <code>}</code> + * <small>(right brace)</small>. + * @exception JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName + * The ResourceBundle base name. + * @param locale + * The Locale to load the ResourceBundle for. + * @throws JSONException + * If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + + // Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof String) { + + // Go through the path, ensuring that there is a nested + // JSONObject for each + // segment except the last. Add the value using the last + // segment's name into + // the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + * + * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is an invalid number or if the key is null. + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = opt(key); + if (object == null) { + put(key, value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the key is null or if the current value associated with + * the key is not a JSONArray. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = opt(key); + if (object == null) { + put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + put(key, ((JSONArray) object).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d + * A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + + // Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key + * A key string. + * @return The object associated with the key. + * @throws JSONException + * if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with a key. + * + * @param key + * A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + /** + * Get the double value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + Object object = get(key); + try { + return object instanceof Number ? ((Number) object).doubleValue() + : Double.parseDouble((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number."); + } + } + + /** + * Get the int value associated with a key. + * + * @param key + * A key string. + * @return The integer value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + Object object = get(key); + try { + return object instanceof Number ? ((Number) object).intValue() + : Integer.parseInt((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not an int."); + } + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + /** + * Get the long value associated with a key. + * + * @param key + * A key string. + * @return The long value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + Object object = get(key); + try { + return object instanceof Number ? ((Number) object).longValue() + : Long.parseLong((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a long."); + } + } + + /** + * Get an array of field names from a JSONObject. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + int length = jo.length(); + if (length == 0) { + return null; + } + Iterator iterator = jo.keys(); + String[] names = new String[length]; + int i = 0; + while (iterator.hasNext()) { + names[i] = (String) iterator.next(); + i += 1; + } + return names; + } + + /** + * Get an array of field names from an Object. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key + * A key string. + * @return A string which is the value. + * @throws JSONException + * if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = get(key); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONObject[" + quote(key) + "] not a string."); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key + * A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if it is + * an Integer, Long, Double, or Float, then add one to it. + * + * @param key + * A key string. + * @return this. + * @throws JSONException + * If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = opt(key); + if (value == null) { + put(key, 1); + } else if (value instanceof Integer) { + put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + put(key, ((Long) value).longValue() + 1); + } else if (value instanceof Double) { + put(key, ((Double) value).doubleValue() + 1); + } else if (value instanceof Float) { + put(key, ((Float) value).floatValue() + 1); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key + * A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return map.keySet().iterator(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return map.size(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + JSONArray ja = new JSONArray(); + Iterator keys = keys(); + while (keys.hasNext()) { + ja.put(keys.next()); + } + return ja.length() == 0 ? null : ja; + } + + /** + * Produce a string from a Number. + * + * @param number + * A Number + * @return A String. + * @throws JSONException + * If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key + * A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : map.get(key); + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key + * A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + try { + return getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + try { + return getDouble(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + try { + return getInt(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + try { + return getLong(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key + * A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = (includeSuperClass) ? klass.getMethods() : klass + .getDeclaredMethods(); + for (int i = 0; i < methods.length; i += 1) { + try { + Method method = methods[i]; + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName(); + String key = ""; + if (name.startsWith("get")) { + if (name.equals("getClass") + || name.equals("getDeclaringClass")) { + key = ""; + } else { + key = name.substring(3); + } + } else if (name.startsWith("is")) { + key = name.substring(2); + } + if (key.length() > 0 + && Character.isUpperCase(key.charAt(0)) + && method.getParameterTypes().length == 0) { + if (key.length() == 1) { + key = key.toLowerCase(); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase() + + key.substring(1); + } + + Object result = method.invoke(bean, (Object[]) null); + if (result != null) { + map.put(key, wrap(result)); + } + } + } + } catch (Exception ignore) { + } + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A boolean which is the value. + * @return this. + * @throws JSONException + * If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + put(key, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key + * A key string. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Collection value) throws JSONException { + put(key, new JSONArray(value)); + return this; + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A double which is the value. + * @return this. + * @throws JSONException + * If the key is null or if the number is invalid. + */ + public JSONObject put(String key, double value) throws JSONException { + put(key, new Double(value)); + return this; + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * An int which is the value. + * @return this. + * @throws JSONException + * If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + put(key, new Integer(value)); + return this; + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A long which is the value. + * @return this. + * @throws JSONException + * If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + put(key, new Long(value)); + return this; + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key + * A key string. + * @param value + * A Map value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Map value) throws JSONException { + put(key, new JSONObject(value)); + return this; + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number or if the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + if (value != null) { + testValidity(value); + map.put(key, value); + } else { + remove(key); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key + * @param value + * @return his. + * @throws JSONException + * if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + put(key, value); + } + return this; + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within </, producing <\/, + * allowing JSON text to be delivered in HTML. In JSON text, a string cannot + * contain a control character or an unescaped quote or backslash. + * + * @param string + * A String + * @return A String correctly formatted for insertion in a JSON text. + */ + public static String quote(String string) { + if (string == null || string.length() == 0) { + return "\"\""; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + StringBuffer sb = new StringBuffer(len + 4); + + sb.append('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + sb.append('\\'); + sb.append(c); + break; + case '/': + if (b == '<') { + sb.append('\\'); + } + sb.append(c); + break; + case '\b': + sb.append("\\b"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + hhhh = "000" + Integer.toHexString(c); + sb.append("\\u" + hhhh.substring(hhhh.length() - 4)); + } else { + sb.append(c); + } + } + } + sb.append('"'); + return sb.toString(); + } + + /** + * Remove a name and its value, if present. + * + * @param key + * The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return map.remove(key); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string + * A String. + * @return A simple JSON value. + */ + public static Object stringToValue(String string) { + Double d; + if (string.equals("")) { + return string; + } + if (string.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (string.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (string.equalsIgnoreCase("null")) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. We support the + * non-standard 0x- convention. If a number cannot be produced, then the + * value will just be a string. Note that the 0x-, plus, and implied + * string conventions are non-standard. A JSON parser may accept + * non-JSON forms as long as it accepts all correct JSON forms. + */ + + char b = string.charAt(0); + if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') { + if (b == '0' && string.length() > 2 + && (string.charAt(1) == 'x' || string.charAt(1) == 'X')) { + try { + return new Integer( + Integer.parseInt(string.substring(2), 16)); + } catch (Exception ignore) { + } + } + try { + if (string.indexOf('.') > -1 || string.indexOf('e') > -1 + || string.indexOf('E') > -1) { + d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = new Long(string); + if (myLong.longValue() == myLong.intValue()) { + return new Integer(myLong.intValue()); + } else { + return myLong; + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o + * The object to test. + * @throws JSONException + * If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names + * A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException + * If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with <code>{</code> <small>(left + * brace)</small> and ending with <code>}</code> <small>(right + * brace)</small>. + */ + @Override + public String toString() { + try { + Iterator keys = keys(); + StringBuffer sb = new StringBuffer("{"); + + while (keys.hasNext()) { + if (sb.length() > 1) { + sb.append(','); + } + Object o = keys.next(); + sb.append(quote(o.toString())); + sb.append(':'); + sb.append(valueToString(map.get(o))); + } + sb.append('}'); + return sb.toString(); + } catch (Exception e) { + return null; + } + } + + /** + * Make a prettyprinted JSON text of this JSONObject. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with <code>{</code> <small>(left + * brace)</small> and ending with <code>}</code> <small>(right + * brace)</small>. + * @throws JSONException + * If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + return toString(indentFactor, 0); + } + + /** + * Make a prettyprinted JSON text of this JSONObject. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return a printable, displayable, transmittable representation of the + * object, beginning with <code>{</code> <small>(left + * brace)</small> and ending with <code>}</code> <small>(right + * brace)</small>. + * @throws JSONException + * If the object contains an invalid number. + */ + String toString(int indentFactor, int indent) throws JSONException { + int i; + int length = length(); + if (length == 0) { + return "{}"; + } + Iterator keys = keys(); + int newindent = indent + indentFactor; + Object object; + StringBuffer sb = new StringBuffer("{"); + if (length == 1) { + object = keys.next(); + sb.append(quote(object.toString())); + sb.append(": "); + sb.append(valueToString(map.get(object), indentFactor, indent)); + } else { + while (keys.hasNext()) { + object = keys.next(); + if (sb.length() > 1) { + sb.append(",\n"); + } else { + sb.append('\n'); + } + for (i = 0; i < newindent; i += 1) { + sb.append(' '); + } + sb.append(quote(object.toString())); + sb.append(": "); + sb.append(valueToString(map.get(object), indentFactor, + newindent)); + } + if (sb.length() > 1) { + sb.append('\n'); + for (i = 0; i < indent; i += 1) { + sb.append(' '); + } + } + } + sb.append('}'); + return sb.toString(); + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with <code>{</code> <small>(left + * brace)</small> and ending with <code>}</code> <small>(right + * brace)</small>. + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + Object object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object instanceof String) { + return (String) object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + return new JSONObject((Map) value).toString(); + } + if (value instanceof Collection) { + return new JSONArray((Collection) value).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + return quote(value.toString()); + } + + /** + * Make a prettyprinted JSON text of an object value. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return a printable, displayable, transmittable representation of the + * object, beginning with <code>{</code> <small>(left + * brace)</small> and ending with <code>}</code> <small>(right + * brace)</small>. + * @throws JSONException + * If the object contains an invalid number. + */ + static String valueToString(Object value, int indentFactor, int indent) + throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + try { + if (value instanceof JSONString) { + Object o = ((JSONString) value).toJSONString(); + if (o instanceof String) { + return (String) o; + } + } + } catch (Exception ignore) { + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean) { + return value.toString(); + } + if (value instanceof JSONObject) { + return ((JSONObject) value).toString(indentFactor, indent); + } + if (value instanceof JSONArray) { + return ((JSONArray) value).toString(indentFactor, indent); + } + if (value instanceof Map) { + return new JSONObject((Map) value).toString(indentFactor, indent); + } + if (value instanceof Collection) { + return new JSONArray((Collection) value).toString(indentFactor, + indent); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(indentFactor, indent); + } + return quote(value.toString()); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object + * The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String) { + return object; + } + + if (object instanceof Collection) { + return new JSONArray((Collection) object); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + return new JSONObject((Map) object); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + try { + boolean commanate = false; + Iterator keys = keys(); + writer.write('{'); + + while (keys.hasNext()) { + if (commanate) { + writer.write(','); + } + Object key = keys.next(); + writer.write(quote(key.toString())); + writer.write(':'); + Object value = map.get(key); + if (value instanceof JSONObject) { + ((JSONObject) value).write(writer); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer); + } else { + writer.write(valueToString(value)); + } + commanate = true; + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/external/json/JSONString.java b/server/src/com/vaadin/external/json/JSONString.java new file mode 100644 index 0000000000..cc7e4d8c07 --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONString.java @@ -0,0 +1,21 @@ +package com.vaadin.external.json; + +import java.io.Serializable; + +/** + * The <code>JSONString</code> interface allows a <code>toJSONString()</code> + * method so that a class can change the behavior of + * <code>JSONObject.toString()</code>, <code>JSONArray.toString()</code>, and + * <code>JSONWriter.value(</code>Object<code>)</code>. The + * <code>toJSONString</code> method will be used instead of the default behavior + * of using the Object's <code>toString()</code> method and quoting the result. + */ +public interface JSONString extends Serializable { + /** + * The <code>toJSONString</code> method allows a class to produce its own + * JSON serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/server/src/com/vaadin/external/json/JSONStringer.java b/server/src/com/vaadin/external/json/JSONStringer.java new file mode 100644 index 0000000000..ae905cb15f --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONStringer.java @@ -0,0 +1,84 @@ +package com.vaadin.external.json; + +/* + Copyright (c) 2006 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.StringWriter; + +/** + * JSONStringer provides a quick and convenient way of producing JSON text. The + * texts produced strictly conform to JSON syntax rules. No whitespace is added, + * so the results are ready for transmission or storage. Each instance of + * JSONStringer can produce one JSON text. + * <p> + * A JSONStringer instance provides a <code>value</code> method for appending + * values to the text, and a <code>key</code> method for adding keys before + * values in objects. There are <code>array</code> and <code>endArray</code> + * methods that make and bound array values, and <code>object</code> and + * <code>endObject</code> methods which make and bound object values. All of + * these methods return the JSONWriter instance, permitting cascade style. For + * example, + * + * <pre> + * myString = new JSONStringer().object().key("JSON").value("Hello, World!") + * .endObject().toString(); + * </pre> + * + * which produces the string + * + * <pre> + * {"JSON":"Hello, World!"} + * </pre> + * <p> + * The first method called must be <code>array</code> or <code>object</code>. + * There are no methods for adding commas or colons. JSONStringer adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + * <p> + * This can sometimes be easier than using a JSONObject to build a string. + * + * @author JSON.org + * @version 2008-09-18 + */ +public class JSONStringer extends JSONWriter { + /** + * Make a fresh JSONStringer. It can be used to build one JSON text. + */ + public JSONStringer() { + super(new StringWriter()); + } + + /** + * Return the JSON text. This method is used to obtain the product of the + * JSONStringer instance. It will return <code>null</code> if there was a + * problem in the construction of the JSON text (such as the calls to + * <code>array</code> were not properly balanced with calls to + * <code>endArray</code>). + * + * @return The JSON text. + */ + @Override + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/server/src/com/vaadin/external/json/JSONTokener.java b/server/src/com/vaadin/external/json/JSONTokener.java new file mode 100644 index 0000000000..c3531cae1d --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONTokener.java @@ -0,0 +1,451 @@ +package com.vaadin.external.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.io.StringReader; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse JSON + * source strings. + * + * @author JSON.org + * @version 2010-12-24 + */ +public class JSONTokener implements Serializable { + + private int character; + private boolean eof; + private int index; + private int line; + private char previous; + private Reader reader; + private boolean usePrevious; + + /** + * Construct a JSONTokener from a Reader. + * + * @param reader + * A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() ? reader : new BufferedReader( + reader); + eof = false; + usePrevious = false; + previous = 0; + index = 0; + character = 1; + line = 1; + } + + /** + * Construct a JSONTokener from an InputStream. + */ + public JSONTokener(InputStream inputStream) throws JSONException { + this(new InputStreamReader(inputStream)); + } + + /** + * Construct a JSONTokener from a string. + * + * @param s + * A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + /** + * Back up one character. This provides a sort of lookahead capability, so + * that you can test for a digit or letter before attempting to parse the + * next number or identifier. + */ + public void back() throws JSONException { + if (usePrevious || index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + index -= 1; + character -= 1; + usePrevious = true; + eof = false; + } + + /** + * Get the hex value of a character (base16). + * + * @param c + * A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return eof && !usePrevious; + } + + /** + * Determine if the source string still contains characters that next() can + * consume. + * + * @return true if not yet at the end of the source. + */ + public boolean more() throws JSONException { + next(); + if (end()) { + return false; + } + back(); + return true; + } + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + */ + public char next() throws JSONException { + int c; + if (usePrevious) { + usePrevious = false; + c = previous; + } else { + try { + c = reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + + if (c <= 0) { // End of stream + eof = true; + c = 0; + } + } + index += 1; + if (previous == '\r') { + line += 1; + character = c == '\n' ? 0 : 1; + } else if (c == '\n') { + line += 1; + character = 0; + } else { + character += 1; + } + previous = (char) c; + return previous; + } + + /** + * Consume the next character, and check that it matches a specified + * character. + * + * @param c + * The character to match. + * @return The character. + * @throws JSONException + * if the character does not match. + */ + public char next(char c) throws JSONException { + char n = next(); + if (n != c) { + throw syntaxError("Expected '" + c + "' and instead saw '" + n + + "'"); + } + return n; + } + + /** + * Get the next n characters. + * + * @param n + * The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not n characters + * remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = next(); + if (end()) { + throw syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + /** + * Get the next char in the string, skipping whitespace. + * + * @throws JSONException + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() throws JSONException { + for (;;) { + char c = next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + /** + * Return the characters up to the next close quote character. Backslash + * processing is done. The formal JSON format does not allow strings in + * single quotes, but an implementation is allowed to accept them. + * + * @param quote + * The quoting character, either <code>"</code> + * <small>(double quote)</small> or <code>'</code> + * <small>(single quote)</small>. + * @return A String. + * @throws JSONException + * Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw syntaxError("Unterminated string"); + case '\\': + c = next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char) Integer.parseInt(next(4), 16)); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + /** + * Get the text up but not including the specified character or the end of + * line, whichever comes first. + * + * @param delimiter + * A delimiter character. + * @return A string. + */ + public String nextTo(char delimiter) throws JSONException { + StringBuffer sb = new StringBuffer(); + for (;;) { + char c = next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * + * @param delimiters + * A set of delimiter characters. + * @return A string, trimmed. + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * + * @throws JSONException + * If syntax error. + * + * @return An object. + */ + public Object nextValue() throws JSONException { + char c = nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return nextString(c); + case '{': + back(); + return new JSONObject(this); + case '[': + back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or null, + * or it can be a number. An implementation (such as this one) is + * allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuffer sb = new StringBuffer(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = next(); + } + back(); + + string = sb.toString().trim(); + if (string.equals("")) { + throw syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + + /** + * Skip characters until the next character is the requested character. If + * the requested character is not found, no characters are skipped. + * + * @param to + * A character to skip to. + * @return The requested character, or zero if the requested character is + * not found. + */ + public char skipTo(char to) throws JSONException { + char c; + try { + int startIndex = index; + int startCharacter = character; + int startLine = line; + reader.mark(Integer.MAX_VALUE); + do { + c = next(); + if (c == 0) { + reader.reset(); + index = startIndex; + character = startCharacter; + line = startLine; + return c; + } + } while (c != to); + } catch (IOException exc) { + throw new JSONException(exc); + } + + back(); + return c; + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message + * The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + toString()); + } + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + @Override + public String toString() { + return " at " + index + " [character " + character + " line " + line + + "]"; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/external/json/JSONWriter.java b/server/src/com/vaadin/external/json/JSONWriter.java new file mode 100644 index 0000000000..5f9ddeeae2 --- /dev/null +++ b/server/src/com/vaadin/external/json/JSONWriter.java @@ -0,0 +1,355 @@ +package com.vaadin.external.json; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; + +/* + Copyright (c) 2006 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/** + * JSONWriter provides a quick and convenient way of producing JSON text. The + * texts produced strictly conform to JSON syntax rules. No whitespace is added, + * so the results are ready for transmission or storage. Each instance of + * JSONWriter can produce one JSON text. + * <p> + * A JSONWriter instance provides a <code>value</code> method for appending + * values to the text, and a <code>key</code> method for adding keys before + * values in objects. There are <code>array</code> and <code>endArray</code> + * methods that make and bound array values, and <code>object</code> and + * <code>endObject</code> methods which make and bound object values. All of + * these methods return the JSONWriter instance, permitting a cascade style. For + * example, + * + * <pre> + * new JSONWriter(myWriter).object().key("JSON").value("Hello, World!") + * .endObject(); + * </pre> + * + * which writes + * + * <pre> + * {"JSON":"Hello, World!"} + * </pre> + * <p> + * The first method called must be <code>array</code> or <code>object</code>. + * There are no methods for adding commas or colons. JSONWriter adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + * <p> + * This can sometimes be easier than using a JSONObject to build a string. + * + * @author JSON.org + * @version 2011-11-14 + */ +public class JSONWriter implements Serializable { + private static final int maxdepth = 200; + + /** + * The comma flag determines if a comma should be output before the next + * value. + */ + private boolean comma; + + /** + * The current mode. Values: 'a' (array), 'd' (done), 'i' (initial), 'k' + * (key), 'o' (object). + */ + protected char mode; + + /** + * The object/array stack. + */ + private final JSONObject stack[]; + + /** + * The stack top index. A value of 0 indicates that the stack is empty. + */ + private int top; + + /** + * The writer that will receive the output. + */ + protected Writer writer; + + /** + * Make a fresh JSONWriter. It can be used to build one JSON text. + */ + public JSONWriter(Writer w) { + comma = false; + mode = 'i'; + stack = new JSONObject[maxdepth]; + top = 0; + writer = w; + } + + /** + * Append a value. + * + * @param string + * A string value. + * @return this + * @throws JSONException + * If the value is out of sequence. + */ + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (mode == 'o' || mode == 'a') { + try { + if (comma && mode == 'a') { + writer.write(','); + } + writer.write(string); + } catch (IOException e) { + throw new JSONException(e); + } + if (mode == 'o') { + mode = 'k'; + } + comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + /** + * Begin appending a new array. All values until the balancing + * <code>endArray</code> will be appended to this array. The + * <code>endArray</code> method must be called to mark the array's end. + * + * @return this + * @throws JSONException + * If the nesting is too deep, or if the object is started in + * the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter array() throws JSONException { + if (mode == 'i' || mode == 'o' || mode == 'a') { + push(null); + append("["); + comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + /** + * End something. + * + * @param mode + * Mode + * @param c + * Closing character + * @return this + * @throws JSONException + * If unbalanced. + */ + private JSONWriter end(char mode, char c) throws JSONException { + if (this.mode != mode) { + throw new JSONException(mode == 'a' ? "Misplaced endArray." + : "Misplaced endObject."); + } + pop(mode); + try { + writer.write(c); + } catch (IOException e) { + throw new JSONException(e); + } + comma = true; + return this; + } + + /** + * End an array. This method most be called to balance calls to + * <code>array</code>. + * + * @return this + * @throws JSONException + * If incorrectly nested. + */ + public JSONWriter endArray() throws JSONException { + return end('a', ']'); + } + + /** + * End an object. This method most be called to balance calls to + * <code>object</code>. + * + * @return this + * @throws JSONException + * If incorrectly nested. + */ + public JSONWriter endObject() throws JSONException { + return end('k', '}'); + } + + /** + * Append a key. The key will be associated with the next value. In an + * object, every value must be preceded by a key. + * + * @param string + * A key string. + * @return this + * @throws JSONException + * If the key is out of place. For example, keys do not belong + * in arrays or if the key is null. + */ + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (mode == 'k') { + try { + stack[top - 1].putOnce(string, Boolean.TRUE); + if (comma) { + writer.write(','); + } + writer.write(JSONObject.quote(string)); + writer.write(':'); + comma = false; + mode = 'o'; + return this; + } catch (IOException e) { + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + + /** + * Begin appending a new object. All keys and values until the balancing + * <code>endObject</code> will be appended to this object. The + * <code>endObject</code> method must be called to mark the object's end. + * + * @return this + * @throws JSONException + * If the nesting is too deep, or if the object is started in + * the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter object() throws JSONException { + if (mode == 'i') { + mode = 'o'; + } + if (mode == 'o' || mode == 'a') { + append("{"); + push(new JSONObject()); + comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + + /** + * Pop an array or object scope. + * + * @param c + * The scope to close. + * @throws JSONException + * If nesting is wrong. + */ + private void pop(char c) throws JSONException { + if (top <= 0) { + throw new JSONException("Nesting error."); + } + char m = stack[top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + top -= 1; + mode = top == 0 ? 'd' : stack[top - 1] == null ? 'a' : 'k'; + } + + /** + * Push an array or object scope. + * + * @param c + * The scope to open. + * @throws JSONException + * If nesting is too deep. + */ + private void push(JSONObject jo) throws JSONException { + if (top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + stack[top] = jo; + mode = jo == null ? 'a' : 'k'; + top += 1; + } + + /** + * Append either the value <code>true</code> or the value <code>false</code> + * . + * + * @param b + * A boolean. + * @return this + * @throws JSONException + */ + public JSONWriter value(boolean b) throws JSONException { + return append(b ? "true" : "false"); + } + + /** + * Append a double value. + * + * @param d + * A double. + * @return this + * @throws JSONException + * If the number is not finite. + */ + public JSONWriter value(double d) throws JSONException { + return this.value(new Double(d)); + } + + /** + * Append a long value. + * + * @param l + * A long. + * @return this + * @throws JSONException + */ + public JSONWriter value(long l) throws JSONException { + return append(Long.toString(l)); + } + + /** + * Append an object value. + * + * @param object + * The object to append. It can be null, or a Boolean, Number, + * String, JSONObject, or JSONArray, or an object that implements + * JSONString. + * @return this + * @throws JSONException + * If the value is out of sequence. + */ + public JSONWriter value(Object object) throws JSONException { + return append(JSONObject.valueToString(object)); + } +} diff --git a/server/src/com/vaadin/external/json/README b/server/src/com/vaadin/external/json/README new file mode 100644 index 0000000000..ca6dc11764 --- /dev/null +++ b/server/src/com/vaadin/external/json/README @@ -0,0 +1,68 @@ +JSON in Java [package org.json] + +Douglas Crockford +douglas@crockford.com + +2011-02-02 + + +JSON is a light-weight, language independent, data interchange format. +See http://www.JSON.org/ + +The files in this package implement JSON encoders/decoders in Java. +It also includes the capability to convert between JSON and XML, HTTP +headers, Cookies, and CDL. + +This is a reference implementation. There is a large number of JSON packages +in Java. Perhaps someday the Java community will standardize on one. Until +then, choose carefully. + +The license includes this restriction: "The software shall be used for good, +not evil." If your conscience cannot live with that, then choose a different +package. + +The package compiles on Java 1.2 thru Java 1.4. + + +JSONObject.java: The JSONObject can parse text from a String or a JSONTokener +to produce a map-like object. The object provides methods for manipulating its +contents, and for producing a JSON compliant object serialization. + +JSONArray.java: The JSONObject can parse text from a String or a JSONTokener +to produce a vector-like object. The object provides methods for manipulating +its contents, and for producing a JSON compliant array serialization. + +JSONTokener.java: The JSONTokener breaks a text into a sequence of individual +tokens. It can be constructed from a String, Reader, or InputStream. + +JSONException.java: The JSONException is the standard exception type thrown +by this package. + + +JSONString.java: The JSONString interface requires a toJSONString method, +allowing an object to provide its own serialization. + +JSONStringer.java: The JSONStringer provides a convenient facility for +building JSON strings. + +JSONWriter.java: The JSONWriter provides a convenient facility for building +JSON text through a writer. + + +CDL.java: CDL provides support for converting between JSON and comma +delimited lists. + +Cookie.java: Cookie provides support for converting between JSON and cookies. + +CookieList.java: CookieList provides support for converting between JSON and +cookie lists. + +HTTP.java: HTTP provides support for converting between JSON and HTTP headers. + +HTTPTokener.java: HTTPTokener extends JSONTokener for parsing HTTP headers. + +XML.java: XML provides support for converting between JSON and XML. + +JSONML.java: JSONML provides support for converting between JSONML and XML. + +XMLTokener.java: XMLTokener extends JSONTokener for parsing XML text.
\ No newline at end of file diff --git a/server/src/com/vaadin/navigator/FragmentManager.java b/server/src/com/vaadin/navigator/FragmentManager.java new file mode 100644 index 0000000000..f1fd90e569 --- /dev/null +++ b/server/src/com/vaadin/navigator/FragmentManager.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.navigator; + +import java.io.Serializable; + +/** + * Fragment manager that handles interaction between Navigator and URI fragments + * or other similar view identification and bookmarking system. + * + * Alternative implementations can be created for HTML5 pushState, for portlet + * URL navigation and other similar systems. + * + * This interface is mostly for internal use by {@link Navigator}. + * + * @author Vaadin Ltd + * @since 7.0 + */ +public interface FragmentManager extends Serializable { + /** + * Return the current fragment (location string) including view name and any + * optional parameters. + * + * @return current view and parameter string, not null + */ + public String getFragment(); + + /** + * Set the current fragment (location string) in the application URL or + * similar location, including view name and any optional parameters. + * + * @param fragment + * new view and parameter string, not null + */ + public void setFragment(String fragment); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/navigator/Navigator.java b/server/src/com/vaadin/navigator/Navigator.java new file mode 100644 index 0000000000..1813301fe6 --- /dev/null +++ b/server/src/com/vaadin/navigator/Navigator.java @@ -0,0 +1,656 @@ +package com.vaadin.navigator; + +/* + @VaadinApache2LicenseForJavaFiles@ + */ + +import java.io.Serializable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; +import com.vaadin.terminal.Page; +import com.vaadin.terminal.Page.FragmentChangedEvent; +import com.vaadin.terminal.Page.FragmentChangedListener; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.CustomComponent; + +/** + * Navigator utility that allows switching of views in a part of an application. + * + * The view switching can be based e.g. on URI fragments containing the view + * name and parameters to the view. There are two types of parameters for views: + * an optional parameter string that is included in the fragment (may be + * bookmarkable). + * + * Views can be explicitly registered or dynamically generated and listening to + * view changes is possible. + * + * Note that {@link Navigator} is not a component itself but comes with + * {@link SimpleViewDisplay} which is a component that displays the selected + * view as its contents. + * + * @author Vaadin Ltd + * @since 7.0 + */ +public class Navigator implements Serializable { + + // TODO divert navigation e.g. if no permissions? Or just show another view + // but keep URL? how best to intercept + // TODO investigate relationship with TouchKit navigation support + + /** + * Empty view component. + */ + public static class EmptyView extends CssLayout implements View { + /** + * Create minimally sized empty view. + */ + public EmptyView() { + setWidth("0px"); + setHeight("0px"); + } + + @Override + public void navigateTo(String fragmentParameters) { + // nothing to do + } + } + + /** + * Fragment manager using URI fragments of a Page to track views and enable + * listening to view changes. + * + * This class is mostly for internal use by Navigator, and is only public + * and static to enable testing. + */ + public static class UriFragmentManager implements FragmentManager, + FragmentChangedListener { + private final Page page; + private final Navigator navigator; + + /** + * Create a new URIFragmentManager and attach it to listen to URI + * fragment changes of a {@link Page}. + * + * @param page + * page whose URI fragment to get and modify + * @param navigator + * {@link Navigator} to notify of fragment changes (using + * {@link Navigator#navigateTo(String)} + */ + public UriFragmentManager(Page page, Navigator navigator) { + this.page = page; + this.navigator = navigator; + + page.addListener(this); + } + + @Override + public String getFragment() { + return page.getFragment(); + } + + @Override + public void setFragment(String fragment) { + page.setFragment(fragment, false); + } + + @Override + public void fragmentChanged(FragmentChangedEvent event) { + UriFragmentManager.this.navigator.navigateTo(getFragment()); + } + } + + /** + * View display that is a component itself and replaces its contents with + * the view. + * + * This display only supports views that are {@link Component}s themselves. + * Attempting to display a view that is not a component causes an exception + * to be thrown. + * + * By default, the view display has full size. + */ + public static class SimpleViewDisplay extends CustomComponent implements + ViewDisplay { + + /** + * Create new {@link ViewDisplay} that is itself a component displaying + * the view. + */ + public SimpleViewDisplay() { + setSizeFull(); + } + + @Override + public void showView(View view) { + if (view instanceof Component) { + setCompositionRoot((Component) view); + } else { + throw new IllegalArgumentException("View is not a component: " + + view); + } + } + } + + /** + * View display that replaces the contents of a {@link ComponentContainer} + * with the active {@link View}. + * + * All components of the container are removed before adding the new view to + * it. + * + * This display only supports views that are {@link Component}s themselves. + * Attempting to display a view that is not a component causes an exception + * to be thrown. + */ + public static class ComponentContainerViewDisplay implements ViewDisplay { + + private final ComponentContainer container; + + /** + * Create new {@link ViewDisplay} that updates a + * {@link ComponentContainer} to show the view. + */ + public ComponentContainerViewDisplay(ComponentContainer container) { + this.container = container; + } + + @Override + public void showView(View view) { + if (view instanceof Component) { + container.removeAllComponents(); + container.addComponent((Component) view); + } else { + throw new IllegalArgumentException("View is not a component: " + + view); + } + } + } + + /** + * View provider which supports mapping a single view name to a single + * pre-initialized view instance. + * + * For most cases, ClassBasedViewProvider should be used instead of this. + */ + public static class StaticViewProvider implements ViewProvider { + private final String viewName; + private final View view; + + /** + * Create a new view provider which returns a pre-created view instance. + * + * @param viewName + * name of the view (not null) + * @param view + * view instance to return (not null), reused on every + * request + */ + public StaticViewProvider(String viewName, View view) { + this.viewName = viewName; + this.view = view; + } + + @Override + public String getViewName(String viewAndParameters) { + if (null == viewAndParameters) { + return null; + } + if (viewAndParameters.startsWith(viewName)) { + return viewName; + } + return null; + } + + @Override + public View getView(String viewName) { + if (this.viewName.equals(viewName)) { + return view; + } + return null; + } + + /** + * Get the view name for this provider. + * + * @return view name for this provider + */ + public String getViewName() { + return viewName; + } + } + + /** + * View provider which maps a single view name to a class to instantiate for + * the view. + * + * Note that the view class must be accessible by the class loader used by + * the provider. This may require its visibility to be public. + * + * This class is primarily for internal use by {@link Navigator}. + */ + public static class ClassBasedViewProvider implements ViewProvider { + + private final String viewName; + private final Class<? extends View> viewClass; + + /** + * Create a new view provider which creates new view instances based on + * a view class. + * + * @param viewName + * name of the views to create (not null) + * @param viewClass + * class to instantiate when a view is requested (not null) + */ + public ClassBasedViewProvider(String viewName, + Class<? extends View> viewClass) { + if (null == viewName || null == viewClass) { + throw new IllegalArgumentException( + "View name and class should not be null"); + } + this.viewName = viewName; + this.viewClass = viewClass; + } + + @Override + public String getViewName(String viewAndParameters) { + if (null == viewAndParameters) { + return null; + } + if (viewAndParameters.equals(viewName) + || viewAndParameters.startsWith(viewName + "/")) { + return viewName; + } + return null; + } + + @Override + public View getView(String viewName) { + if (this.viewName.equals(viewName)) { + try { + View view = viewClass.newInstance(); + return view; + } catch (InstantiationException e) { + // TODO error handling + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + // TODO error handling + throw new RuntimeException(e); + } + } + return null; + } + + /** + * Get the view name for this provider. + * + * @return view name for this provider + */ + public String getViewName() { + return viewName; + } + + /** + * Get the view class for this provider. + * + * @return {@link View} class + */ + public Class<? extends View> getViewClass() { + return viewClass; + } + } + + private final FragmentManager fragmentManager; + private final ViewDisplay display; + private View currentView = null; + private List<ViewChangeListener> listeners = new LinkedList<ViewChangeListener>(); + private List<ViewProvider> providers = new LinkedList<ViewProvider>(); + + /** + * Create a navigator that is tracking the active view using URI fragments + * of the current {@link Page} and replacing the contents of a + * {@link ComponentContainer} with the active view. + * + * In case the container is not on the current page, use another + * {@link Navigator#Navigator(Page, ViewDisplay)} with an explicitly created + * {@link ComponentContainerViewDisplay}. + * + * All components of the container are removed each time before adding the + * active {@link View}. Views must implement {@link Component} when using + * this constructor. + * + * <p> + * After all {@link View}s and {@link ViewProvider}s have been registered, + * the application should trigger navigation to the current fragment using + * e.g. + * + * <pre> + * navigator.navigateTo(Page.getCurrent().getFragment()); + * </pre> + * + * @param container + * ComponentContainer whose contents should be replaced with the + * active view on view change + */ + public Navigator(ComponentContainer container) { + display = new ComponentContainerViewDisplay(container); + fragmentManager = new UriFragmentManager(Page.getCurrent(), this); + } + + /** + * Create a navigator that is tracking the active view using URI fragments. + * + * <p> + * After all {@link View}s and {@link ViewProvider}s have been registered, + * the application should trigger navigation to the current fragment using + * e.g. + * + * <pre> + * navigator.navigateTo(Page.getCurrent().getFragment()); + * </pre> + * + * @param page + * whose URI fragments are used + * @param display + * where to display the views + */ + public Navigator(Page page, ViewDisplay display) { + this.display = display; + fragmentManager = new UriFragmentManager(page, this); + } + + /** + * Create a navigator. + * + * When a custom fragment manager is not needed, use the constructor + * {@link #Navigator(Page, ViewDisplay)} which uses a URI fragment based + * fragment manager. + * + * Note that navigation to the initial view must be performed explicitly by + * the application after creating a Navigator using this constructor. + * + * @param fragmentManager + * fragment manager keeping track of the active view and enabling + * bookmarking and direct navigation + * @param display + * where to display the views + */ + public Navigator(FragmentManager fragmentManager, ViewDisplay display) { + this.display = display; + this.fragmentManager = fragmentManager; + } + + /** + * Navigate to a view and initialize the view with given parameters. + * + * The view string consists of a view name optionally followed by a slash + * and (fragment) parameters. ViewProviders are used to find and create the + * correct type of view. + * + * If multiple providers return a matching view, the view with the longest + * name is selected. This way, e.g. hierarchies of subviews can be + * registered like "admin/", "admin/users", "admin/settings" and the longest + * match is used. + * + * If the view being deactivated indicates it wants a confirmation for the + * navigation operation, the user is asked for the confirmation. + * + * Registered {@link ViewChangeListener}s are called upon successful view + * change. + * + * @param viewAndParameters + * view name and parameters + */ + public void navigateTo(String viewAndParameters) { + String longestViewName = null; + View viewWithLongestName = null; + for (ViewProvider provider : providers) { + String viewName = provider.getViewName(viewAndParameters); + if (null != viewName + && (longestViewName == null || viewName.length() > longestViewName + .length())) { + View view = provider.getView(viewName); + if (null != view) { + longestViewName = viewName; + viewWithLongestName = view; + } + } + } + if (viewWithLongestName != null) { + String parameters = null; + if (viewAndParameters.length() > longestViewName.length() + 1) { + parameters = viewAndParameters.substring(longestViewName + .length() + 1); + } + navigateTo(viewWithLongestName, longestViewName, parameters); + } + // TODO if no view is found, what to do? + } + + /** + * Internal method activating a view, setting its parameters and calling + * listeners. + * + * This method also verifies that the user is allowed to perform the + * navigation operation. + * + * @param view + * view to activate + * @param viewName + * (optional) name of the view or null not to set the fragment + * @param fragmentParameters + * parameters passed in the fragment for the view + */ + protected void navigateTo(View view, String viewName, + String fragmentParameters) { + ViewChangeEvent event = new ViewChangeEvent(this, currentView, view, + viewName, fragmentParameters); + if (!isViewChangeAllowed(event)) { + return; + } + + if (null != viewName && getFragmentManager() != null) { + String currentFragment = viewName; + if (fragmentParameters != null) { + currentFragment += "/" + fragmentParameters; + } + if (!currentFragment.equals(getFragmentManager().getFragment())) { + getFragmentManager().setFragment(currentFragment); + } + } + + view.navigateTo(fragmentParameters); + currentView = view; + + if (display != null) { + display.showView(view); + } + + fireViewChange(event); + } + + /** + * Check whether view change is allowed. + * + * All related listeners are called. The view change is blocked if any of + * them wants to block the navigation operation. + * + * The view change listeners may also e.g. open a warning or question dialog + * and save the parameters to re-initiate the navigation operation upon user + * action. + * + * @param event + * view change event (not null, view change not yet performed) + * @return true if the view change should be allowed, false to silently + * block the navigation operation + */ + protected boolean isViewChangeAllowed(ViewChangeEvent event) { + for (ViewChangeListener l : listeners) { + if (!l.isViewChangeAllowed(event)) { + return false; + } + } + return true; + } + + /** + * Return the fragment manager that is used to get, listen to and manipulate + * the URI fragment or other source of navigation information. + * + * @return fragment manager in use + */ + protected FragmentManager getFragmentManager() { + return fragmentManager; + } + + /** + * Returns the ViewDisplay used by the navigator. Unless another display is + * specified, a {@link SimpleViewDisplay} (which is a {@link Component}) is + * used by default. + * + * @return current ViewDisplay + */ + public ViewDisplay getDisplay() { + return display; + } + + /** + * Fire an event when the current view has changed. + * + * @param event + * view change event (not null) + */ + protected void fireViewChange(ViewChangeEvent event) { + for (ViewChangeListener l : listeners) { + l.navigatorViewChanged(event); + } + } + + /** + * Register a static, pre-initialized view instance for a view name. + * + * Registering another view with a name that is already registered + * overwrites the old registration of the same type. + * + * @param viewName + * String that identifies a view (not null nor empty string) + * @param view + * {@link View} instance (not null) + */ + public void addView(String viewName, View view) { + + // Check parameters + if (viewName == null || view == null) { + throw new IllegalArgumentException( + "view and viewName must be non-null"); + } + + removeView(viewName); + registerProvider(new StaticViewProvider(viewName, view)); + } + + /** + * Register for a view name a view class. + * + * Registering another view with a name that is already registered + * overwrites the old registration of the same type. + * + * A new view instance is created every time a view is requested. + * + * @param viewName + * String that identifies a view (not null nor empty string) + * @param viewClass + * {@link View} class to instantiate when a view is requested + * (not null) + */ + public void addView(String viewName, Class<? extends View> viewClass) { + + // Check parameters + if (viewName == null || viewClass == null) { + throw new IllegalArgumentException( + "view and viewClass must be non-null"); + } + + removeView(viewName); + registerProvider(new ClassBasedViewProvider(viewName, viewClass)); + } + + /** + * Remove view from navigator. + * + * This method only applies to views registered using + * {@link #addView(String, View)} or {@link #addView(String, Class)}. + * + * @param viewName + * name of the view to remove + */ + public void removeView(String viewName) { + Iterator<ViewProvider> it = providers.iterator(); + while (it.hasNext()) { + ViewProvider provider = it.next(); + if (provider instanceof StaticViewProvider) { + StaticViewProvider staticProvider = (StaticViewProvider) provider; + if (staticProvider.getViewName().equals(viewName)) { + it.remove(); + } + } else if (provider instanceof ClassBasedViewProvider) { + ClassBasedViewProvider classBasedProvider = (ClassBasedViewProvider) provider; + if (classBasedProvider.getViewName().equals(viewName)) { + it.remove(); + } + } + } + } + + /** + * Register a view provider (factory). + * + * Providers are called in order of registration until one that can handle + * the requested view name is found. + * + * @param provider + * provider to register + */ + public void registerProvider(ViewProvider provider) { + providers.add(provider); + } + + /** + * Unregister a view provider (factory). + * + * @param provider + * provider to unregister + */ + public void unregisterProvider(ViewProvider provider) { + providers.remove(provider); + } + + /** + * Listen to changes of the active view. + * + * The listener will get notified after the view has changed. + * + * @param listener + * Listener to invoke after view changes. + */ + public void addListener(ViewChangeListener listener) { + listeners.add(listener); + } + + /** + * Remove a view change listener. + * + * @param listener + * Listener to remove. + */ + public void removeListener(ViewChangeListener listener) { + listeners.remove(listener); + } + +} diff --git a/server/src/com/vaadin/navigator/View.java b/server/src/com/vaadin/navigator/View.java new file mode 100644 index 0000000000..4d135b4c0b --- /dev/null +++ b/server/src/com/vaadin/navigator/View.java @@ -0,0 +1,36 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.navigator; + +import java.io.Serializable; + +import com.vaadin.ui.Component; + +/** + * Interface for all views controlled by the navigator. + * + * Each view added to the navigator must implement this interface. Typically, a + * view is a {@link Component}. + * + * @author Vaadin Ltd + * @since 7.0 + */ +public interface View extends Serializable { + + /** + * This view is navigated to. + * + * This method is always called before the view is shown on screen. If there + * is any additional id to data what should be shown in the view, it is also + * optionally passed as parameter. + * + * TODO fragmentParameters null if no parameters or empty string? + * + * @param fragmentParameters + * parameters to the view or null if none given. This is the + * string that appears e.g. in URI after "viewname/" + */ + public void navigateTo(String fragmentParameters); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/navigator/ViewChangeListener.java b/server/src/com/vaadin/navigator/ViewChangeListener.java new file mode 100644 index 0000000000..2eb34e6fcf --- /dev/null +++ b/server/src/com/vaadin/navigator/ViewChangeListener.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.navigator; + +import java.io.Serializable; +import java.util.EventObject; + +/** + * Interface for listening to View changes before and after they occur. + * + * Implementations of this interface can also block navigation between views + * before it is performed. + * + * @author Vaadin Ltd + * @since 7.0 + */ +public interface ViewChangeListener extends Serializable { + + /** + * Event received by the listener for attempted and executed view changes. + */ + public static class ViewChangeEvent extends EventObject { + private final View oldView; + private final View newView; + private final String viewName; + private final String fragmentParameters; + + /** + * Create a new view change event. + * + * @param navigator + * Navigator that triggered the event, not null + */ + public ViewChangeEvent(Navigator navigator, View oldView, View newView, + String viewName, String fragmentParameters) { + super(navigator); + this.oldView = oldView; + this.newView = newView; + this.viewName = viewName; + this.fragmentParameters = fragmentParameters; + } + + /** + * Returns the navigator that triggered this event. + * + * @return Navigator (not null) + */ + public Navigator getNavigator() { + return (Navigator) getSource(); + } + + /** + * Returns the view being deactivated. + * + * @return old View + */ + public View getOldView() { + return oldView; + } + + /** + * Returns the view being activated. + * + * @return new View + */ + public View getNewView() { + return newView; + } + + /** + * Returns the view name of the view being activated. + * + * @return view name of the new View + */ + public String getViewName() { + return viewName; + } + + /** + * Returns the parameters for the view being activated. + * + * @return fragment parameters (potentially bookmarkable) for the new + * view + */ + public String getFragmentParameters() { + return fragmentParameters; + } + } + + /** + * Check whether changing the view is permissible. + * + * This method may also e.g. open a "save" dialog or question about the + * change, which may re-initiate the navigation operation after user action. + * + * If this listener does not want to block the view change (e.g. does not + * know the view in question), it should return true. If any listener + * returns false, the view change is not allowed. + * + * @param event + * view change event + * @return true if the view change should be allowed or this listener does + * not care about the view change, false to block the change + */ + public boolean isViewChangeAllowed(ViewChangeEvent event); + + /** + * Invoked after the view has changed. Be careful for deadlocks if you + * decide to change the view again in the listener. + * + * @param event + * view change event + */ + public void navigatorViewChanged(ViewChangeEvent event); + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/navigator/ViewDisplay.java b/server/src/com/vaadin/navigator/ViewDisplay.java new file mode 100644 index 0000000000..6016951394 --- /dev/null +++ b/server/src/com/vaadin/navigator/ViewDisplay.java @@ -0,0 +1,29 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.navigator; + +import java.io.Serializable; + +/** + * Interface for displaying a view in an appropriate location. + * + * The view display can be a component/layout itself or can modify a separate + * layout. + * + * @author Vaadin Ltd + * @since 7.0 + */ +public interface ViewDisplay extends Serializable { + /** + * Remove previously shown view and show the newly selected view in its + * place. + * + * The parameters for the view have been set before this method is called. + * + * @param view + * new view to show + */ + public void showView(View view); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/navigator/ViewProvider.java b/server/src/com/vaadin/navigator/ViewProvider.java new file mode 100644 index 0000000000..4d9d22acab --- /dev/null +++ b/server/src/com/vaadin/navigator/ViewProvider.java @@ -0,0 +1,44 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.navigator; + +import java.io.Serializable; + +/** + * A provider for view instances that can return pre-registered views or + * dynamically create new views. + * + * If multiple providers are used, {@link #getViewName(String)} of each is + * called (in registration order) until one of them returns a non-null value. + * The {@link #getView(String)} method of that provider is then used. + * + * @author Vaadin Ltd + * @since 7.0 + */ +public interface ViewProvider extends Serializable { + /** + * Extract the view name from a combined view name and parameter string. + * This method should return a view name if and only if this provider + * handles creation of such views. + * + * @param viewAndParameters + * string with view name and its fragment parameters (if given), + * not null + * @return view name if the view is handled by this provider, null otherwise + */ + public String getViewName(String viewAndParameters); + + /** + * Create or return a pre-created instance of a view. + * + * The parameters for the view are set separately by the navigator when the + * view is activated. + * + * @param viewName + * name of the view, not null + * @return newly created view (null if none available for the view name) + */ + public View getView(String viewName); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/package.html b/server/src/com/vaadin/package.html new file mode 100644 index 0000000000..f771019709 --- /dev/null +++ b/server/src/com/vaadin/package.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> +</head> + +<body bgcolor="white"> + +<p>The Vaadin base package. Contains the Application class, the +starting point of any application that uses Vaadin.</p> + +<p>Contains all Vaadin core classes. A Vaadin application is based +on the {@link com.vaadin.Application} class and deployed as a servlet +using {@link com.vaadin.terminal.gwt.server.ApplicationServlet} or +{@link com.vaadin.terminal.gwt.server.GAEApplicationServlet} (for Google +App Engine).</p> + +<p>Vaadin applications can also be deployed as portlets using {@link +com.vaadin.terminal.gwt.server.ApplicationPortlet} (JSR-168) or {@link +com.vaadin.terminal.gwt.server.ApplicationPortlet2} (JSR-286).</p> + +<p>All classes in Vaadin are serializable unless otherwise noted. +This allows Vaadin applications to run in cluster and cloud +environments.</p> + + +</body> +</html> diff --git a/server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml b/server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml new file mode 100644 index 0000000000..bd91d05b02 --- /dev/null +++ b/server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml @@ -0,0 +1,6 @@ +<module> + <!-- WS Compiler: manually edited --> + + <!-- Inherit the DefaultWidgetSet --> + <inherits name="com.vaadin.terminal.gwt.DefaultWidgetSet" /> +</module> diff --git a/server/src/com/vaadin/service/ApplicationContext.java b/server/src/com/vaadin/service/ApplicationContext.java new file mode 100644 index 0000000000..71bff7b865 --- /dev/null +++ b/server/src/com/vaadin/service/ApplicationContext.java @@ -0,0 +1,165 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.service; + +import java.io.File; +import java.io.Serializable; +import java.net.URL; +import java.util.Collection; + +import com.vaadin.Application; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.gwt.server.AbstractCommunicationManager; + +/** + * <code>ApplicationContext</code> provides information about the running + * context of the application. Each context is shared by all applications that + * are open for one user. In a web-environment this corresponds to a + * HttpSession. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.1 + */ +public interface ApplicationContext extends Serializable { + + /** + * Returns application context base directory. + * + * Typically an application is deployed in a such way that is has an + * application directory. For web applications this directory is the root + * directory of the web applications. In some cases applications might not + * have an application directory (for example web applications running + * inside a war). + * + * @return The application base directory or null if the application has no + * base directory. + */ + public File getBaseDirectory(); + + /** + * Returns a collection of all the applications in this context. + * + * Each application context contains all active applications for one user. + * + * @return A collection containing all the applications in this context. + */ + public Collection<Application> getApplications(); + + /** + * Adds a transaction listener to this context. The transaction listener is + * called before and after each each request related to this session except + * when serving static resources. + * + * The transaction listener must not be null. + * + * @see com.vaadin.service.ApplicationContext#addTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener) + */ + public void addTransactionListener(TransactionListener listener); + + /** + * Removes a transaction listener from this context. + * + * @param listener + * the listener to be removed. + * @see TransactionListener + */ + public void removeTransactionListener(TransactionListener listener); + + /** + * Generate a URL that can be used as the relative location of e.g. an + * {@link ApplicationResource}. + * + * This method should only be called from the processing of a UIDL request, + * not from a background thread. The return value is null if used outside a + * suitable request. + * + * @deprecated this method is intended for terminal implementation only and + * is subject to change/removal from the interface (to + * {@link AbstractCommunicationManager}) + * + * @param resource + * @param urlKey + * a key for the resource that can later be extracted from a URL + * with {@link #getURLKey(URL, String)} + */ + @Deprecated + public String generateApplicationResourceURL(ApplicationResource resource, + String urlKey); + + /** + * Tests if a URL is for an application resource (APP/...). + * + * @deprecated this method is intended for terminal implementation only and + * is subject to change/removal from the interface (to + * {@link AbstractCommunicationManager}) + * + * @param context + * @param relativeUri + * @return + */ + @Deprecated + public boolean isApplicationResourceURL(URL context, String relativeUri); + + /** + * Gets the identifier (key) from an application resource URL. This key is + * the one that was given to + * {@link #generateApplicationResourceURL(ApplicationResource, String)} when + * creating the URL. + * + * @deprecated this method is intended for terminal implementation only and + * is subject to change/removal from the interface (to + * {@link AbstractCommunicationManager}) + * + * + * @param context + * @param relativeUri + * @return + */ + @Deprecated + public String getURLKey(URL context, String relativeUri); + + /** + * Interface for listening to transaction events. Implement this interface + * to listen to all transactions between the client and the application. + * + */ + public interface TransactionListener extends Serializable { + + /** + * Invoked at the beginning of every transaction. + * + * The transaction is linked to the context, not the application so if + * you have multiple applications running in the same context you need + * to check that the request is associated with the application you are + * interested in. This can be done looking at the application parameter. + * + * @param application + * the Application object. + * @param transactionData + * the Data identifying the transaction. + */ + public void transactionStart(Application application, + Object transactionData); + + /** + * Invoked at the end of every transaction. + * + * The transaction is linked to the context, not the application so if + * you have multiple applications running in the same context you need + * to check that the request is associated with the application you are + * interested in. This can be done looking at the application parameter. + * + * @param applcation + * the Application object. + * @param transactionData + * the Data identifying the transaction. + */ + public void transactionEnd(Application application, + Object transactionData); + + } +} diff --git a/server/src/com/vaadin/service/FileTypeResolver.java b/server/src/com/vaadin/service/FileTypeResolver.java new file mode 100644 index 0000000000..c457c16eb4 --- /dev/null +++ b/server/src/com/vaadin/service/FileTypeResolver.java @@ -0,0 +1,385 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.service; + +import java.io.File; +import java.io.Serializable; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Map; +import java.util.StringTokenizer; + +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.ThemeResource; + +/** + * Utility class that can figure out mime-types and icons related to files. + * <p> + * Note : The icons are associated purely to mime-types, so a file may not have + * a custom icon accessible with this class. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class FileTypeResolver implements Serializable { + + /** + * Default icon given if no icon is specified for a mime-type. + */ + static public Resource DEFAULT_ICON = new ThemeResource( + "../runo/icons/16/document.png"); + + /** + * Default mime-type. + */ + static public String DEFAULT_MIME_TYPE = "application/octet-stream"; + + /** + * Initial file extension to mime-type mapping. + */ + static private String initialExtToMIMEMap = "application/cu-seeme csm cu," + + "application/dsptype tsp," + + "application/futuresplash spl," + + "application/mac-binhex40 hqx," + + "application/msaccess mdb," + + "application/msword doc dot," + + "application/octet-stream bin," + + "application/oda oda," + + "application/pdf pdf," + + "application/pgp-signature pgp," + + "application/postscript ps ai eps," + + "application/rtf rtf," + + "application/vnd.ms-excel xls xlb," + + "application/vnd.ms-powerpoint ppt pps pot," + + "application/vnd.wap.wmlc wmlc," + + "application/vnd.wap.wmlscriptc wmlsc," + + "application/wordperfect5.1 wp5," + + "application/zip zip," + + "application/x-123 wk," + + "application/x-bcpio bcpio," + + "application/x-chess-pgn pgn," + + "application/x-cpio cpio," + + "application/x-debian-package deb," + + "application/x-director dcr dir dxr," + + "application/x-dms dms," + + "application/x-dvi dvi," + + "application/x-xfig fig," + + "application/x-font pfa pfb gsf pcf pcf.Z," + + "application/x-gnumeric gnumeric," + + "application/x-gtar gtar tgz taz," + + "application/x-hdf hdf," + + "application/x-httpd-php phtml pht php," + + "application/x-httpd-php3 php3," + + "application/x-httpd-php3-source phps," + + "application/x-httpd-php3-preprocessed php3p," + + "application/x-httpd-php4 php4," + + "application/x-ica ica," + + "application/x-java-archive jar," + + "application/x-java-serialized-object ser," + + "application/x-java-vm class," + + "application/x-javascript js," + + "application/x-kchart chrt," + + "application/x-killustrator kil," + + "application/x-kpresenter kpr kpt," + + "application/x-kspread ksp," + + "application/x-kword kwd kwt," + + "application/x-latex latex," + + "application/x-lha lha," + + "application/x-lzh lzh," + + "application/x-lzx lzx," + + "application/x-maker frm maker frame fm fb book fbdoc," + + "application/x-mif mif," + + "application/x-msdos-program com exe bat dll," + + "application/x-msi msi," + + "application/x-netcdf nc cdf," + + "application/x-ns-proxy-autoconfig pac," + + "application/x-object o," + + "application/x-ogg ogg," + + "application/x-oz-application oza," + + "application/x-perl pl pm," + + "application/x-pkcs7-crl crl," + + "application/x-redhat-package-manager rpm," + + "application/x-shar shar," + + "application/x-shockwave-flash swf swfl," + + "application/x-star-office sdd sda," + + "application/x-stuffit sit," + + "application/x-sv4cpio sv4cpio," + + "application/x-sv4crc sv4crc," + + "application/x-tar tar," + + "application/x-tex-gf gf," + + "application/x-tex-pk pk PK," + + "application/x-texinfo texinfo texi," + + "application/x-trash ~ % bak old sik," + + "application/x-troff t tr roff," + + "application/x-troff-man man," + + "application/x-troff-me me," + + "application/x-troff-ms ms," + + "application/x-ustar ustar," + + "application/x-wais-source src," + + "application/x-wingz wz," + + "application/x-x509-ca-cert crt," + + "audio/basic au snd," + + "audio/midi mid midi," + + "audio/mpeg mpga mpega mp2 mp3," + + "audio/mpegurl m3u," + + "audio/prs.sid sid," + + "audio/x-aiff aif aiff aifc," + + "audio/x-gsm gsm," + + "audio/x-pn-realaudio ra rm ram," + + "audio/x-scpls pls," + + "audio/x-wav wav," + + "audio/ogg ogg," + + "audio/mp4 m4a," + + "audio/x-aac aac," + + "image/bitmap bmp," + + "image/gif gif," + + "image/ief ief," + + "image/jpeg jpeg jpg jpe," + + "image/pcx pcx," + + "image/png png," + + "image/svg+xml svg svgz," + + "image/tiff tiff tif," + + "image/vnd.wap.wbmp wbmp," + + "image/x-cmu-raster ras," + + "image/x-coreldraw cdr," + + "image/x-coreldrawpattern pat," + + "image/x-coreldrawtemplate cdt," + + "image/x-corelphotopaint cpt," + + "image/x-jng jng," + + "image/x-portable-anymap pnm," + + "image/x-portable-bitmap pbm," + + "image/x-portable-graymap pgm," + + "image/x-portable-pixmap ppm," + + "image/x-rgb rgb," + + "image/x-xbitmap xbm," + + "image/x-xpixmap xpm," + + "image/x-xwindowdump xwd," + + "text/comma-separated-values csv," + + "text/css css," + + "text/html htm html xhtml," + + "text/mathml mml," + + "text/plain txt text diff," + + "text/richtext rtx," + + "text/tab-separated-values tsv," + + "text/vnd.wap.wml wml," + + "text/vnd.wap.wmlscript wmls," + + "text/xml xml," + + "text/x-c++hdr h++ hpp hxx hh," + + "text/x-c++src c++ cpp cxx cc," + + "text/x-chdr h," + + "text/x-csh csh," + + "text/x-csrc c," + + "text/x-java java," + + "text/x-moc moc," + + "text/x-pascal p pas," + + "text/x-setext etx," + + "text/x-sh sh," + + "text/x-tcl tcl tk," + + "text/x-tex tex ltx sty cls," + + "text/x-vcalendar vcs," + + "text/x-vcard vcf," + + "video/dl dl," + + "video/fli fli," + + "video/gl gl," + + "video/mpeg mpeg mpg mpe," + + "video/quicktime qt mov," + + "video/x-mng mng," + + "video/x-ms-asf asf asx," + + "video/x-msvideo avi," + + "video/x-sgi-movie movie," + + "video/ogg ogv," + + "video/mp4 mp4," + + "x-world/x-vrml vrm vrml wrl"; + + /** + * File extension to MIME type mapping. All extensions are in lower case. + */ + static private Hashtable<String, String> extToMIMEMap = new Hashtable<String, String>(); + + /** + * MIME type to Icon mapping. + */ + static private Hashtable<String, Resource> MIMEToIconMap = new Hashtable<String, Resource>(); + + static { + + // Initialize extension to MIME map + final StringTokenizer lines = new StringTokenizer(initialExtToMIMEMap, + ","); + while (lines.hasMoreTokens()) { + final String line = lines.nextToken(); + final StringTokenizer exts = new StringTokenizer(line); + final String type = exts.nextToken(); + while (exts.hasMoreTokens()) { + final String ext = exts.nextToken(); + addExtension(ext, type); + } + } + + // Initialize Icons + ThemeResource folder = new ThemeResource("../runo/icons/16/folder.png"); + addIcon("inode/drive", folder); + addIcon("inode/directory", folder); + } + + /** + * Gets the mime-type of a file. Currently the mime-type is resolved based + * only on the file name extension. + * + * @param fileName + * the name of the file whose mime-type is requested. + * @return mime-type <code>String</code> for the given filename + */ + public static String getMIMEType(String fileName) { + + // Checks for nulls + if (fileName == null) { + throw new NullPointerException("Filename can not be null"); + } + + // Calculates the extension of the file + int dotIndex = fileName.indexOf("."); + while (dotIndex >= 0 && fileName.indexOf(".", dotIndex + 1) >= 0) { + dotIndex = fileName.indexOf(".", dotIndex + 1); + } + dotIndex++; + + if (fileName.length() > dotIndex) { + String ext = fileName.substring(dotIndex); + + // Ignore any query parameters + int queryStringStart = ext.indexOf('?'); + if (queryStringStart > 0) { + ext = ext.substring(0, queryStringStart); + } + + // Return type from extension map, if found + final String type = extToMIMEMap.get(ext.toLowerCase()); + if (type != null) { + return type; + } + } + + return DEFAULT_MIME_TYPE; + } + + /** + * Gets the descriptive icon representing file, based on the filename. First + * the mime-type for the given filename is resolved, and then the + * corresponding icon is fetched from the internal icon storage. If it is + * not found the default icon is returned. + * + * @param fileName + * the name of the file whose icon is requested. + * @return the icon corresponding to the given file + */ + public static Resource getIcon(String fileName) { + return getIconByMimeType(getMIMEType(fileName)); + } + + private static Resource getIconByMimeType(String mimeType) { + final Resource icon = MIMEToIconMap.get(mimeType); + if (icon != null) { + return icon; + } + + // If nothing is known about the file-type, general file + // icon is used + return DEFAULT_ICON; + } + + /** + * Gets the descriptive icon representing a file. First the mime-type for + * the given file name is resolved, and then the corresponding icon is + * fetched from the internal icon storage. If it is not found the default + * icon is returned. + * + * @param file + * the file whose icon is requested. + * @return the icon corresponding to the given file + */ + public static Resource getIcon(File file) { + return getIconByMimeType(getMIMEType(file)); + } + + /** + * Gets the mime-type for a file. Currently the returned file type is + * resolved by the filename extension only. + * + * @param file + * the file whose mime-type is requested. + * @return the files mime-type <code>String</code> + */ + public static String getMIMEType(File file) { + + // Checks for nulls + if (file == null) { + throw new NullPointerException("File can not be null"); + } + + // Directories + if (file.isDirectory()) { + // Drives + if (file.getParentFile() == null) { + return "inode/drive"; + } else { + return "inode/directory"; + } + } + + // Return type from extension + return getMIMEType(file.getName()); + } + + /** + * Adds a mime-type mapping for the given filename extension. If the + * extension is already in the internal mapping it is overwritten. + * + * @param extension + * the filename extension to be associated with + * <code>MIMEType</code>. + * @param MIMEType + * the new mime-type for <code>extension</code>. + */ + public static void addExtension(String extension, String MIMEType) { + extToMIMEMap.put(extension.toLowerCase(), MIMEType); + } + + /** + * Adds a icon for the given mime-type. If the mime-type also has a + * corresponding icon, it is replaced with the new icon. + * + * @param MIMEType + * the mime-type whose icon is to be changed. + * @param icon + * the new icon to be associated with <code>MIMEType</code>. + */ + public static void addIcon(String MIMEType, Resource icon) { + MIMEToIconMap.put(MIMEType, icon); + } + + /** + * Gets the internal file extension to mime-type mapping. + * + * @return unmodifiable map containing the current file extension to + * mime-type mapping + */ + public static Map<String, String> getExtensionToMIMETypeMapping() { + return Collections.unmodifiableMap(extToMIMEMap); + } + + /** + * Gets the internal mime-type to icon mapping. + * + * @return unmodifiable map containing the current mime-type to icon mapping + */ + public static Map<String, Resource> getMIMETypeToIconMapping() { + return Collections.unmodifiableMap(MIMEToIconMap); + } +} diff --git a/server/src/com/vaadin/service/package.html b/server/src/com/vaadin/service/package.html new file mode 100644 index 0000000000..ea21139b91 --- /dev/null +++ b/server/src/com/vaadin/service/package.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides some general service classes used throughout Vaadin +based applications.</p> + +<!-- <h2>Package Specification</h2> --> + +<!-- Package spec here --> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> diff --git a/server/src/com/vaadin/terminal/AbstractClientConnector.java b/server/src/com/vaadin/terminal/AbstractClientConnector.java new file mode 100644 index 0000000000..9c68361382 --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractClientConnector.java @@ -0,0 +1,510 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.logging.Logger; + +import com.vaadin.Application; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; +import com.vaadin.terminal.gwt.server.RpcManager; +import com.vaadin.terminal.gwt.server.RpcTarget; +import com.vaadin.terminal.gwt.server.ServerRpcManager; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.Root; + +/** + * An abstract base class for ClientConnector implementations. This class + * provides all the basic functionality required for connectors. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractClientConnector implements ClientConnector { + /** + * A map from client to server RPC interface class to the RPC call manager + * that handles incoming RPC calls for that interface. + */ + private Map<Class<?>, RpcManager> rpcManagerMap = new HashMap<Class<?>, RpcManager>(); + + /** + * A map from server to client RPC interface class to the RPC proxy that + * sends ourgoing RPC calls for that interface. + */ + private Map<Class<?>, ClientRpc> rpcProxyMap = new HashMap<Class<?>, ClientRpc>(); + + /** + * Shared state object to be communicated from the server to the client when + * modified. + */ + private SharedState sharedState; + + /** + * Pending RPC method invocations to be sent. + */ + private ArrayList<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); + + private String connectorId; + + private ArrayList<Extension> extensions = new ArrayList<Extension>(); + + private ClientConnector parent; + + /* Documentation copied from interface */ + @Override + public void requestRepaint() { + Root root = getRoot(); + if (root != null) { + root.getConnectorTracker().markDirty(this); + } + } + + /** + * Registers an RPC interface implementation for this component. + * + * A component can listen to multiple RPC interfaces, and subclasses can + * register additional implementations. + * + * @since 7.0 + * + * @param implementation + * RPC interface implementation + * @param rpcInterfaceType + * RPC interface class for which the implementation should be + * registered + */ + protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) { + rpcManagerMap.put(rpcInterfaceType, new ServerRpcManager<T>( + implementation, rpcInterfaceType)); + } + + /** + * Registers an RPC interface implementation for this component. + * + * A component can listen to multiple RPC interfaces, and subclasses can + * register additional implementations. + * + * @since 7.0 + * + * @param implementation + * RPC interface implementation. Also used to deduce the type. + */ + protected <T extends ServerRpc> void registerRpc(T implementation) { + Class<?> cls = implementation.getClass(); + Class<?>[] interfaces = cls.getInterfaces(); + while (interfaces.length == 0) { + // Search upwards until an interface is found. It must be found as T + // extends ServerRpc + cls = cls.getSuperclass(); + interfaces = cls.getInterfaces(); + } + if (interfaces.length != 1 + || !(ServerRpc.class.isAssignableFrom(interfaces[0]))) { + throw new RuntimeException( + "Use registerRpc(T implementation, Class<T> rpcInterfaceType) if the Rpc implementation implements more than one interface"); + } + @SuppressWarnings("unchecked") + Class<T> type = (Class<T>) interfaces[0]; + registerRpc(implementation, type); + } + + @Override + public SharedState getState() { + if (null == sharedState) { + sharedState = createState(); + } + return sharedState; + } + + /** + * Creates the shared state bean to be used in server to client + * communication. + * <p> + * By default a state object of the defined return type of + * {@link #getState()} is created. Subclasses can override this method and + * return a new instance of the correct state class but this should rarely + * be necessary. + * </p> + * <p> + * No configuration of the values of the state should be performed in + * {@link #createState()}. + * + * @since 7.0 + * + * @return new shared state object + */ + protected SharedState createState() { + try { + return getStateType().newInstance(); + } catch (Exception e) { + throw new RuntimeException( + "Error creating state of type " + getStateType().getName() + + " for " + getClass().getName(), e); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.server.ClientConnector#getStateType() + */ + @Override + public Class<? extends SharedState> getStateType() { + try { + Method m = getClass().getMethod("getState", (Class[]) null); + Class<?> type = m.getReturnType(); + return type.asSubclass(SharedState.class); + } catch (Exception e) { + throw new RuntimeException("Error finding state type for " + + getClass().getName(), e); + } + } + + /** + * Returns an RPC proxy for a given server to client RPC interface for this + * component. + * + * TODO more javadoc, subclasses, ... + * + * @param rpcInterface + * RPC interface type + * + * @since 7.0 + */ + public <T extends ClientRpc> T getRpcProxy(final Class<T> rpcInterface) { + // create, initialize and return a dynamic proxy for RPC + try { + if (!rpcProxyMap.containsKey(rpcInterface)) { + Class<?> proxyClass = Proxy.getProxyClass( + rpcInterface.getClassLoader(), rpcInterface); + Constructor<?> constructor = proxyClass + .getConstructor(InvocationHandler.class); + T rpcProxy = rpcInterface.cast(constructor + .newInstance(new RpcInvoicationHandler(rpcInterface))); + // cache the proxy + rpcProxyMap.put(rpcInterface, rpcProxy); + } + return (T) rpcProxyMap.get(rpcInterface); + } catch (Exception e) { + // TODO exception handling? + throw new RuntimeException(e); + } + } + + private static final class AllChildrenIterable implements + Iterable<ClientConnector>, Serializable { + private final ClientConnector connector; + + private AllChildrenIterable(ClientConnector connector) { + this.connector = connector; + } + + @Override + public Iterator<ClientConnector> iterator() { + CombinedIterator<ClientConnector> iterator = new CombinedIterator<ClientConnector>(); + iterator.addIterator(connector.getExtensions().iterator()); + + if (connector instanceof HasComponents) { + HasComponents hasComponents = (HasComponents) connector; + iterator.addIterator(hasComponents.iterator()); + } + + return iterator; + } + } + + private class RpcInvoicationHandler implements InvocationHandler, + Serializable { + + private String rpcInterfaceName; + + public RpcInvoicationHandler(Class<?> rpcInterface) { + rpcInterfaceName = rpcInterface.getName().replaceAll("\\$", "."); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + addMethodInvocationToQueue(rpcInterfaceName, method, args); + // TODO no need to do full repaint if only RPC calls + requestRepaint(); + return null; + } + + } + + /** + * For internal use: adds a method invocation to the pending RPC call queue. + * + * @param interfaceName + * RPC interface name + * @param method + * RPC method + * @param parameters + * RPC all parameters + * + * @since 7.0 + */ + protected void addMethodInvocationToQueue(String interfaceName, + Method method, Object[] parameters) { + // add to queue + pendingInvocations.add(new ClientMethodInvocation(this, interfaceName, + method, parameters)); + } + + /** + * @see RpcTarget#getRpcManager(Class) + * + * @param rpcInterface + * RPC interface for which a call was made + * @return RPC Manager handling calls for the interface + * + * @since 7.0 + */ + @Override + public RpcManager getRpcManager(Class<?> rpcInterface) { + return rpcManagerMap.get(rpcInterface); + } + + @Override + public List<ClientMethodInvocation> retrievePendingRpcCalls() { + if (pendingInvocations.isEmpty()) { + return Collections.emptyList(); + } else { + List<ClientMethodInvocation> result = pendingInvocations; + pendingInvocations = new ArrayList<ClientMethodInvocation>(); + return Collections.unmodifiableList(result); + } + } + + @Override + public String getConnectorId() { + if (connectorId == null) { + if (getApplication() == null) { + throw new RuntimeException( + "Component must be attached to an application when getConnectorId() is called for the first time"); + } + connectorId = getApplication().createConnectorId(this); + } + return connectorId; + } + + /** + * Finds the Application to which this connector belongs. If the connector + * has not been attached, <code>null</code> is returned. + * + * @return The connector's application, or <code>null</code> if not attached + */ + protected Application getApplication() { + Root root = getRoot(); + if (root == null) { + return null; + } else { + return root.getApplication(); + } + } + + /** + * Finds a Root ancestor of this connector. <code>null</code> is returned if + * no Root ancestor is found (typically because the connector is not + * attached to a proper hierarchy). + * + * @return the Root ancestor of this connector, or <code>null</code> if none + * is found. + */ + @Override + public Root getRoot() { + ClientConnector connector = this; + while (connector != null) { + if (connector instanceof Root) { + return (Root) connector; + } + connector = connector.getParent(); + } + return null; + } + + private static Logger getLogger() { + return Logger.getLogger(AbstractClientConnector.class.getName()); + } + + @Override + public void requestRepaintAll() { + requestRepaint(); + + for (ClientConnector connector : getAllChildrenIterable(this)) { + connector.requestRepaintAll(); + } + } + + private static final class CombinedIterator<T> implements Iterator<T>, + Serializable { + + private final Collection<Iterator<? extends T>> iterators = new ArrayList<Iterator<? extends T>>(); + + public void addIterator(Iterator<? extends T> iterator) { + iterators.add(iterator); + } + + @Override + public boolean hasNext() { + for (Iterator<? extends T> i : iterators) { + if (i.hasNext()) { + return true; + } + } + return false; + } + + @Override + public T next() { + for (Iterator<? extends T> i : iterators) { + if (i.hasNext()) { + return i.next(); + } + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * Get an Iterable for iterating over all child connectors, including both + * extensions and child components. + * + * @param connector + * the connector to get children for + * @return an Iterable giving all child connectors. + */ + public static Iterable<ClientConnector> getAllChildrenIterable( + final ClientConnector connector) { + return new AllChildrenIterable(connector); + } + + @Override + public Collection<Extension> getExtensions() { + return Collections.unmodifiableCollection(extensions); + } + + /** + * Add an extension to this connector. This method is protected to allow + * extensions to select which targets they can extend. + * + * @param extension + * the extension to add + */ + protected void addExtension(Extension extension) { + ClientConnector previousParent = extension.getParent(); + if (previousParent == this) { + // Nothing to do, already attached + return; + } else if (previousParent != null) { + throw new IllegalStateException( + "Moving an extension from one parent to another is not supported"); + } + + extensions.add(extension); + extension.setParent(this); + requestRepaint(); + } + + @Override + public void removeExtension(Extension extension) { + extension.setParent(null); + extensions.remove(extension); + requestRepaint(); + } + + @Override + public void setParent(ClientConnector parent) { + + // If the parent is not changed, don't do anything + if (parent == this.parent) { + return; + } + + if (parent != null && this.parent != null) { + throw new IllegalStateException(getClass().getName() + + " already has a parent."); + } + + // Send detach event if the component have been connected to a window + if (getApplication() != null) { + detach(); + } + + // Connect to new parent + this.parent = parent; + + // Send attach event if connected to an application + if (getApplication() != null) { + attach(); + } + } + + @Override + public ClientConnector getParent() { + return parent; + } + + @Override + public void attach() { + requestRepaint(); + + getRoot().getConnectorTracker().registerConnector(this); + + for (ClientConnector connector : getAllChildrenIterable(this)) { + connector.attach(); + } + + } + + /** + * {@inheritDoc} + * + * <p> + * The {@link #getApplication()} and {@link #getRoot()} methods might return + * <code>null</code> after this method is called. + * </p> + */ + @Override + public void detach() { + for (ClientConnector connector : getAllChildrenIterable(this)) { + connector.detach(); + } + + getRoot().getConnectorTracker().unregisterConnector(this); + } + + @Override + public boolean isConnectorEnabled() { + if (getParent() == null) { + // No parent -> the component cannot receive updates from the client + return false; + } else { + return getParent().isConnectorEnabled(); + } + } +} diff --git a/server/src/com/vaadin/terminal/AbstractErrorMessage.java b/server/src/com/vaadin/terminal/AbstractErrorMessage.java new file mode 100644 index 0000000000..f7cd0e6aad --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractErrorMessage.java @@ -0,0 +1,176 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.data.Buffered; +import com.vaadin.data.Validator; +import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; + +/** + * Base class for component error messages. + * + * This class is used on the server side to construct the error messages to send + * to the client. + * + * @since 7.0 + */ +public abstract class AbstractErrorMessage implements ErrorMessage { + + public enum ContentMode { + /** + * Content mode, where the error contains only plain text. + */ + TEXT, + /** + * Content mode, where the error contains preformatted text. + */ + PREFORMATTED, + /** + * Content mode, where the error contains XHTML. + */ + XHTML; + } + + /** + * Content mode. + */ + private ContentMode mode = ContentMode.TEXT; + + /** + * Message in content mode. + */ + private String message; + + /** + * Error level. + */ + private ErrorLevel level = ErrorLevel.ERROR; + + private List<ErrorMessage> causes = new ArrayList<ErrorMessage>(); + + protected AbstractErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + protected void setMessage(String message) { + this.message = message; + } + + /* Documented in interface */ + @Override + public ErrorLevel getErrorLevel() { + return level; + } + + protected void setErrorLevel(ErrorLevel level) { + this.level = level; + } + + protected ContentMode getMode() { + return mode; + } + + protected void setMode(ContentMode mode) { + this.mode = mode; + } + + protected List<ErrorMessage> getCauses() { + return causes; + } + + protected void addCause(ErrorMessage cause) { + causes.add(cause); + } + + @Override + public String getFormattedHtmlMessage() { + String result = null; + switch (getMode()) { + case TEXT: + result = AbstractApplicationServlet.safeEscapeForHtml(getMessage()); + break; + case PREFORMATTED: + result = "<pre>" + + AbstractApplicationServlet + .safeEscapeForHtml(getMessage()) + "</pre>"; + break; + case XHTML: + result = getMessage(); + break; + } + // if no message, combine the messages of all children + if (null == result && null != getCauses() && getCauses().size() > 0) { + StringBuilder sb = new StringBuilder(); + for (ErrorMessage cause : getCauses()) { + String childMessage = cause.getFormattedHtmlMessage(); + if (null != childMessage) { + sb.append("<div>"); + sb.append(childMessage); + sb.append("</div>\n"); + } + } + if (sb.length() > 0) { + result = sb.toString(); + } + } + // still no message? use an empty string for backwards compatibility + if (null == result) { + result = ""; + } + return result; + } + + // TODO replace this with a helper method elsewhere? + public static ErrorMessage getErrorMessageForException(Throwable t) { + if (null == t) { + return null; + } else if (t instanceof ErrorMessage) { + // legacy case for custom error messages + return (ErrorMessage) t; + } else if (t instanceof Validator.InvalidValueException) { + UserError error = new UserError( + ((Validator.InvalidValueException) t).getHtmlMessage(), + ContentMode.XHTML, ErrorLevel.ERROR); + for (Validator.InvalidValueException nestedException : ((Validator.InvalidValueException) t) + .getCauses()) { + error.addCause(getErrorMessageForException(nestedException)); + } + return error; + } else if (t instanceof Buffered.SourceException) { + // no message, only the causes to be painted + UserError error = new UserError(null); + // in practice, this was always ERROR in Vaadin 6 unless tweaked in + // custom exceptions implementing ErrorMessage + error.setErrorLevel(ErrorLevel.ERROR); + // causes + for (Throwable nestedException : ((Buffered.SourceException) t) + .getCauses()) { + error.addCause(getErrorMessageForException(nestedException)); + } + return error; + } else { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + return new SystemError(sw.toString()); + } + } + + /* Documented in superclass */ + @Override + public String toString() { + return getMessage(); + } + +} diff --git a/server/src/com/vaadin/terminal/AbstractExtension.java b/server/src/com/vaadin/terminal/AbstractExtension.java new file mode 100644 index 0000000000..33a60e39ef --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractExtension.java @@ -0,0 +1,76 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * An extension is an entity that is attached to a Component or another + * Extension and independently communicates between client and server. + * <p> + * Extensions can use shared state and RPC in the same way as components. + * <p> + * AbstractExtension adds a mechanism for adding the extension to any Connector + * (extend). To let the Extension determine what kind target it can be added to, + * the extend method is declared as protected. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractExtension extends AbstractClientConnector + implements Extension { + private boolean previouslyAttached = false; + + /** + * Gets a type that the parent must be an instance of. Override this if the + * extension only support certain targets, e.g. if only TextFields can be + * extended. + * + * @return a type that the parent must be an instance of + */ + protected Class<? extends ClientConnector> getSupportedParentType() { + return ClientConnector.class; + } + + /** + * Add this extension to the target connector. This method is protected to + * allow subclasses to require a more specific type of target. + * + * @param target + * the connector to attach this extension to + */ + protected void extend(AbstractClientConnector target) { + target.addExtension(this); + } + + /** + * Remove this extension from its target. After an extension has been + * removed, it can not be attached again. + */ + public void removeFromTarget() { + getParent().removeExtension(this); + } + + @Override + public void setParent(ClientConnector parent) { + if (previouslyAttached && parent != null) { + throw new IllegalStateException( + "An extension can not be set to extend a new target after getting detached from the previous."); + } + + Class<? extends ClientConnector> supportedParentType = getSupportedParentType(); + if (parent == null || supportedParentType.isInstance(parent)) { + super.setParent(parent); + previouslyAttached = true; + } else { + throw new IllegalArgumentException(getClass().getName() + + " can only be attached to targets of type " + + supportedParentType.getName() + " but attach to " + + parent.getClass().getName() + " was attempted."); + } + } + +} diff --git a/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java b/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java new file mode 100644 index 0000000000..7bafb6d2b3 --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java @@ -0,0 +1,162 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.shared.JavaScriptExtensionState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.JavaScriptFunction; + +/** + * Base class for Extensions with all client-side logic implemented using + * JavaScript. + * <p> + * When a new JavaScript extension is initialized in the browser, the framework + * will look for a globally defined JavaScript function that will initialize the + * extension. The name of the initialization function is formed by replacing . + * with _ in the name of the server-side class. If no such function is defined, + * each super class is used in turn until a match is found. The framework will + * thus first attempt with <code>com_example_MyExtension</code> for the + * server-side + * <code>com.example.MyExtension extends AbstractJavaScriptExtension</code> + * class. If MyExtension instead extends <code>com.example.SuperExtension</code> + * , then <code>com_example_SuperExtension</code> will also be attempted if + * <code>com_example_MyExtension</code> has not been defined. + * <p> + * + * The initialization function will be called with <code>this</code> pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + * <ul> + * <li><code>getConnectorId()</code> - returns a string with the id of the + * connector.</li> + * <li><code>getParentId([connectorId])</code> - returns a string with the id of + * the connector's parent. If <code>connectorId</code> is provided, the id of + * the parent of the corresponding connector with the passed id is returned + * instead.</li> + * <li><code>getElement([connectorId])</code> - returns the DOM Element that is + * the root of a connector's widget. <code>null</code> is returned if the + * connector can not be found or if the connector doesn't have a widget. If + * <code>connectorId</code> is not provided, the connector id of the current + * connector will be used.</li> + * <li><code>getState()</code> - returns an object corresponding to the shared + * state defined on the server. The scheme for conversion between Java and + * JavaScript types is described bellow.</li> + * <li><code>registerRpc([name, ] rpcObject)</code> - registers the + * <code>rpcObject</code> as a RPC handler. <code>rpcObject</code> should be an + * object with field containing functions for all eligible RPC functions. If + * <code>name</code> is provided, the RPC handler will only used for RPC calls + * for the RPC interface with the same fully qualified Java name. If no + * <code>name</code> is provided, the RPC handler will be used for all incoming + * RPC invocations where the RPC method name is defined as a function field in + * the handler. The scheme for conversion between Java types in the RPC + * interface definition and the JavaScript values passed as arguments to the + * handler functions is described bellow.</li> + * <li><code>getRpcProxy([name])</code> - returns an RPC proxy object. If + * <code>name</code> is provided, the proxy object will contain functions for + * all methods in the RPC interface with the same fully qualified name, provided + * a RPC handler has been registered by the server-side code. If no + * <code>name</code> is provided, the returned RPC proxy object will contain + * functions for all methods in all RPC interfaces registered for the connector + * on the server. If the same method name is present in multiple registered RPC + * interfaces, the corresponding function in the RPC proxy object will throw an + * exception when called. The scheme for conversion between Java types in the + * RPC interface and the JavaScript values that should be passed to the + * functions is described bellow.</li> + * <li><code>translateVaadinUri(uri)</code> - Translates a Vaadin URI to a URL + * that can be used in the browser. This is just way of accessing + * {@link ApplicationConnection#translateVaadinUri(String)}</li> + * </ul> + * The connector wrapper also supports these special functions: + * <ul> + * <li><code>onStateChange</code> - If the JavaScript code assigns a function to + * the field, that function is called whenever the contents of the shared state + * is changed.</li> + * <li>Any field name corresponding to a call to + * {@link #addFunction(String, JavaScriptFunction)} on the server will + * automatically be present as a function that triggers the registered function + * on the server.</li> + * <li>Any field name referred to using + * {@link #callFunction(String, Object...)} on the server will be called if a + * function has been assigned to the field.</li> + * </ul> + * <p> + * + * Values in the Shared State and in RPC calls are converted between Java and + * JavaScript using the following conventions: + * <ul> + * <li>Primitive Java numbers (byte, char, int, long, float, double) and their + * boxed types (Byte, Character, Integer, Long, Float, Double) are represented + * by JavaScript numbers.</li> + * <li>The primitive Java boolean and the boxed Boolean are represented by + * JavaScript booleans.</li> + * <li>Java Strings are represented by JavaScript strings.</li> + * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li> + * <li>Map<String, ?> in Java is represented by JavaScript object with fields + * corresponding to the map keys.</li> + * <li>Any other Java Map is represented by a JavaScript array containing two + * arrays, the first contains the keys and the second contains the values in the + * same order.</li> + * <li>A Java Bean is represented by a JavaScript object with fields + * corresponding to the bean's properties.</li> + * <li>A Java Connector is represented by a JavaScript string containing the + * connector's id.</li> + * <li>A pluggable serialization mechanism is provided for types not described + * here. Please refer to the documentation for specific types for serialization + * information.</li> + * </ul> + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractJavaScriptExtension extends AbstractExtension { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + @Override + protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) { + super.registerRpc(implementation, rpcInterfaceType); + callbackHelper.registerRpc(rpcInterfaceType); + } + + /** + * Register a {@link JavaScriptFunction} that can be called from the + * JavaScript using the provided name. A JavaScript function with the + * provided name will be added to the connector wrapper object (initially + * available as <code>this</code>). Calling that JavaScript function will + * cause the call method in the registered {@link JavaScriptFunction} to be + * invoked with the same arguments. + * + * @param functionName + * the name that should be used for client-side callback + * @param function + * the {@link JavaScriptFunction} object that will be invoked + * when the JavaScript function is called + */ + protected void addFunction(String functionName, JavaScriptFunction function) { + callbackHelper.registerCallback(functionName, function); + } + + /** + * Invoke a named function that the connector JavaScript has added to the + * JavaScript connector wrapper object. The arguments should only contain + * data types that can be represented in JavaScript including primitives, + * their boxed types, arrays, String, List, Set, Map, Connector and + * JavaBeans. + * + * @param name + * the name of the function + * @param arguments + * function arguments + */ + protected void callFunction(String name, Object... arguments) { + callbackHelper.invokeCallback(name, arguments); + } + + @Override + public JavaScriptExtensionState getState() { + return (JavaScriptExtensionState) super.getState(); + } +} diff --git a/server/src/com/vaadin/terminal/ApplicationResource.java b/server/src/com/vaadin/terminal/ApplicationResource.java new file mode 100644 index 0000000000..da92642d02 --- /dev/null +++ b/server/src/com/vaadin/terminal/ApplicationResource.java @@ -0,0 +1,75 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; + +/** + * This interface must be implemented by classes wishing to provide Application + * resources. + * <p> + * <code>ApplicationResource</code> are a set of named resources (pictures, + * sounds, etc) associated with some specific application. Having named + * application resources provides a convenient method for having inter-theme + * common resources for an application. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ApplicationResource extends Resource, Serializable { + + /** + * Default cache time. + */ + public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24; + + /** + * Gets resource as stream. + */ + public DownloadStream getStream(); + + /** + * Gets the application of the resource. + */ + public Application getApplication(); + + /** + * Gets the virtual filename for this resource. + * + * @return the file name associated to this resource. + */ + public String getFilename(); + + /** + * Gets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Default is <code>DEFAULT_CACHETIME</code>. + * </p> + * + * @return Cache time in milliseconds + */ + public long getCacheTime(); + + /** + * Gets the size of the download buffer used for this resource. + * + * <p> + * If the buffer size is 0, the buffer size is decided by the terminal + * adapter. The default value is 0. + * </p> + * + * @return int the size of the buffer in bytes. + */ + public int getBufferSize(); + +} diff --git a/server/src/com/vaadin/terminal/ClassResource.java b/server/src/com/vaadin/terminal/ClassResource.java new file mode 100644 index 0000000000..b74c8e7bb7 --- /dev/null +++ b/server/src/com/vaadin/terminal/ClassResource.java @@ -0,0 +1,178 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ClassResource</code> is a named resource accessed with the class + * loader. + * + * This can be used to access resources such as icons, files, etc. + * + * @see java.lang.Class#getResource(java.lang.String) + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ClassResource implements ApplicationResource, Serializable { + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DEFAULT_CACHETIME; + + /** + * Associated class used for indetifying the source of the resource. + */ + private final Class<?> associatedClass; + + /** + * Name of the resource is relative to the associated class. + */ + private final String resourceName; + + /** + * Application used for serving the class. + */ + private final Application application; + + /** + * Creates a new application resource instance. The resource id is relative + * to the location of the application class. + * + * @param resourceName + * the Unique identifier of the resource within the application. + * @param application + * the application this resource will be added to. + */ + public ClassResource(String resourceName, Application application) { + this(application.getClass(), resourceName, application); + } + + /** + * Creates a new application resource instance. + * + * @param associatedClass + * the class of the which the resource is associated. + * @param resourceName + * the Unique identifier of the resource within the application. + * @param application + * the application this resource will be added to. + */ + public ClassResource(Class<?> associatedClass, String resourceName, + Application application) { + this.associatedClass = associatedClass; + this.resourceName = resourceName; + this.application = application; + if (resourceName == null || associatedClass == null) { + throw new NullPointerException(); + } + application.addResource(this); + } + + /** + * Gets the MIME type of this resource. + * + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + return FileTypeResolver.getMIMEType(resourceName); + } + + /** + * Gets the application of this resource. + * + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + @Override + public Application getApplication() { + return application; + } + + /** + * Gets the virtual filename for this resource. + * + * @return the file name associated to this resource. + * @see com.vaadin.terminal.ApplicationResource#getFilename() + */ + @Override + public String getFilename() { + int index = 0; + int next = 0; + while ((next = resourceName.indexOf('/', index)) > 0 + && next + 1 < resourceName.length()) { + index = next + 1; + } + return resourceName.substring(index); + } + + /** + * Gets resource as stream. + * + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + @Override + public DownloadStream getStream() { + final DownloadStream ds = new DownloadStream( + associatedClass.getResourceAsStream(resourceName), + getMIMEType(), getFilename()); + ds.setBufferSize(getBufferSize()); + ds.setCacheTime(cacheTime); + return ds; + } + + /* documented in superclass */ + @Override + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /* documented in superclass */ + @Override + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Zero or negavive value disbales the caching of this + * stream. + * </p> + * + * @param cacheTime + * the cache time in milliseconds. + * + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } +} diff --git a/server/src/com/vaadin/terminal/CombinedRequest.java b/server/src/com/vaadin/terminal/CombinedRequest.java new file mode 100644 index 0000000000..5b92feb39a --- /dev/null +++ b/server/src/com/vaadin/terminal/CombinedRequest.java @@ -0,0 +1,187 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.terminal.gwt.server.WebApplicationContext; +import com.vaadin.terminal.gwt.server.WebBrowser; + +/** + * A {@link WrappedRequest} with path and parameters from one request and + * {@link WrappedRequest.BrowserDetails} extracted from another request. + * + * This class is intended to be used for a two request initialization where the + * first request fetches the actual application page and the second request + * contains information extracted from the browser using javascript. + * + */ +public class CombinedRequest implements WrappedRequest { + + private final WrappedRequest secondRequest; + private Map<String, String[]> parameterMap; + + /** + * Creates a new combined request based on the second request and some + * details from the first request. + * + * @param secondRequest + * the second request which will be used as the foundation of the + * combined request + * @throws JSONException + * if the initialParams parameter can not be decoded + */ + public CombinedRequest(WrappedRequest secondRequest) throws JSONException { + this.secondRequest = secondRequest; + + HashMap<String, String[]> map = new HashMap<String, String[]>(); + JSONObject initialParams = new JSONObject( + secondRequest.getParameter("initialParams")); + for (Iterator<?> keys = initialParams.keys(); keys.hasNext();) { + String name = (String) keys.next(); + JSONArray jsonValues = initialParams.getJSONArray(name); + String[] values = new String[jsonValues.length()]; + for (int i = 0; i < values.length; i++) { + values[i] = jsonValues.getString(i); + } + map.put(name, values); + } + + parameterMap = Collections.unmodifiableMap(map); + + } + + @Override + public String getParameter(String parameter) { + String[] strings = getParameterMap().get(parameter); + if (strings == null || strings.length == 0) { + return null; + } else { + return strings[0]; + } + } + + @Override + public Map<String, String[]> getParameterMap() { + return parameterMap; + } + + @Override + public int getContentLength() { + return secondRequest.getContentLength(); + } + + @Override + public InputStream getInputStream() throws IOException { + return secondRequest.getInputStream(); + } + + @Override + public Object getAttribute(String name) { + return secondRequest.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + secondRequest.setAttribute(name, value); + } + + @Override + public String getRequestPathInfo() { + return secondRequest.getParameter("initialPath"); + } + + @Override + public int getSessionMaxInactiveInterval() { + return secondRequest.getSessionMaxInactiveInterval(); + } + + @Override + public Object getSessionAttribute(String name) { + return secondRequest.getSessionAttribute(name); + } + + @Override + public void setSessionAttribute(String name, Object attribute) { + secondRequest.setSessionAttribute(name, attribute); + } + + @Override + public String getContentType() { + return secondRequest.getContentType(); + } + + @Override + public BrowserDetails getBrowserDetails() { + return new BrowserDetails() { + @Override + public String getUriFragment() { + String fragment = secondRequest.getParameter("fr"); + if (fragment == null) { + return ""; + } else { + return fragment; + } + } + + @Override + public String getWindowName() { + return secondRequest.getParameter("wn"); + } + + @Override + public WebBrowser getWebBrowser() { + WebApplicationContext context = (WebApplicationContext) Application + .getCurrent().getContext(); + return context.getBrowser(); + } + }; + } + + /** + * Gets the original second request. This can be used e.g. if a request + * parameter from the second request is required. + * + * @return the original second wrapped request + */ + public WrappedRequest getSecondRequest() { + return secondRequest; + } + + @Override + public Locale getLocale() { + return secondRequest.getLocale(); + } + + @Override + public String getRemoteAddr() { + return secondRequest.getRemoteAddr(); + } + + @Override + public boolean isSecure() { + return secondRequest.isSecure(); + } + + @Override + public String getHeader(String name) { + return secondRequest.getHeader(name); + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return secondRequest.getDeploymentConfiguration(); + } +} diff --git a/server/src/com/vaadin/terminal/CompositeErrorMessage.java b/server/src/com/vaadin/terminal/CompositeErrorMessage.java new file mode 100644 index 0000000000..b82b622f54 --- /dev/null +++ b/server/src/com/vaadin/terminal/CompositeErrorMessage.java @@ -0,0 +1,112 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Class for combining multiple error messages together. + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CompositeErrorMessage extends AbstractErrorMessage { + + /** + * Constructor for CompositeErrorMessage. + * + * @param errorMessages + * the Array of error messages that are listed togeter. Nulls are + * ignored, but at least one message is required. + */ + public CompositeErrorMessage(ErrorMessage[] errorMessages) { + super(null); + setErrorLevel(ErrorLevel.INFORMATION); + + for (int i = 0; i < errorMessages.length; i++) { + addErrorMessage(errorMessages[i]); + } + + if (getCauses().size() == 0) { + throw new IllegalArgumentException( + "Composite error message must have at least one error"); + } + + } + + /** + * Constructor for CompositeErrorMessage. + * + * @param errorMessages + * the Collection of error messages that are listed together. At + * least one message is required. + */ + public CompositeErrorMessage( + Collection<? extends ErrorMessage> errorMessages) { + super(null); + setErrorLevel(ErrorLevel.INFORMATION); + + for (final Iterator<? extends ErrorMessage> i = errorMessages + .iterator(); i.hasNext();) { + addErrorMessage(i.next()); + } + + if (getCauses().size() == 0) { + throw new IllegalArgumentException( + "Composite error message must have at least one error"); + } + } + + /** + * Adds a error message into this composite message. Updates the level + * field. + * + * @param error + * the error message to be added. Duplicate errors are ignored. + */ + private void addErrorMessage(ErrorMessage error) { + if (error != null && !getCauses().contains(error)) { + addCause(error); + if (error.getErrorLevel().intValue() > getErrorLevel().intValue()) { + setErrorLevel(error.getErrorLevel()); + } + } + } + + /** + * Gets Error Iterator. + * + * @return the error iterator. + */ + public Iterator<ErrorMessage> iterator() { + return getCauses().iterator(); + } + + /** + * Returns a comma separated list of the error messages. + * + * @return String, comma separated list of error messages. + */ + @Override + public String toString() { + String retval = "["; + int pos = 0; + for (final Iterator<ErrorMessage> i = getCauses().iterator(); i + .hasNext();) { + if (pos > 0) { + retval += ","; + } + pos++; + retval += i.next().toString(); + } + retval += "]"; + + return retval; + } +} diff --git a/server/src/com/vaadin/terminal/DeploymentConfiguration.java b/server/src/com/vaadin/terminal/DeploymentConfiguration.java new file mode 100644 index 0000000000..ae96dcaec5 --- /dev/null +++ b/server/src/com/vaadin/terminal/DeploymentConfiguration.java @@ -0,0 +1,123 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.Properties; + +import javax.portlet.PortletContext; +import javax.servlet.ServletContext; + +import com.vaadin.terminal.gwt.server.AddonContext; +import com.vaadin.terminal.gwt.server.AddonContextListener; + +/** + * Provide deployment specific settings that are required outside terminal + * specific code. + * + * @author Vaadin Ltd. + * + * @since 7.0 + */ +public interface DeploymentConfiguration extends Serializable { + + /** + * Gets the base URL of the location of Vaadin's static files. + * + * @param request + * the request for which the location should be determined + * + * @return a string with the base URL for static files + */ + public String getStaticFileLocation(WrappedRequest request); + + /** + * Gets the widgetset that is configured for this deployment, e.g. from a + * parameter in web.xml. + * + * @param request + * the request for which a widgetset is required + * @return the name of the widgetset + */ + public String getConfiguredWidgetset(WrappedRequest request); + + /** + * Gets the theme that is configured for this deployment, e.g. from a portal + * parameter or just some sensible default value. + * + * @param request + * the request for which a theme is required + * @return the name of the theme + */ + public String getConfiguredTheme(WrappedRequest request); + + /** + * Checks whether the Vaadin application will be rendered on its own in the + * browser or whether it will be included into some other context. A + * standalone application may do things that might interfere with other + * parts of a page, e.g. changing the page title and requesting focus upon + * loading. + * + * @param request + * the request for which the application is loaded + * @return a boolean indicating whether the application should be standalone + */ + public boolean isStandalone(WrappedRequest request); + + /** + * Gets a configured property. The properties are typically read from e.g. + * web.xml or from system properties of the JVM. + * + * @param propertyName + * The simple of the property, in some contexts, lookup might be + * performed using variations of the provided name. + * @param defaultValue + * the default value that should be used if no value has been + * defined + * @return the property value, or the passed default value if no property + * value is found + */ + public String getApplicationOrSystemProperty(String propertyName, + String defaultValue); + + /** + * Get the class loader to use for loading classes loaded by name, e.g. + * custom Root classes. <code>null</code> indicates that the default class + * loader should be used. + * + * @return the class loader to use, or <code>null</code> + */ + public ClassLoader getClassLoader(); + + /** + * Returns the MIME type of the specified file, or null if the MIME type is + * not known. The MIME type is determined by the configuration of the + * container, and may be specified in a deployment descriptor. Common MIME + * types are "text/html" and "image/gif". + * + * @param resourceName + * a String specifying the name of a file + * @return a String specifying the file's MIME type + * + * @see ServletContext#getMimeType(String) + * @see PortletContext#getMimeType(String) + */ + public String getMimeType(String resourceName); + + /** + * Gets the properties configured for the deployment, e.g. as init + * parameters to the servlet or portlet. + * + * @return properties for the application. + */ + public Properties getInitParameters(); + + public Iterator<AddonContextListener> getAddonContextListeners(); + + public AddonContext getAddonContext(); + + public void setAddonContext(AddonContext vaadinContext); +} diff --git a/server/src/com/vaadin/terminal/DownloadStream.java b/server/src/com/vaadin/terminal/DownloadStream.java new file mode 100644 index 0000000000..9853b0eee2 --- /dev/null +++ b/server/src/com/vaadin/terminal/DownloadStream.java @@ -0,0 +1,335 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.terminal.gwt.server.Constants; + +/** + * Downloadable stream. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class DownloadStream implements Serializable { + + /** + * Maximum cache time. + */ + public static final long MAX_CACHETIME = Long.MAX_VALUE; + + /** + * Default cache time. + */ + public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24; + + private InputStream stream; + + private String contentType; + + private String fileName; + + private Map<String, String> params; + + private long cacheTime = DEFAULT_CACHETIME; + + private int bufferSize = 0; + + /** + * Creates a new instance of DownloadStream. + */ + public DownloadStream(InputStream stream, String contentType, + String fileName) { + setStream(stream); + setContentType(contentType); + setFileName(fileName); + } + + /** + * Gets downloadable stream. + * + * @return output stream. + */ + public InputStream getStream() { + return stream; + } + + /** + * Sets the stream. + * + * @param stream + * The stream to set + */ + public void setStream(InputStream stream) { + this.stream = stream; + } + + /** + * Gets stream content type. + * + * @return type of the stream content. + */ + public String getContentType() { + return contentType; + } + + /** + * Sets stream content type. + * + * @param contentType + * the contentType to set + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Returns the file name. + * + * @return the name of the file. + */ + public String getFileName() { + return fileName; + } + + /** + * Sets the file name. + * + * @param fileName + * the file name to set. + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * Sets a paramater for download stream. Parameters are optional information + * about the downloadable stream and their meaning depends on the used + * adapter. For example in WebAdapter they are interpreted as HTTP response + * headers. + * + * If the parameters by this name exists, the old value is replaced. + * + * @param name + * the Name of the parameter to set. + * @param value + * the Value of the parameter to set. + */ + public void setParameter(String name, String value) { + if (params == null) { + params = new HashMap<String, String>(); + } + params.put(name, value); + } + + /** + * Gets a paramater for download stream. Parameters are optional information + * about the downloadable stream and their meaning depends on the used + * adapter. For example in WebAdapter they are interpreted as HTTP response + * headers. + * + * @param name + * the Name of the parameter to set. + * @return Value of the parameter or null if the parameter does not exist. + */ + public String getParameter(String name) { + if (params != null) { + return params.get(name); + } + return null; + } + + /** + * Gets the names of the parameters. + * + * @return Iterator of names or null if no parameters are set. + */ + public Iterator<String> getParameterNames() { + if (params != null) { + return params.keySet().iterator(); + } + return null; + } + + /** + * Gets length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Default is + * <code>DEFAULT_CACHETIME</code>. + * + * @return Cache time in milliseconds + */ + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Zero or negavive + * value disbales the caching of this stream. + * + * @param cacheTime + * the cache time in milliseconds. + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + + /** + * Gets the size of the download buffer. + * + * @return int The size of the buffer in bytes. + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer. + * + * @param bufferSize + * the size of the buffer in bytes. + * + * @since 7.0 + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /** + * Writes this download stream to a wrapped response. This takes care of + * setting response headers according to what is defined in this download + * stream ({@link #getContentType()}, {@link #getCacheTime()}, + * {@link #getFileName()}) and transferring the data from the stream ( + * {@link #getStream()}) to the response. Defined parameters ( + * {@link #getParameterNames()}) are also included as headers in the + * response. If there's is a parameter named <code>Location</code>, a + * redirect (302 Moved temporarily) is sent instead of the contents of this + * stream. + * + * @param response + * the wrapped response to write this download stream to + * @throws IOException + * passed through from the wrapped response + * + * @since 7.0 + */ + public void writeTo(WrappedResponse response) throws IOException { + if (getParameter("Location") != null) { + response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + response.setHeader("Location", getParameter("Location")); + return; + } + + // Download from given stream + final InputStream data = getStream(); + if (data != null) { + + OutputStream out = null; + try { + // Sets content type + response.setContentType(getContentType()); + + // Sets cache headers + response.setCacheTime(getCacheTime()); + + // Copy download stream parameters directly + // to HTTP headers. + final Iterator<String> i = getParameterNames(); + if (i != null) { + while (i.hasNext()) { + final String param = i.next(); + response.setHeader(param, getParameter(param)); + } + } + + // suggest local filename from DownloadStream if + // Content-Disposition + // not explicitly set + String contentDispositionValue = getParameter("Content-Disposition"); + if (contentDispositionValue == null) { + contentDispositionValue = "filename=\"" + getFileName() + + "\""; + response.setHeader("Content-Disposition", + contentDispositionValue); + } + + int bufferSize = getBufferSize(); + if (bufferSize <= 0 || bufferSize > Constants.MAX_BUFFER_SIZE) { + bufferSize = Constants.DEFAULT_BUFFER_SIZE; + } + final byte[] buffer = new byte[bufferSize]; + int bytesRead = 0; + + out = response.getOutputStream(); + + long totalWritten = 0; + while ((bytesRead = data.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + + totalWritten += bytesRead; + if (totalWritten >= buffer.length) { + // Avoid chunked encoding for small resources + out.flush(); + } + } + } finally { + tryToCloseStream(out); + tryToCloseStream(data); + } + } + } + + /** + * Helper method that tries to close an output stream and ignores any + * exceptions. + * + * @param out + * the output stream to close, <code>null</code> is also + * supported + */ + static void tryToCloseStream(OutputStream out) { + try { + // try to close output stream (e.g. file handle) + if (out != null) { + out.close(); + } + } catch (IOException e1) { + // NOP + } + } + + /** + * Helper method that tries to close an input stream and ignores any + * exceptions. + * + * @param in + * the input stream to close, <code>null</code> is also supported + */ + static void tryToCloseStream(InputStream in) { + try { + // try to close output stream (e.g. file handle) + if (in != null) { + in.close(); + } + } catch (IOException e1) { + // NOP + } + } + +} diff --git a/server/src/com/vaadin/terminal/ErrorMessage.java b/server/src/com/vaadin/terminal/ErrorMessage.java new file mode 100644 index 0000000000..60a0780a72 --- /dev/null +++ b/server/src/com/vaadin/terminal/ErrorMessage.java @@ -0,0 +1,126 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface for rendering error messages to terminal. All the visible errors + * shown to user must implement this interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ErrorMessage extends Serializable { + + public enum ErrorLevel { + /** + * Error code for informational messages. + */ + INFORMATION("info", 0), + /** + * Error code for warning messages. + */ + WARNING("warning", 1), + /** + * Error code for regular error messages. + */ + ERROR("error", 2), + /** + * Error code for critical error messages. + */ + CRITICAL("critical", 3), + /** + * Error code for system errors and bugs. + */ + SYSTEMERROR("system", 4); + + String text; + int errorLevel; + + private ErrorLevel(String text, int errorLevel) { + this.text = text; + this.errorLevel = errorLevel; + } + + /** + * Textual representation for server-client communication of level + * + * @return String for error severity + */ + public String getText() { + return text; + } + + /** + * Integer representation of error severity for comparison + * + * @return integer for error severity + */ + public int intValue() { + return errorLevel; + } + + @Override + public String toString() { + return text; + } + + } + + /** + * @deprecated from 7.0, use {@link ErrorLevel#SYSTEMERROR} instead   + */ + @Deprecated + public static final ErrorLevel SYSTEMERROR = ErrorLevel.SYSTEMERROR; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#CRITICAL} instead   + */ + @Deprecated + public static final ErrorLevel CRITICAL = ErrorLevel.CRITICAL; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#ERROR} instead   + */ + + @Deprecated + public static final ErrorLevel ERROR = ErrorLevel.ERROR; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#WARNING} instead   + */ + @Deprecated + public static final ErrorLevel WARNING = ErrorLevel.WARNING; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#INFORMATION} instead   + */ + @Deprecated + public static final ErrorLevel INFORMATION = ErrorLevel.INFORMATION; + + /** + * Gets the errors level. + * + * @return the level of error as an integer. + */ + public ErrorLevel getErrorLevel(); + + /** + * Returns the HTML formatted message to show in as the error message on the + * client. + * + * This method should perform any necessary escaping to avoid XSS attacks. + * + * TODO this API may still change to use a separate data transfer object + * + * @return HTML formatted string for the error message + * @since 7.0 + */ + public String getFormattedHtmlMessage(); + +} diff --git a/server/src/com/vaadin/terminal/Extension.java b/server/src/com/vaadin/terminal/Extension.java new file mode 100644 index 0000000000..ef5bb4cf8d --- /dev/null +++ b/server/src/com/vaadin/terminal/Extension.java @@ -0,0 +1,27 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * An extension is an entity that is attached to a Component or another + * Extension and independently communicates between client and server. + * <p> + * An extension can only be attached once. It is not supported to move an + * extension from one target to another. + * <p> + * Extensions can use shared state and RPC in the same way as components. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public interface Extension extends ClientConnector { + /* + * Currently just an empty marker interface to distinguish between + * extensions and other connectors, e.g. components + */ +} diff --git a/server/src/com/vaadin/terminal/ExternalResource.java b/server/src/com/vaadin/terminal/ExternalResource.java new file mode 100644 index 0000000000..84fcc65a44 --- /dev/null +++ b/server/src/com/vaadin/terminal/ExternalResource.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.net.URL; + +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ExternalResource</code> implements source for resources fetched from + * location specified by URL:s. The resources are fetched directly by the client + * terminal and are not fetched trough the terminal adapter. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ExternalResource implements Resource, Serializable { + + /** + * Url of the download. + */ + private String sourceURL = null; + + /** + * MIME Type for the resource + */ + private String mimeType = null; + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + */ + public ExternalResource(URL sourceURL) { + if (sourceURL == null) { + throw new RuntimeException("Source must be non-null"); + } + + this.sourceURL = sourceURL.toString(); + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + * @param mimeType + * the MIME Type + */ + public ExternalResource(URL sourceURL, String mimeType) { + this(sourceURL); + this.mimeType = mimeType; + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + */ + public ExternalResource(String sourceURL) { + if (sourceURL == null) { + throw new RuntimeException("Source must be non-null"); + } + + this.sourceURL = sourceURL.toString(); + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + * @param mimeType + * the MIME Type + */ + public ExternalResource(String sourceURL, String mimeType) { + this(sourceURL); + this.mimeType = mimeType; + } + + /** + * Gets the URL of the external resource. + * + * @return the URL of the external resource. + */ + public String getURL() { + return sourceURL; + } + + /** + * Gets the MIME type of the resource. + * + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + if (mimeType == null) { + mimeType = FileTypeResolver.getMIMEType(getURL().toString()); + } + return mimeType; + } + + /** + * Sets the MIME type of the resource. + */ + public void setMIMEType(String mimeType) { + this.mimeType = mimeType; + } + +} diff --git a/server/src/com/vaadin/terminal/FileResource.java b/server/src/com/vaadin/terminal/FileResource.java new file mode 100644 index 0000000000..e3c9f0172a --- /dev/null +++ b/server/src/com/vaadin/terminal/FileResource.java @@ -0,0 +1,174 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; +import com.vaadin.terminal.Terminal.ErrorEvent; + +/** + * <code>FileResources</code> are files or directories on local filesystem. The + * files and directories are served through URI:s to the client terminal and + * thus must be registered to an URI context before they can be used. The + * resource is automatically registered to the application when it is created. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class FileResource implements ApplicationResource { + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * File where the downloaded content is fetched from. + */ + private File sourceFile; + + /** + * Application. + */ + private final Application application; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DownloadStream.DEFAULT_CACHETIME; + + /** + * Creates a new file resource for providing given file for client + * terminals. + */ + public FileResource(File sourceFile, Application application) { + this.application = application; + setSourceFile(sourceFile); + application.addResource(this); + } + + /** + * Gets the resource as stream. + * + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + @Override + public DownloadStream getStream() { + try { + final DownloadStream ds = new DownloadStream(new FileInputStream( + sourceFile), getMIMEType(), getFilename()); + ds.setParameter("Content-Length", + String.valueOf(sourceFile.length())); + + ds.setCacheTime(cacheTime); + return ds; + } catch (final FileNotFoundException e) { + // Log the exception using the application error handler + getApplication().getErrorHandler().terminalError(new ErrorEvent() { + + @Override + public Throwable getThrowable() { + return e; + } + + }); + + return null; + } + } + + /** + * Gets the source file. + * + * @return the source File. + */ + public File getSourceFile() { + return sourceFile; + } + + /** + * Sets the source file. + * + * @param sourceFile + * the source file to set. + */ + public void setSourceFile(File sourceFile) { + this.sourceFile = sourceFile; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + @Override + public Application getApplication() { + return application; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getFilename() + */ + @Override + public String getFilename() { + return sourceFile.getName(); + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + return FileTypeResolver.getMIMEType(sourceFile); + } + + /** + * Gets the length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Default is + * <code>DownloadStream.DEFAULT_CACHETIME</code>. + * + * @return Cache time in milliseconds. + */ + @Override + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Zero or negavive + * value disbales the caching of this stream. + * + * @param cacheTime + * the cache time in milliseconds. + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + + /* documented in superclass */ + @Override + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + +} diff --git a/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java b/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java new file mode 100644 index 0000000000..265e578c6d --- /dev/null +++ b/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java @@ -0,0 +1,116 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.shared.JavaScriptConnectorState; +import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.AbstractJavaScriptComponent; +import com.vaadin.ui.JavaScript.JavaScriptCallbackRpc; +import com.vaadin.ui.JavaScriptFunction; + +/** + * Internal helper class used to implement functionality common to + * {@link AbstractJavaScriptComponent} and {@link AbstractJavaScriptExtension}. + * Corresponding support in client-side code is in + * {@link JavaScriptConnectorHelper}. + * <p> + * You should most likely no use this class directly. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public class JavaScriptCallbackHelper implements Serializable { + + private static final Method CALL_METHOD = ReflectTools.findMethod( + JavaScriptCallbackRpc.class, "call", String.class, JSONArray.class); + private AbstractClientConnector connector; + + private Map<String, JavaScriptFunction> callbacks = new HashMap<String, JavaScriptFunction>(); + private JavaScriptCallbackRpc javascriptCallbackRpc; + + public JavaScriptCallbackHelper(AbstractClientConnector connector) { + this.connector = connector; + } + + public void registerCallback(String functionName, + JavaScriptFunction javaScriptCallback) { + callbacks.put(functionName, javaScriptCallback); + JavaScriptConnectorState state = getConnectorState(); + if (state.getCallbackNames().add(functionName)) { + connector.requestRepaint(); + } + ensureRpc(); + } + + private JavaScriptConnectorState getConnectorState() { + JavaScriptConnectorState state = (JavaScriptConnectorState) connector + .getState(); + return state; + } + + private void ensureRpc() { + if (javascriptCallbackRpc == null) { + javascriptCallbackRpc = new JavaScriptCallbackRpc() { + @Override + public void call(String name, JSONArray arguments) { + JavaScriptFunction callback = callbacks.get(name); + try { + callback.call(arguments); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + }; + connector.registerRpc(javascriptCallbackRpc); + } + } + + public void invokeCallback(String name, Object... arguments) { + if (callbacks.containsKey(name)) { + throw new IllegalStateException( + "Can't call callback " + + name + + " on the client because a callback with the same name is registered on the server."); + } + JSONArray args = new JSONArray(Arrays.asList(arguments)); + connector.addMethodInvocationToQueue( + JavaScriptCallbackRpc.class.getName(), CALL_METHOD, + new Object[] { name, args }); + connector.requestRepaint(); + } + + public void registerRpc(Class<?> rpcInterfaceType) { + if (rpcInterfaceType == JavaScriptCallbackRpc.class) { + // Ignore + return; + } + Map<String, Set<String>> rpcInterfaces = getConnectorState() + .getRpcInterfaces(); + String interfaceName = rpcInterfaceType.getName(); + if (!rpcInterfaces.containsKey(interfaceName)) { + Set<String> methodNames = new HashSet<String>(); + + for (Method method : rpcInterfaceType.getMethods()) { + methodNames.add(method.getName()); + } + + rpcInterfaces.put(interfaceName, methodNames); + connector.requestRepaint(); + } + } + +} diff --git a/server/src/com/vaadin/terminal/KeyMapper.java b/server/src/com/vaadin/terminal/KeyMapper.java new file mode 100644 index 0000000000..3f19692ef1 --- /dev/null +++ b/server/src/com/vaadin/terminal/KeyMapper.java @@ -0,0 +1,86 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.HashMap; + +/** + * <code>KeyMapper</code> is the simple two-way map for generating textual keys + * for objects and retrieving the objects later with the key. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public class KeyMapper<V> implements Serializable { + + private int lastKey = 0; + + private final HashMap<V, String> objectKeyMap = new HashMap<V, String>(); + + private final HashMap<String, V> keyObjectMap = new HashMap<String, V>(); + + /** + * Gets key for an object. + * + * @param o + * the object. + */ + public String key(V o) { + + if (o == null) { + return "null"; + } + + // If the object is already mapped, use existing key + String key = objectKeyMap.get(o); + if (key != null) { + return key; + } + + // If the object is not yet mapped, map it + key = String.valueOf(++lastKey); + objectKeyMap.put(o, key); + keyObjectMap.put(key, o); + + return key; + } + + /** + * Retrieves object with the key. + * + * @param key + * the name with the desired value. + * @return the object with the key. + */ + public V get(String key) { + return keyObjectMap.get(key); + } + + /** + * Removes object from the mapper. + * + * @param removeobj + * the object to be removed. + */ + public void remove(V removeobj) { + final String key = objectKeyMap.get(removeobj); + + if (key != null) { + objectKeyMap.remove(removeobj); + keyObjectMap.remove(key); + } + } + + /** + * Removes all objects from the mapper. + */ + public void removeAll() { + objectKeyMap.clear(); + keyObjectMap.clear(); + } +} diff --git a/server/src/com/vaadin/terminal/LegacyPaint.java b/server/src/com/vaadin/terminal/LegacyPaint.java new file mode 100644 index 0000000000..ea93e3db7f --- /dev/null +++ b/server/src/com/vaadin/terminal/LegacyPaint.java @@ -0,0 +1,85 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.terminal.PaintTarget.PaintStatus; +import com.vaadin.ui.Component; +import com.vaadin.ui.HasComponents; + +public class LegacyPaint implements Serializable { + /** + * + * <p> + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + * </p> + * + * <p> + * It is called when the contents of the component should be painted in + * response to the component first being shown or having been altered so + * that its visual representation is changed. + * </p> + * + * <p> + * <b>Do not override this to paint your component.</b> Override + * {@link #paintContent(PaintTarget)} instead. + * </p> + * + * + * @param target + * the target UIDL stream where the component should paint itself + * to. + * @throws PaintException + * if the paint operation failed. + */ + public static void paint(Component component, PaintTarget target) + throws PaintException { + // Only paint content of visible components. + if (!isVisibleInContext(component)) { + return; + } + + final String tag = target.getTag(component); + final PaintStatus status = target.startPaintable(component, tag); + if (PaintStatus.CACHED == status) { + // nothing to do but flag as cached and close the paintable tag + target.addAttribute("cached", true); + } else { + // Paint the contents of the component + if (component instanceof Vaadin6Component) { + ((Vaadin6Component) component).paintContent(target); + } + + } + target.endPaintable(component); + + } + + /** + * Checks if the component is visible and its parent is visible, + * recursively. + * <p> + * This is only a helper until paint is moved away from this class. + * + * @return + */ + protected static boolean isVisibleInContext(Component c) { + HasComponents p = c.getParent(); + while (p != null) { + if (!p.isVisible()) { + return false; + } + p = p.getParent(); + } + if (c.getParent() != null && !c.getParent().isComponentVisible(c)) { + return false; + } + + // All parents visible, return this state + return c.isVisible(); + } + +} diff --git a/server/src/com/vaadin/terminal/Page.java b/server/src/com/vaadin/terminal/Page.java new file mode 100644 index 0000000000..a068e7573e --- /dev/null +++ b/server/src/com/vaadin/terminal/Page.java @@ -0,0 +1,646 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.EventObject; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.event.EventRouter; +import com.vaadin.shared.ui.root.PageClientRpc; +import com.vaadin.terminal.WrappedRequest.BrowserDetails; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; +import com.vaadin.terminal.gwt.server.WebApplicationContext; +import com.vaadin.terminal.gwt.server.WebBrowser; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.JavaScript; +import com.vaadin.ui.Notification; +import com.vaadin.ui.Root; + +public class Page implements Serializable { + + /** + * Listener that gets notified when the size of the browser window + * containing the root has changed. + * + * @see Root#addListener(BrowserWindowResizeListener) + */ + public interface BrowserWindowResizeListener extends Serializable { + /** + * Invoked when the browser window containing a Root has been resized. + * + * @param event + * a browser window resize event + */ + public void browserWindowResized(BrowserWindowResizeEvent event); + } + + /** + * Event that is fired when a browser window containing a root is resized. + */ + public class BrowserWindowResizeEvent extends EventObject { + + private final int width; + private final int height; + + /** + * Creates a new event + * + * @param source + * the root for which the browser window has been resized + * @param width + * the new width of the browser window + * @param height + * the new height of the browser window + */ + public BrowserWindowResizeEvent(Page source, int width, int height) { + super(source); + this.width = width; + this.height = height; + } + + @Override + public Page getSource() { + return (Page) super.getSource(); + } + + /** + * Gets the new browser window height + * + * @return an integer with the new pixel height of the browser window + */ + public int getHeight() { + return height; + } + + /** + * Gets the new browser window width + * + * @return an integer with the new pixel width of the browser window + */ + public int getWidth() { + return width; + } + } + + /** + * Private class for storing properties related to opening resources. + */ + private class OpenResource implements Serializable { + + /** + * The resource to open + */ + private final Resource resource; + + /** + * The name of the target window + */ + private final String name; + + /** + * The width of the target window + */ + private final int width; + + /** + * The height of the target window + */ + private final int height; + + /** + * The border style of the target window + */ + private final int border; + + /** + * Creates a new open resource. + * + * @param resource + * The resource to open + * @param name + * The name of the target window + * @param width + * The width of the target window + * @param height + * The height of the target window + * @param border + * The border style of the target window + */ + private OpenResource(Resource resource, String name, int width, + int height, int border) { + this.resource = resource; + this.name = name; + this.width = width; + this.height = height; + this.border = border; + } + + /** + * Paints the open request. Should be painted inside the window. + * + * @param target + * the paint target + * @throws PaintException + * if the paint operation fails + */ + private void paintContent(PaintTarget target) throws PaintException { + target.startTag("open"); + target.addAttribute("src", resource); + if (name != null && name.length() > 0) { + target.addAttribute("name", name); + } + if (width >= 0) { + target.addAttribute("width", width); + } + if (height >= 0) { + target.addAttribute("height", height); + } + switch (border) { + case BORDER_MINIMAL: + target.addAttribute("border", "minimal"); + break; + case BORDER_NONE: + target.addAttribute("border", "none"); + break; + } + + target.endTag("open"); + } + } + + private static final Method BROWSWER_RESIZE_METHOD = ReflectTools + .findMethod(BrowserWindowResizeListener.class, + "browserWindowResized", BrowserWindowResizeEvent.class); + + /** + * A border style used for opening resources in a window without a border. + */ + public static final int BORDER_NONE = 0; + + /** + * A border style used for opening resources in a window with a minimal + * border. + */ + public static final int BORDER_MINIMAL = 1; + + /** + * A border style that indicates that the default border style should be + * used when opening resources. + */ + public static final int BORDER_DEFAULT = 2; + + /** + * Listener that listens changes in URI fragment. + */ + public interface FragmentChangedListener extends Serializable { + public void fragmentChanged(FragmentChangedEvent event); + } + + private static final Method FRAGMENT_CHANGED_METHOD = ReflectTools + .findMethod(Page.FragmentChangedListener.class, "fragmentChanged", + FragmentChangedEvent.class); + + /** + * Resources to be opened automatically on next repaint. The list is + * automatically cleared when it has been sent to the client. + */ + private final LinkedList<OpenResource> openList = new LinkedList<OpenResource>(); + + /** + * A list of notifications that are waiting to be sent to the client. + * Cleared (set to null) when the notifications have been sent. + */ + private List<Notification> notifications; + + /** + * Event fired when uri fragment changes. + */ + public class FragmentChangedEvent extends EventObject { + + /** + * The new uri fragment + */ + private final String fragment; + + /** + * Creates a new instance of UriFragmentReader change event. + * + * @param source + * the Source of the event. + */ + public FragmentChangedEvent(Page source, String fragment) { + super(source); + this.fragment = fragment; + } + + /** + * Gets the root in which the fragment has changed. + * + * @return the root in which the fragment has changed + */ + public Page getPage() { + return (Page) getSource(); + } + + /** + * Get the new fragment + * + * @return the new fragment + */ + public String getFragment() { + return fragment; + } + } + + private EventRouter eventRouter; + + /** + * The current URI fragment. + */ + private String fragment; + + private final Root root; + + private int browserWindowWidth = -1; + private int browserWindowHeight = -1; + + private JavaScript javaScript; + + public Page(Root root) { + this.root = root; + } + + private void addListener(Class<?> eventType, Object target, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, target, method); + } + + private void removeListener(Class<?> eventType, Object target, Method method) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, method); + } + } + + public void addListener(Page.FragmentChangedListener listener) { + addListener(FragmentChangedEvent.class, listener, + FRAGMENT_CHANGED_METHOD); + } + + public void removeListener(Page.FragmentChangedListener listener) { + removeListener(FragmentChangedEvent.class, listener, + FRAGMENT_CHANGED_METHOD); + } + + /** + * Sets URI fragment. Optionally fires a {@link FragmentChangedEvent} + * + * @param newFragment + * id of the new fragment + * @param fireEvent + * true to fire event + * @see FragmentChangedEvent + * @see Page.FragmentChangedListener + */ + public void setFragment(String newFragment, boolean fireEvents) { + if (newFragment == null) { + throw new NullPointerException("The fragment may not be null"); + } + if (!newFragment.equals(fragment)) { + fragment = newFragment; + if (fireEvents) { + fireEvent(new FragmentChangedEvent(this, newFragment)); + } + root.requestRepaint(); + } + } + + private void fireEvent(EventObject event) { + if (eventRouter != null) { + eventRouter.fireEvent(event); + } + } + + /** + * Sets URI fragment. This method fires a {@link FragmentChangedEvent} + * + * @param newFragment + * id of the new fragment + * @see FragmentChangedEvent + * @see Page.FragmentChangedListener + */ + public void setFragment(String newFragment) { + setFragment(newFragment, true); + } + + /** + * Gets currently set URI fragment. + * <p> + * To listen changes in fragment, hook a + * {@link Page.FragmentChangedListener}. + * + * @return the current fragment in browser uri or null if not known + */ + public String getFragment() { + return fragment; + } + + public void init(WrappedRequest request) { + BrowserDetails browserDetails = request.getBrowserDetails(); + if (browserDetails != null) { + fragment = browserDetails.getUriFragment(); + } + } + + public WebBrowser getWebBrowser() { + return ((WebApplicationContext) root.getApplication().getContext()) + .getBrowser(); + } + + public void setBrowserWindowSize(Integer width, Integer height) { + boolean fireEvent = false; + + if (width != null) { + int newWidth = width.intValue(); + if (newWidth != browserWindowWidth) { + browserWindowWidth = newWidth; + fireEvent = true; + } + } + + if (height != null) { + int newHeight = height.intValue(); + if (newHeight != browserWindowHeight) { + browserWindowHeight = newHeight; + fireEvent = true; + } + } + + if (fireEvent) { + fireEvent(new BrowserWindowResizeEvent(this, browserWindowWidth, + browserWindowHeight)); + } + + } + + /** + * Adds a new {@link BrowserWindowResizeListener} to this root. The listener + * will be notified whenever the browser window within which this root + * resides is resized. + * + * @param resizeListener + * the listener to add + * + * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent) + * @see #setResizeLazy(boolean) + */ + public void addListener(BrowserWindowResizeListener resizeListener) { + addListener(BrowserWindowResizeEvent.class, resizeListener, + BROWSWER_RESIZE_METHOD); + } + + /** + * Removes a {@link BrowserWindowResizeListener} from this root. The + * listener will no longer be notified when the browser window is resized. + * + * @param resizeListener + * the listener to remove + */ + public void removeListener(BrowserWindowResizeListener resizeListener) { + removeListener(BrowserWindowResizeEvent.class, resizeListener, + BROWSWER_RESIZE_METHOD); + } + + /** + * Gets the last known height of the browser window in which this root + * resides. + * + * @return the browser window height in pixels + */ + public int getBrowserWindowHeight() { + return browserWindowHeight; + } + + /** + * Gets the last known width of the browser window in which this root + * resides. + * + * @return the browser window width in pixels + */ + public int getBrowserWindowWidth() { + return browserWindowWidth; + } + + public JavaScript getJavaScript() { + if (javaScript == null) { + // Create and attach on first use + javaScript = new JavaScript(); + javaScript.extend(root); + } + + return javaScript; + } + + public void paintContent(PaintTarget target) throws PaintException { + if (!openList.isEmpty()) { + for (final Iterator<OpenResource> i = openList.iterator(); i + .hasNext();) { + (i.next()).paintContent(target); + } + openList.clear(); + } + + // Paint notifications + if (notifications != null) { + target.startTag("notifications"); + for (final Iterator<Notification> it = notifications.iterator(); it + .hasNext();) { + final Notification n = it.next(); + target.startTag("notification"); + if (n.getCaption() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_CAPTION, + n.getCaption()); + } + if (n.getDescription() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_MESSAGE, + n.getDescription()); + } + if (n.getIcon() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_ICON, + n.getIcon()); + } + if (!n.isHtmlContentAllowed()) { + target.addAttribute( + VRoot.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED, true); + } + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_POSITION, + n.getPosition()); + target.addAttribute(VNotification.ATTRIBUTE_NOTIFICATION_DELAY, + n.getDelayMsec()); + if (n.getStyleName() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_STYLE, + n.getStyleName()); + } + target.endTag("notification"); + } + target.endTag("notifications"); + notifications = null; + } + + if (fragment != null) { + target.addAttribute(VRoot.FRAGMENT_VARIABLE, fragment); + } + + } + + /** + * Opens the given resource in this root. The contents of this Root is + * replaced by the {@code Resource}. + * + * @param resource + * the resource to show in this root + */ + public void open(Resource resource) { + openList.add(new OpenResource(resource, null, -1, -1, BORDER_DEFAULT)); + root.requestRepaint(); + } + + /** + * Opens the given resource in a window with the given name. + * <p> + * The supplied {@code windowName} is used as the target name in a + * window.open call in the client. This means that special values such as + * "_blank", "_self", "_top", "_parent" have special meaning. An empty or + * <code>null</code> window name is also a special case. + * </p> + * <p> + * "", null and "_self" as {@code windowName} all causes the resource to be + * opened in the current window, replacing any old contents. For + * downloadable content you should avoid "_self" as "_self" causes the + * client to skip rendering of any other changes as it considers them + * irrelevant (the page will be replaced by the resource). This can speed up + * the opening of a resource, but it might also put the client side into an + * inconsistent state if the window content is not completely replaced e.g., + * if the resource is downloaded instead of displayed in the browser. + * </p> + * <p> + * "_blank" as {@code windowName} causes the resource to always be opened in + * a new window or tab (depends on the browser and browser settings). + * </p> + * <p> + * "_top" and "_parent" as {@code windowName} works as specified by the HTML + * standard. + * </p> + * <p> + * Any other {@code windowName} will open the resource in a window with that + * name, either by opening a new window/tab in the browser or by replacing + * the contents of an existing window with that name. + * </p> + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + */ + public void open(Resource resource, String windowName) { + openList.add(new OpenResource(resource, windowName, -1, -1, + BORDER_DEFAULT)); + root.requestRepaint(); + } + + /** + * Opens the given resource in a window with the given size, border and + * name. For more information on the meaning of {@code windowName}, see + * {@link #open(Resource, String)}. + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + * @param width + * the width of the window in pixels + * @param height + * the height of the window in pixels + * @param border + * the border style of the window. See {@link #BORDER_NONE + * Window.BORDER_* constants} + */ + public void open(Resource resource, String windowName, int width, + int height, int border) { + openList.add(new OpenResource(resource, windowName, width, height, + border)); + root.requestRepaint(); + } + + /** + * Internal helper method to actually add a notification. + * + * @param notification + * the notification to add + */ + private void addNotification(Notification notification) { + if (notifications == null) { + notifications = new LinkedList<Notification>(); + } + notifications.add(notification); + root.requestRepaint(); + } + + /** + * Shows a notification message. + * + * @see Notification + * + * @param notification + * The notification message to show + * + * @deprecated Use Notification.show(Page) instead. + */ + @Deprecated + public void showNotification(Notification notification) { + addNotification(notification); + } + + /** + * Gets the Page to which the current root belongs. This is automatically + * defined when processing requests to the server. In other cases, (e.g. + * from background threads), the current root is not automatically defined. + * + * @see Root#getCurrent() + * + * @return the current page instance if available, otherwise + * <code>null</code> + */ + public static Page getCurrent() { + Root currentRoot = Root.getCurrent(); + if (currentRoot == null) { + return null; + } + return currentRoot.getPage(); + } + + /** + * Sets the page title. The page title is displayed by the browser e.g. as + * the title of the browser window or as the title of the tab. + * + * @param title + * the new page title to set + */ + public void setTitle(String title) { + root.getRpcProxy(PageClientRpc.class).setTitle(title); + } + +} diff --git a/server/src/com/vaadin/terminal/PaintException.java b/server/src/com/vaadin/terminal/PaintException.java new file mode 100644 index 0000000000..68f689b7f1 --- /dev/null +++ b/server/src/com/vaadin/terminal/PaintException.java @@ -0,0 +1,54 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.Serializable; + +/** + * <code>PaintExcepection</code> is thrown if painting of a component fails. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class PaintException extends IOException implements Serializable { + + /** + * Constructs an instance of <code>PaintExeception</code> with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public PaintException(String msg) { + super(msg); + } + + /** + * Constructs an instance of <code>PaintExeception</code> with the specified + * detail message and cause. + * + * @param msg + * the detail message. + * @param cause + * the cause + */ + public PaintException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs an instance of <code>PaintExeception</code> from IOException. + * + * @param exception + * the original exception. + */ + public PaintException(IOException exception) { + super(exception.getMessage()); + } +} diff --git a/server/src/com/vaadin/terminal/PaintTarget.java b/server/src/com/vaadin/terminal/PaintTarget.java new file mode 100644 index 0000000000..b658c9f4a3 --- /dev/null +++ b/server/src/com/vaadin/terminal/PaintTarget.java @@ -0,0 +1,509 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Map; + +import com.vaadin.terminal.StreamVariable.StreamingStartEvent; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.ui.Component; + +/** + * This interface defines the methods for painting XML to the UIDL stream. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface PaintTarget extends Serializable { + + /** + * Prints single XMLsection. + * + * Prints full XML section. The section data is escaped from XML tags and + * surrounded by XML start and end-tags. + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the scetion data. + * @throws PaintException + * if the paint operation failed. + */ + public void addSection(String sectionTagName, String sectionData) + throws PaintException; + + /** + * Result of starting to paint a Paintable ( + * {@link PaintTarget#startPaintable(Component, String)}). + * + * @since 7.0 + */ + public enum PaintStatus { + /** + * Painting started, addVariable() and addAttribute() etc. methods may + * be called. + */ + PAINTING, + /** + * A previously unpainted or painted {@link Paintable} has been queued + * be created/update later in a separate change in the same set of + * changes. + */ + CACHED + } + + /** + * Prints element start tag of a paintable section. Starts a paintable + * section using the given tag. The PaintTarget may implement a caching + * scheme, that checks the paintable has actually changed or can a cached + * version be used instead. This method should call the startTag method. + * <p> + * If the Paintable is found in cache and this function returns true it may + * omit the content and close the tag, in which case cached content should + * be used. + * </p> + * <p> + * This method may also add only a reference to the paintable and queue the + * paintable to be painted separately. + * </p> + * <p> + * Each paintable being painted should be closed by a matching + * {@link #endPaintable(Component)} regardless of the {@link PaintStatus} + * returned. + * </p> + * + * @param paintable + * the paintable to start. + * @param tag + * the name of the start tag. + * @return {@link PaintStatus} - ready to paint or already cached on the + * client (also used for sub paintables that are painted later + * separately) + * @throws PaintException + * if the paint operation failed. + * @see #startTag(String) + * @since 7.0 (previously using startTag(Paintable, String)) + */ + public PaintStatus startPaintable(Component paintable, String tag) + throws PaintException; + + /** + * Prints paintable element end tag. + * + * Calls to {@link #startPaintable(Component, String)}should be matched by + * {@link #endPaintable(Component)}. If the parent tag is closed before + * every child tag is closed a PaintException is raised. + * + * @param paintable + * the paintable to close. + * @throws PaintException + * if the paint operation failed. + * @since 7.0 (previously using engTag(String)) + */ + public void endPaintable(Component paintable) throws PaintException; + + /** + * Prints element start tag. + * + * <pre> + * Todo: + * Checking of input values + * </pre> + * + * @param tagName + * the name of the start tag. + * @throws PaintException + * if the paint operation failed. + */ + public void startTag(String tagName) throws PaintException; + + /** + * Prints element end tag. + * + * If the parent tag is closed before every child tag is closed an + * PaintException is raised. + * + * @param tagName + * the name of the end tag. + * @throws PaintException + * if the paint operation failed. + */ + public void endTag(String tagName) throws PaintException; + + /** + * Adds a boolean attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, boolean value) throws PaintException; + + /** + * Adds a integer attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, int value) throws PaintException; + + /** + * Adds a resource attribute to component. Atributes must be added before + * any content is written. + * + * @param name + * the Attribute name + * @param value + * the Attribute value + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, Resource value) throws PaintException; + + /** + * Adds details about {@link StreamVariable} to the UIDL stream. Eg. in web + * terminals Receivers are typically rendered for the client side as URLs, + * where the client side implementation can do an http post request. + * <p> + * The urls in UIDL message may use Vaadin specific protocol. Before + * actually using the urls on the client side, they should be passed via + * {@link ApplicationConnection#translateVaadinUri(String)}. + * <p> + * Note that in current terminal implementation StreamVariables are cleaned + * from the terminal only when: + * <ul> + * <li>a StreamVariable with same name replaces an old one + * <li>the variable owner is no more attached + * <li>the developer signals this by calling + * {@link StreamingStartEvent#disposeStreamVariable()} + * </ul> + * Most commonly a component developer can just ignore this issue, but with + * strict memory requirements and lots of StreamVariables implementations + * that reserve a lot of memory this may be a critical issue. + * + * @param owner + * the ReceiverOwner that can track the progress of streaming to + * the given StreamVariable + * @param name + * an identifying name for the StreamVariable + * @param value + * the StreamVariable to paint + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, + StreamVariable value) throws PaintException; + + /** + * Adds a long attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, long value) throws PaintException; + + /** + * Adds a float attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, float value) throws PaintException; + + /** + * Adds a double attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, double value) throws PaintException; + + /** + * Adds a string attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Boolean attribute name. + * @param value + * the Boolean attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, String value) throws PaintException; + + /** + * TODO + * + * @param name + * @param value + * @throws PaintException + */ + public void addAttribute(String name, Map<?, ?> value) + throws PaintException; + + /** + * Adds a Paintable type attribute. On client side the value will be a + * terminal specific reference to corresponding component on client side + * implementation. + * + * @param name + * the name of the attribute + * @param value + * the Paintable to be referenced on client side + * @throws PaintException + */ + public void addAttribute(String name, Component value) + throws PaintException; + + /** + * Adds a string type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String value) + throws PaintException; + + /** + * Adds a int type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, int value) + throws PaintException; + + /** + * Adds a long type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, long value) + throws PaintException; + + /** + * Adds a float type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, float value) + throws PaintException; + + /** + * Adds a double type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, double value) + throws PaintException; + + /** + * Adds a boolean type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, boolean value) + throws PaintException; + + /** + * Adds a string array type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String[] value) + throws PaintException; + + /** + * Adds a Paintable type variable. On client side the variable value will be + * a terminal specific reference to corresponding component on client side + * implementation. When updated from client side, terminal will map the + * client side component reference back to a corresponding server side + * reference. + * + * @param owner + * the Listener for variable changes + * @param name + * the name of the variable + * @param value + * the initial value of the variable + * + * @throws PaintException + * if the paint oparation fails + */ + public void addVariable(VariableOwner owner, String name, Component value) + throws PaintException; + + /** + * Adds a upload stream type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addUploadStreamVariable(VariableOwner owner, String name) + throws PaintException; + + /** + * Prints single XML section. + * <p> + * Prints full XML section. The section data must be XML and it is + * surrounded by XML start and end-tags. + * </p> + * + * @param sectionTagName + * the tag name. + * @param sectionData + * the section data to be printed. + * @param namespace + * the namespace. + * @throws PaintException + * if the paint operation failed. + */ + public void addXMLSection(String sectionTagName, String sectionData, + String namespace) throws PaintException; + + /** + * Adds UIDL directly. The UIDL must be valid in accordance with the + * UIDL.dtd + * + * @param uidl + * the UIDL to be added. + * @throws PaintException + * if the paint operation failed. + */ + public void addUIDL(java.lang.String uidl) throws PaintException; + + /** + * Adds text node. All the contents of the text are XML-escaped. + * + * @param text + * the Text to add + * @throws PaintException + * if the paint operation failed. + */ + void addText(String text) throws PaintException; + + /** + * Adds CDATA node to target UIDL-tree. + * + * @param text + * the Character data to add + * @throws PaintException + * if the paint operation failed. + * @since 3.1 + */ + void addCharacterData(String text) throws PaintException; + + public void addAttribute(String string, Object[] keys); + + /** + * @return the "tag" string used in communication to present given + * {@link ClientConnector} type. Terminal may define how to present + * the connector. + */ + public String getTag(ClientConnector paintable); + + /** + * @return true if a full repaint has been requested. E.g. refresh in a + * browser window or such. + */ + public boolean isFullRepaint(); + +} diff --git a/server/src/com/vaadin/terminal/RequestHandler.java b/server/src/com/vaadin/terminal/RequestHandler.java new file mode 100644 index 0000000000..f37201715d --- /dev/null +++ b/server/src/com/vaadin/terminal/RequestHandler.java @@ -0,0 +1,36 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.Serializable; + +import com.vaadin.Application; + +/** + * Handler for producing a response to non-UIDL requests. Handlers can be added + * to applications using {@link Application#addRequestHandler(RequestHandler)} + */ +public interface RequestHandler extends Serializable { + + /** + * Handles a non-UIDL request. If a response is written, this method should + * return <code>false</code> to indicate that no more request handlers + * should be invoked for the request. + * + * @param application + * The application to which the request belongs + * @param request + * The request to handle + * @param response + * The response object to which a response can be written. + * @return true if a response has been written and no further request + * handlers should be called, otherwise false + * @throws IOException + */ + boolean handleRequest(Application application, WrappedRequest request, + WrappedResponse response) throws IOException; + +} diff --git a/server/src/com/vaadin/terminal/Resource.java b/server/src/com/vaadin/terminal/Resource.java new file mode 100644 index 0000000000..58dc4fea9d --- /dev/null +++ b/server/src/com/vaadin/terminal/Resource.java @@ -0,0 +1,26 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * <code>Resource</code> provided to the client terminal. Support for actually + * displaying the resource type is left to the terminal. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Resource extends Serializable { + + /** + * Gets the MIME type of the resource. + * + * @return the MIME type of the resource. + */ + public String getMIMEType(); +} diff --git a/server/src/com/vaadin/terminal/Scrollable.java b/server/src/com/vaadin/terminal/Scrollable.java new file mode 100644 index 0000000000..472954c556 --- /dev/null +++ b/server/src/com/vaadin/terminal/Scrollable.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * <p> + * This interface is implemented by all visual objects that can be scrolled + * programmatically from the server-side. The unit of scrolling is pixel. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Scrollable extends Serializable { + + /** + * Gets scroll left offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + * </p> + * + * @return Horizontal scrolling position in pixels. + */ + public int getScrollLeft(); + + /** + * Sets scroll left offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + * </p> + * + * @param scrollLeft + * the xOffset. + */ + public void setScrollLeft(int scrollLeft); + + /** + * Gets scroll top offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + * </p> + * + * @return Vertical scrolling position in pixels. + */ + public int getScrollTop(); + + /** + * Sets scroll top offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + * </p> + * + * <p> + * The scrolling position is limited by the current height of the content + * area. If the position is below the height, it is scrolled to the bottom. + * However, if the same response also adds height to the content area, + * scrolling to bottom only scrolls to the bottom of the previous content + * area. + * </p> + * + * @param scrollTop + * the yOffset. + */ + public void setScrollTop(int scrollTop); + +} diff --git a/server/src/com/vaadin/terminal/Sizeable.java b/server/src/com/vaadin/terminal/Sizeable.java new file mode 100644 index 0000000000..e3c98e0fa9 --- /dev/null +++ b/server/src/com/vaadin/terminal/Sizeable.java @@ -0,0 +1,242 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface to be implemented by components wishing to display some object that + * may be dynamically resized during runtime. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Sizeable extends Serializable { + + /** + * @deprecated from 7.0, use {@link Unit#PIXELS} instead   + */ + @Deprecated + public static final Unit UNITS_PIXELS = Unit.PIXELS; + + /** + * @deprecated from 7.0, use {@link Unit#POINTS} instead   + */ + @Deprecated + public static final Unit UNITS_POINTS = Unit.POINTS; + + /** + * @deprecated from 7.0, use {@link Unit#PICAS} instead   + */ + @Deprecated + public static final Unit UNITS_PICAS = Unit.PICAS; + + /** + * @deprecated from 7.0, use {@link Unit#EM} instead   + */ + @Deprecated + public static final Unit UNITS_EM = Unit.EM; + + /** + * @deprecated from 7.0, use {@link Unit#EX} instead   + */ + @Deprecated + public static final Unit UNITS_EX = Unit.EX; + + /** + * @deprecated from 7.0, use {@link Unit#MM} instead   + */ + @Deprecated + public static final Unit UNITS_MM = Unit.MM; + + /** + * @deprecated from 7.0, use {@link Unit#CM} instead   + */ + @Deprecated + public static final Unit UNITS_CM = Unit.CM; + + /** + * @deprecated from 7.0, use {@link Unit#INCH} instead   + */ + @Deprecated + public static final Unit UNITS_INCH = Unit.INCH; + + /** + * @deprecated from 7.0, use {@link Unit#PERCENTAGE} instead   + */ + @Deprecated + public static final Unit UNITS_PERCENTAGE = Unit.PERCENTAGE; + + public static final float SIZE_UNDEFINED = -1; + + public enum Unit { + /** + * Unit code representing pixels. + */ + PIXELS("px"), + /** + * Unit code representing points (1/72nd of an inch). + */ + POINTS("pt"), + /** + * Unit code representing picas (12 points). + */ + PICAS("pc"), + /** + * Unit code representing the font-size of the relevant font. + */ + EM("em"), + /** + * Unit code representing the x-height of the relevant font. + */ + EX("ex"), + /** + * Unit code representing millimeters. + */ + MM("mm"), + /** + * Unit code representing centimeters. + */ + CM("cm"), + /** + * Unit code representing inches. + */ + INCH("in"), + /** + * Unit code representing in percentage of the containing element + * defined by terminal. + */ + PERCENTAGE("%"); + + private String symbol; + + private Unit(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + @Override + public String toString() { + return symbol; + } + + public static Unit getUnitFromSymbol(String symbol) { + if (symbol == null) { + return Unit.PIXELS; // Defaults to pixels + } + for (Unit unit : Unit.values()) { + if (symbol.equals(unit.getSymbol())) { + return unit; + } + } + return Unit.PIXELS; // Defaults to pixels + } + } + + /** + * Gets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @return width of the object in units specified by widthUnits property. + */ + public float getWidth(); + + /** + * Gets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @return height of the object in units specified by heightUnits property. + */ + public float getHeight(); + + /** + * Gets the width property units. + * + * @return units used in width property. + */ + public Unit getWidthUnits(); + + /** + * Gets the height property units. + * + * @return units used in height property. + */ + public Unit getHeightUnits(); + + /** + * Sets the height of the component using String presentation. + * + * String presentation is similar to what is used in Cascading Style Sheets. + * Size can be length or percentage of available size. + * + * The empty string ("") or null will unset the height and set the units to + * pixels. + * + * See <a + * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS + * specification</a> for more details. + * + * @param height + * in CSS style string representation + */ + public void setHeight(String height); + + /** + * Sets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param width + * the width of the object. + * @param unit + * the unit used for the width. + */ + public void setWidth(float width, Unit unit); + + /** + * Sets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param height + * the height of the object. + * @param unit + * the unit used for the width. + */ + public void setHeight(float height, Unit unit); + + /** + * Sets the width of the component using String presentation. + * + * String presentation is similar to what is used in Cascading Style Sheets. + * Size can be length or percentage of available size. + * + * The empty string ("") or null will unset the width and set the units to + * pixels. + * + * See <a + * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS + * specification</a> for more details. + * + * @param width + * in CSS style string representation, null or empty string to + * reset + */ + public void setWidth(String width); + + /** + * Sets the size to 100% x 100%. + */ + public void setSizeFull(); + + /** + * Clears any size settings. + */ + public void setSizeUndefined(); + +} diff --git a/server/src/com/vaadin/terminal/StreamResource.java b/server/src/com/vaadin/terminal/StreamResource.java new file mode 100644 index 0000000000..1afd91dc08 --- /dev/null +++ b/server/src/com/vaadin/terminal/StreamResource.java @@ -0,0 +1,222 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.InputStream; +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>StreamResource</code> is a resource provided to the client directly by + * the application. The strean resource is fetched from URI that is most often + * in the context of the application or window. The resource is automatically + * registered to window in creation. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class StreamResource implements ApplicationResource { + + /** + * Source stream the downloaded content is fetched from. + */ + private StreamSource streamSource = null; + + /** + * Explicit mime-type. + */ + private String MIMEType = null; + + /** + * Filename. + */ + private String filename; + + /** + * Application. + */ + private final Application application; + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DEFAULT_CACHETIME; + + /** + * Creates a new stream resource for downloading from stream. + * + * @param streamSource + * the source Stream. + * @param filename + * the name of the file. + * @param application + * the Application object. + */ + public StreamResource(StreamSource streamSource, String filename, + Application application) { + + this.application = application; + setFilename(filename); + setStreamSource(streamSource); + + // Register to application + application.addResource(this); + + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + if (MIMEType != null) { + return MIMEType; + } + return FileTypeResolver.getMIMEType(filename); + } + + /** + * Sets the mime type of the resource. + * + * @param MIMEType + * the MIME type to be set. + */ + public void setMIMEType(String MIMEType) { + this.MIMEType = MIMEType; + } + + /** + * Returns the source for this <code>StreamResource</code>. StreamSource is + * queried when the resource is about to be streamed to the client. + * + * @return Source of the StreamResource. + */ + public StreamSource getStreamSource() { + return streamSource; + } + + /** + * Sets the source for this <code>StreamResource</code>. + * <code>StreamSource</code> is queried when the resource is about to be + * streamed to the client. + * + * @param streamSource + * the source to set. + */ + public void setStreamSource(StreamSource streamSource) { + this.streamSource = streamSource; + } + + /** + * Gets the filename. + * + * @return the filename. + */ + @Override + public String getFilename() { + return filename; + } + + /** + * Sets the filename. + * + * @param filename + * the filename to set. + */ + public void setFilename(String filename) { + this.filename = filename; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + @Override + public Application getApplication() { + return application; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + @Override + public DownloadStream getStream() { + final StreamSource ss = getStreamSource(); + if (ss == null) { + return null; + } + final DownloadStream ds = new DownloadStream(ss.getStream(), + getMIMEType(), getFilename()); + ds.setBufferSize(getBufferSize()); + ds.setCacheTime(cacheTime); + return ds; + } + + /** + * Interface implemented by the source of a StreamResource. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface StreamSource extends Serializable { + + /** + * Returns new input stream that is used for reading the resource. + */ + public InputStream getStream(); + } + + /* documented in superclass */ + @Override + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /* documented in superclass */ + @Override + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Zero or negavive value disbales the caching of this + * stream. + * </p> + * + * @param cacheTime + * the cache time in milliseconds. + * + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + +} diff --git a/server/src/com/vaadin/terminal/StreamVariable.java b/server/src/com/vaadin/terminal/StreamVariable.java new file mode 100644 index 0000000000..63763a5751 --- /dev/null +++ b/server/src/com/vaadin/terminal/StreamVariable.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.io.OutputStream; +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.terminal.StreamVariable.StreamingEndEvent; +import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; +import com.vaadin.terminal.StreamVariable.StreamingStartEvent; + +/** + * StreamVariable is a special kind of variable whose value is streamed to an + * {@link OutputStream} provided by the {@link #getOutputStream()} method. E.g. + * in web terminals {@link StreamVariable} can be used to send large files from + * browsers to the server without consuming large amounts of memory. + * <p> + * Note, writing to the {@link OutputStream} is not synchronized by the terminal + * (to avoid stalls in other operations when eg. streaming to a slow network + * service or file system). If UI is changed as a side effect of writing to the + * output stream, developer must handle synchronization manually. + * <p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.5 + * @see PaintTarget#addVariable(VariableOwner, String, StreamVariable) + */ +public interface StreamVariable extends Serializable { + + /** + * Invoked by the terminal when a new upload arrives, after + * {@link #streamingStarted(StreamingStartEvent)} method has been called. + * The terminal implementation will write the streamed variable to the + * returned output stream. + * + * @return Stream to which the uploaded file should be written. + */ + public OutputStream getOutputStream(); + + /** + * Whether the {@link #onProgress(long, long)} method should be called + * during the upload. + * <p> + * {@link #onProgress(long, long)} is called in a synchronized block when + * the content is being received. This is potentially bit slow, so we are + * calling that method only if requested. The value is requested after the + * {@link #uploadStarted(StreamingStartEvent)} event, but not after reading + * each buffer. + * + * @return true if this {@link StreamVariable} wants to by notified during + * the upload of the progress of streaming. + * @see #onProgress(StreamingProgressEvent) + */ + public boolean listenProgress(); + + /** + * This method is called by the terminal if {@link #listenProgress()} + * returns true when the streaming starts. + */ + public void onProgress(StreamingProgressEvent event); + + public void streamingStarted(StreamingStartEvent event); + + public void streamingFinished(StreamingEndEvent event); + + public void streamingFailed(StreamingErrorEvent event); + + /* + * Not synchronized to avoid stalls (caused by UIDL requests) while + * streaming the content. Implementations also most commonly atomic even + * without the restriction. + */ + /** + * If this method returns true while the content is being streamed the + * Terminal to stop receiving current upload. + * <p> + * Note, the usage of this method is not synchronized over the Application + * instance by the terminal like other methods. The implementation should + * only return a boolean field and especially not modify UI or implement a + * synchronization by itself. + * + * @return true if the streaming should be interrupted as soon as possible. + */ + public boolean isInterrupted(); + + public interface StreamingEvent extends Serializable { + + /** + * @return the file name of the streamed file if known + */ + public String getFileName(); + + /** + * @return the mime type of the streamed file if known + */ + public String getMimeType(); + + /** + * @return the length of the stream (in bytes) if known, else -1 + */ + public long getContentLength(); + + /** + * @return then number of bytes streamed to StreamVariable + */ + public long getBytesReceived(); + } + + /** + * Event passed to {@link #uploadStarted(StreamingStartEvent)} method before + * the streaming of the content to {@link StreamVariable} starts. + */ + public interface StreamingStartEvent extends StreamingEvent { + /** + * The owner of the StreamVariable can call this method to inform the + * terminal implementation that this StreamVariable will not be used to + * accept more post. + */ + public void disposeStreamVariable(); + } + + /** + * Event passed to {@link #onProgress(StreamingProgressEvent)} method during + * the streaming progresses. + */ + public interface StreamingProgressEvent extends StreamingEvent { + } + + /** + * Event passed to {@link #uploadFinished(StreamingEndEvent)} method the + * contents have been streamed to StreamVariable successfully. + */ + public interface StreamingEndEvent extends StreamingEvent { + } + + /** + * Event passed to {@link #uploadFailed(StreamingErrorEvent)} method when + * the streaming ended before the end of the input. The streaming may fail + * due an interruption by {@link } or due an other unknown exception in + * communication. In the latter case the exception is also passed to + * {@link Application#terminalError(com.vaadin.terminal.Terminal.ErrorEvent)} + * . + */ + public interface StreamingErrorEvent extends StreamingEvent { + + /** + * @return the exception that caused the receiving not to finish cleanly + */ + public Exception getException(); + + } + +} diff --git a/server/src/com/vaadin/terminal/SystemError.java b/server/src/com/vaadin/terminal/SystemError.java new file mode 100644 index 0000000000..bae135ee6b --- /dev/null +++ b/server/src/com/vaadin/terminal/SystemError.java @@ -0,0 +1,82 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; + +/** + * <code>SystemError</code> is an error message for a problem caused by error in + * system, not the user application code. The system error can contain technical + * information such as stack trace and exception. + * + * SystemError does not support HTML in error messages or stack traces. If HTML + * messages are required, use {@link UserError} or a custom implementation of + * {@link ErrorMessage}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class SystemError extends AbstractErrorMessage { + + /** + * Constructor for SystemError with error message specified. + * + * @param message + * the Textual error description. + */ + public SystemError(String message) { + super(message); + setErrorLevel(ErrorLevel.SYSTEMERROR); + setMode(ContentMode.XHTML); + setMessage(getHtmlMessage()); + } + + /** + * Constructor for SystemError with causing exception and error message. + * + * @param message + * the Textual error description. + * @param cause + * the throwable causing the system error. + */ + public SystemError(String message, Throwable cause) { + this(message); + addCause(AbstractErrorMessage.getErrorMessageForException(cause)); + } + + /** + * Constructor for SystemError with cause. + * + * @param cause + * the throwable causing the system error. + */ + public SystemError(Throwable cause) { + this(null, cause); + } + + /** + * Returns the message of the error in HTML. + * + * Note that this API may change in future versions. + */ + protected String getHtmlMessage() { + // TODO wrapping div with namespace? See the old code: + // target.addXMLSection("div", message, + // "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"); + + StringBuilder sb = new StringBuilder(); + if (getMessage() != null) { + sb.append("<h2>"); + sb.append(AbstractApplicationServlet + .safeEscapeForHtml(getMessage())); + sb.append("</h2>"); + } + return sb.toString(); + } + +} diff --git a/server/src/com/vaadin/terminal/Terminal.java b/server/src/com/vaadin/terminal/Terminal.java new file mode 100644 index 0000000000..9dc6ced6a7 --- /dev/null +++ b/server/src/com/vaadin/terminal/Terminal.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * An interface that provides information about the user's terminal. + * Implementors typically provide additional information using methods not in + * this interface. </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Terminal extends Serializable { + + /** + * Gets the name of the default theme for this terminal. + * + * @return the name of the theme that is used by default by this terminal. + */ + public String getDefaultTheme(); + + /** + * Gets the width of the terminal screen in pixels. This is the width of the + * screen and not the width available for the application. + * <p> + * Note that the screen width is typically not available in the + * {@link com.vaadin.Application#init()} method as this is called before the + * browser has a chance to report the screen size to the server. + * </p> + * + * @return the width of the terminal screen. + */ + public int getScreenWidth(); + + /** + * Gets the height of the terminal screen in pixels. This is the height of + * the screen and not the height available for the application. + * + * <p> + * Note that the screen height is typically not available in the + * {@link com.vaadin.Application#init()} method as this is called before the + * browser has a chance to report the screen size to the server. + * </p> + * + * @return the height of the terminal screen. + */ + public int getScreenHeight(); + + /** + * An error event implementation for Terminal. + */ + public interface ErrorEvent extends Serializable { + + /** + * Gets the contained throwable, the cause of the error. + */ + public Throwable getThrowable(); + + } + + /** + * Interface for listening to Terminal errors. + */ + public interface ErrorListener extends Serializable { + + /** + * Invoked when a terminal error occurs. + * + * @param event + * the fired event. + */ + public void terminalError(Terminal.ErrorEvent event); + } +} diff --git a/server/src/com/vaadin/terminal/ThemeResource.java b/server/src/com/vaadin/terminal/ThemeResource.java new file mode 100644 index 0000000000..41674b2373 --- /dev/null +++ b/server/src/com/vaadin/terminal/ThemeResource.java @@ -0,0 +1,96 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ThemeResource</code> is a named theme dependant resource provided and + * managed by a theme. The actual resource contents are dynamically resolved to + * comply with the used theme by the terminal adapter. This is commonly used to + * provide static images, flash, java-applets, etc for the terminals. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ThemeResource implements Resource { + + /** + * Id of the terminal managed resource. + */ + private String resourceID = null; + + /** + * Creates a resource. + * + * @param resourceId + * the Id of the resource. + */ + public ThemeResource(String resourceId) { + if (resourceId == null) { + throw new NullPointerException("Resource ID must not be null"); + } + if (resourceId.length() == 0) { + throw new IllegalArgumentException("Resource ID can not be empty"); + } + if (resourceId.charAt(0) == '/') { + throw new IllegalArgumentException( + "Resource ID must be relative (can not begin with /)"); + } + + resourceID = resourceId; + } + + /** + * Tests if the given object equals this Resource. + * + * @param obj + * the object to be tested for equality. + * @return <code>true</code> if the given object equals this Icon, + * <code>false</code> if not. + * @see java.lang.Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + return obj instanceof ThemeResource + && resourceID.equals(((ThemeResource) obj).resourceID); + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return resourceID.hashCode(); + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return resourceID.toString(); + } + + /** + * Gets the resource id. + * + * @return the resource id. + */ + public String getResourceId() { + return resourceID; + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + return FileTypeResolver.getMIMEType(getResourceId()); + } +} diff --git a/server/src/com/vaadin/terminal/UserError.java b/server/src/com/vaadin/terminal/UserError.java new file mode 100644 index 0000000000..a7a4fd89e2 --- /dev/null +++ b/server/src/com/vaadin/terminal/UserError.java @@ -0,0 +1,70 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +/** + * <code>UserError</code> is a controlled error occurred in application. User + * errors are occur in normal usage of the application and guide the user. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class UserError extends AbstractErrorMessage { + + /** + * @deprecated from 7.0, use {@link ContentMode#TEXT} instead   + */ + @Deprecated + public static final ContentMode CONTENT_TEXT = ContentMode.TEXT; + + /** + * @deprecated from 7.0, use {@link ContentMode#PREFORMATTED} instead   + */ + @Deprecated + public static final ContentMode CONTENT_PREFORMATTED = ContentMode.PREFORMATTED; + + /** + * @deprecated from 7.0, use {@link ContentMode#XHTML} instead   + */ + @Deprecated + public static final ContentMode CONTENT_XHTML = ContentMode.XHTML; + + /** + * Creates a textual error message of level ERROR. + * + * @param textErrorMessage + * the text of the error message. + */ + public UserError(String textErrorMessage) { + super(textErrorMessage); + } + + /** + * Creates an error message with level and content mode. + * + * @param message + * the error message. + * @param contentMode + * the content Mode. + * @param errorLevel + * the level of error. + */ + public UserError(String message, ContentMode contentMode, + ErrorLevel errorLevel) { + super(message); + if (contentMode == null) { + contentMode = ContentMode.TEXT; + } + if (errorLevel == null) { + errorLevel = ErrorLevel.ERROR; + } + setMode(contentMode); + setErrorLevel(errorLevel); + } + +} diff --git a/server/src/com/vaadin/terminal/Vaadin6Component.java b/server/src/com/vaadin/terminal/Vaadin6Component.java new file mode 100644 index 0000000000..59cbf956ca --- /dev/null +++ b/server/src/com/vaadin/terminal/Vaadin6Component.java @@ -0,0 +1,44 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.util.EventListener; + +import com.vaadin.ui.Component; + +/** + * Interface provided to ease porting of Vaadin 6 components to Vaadin 7. By + * implementing this interface your Component will be able to use + * {@link #paintContent(PaintTarget)} and + * {@link #changeVariables(Object, java.util.Map)} just like in Vaadin 6. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public interface Vaadin6Component extends VariableOwner, Component, + EventListener { + + /** + * <p> + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + * </p> + * + * <p> + * It is called when the contents of the component should be painted in + * response to the component first being shown or having been altered so + * that its visual representation is changed. + * </p> + * + * @param target + * the target UIDL stream where the component should paint itself + * to. + * @throws PaintException + * if the paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException; + +} diff --git a/server/src/com/vaadin/terminal/VariableOwner.java b/server/src/com/vaadin/terminal/VariableOwner.java new file mode 100644 index 0000000000..c52e04c008 --- /dev/null +++ b/server/src/com/vaadin/terminal/VariableOwner.java @@ -0,0 +1,85 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Map; + +/** + * <p> + * Listener interface for UI variable changes. The user communicates with the + * application using the so-called <i>variables</i>. When the user makes a + * change using the UI the terminal trasmits the changed variables to the + * application, and the components owning those variables may then process those + * changes. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @deprecated in 7.0. Only provided to ease porting of Vaadin 6 components. Do + * not implement this directly, implement {@link Vaadin6Component}. + */ +@Deprecated +public interface VariableOwner extends Serializable { + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @param source + * the Source of the variable change. This is the origin of the + * event. For example in Web Adapter this is the request. + * @param variables + * the Mapping from variable names to new variable values. + */ + public void changeVariables(Object source, Map<String, Object> variables); + + /** + * <p> + * Tests if the variable owner is enabled or not. The terminal should not + * send any variable changes to disabled variable owners. + * </p> + * + * @return <code>true</code> if the variable owner is enabled, + * <code>false</code> if not + */ + public boolean isEnabled(); + + /** + * <p> + * Tests if the variable owner is in immediate mode or not. Being in + * immediate mode means that all variable changes are required to be sent + * back from the terminal immediately when they occur. + * </p> + * + * <p> + * <strong>Note:</strong> <code>VariableOwner</code> does not include a set- + * method for the immediateness property. This is because not all + * VariableOwners wish to offer the functionality. Such VariableOwners are + * never in the immediate mode, thus they always return <code>false</code> + * in {@link #isImmediate()}. + * </p> + * + * @return <code>true</code> if the component is in immediate mode, + * <code>false</code> if not. + */ + public boolean isImmediate(); + + /** + * VariableOwner error event. + */ + public interface ErrorEvent extends Terminal.ErrorEvent { + + /** + * Gets the source VariableOwner. + * + * @return the variable owner. + */ + public VariableOwner getVariableOwner(); + + } +} diff --git a/server/src/com/vaadin/terminal/WrappedRequest.java b/server/src/com/vaadin/terminal/WrappedRequest.java new file mode 100644 index 0000000000..a27213d921 --- /dev/null +++ b/server/src/com/vaadin/terminal/WrappedRequest.java @@ -0,0 +1,277 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.Locale; +import java.util.Map; + +import javax.portlet.PortletRequest; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import com.vaadin.Application; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.annotations.EagerInit; +import com.vaadin.terminal.gwt.server.WebBrowser; +import com.vaadin.ui.Root; + +/** + * A generic request to the server, wrapping a more specific request type, e.g. + * HttpServletReqest or PortletRequest. + * + * @since 7.0 + */ +public interface WrappedRequest extends Serializable { + + /** + * Detailed information extracted from the browser. + * + * @see WrappedRequest#getBrowserDetails() + */ + public interface BrowserDetails extends Serializable { + /** + * Gets the URI hash fragment for the request. This is typically used to + * encode navigation within an application. + * + * @return the URI hash fragment + */ + public String getUriFragment(); + + /** + * Gets the value of window.name from the browser. This can be used to + * keep track of the specific window between browser reloads. + * + * @return the string value of window.name in the browser + */ + public String getWindowName(); + + /** + * Gets a reference to the {@link WebBrowser} object containing + * additional information, e.g. screen size and the time zone offset. + * + * @return the web browser object + */ + public WebBrowser getWebBrowser(); + } + + /** + * Gets the named request parameter This is typically a HTTP GET or POST + * parameter, though other request types might have other ways of + * representing parameters. + * + * @see javax.servlet.ServletRequest#getParameter(String) + * @see javax.portlet.PortletRequest#getParameter(String) + * + * @param parameter + * the name of the parameter + * @return The paramter value, or <code>null</code> if no parameter with the + * given name is present + */ + public String getParameter(String parameter); + + /** + * Gets all the parameters of the request. + * + * @see #getParameter(String) + * + * @see javax.servlet.ServletRequest#getParameterMap() + * @see javax.portlet.PortletRequest#getParameter(String) + * + * @return A mapping of parameter names to arrays of parameter values + */ + public Map<String, String[]> getParameterMap(); + + /** + * Returns the length of the request content that can be read from the input + * stream returned by {@link #getInputStream()}. + * + * @see javax.servlet.ServletRequest#getContentLength() + * @see javax.portlet.ClientDataRequest#getContentLength() + * + * @return content length in bytes + */ + public int getContentLength(); + + /** + * Returns an input stream from which the request content can be read. The + * request content length can be obtained with {@link #getContentLength()} + * without reading the full stream contents. + * + * @see javax.servlet.ServletRequest#getInputStream() + * @see javax.portlet.ClientDataRequest#getPortletInputStream() + * + * @return the input stream from which the contents of the request can be + * read + * @throws IOException + * if the input stream can not be opened + */ + public InputStream getInputStream() throws IOException; + + /** + * Gets a request attribute. + * + * @param name + * the name of the attribute + * @return the value of the attribute, or <code>null</code> if there is no + * attribute with the given name + * + * @see javax.servlet.ServletRequest#getAttribute(String) + * @see javax.portlet.PortletRequest#getAttribute(String) + */ + public Object getAttribute(String name); + + /** + * Defines a request attribute. + * + * @param name + * the name of the attribute + * @param value + * the attribute value + * + * @see javax.servlet.ServletRequest#setAttribute(String, Object) + * @see javax.portlet.PortletRequest#setAttribute(String, Object) + */ + public void setAttribute(String name, Object value); + + /** + * Gets the path of the requested resource relative to the application. The + * path be <code>null</code> if no path information is available. Does + * always start with / if the path isn't <code>null</code>. + * + * @return a string with the path relative to the application. + * + * @see javax.servlet.http.HttpServletRequest#getPathInfo() + */ + public String getRequestPathInfo(); + + /** + * Returns the maximum time interval, in seconds, that the session + * associated with this request will be kept open between client accesses. + * + * @return an integer specifying the number of seconds the session + * associated with this request remains open between client requests + * + * @see javax.servlet.http.HttpSession#getMaxInactiveInterval() + * @see javax.portlet.PortletSession#getMaxInactiveInterval() + */ + public int getSessionMaxInactiveInterval(); + + /** + * Gets an attribute from the session associated with this request. + * + * @param name + * the name of the attribute + * @return the attribute value, or <code>null</code> if the attribute is not + * defined in the session + * + * @see javax.servlet.http.HttpSession#getAttribute(String) + * @see javax.portlet.PortletSession#getAttribute(String) + */ + public Object getSessionAttribute(String name); + + /** + * Saves an attribute value in the session associated with this request. + * + * @param name + * the name of the attribute + * @param attribute + * the attribute value + * + * @see javax.servlet.http.HttpSession#setAttribute(String, Object) + * @see javax.portlet.PortletSession#setAttribute(String, Object) + */ + public void setSessionAttribute(String name, Object attribute); + + /** + * Returns the MIME type of the body of the request, or null if the type is + * not known. + * + * @return a string containing the name of the MIME type of the request, or + * null if the type is not known + * + * @see javax.servlet.ServletRequest#getContentType() + * @see javax.portlet.ResourceRequest#getContentType() + * + */ + public String getContentType(); + + /** + * Gets detailed information about the browser from which the request + * originated. This consists of information that is not available from + * normal HTTP requests, but requires additional information to be extracted + * for instance using javascript in the browser. + * + * This information is only guaranteed to be available in some special + * cases, for instance when {@link Application#getRoot} is called again + * after throwing {@link RootRequiresMoreInformationException} or in + * {@link Root#init(WrappedRequest)} for a Root class not annotated with + * {@link EagerInit} + * + * @return the browser details, or <code>null</code> if details are not + * available + * + * @see BrowserDetails + */ + public BrowserDetails getBrowserDetails(); + + /** + * Gets locale information from the query, e.g. using the Accept-Language + * header. + * + * @return the preferred Locale + * + * @see ServletRequest#getLocale() + * @see PortletRequest#getLocale() + */ + public Locale getLocale(); + + /** + * Returns the IP address from which the request came. This might also be + * the address of a proxy between the server and the original requester. + * + * @return a string containing the IP address, or <code>null</code> if the + * address is not available + * + * @see ServletRequest#getRemoteAddr() + */ + public String getRemoteAddr(); + + /** + * Checks whether the request was made using a secure channel, e.g. using + * https. + * + * @return a boolean indicating if the request is secure + * + * @see ServletRequest#isSecure() + * @see PortletRequest#isSecure() + */ + public boolean isSecure(); + + /** + * Gets the value of a request header, e.g. a http header for a + * {@link HttpServletRequest}. + * + * @param headerName + * the name of the header + * @return the header value, or <code>null</code> if the header is not + * present in the request + * + * @see HttpServletRequest#getHeader(String) + */ + public String getHeader(String headerName); + + /** + * Gets the deployment configuration for the context of this request. + * + * @return the deployment configuration + * + * @see DeploymentConfiguration + */ + public DeploymentConfiguration getDeploymentConfiguration(); + +} diff --git a/server/src/com/vaadin/terminal/WrappedResponse.java b/server/src/com/vaadin/terminal/WrappedResponse.java new file mode 100644 index 0000000000..995133a269 --- /dev/null +++ b/server/src/com/vaadin/terminal/WrappedResponse.java @@ -0,0 +1,147 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Serializable; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletResponse; +import javax.portlet.ResourceResponse; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * A generic response from the server, wrapping a more specific response type, + * e.g. HttpServletResponse or PortletResponse. + * + * @since 7.0 + */ +public interface WrappedResponse extends Serializable { + + /** + * Sets the (http) status code for the response. If you want to include an + * error message along the status code, use {@link #sendError(int, String)} + * instead. + * + * @param statusCode + * the status code to set + * @see HttpServletResponse#setStatus(int) + * + * @see ResourceResponse#HTTP_STATUS_CODE + */ + public void setStatus(int statusCode); + + /** + * Sets the content type of this response. If the content type including a + * charset is set before {@link #getWriter()} is invoked, the returned + * PrintWriter will automatically use the defined charset. + * + * @param contentType + * a string specifying the MIME type of the content + * + * @see ServletResponse#setContentType(String) + * @see MimeResponse#setContentType(String) + */ + public void setContentType(String contentType); + + /** + * Sets the value of a generic response header. If the header had already + * been set, the new value overwrites the previous one. + * + * @param name + * the name of the header + * @param value + * the header value. + * + * @see HttpServletResponse#setHeader(String, String) + * @see PortletResponse#setProperty(String, String) + */ + public void setHeader(String name, String value); + + /** + * Properly formats a timestamp as a date header. If the header had already + * been set, the new value overwrites the previous one. + * + * @param name + * the name of the header + * @param timestamp + * the number of milliseconds since epoch + * + * @see HttpServletResponse#setDateHeader(String, long) + */ + public void setDateHeader(String name, long timestamp); + + /** + * Returns a <code>OutputStream</code> for writing binary data in the + * response. + * <p> + * Either this method or getWriter() may be called to write the response, + * not both. + * + * @return a <code>OutputStream</code> for writing binary data + * @throws IOException + * if an input or output exception occurred + * + * @see #getWriter() + * @see ServletResponse#getOutputStream() + * @see MimeResponse#getPortletOutputStream() + */ + public OutputStream getOutputStream() throws IOException; + + /** + * Returns a <code>PrintWriter</code> object that can send character text to + * the client. The PrintWriter uses the character encoding defined using + * setContentType. + * <p> + * Either this method or getOutputStream() may be called to write the + * response, not both. + * + * @return a <code>PrintWriter</code> for writing character text + * @throws IOException + * if an input or output exception occurred + * + * @see #getOutputStream() + * @see ServletResponse#getWriter() + * @see MimeResponse#getWriter() + */ + public PrintWriter getWriter() throws IOException; + + /** + * Sets cache time in milliseconds, -1 means no cache at all. All required + * headers related to caching in the response are set based on the time. + * + * @param milliseconds + * Cache time in milliseconds + */ + public void setCacheTime(long milliseconds); + + /** + * Sends an error response to the client using the specified status code and + * clears the buffer. In some configurations, this can cause a predefined + * error page to be displayed. + * + * @param errorCode + * the HTTP status code + * @param message + * a message to accompany the error + * @throws IOException + * if an input or output exception occurs + * + * @see HttpServletResponse#sendError(int, String) + */ + public void sendError(int errorCode, String message) throws IOException; + + /** + * Gets the deployment configuration for the context of this response. + * + * @return the deployment configuration + * + * @see DeploymentConfiguration + */ + public DeploymentConfiguration getDeploymentConfiguration(); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java new file mode 100644 index 0000000000..40958e2868 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java @@ -0,0 +1,1079 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.security.GeneralSecurityException; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; +import javax.portlet.GenericPortlet; +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import com.liferay.portal.kernel.util.PortalClassInvoker; +import com.liferay.portal.kernel.util.PropsUtil; +import com.vaadin.Application; +import com.vaadin.Application.ApplicationStartEvent; +import com.vaadin.Application.SystemMessages; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.server.AbstractCommunicationManager.Callback; +import com.vaadin.ui.Root; + +/** + * Portlet 2.0 base class. This replaces the servlet in servlet/portlet 1.0 + * deployments and handles various portlet requests from the browser. + * + * TODO Document me! + * + * @author peholmst + */ +public abstract class AbstractApplicationPortlet extends GenericPortlet + implements Constants { + + public static final String RESOURCE_URL_ID = "APP"; + + public static class WrappedHttpAndPortletRequest extends + WrappedPortletRequest { + + public WrappedHttpAndPortletRequest(PortletRequest request, + HttpServletRequest originalRequest, + DeploymentConfiguration deploymentConfiguration) { + super(request, deploymentConfiguration); + this.originalRequest = originalRequest; + } + + private final HttpServletRequest originalRequest; + + @Override + public String getParameter(String name) { + String parameter = super.getParameter(name); + if (parameter == null) { + parameter = originalRequest.getParameter(name); + } + return parameter; + } + + @Override + public String getRemoteAddr() { + return originalRequest.getRemoteAddr(); + } + + @Override + public String getHeader(String name) { + String header = super.getHeader(name); + if (header == null) { + header = originalRequest.getHeader(name); + } + return header; + } + + @Override + public Map<String, String[]> getParameterMap() { + Map<String, String[]> parameterMap = super.getParameterMap(); + if (parameterMap == null) { + parameterMap = originalRequest.getParameterMap(); + } + return parameterMap; + } + } + + public static class WrappedGateinRequest extends + WrappedHttpAndPortletRequest { + public WrappedGateinRequest(PortletRequest request, + DeploymentConfiguration deploymentConfiguration) { + super(request, getOriginalRequest(request), deploymentConfiguration); + } + + private static final HttpServletRequest getOriginalRequest( + PortletRequest request) { + try { + Method getRealReq = request.getClass().getMethod( + "getRealRequest"); + HttpServletRequestWrapper origRequest = (HttpServletRequestWrapper) getRealReq + .invoke(request); + return origRequest; + } catch (Exception e) { + throw new IllegalStateException("GateIn request not detected", + e); + } + } + } + + public static class WrappedLiferayRequest extends + WrappedHttpAndPortletRequest { + + public WrappedLiferayRequest(PortletRequest request, + DeploymentConfiguration deploymentConfiguration) { + super(request, getOriginalRequest(request), deploymentConfiguration); + } + + @Override + public String getPortalProperty(String name) { + return PropsUtil.get(name); + } + + private static HttpServletRequest getOriginalRequest( + PortletRequest request) { + try { + // httpRequest = PortalUtil.getHttpServletRequest(request); + HttpServletRequest httpRequest = (HttpServletRequest) PortalClassInvoker + .invoke("com.liferay.portal.util.PortalUtil", + "getHttpServletRequest", request); + + // httpRequest = + // PortalUtil.getOriginalServletRequest(httpRequest); + httpRequest = (HttpServletRequest) PortalClassInvoker.invoke( + "com.liferay.portal.util.PortalUtil", + "getOriginalServletRequest", httpRequest); + return httpRequest; + } catch (Exception e) { + throw new IllegalStateException("Liferay request not detected", + e); + } + } + + } + + public static class AbstractApplicationPortletWrapper implements Callback { + + private final AbstractApplicationPortlet portlet; + + public AbstractApplicationPortletWrapper( + AbstractApplicationPortlet portlet) { + this.portlet = portlet; + } + + @Override + public void criticalNotification(WrappedRequest request, + WrappedResponse response, String cap, String msg, + String details, String outOfSyncURL) throws IOException { + portlet.criticalNotification(WrappedPortletRequest.cast(request), + (WrappedPortletResponse) response, cap, msg, details, + outOfSyncURL); + } + } + + /** + * This portlet parameter is used to add styles to the main element. E.g + * "height:500px" generates a style="height:500px" to the main element. + */ + public static final String PORTLET_PARAMETER_STYLE = "style"; + + /** + * This portal parameter is used to define the name of the Vaadin theme that + * is used for all Vaadin applications in the portal. + */ + public static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme"; + + public static final String WRITE_AJAX_PAGE_SCRIPT_WIDGETSET_SHOULD_WRITE = "writeAjaxPageScriptWidgetsetShouldWrite"; + + // TODO some parts could be shared with AbstractApplicationServlet + + // TODO Can we close the application when the portlet is removed? Do we know + // when the portlet is removed? + + private boolean productionMode = false; + + private DeploymentConfiguration deploymentConfiguration = new AbstractDeploymentConfiguration( + getClass()) { + @Override + public String getConfiguredWidgetset(WrappedRequest request) { + + String widgetset = getApplicationOrSystemProperty( + PARAMETER_WIDGETSET, null); + + if (widgetset == null) { + // If no widgetset defined for the application, check the + // portal + // property + widgetset = WrappedPortletRequest.cast(request) + .getPortalProperty(PORTAL_PARAMETER_VAADIN_WIDGETSET); + } + + if (widgetset == null) { + // If no widgetset defined for the portal, use the default + widgetset = DEFAULT_WIDGETSET; + } + + return widgetset; + } + + @Override + public String getConfiguredTheme(WrappedRequest request) { + + // is the default theme defined by the portal? + String themeName = WrappedPortletRequest.cast(request) + .getPortalProperty(Constants.PORTAL_PARAMETER_VAADIN_THEME); + + if (themeName == null) { + // no, using the default theme defined by Vaadin + themeName = DEFAULT_THEME_NAME; + } + + return themeName; + } + + @Override + public boolean isStandalone(WrappedRequest request) { + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.DeploymentConfiguration#getStaticFileLocation + * (com.vaadin.terminal.WrappedRequest) + * + * Return the URL from where static files, e.g. the widgetset and the + * theme, are served. In a standard configuration the VAADIN folder + * inside the returned folder is what is used for widgetsets and themes. + * + * @return The location of static resources (inside which there should + * be a VAADIN directory). Does not end with a slash (/). + */ + + @Override + public String getStaticFileLocation(WrappedRequest request) { + String staticFileLocation = WrappedPortletRequest.cast(request) + .getPortalProperty( + Constants.PORTAL_PARAMETER_VAADIN_RESOURCE_PATH); + if (staticFileLocation != null) { + // remove trailing slash if any + while (staticFileLocation.endsWith(".")) { + staticFileLocation = staticFileLocation.substring(0, + staticFileLocation.length() - 1); + } + return staticFileLocation; + } else { + // default for Liferay + return "/html"; + } + } + + @Override + public String getMimeType(String resourceName) { + return getPortletContext().getMimeType(resourceName); + } + }; + + private final AddonContext addonContext = new AddonContext( + getDeploymentConfiguration()); + + @Override + public void init(PortletConfig config) throws PortletException { + super.init(config); + Properties applicationProperties = getDeploymentConfiguration() + .getInitParameters(); + + // Read default parameters from the context + final PortletContext context = config.getPortletContext(); + for (final Enumeration<String> e = context.getInitParameterNames(); e + .hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + context.getInitParameter(name)); + } + + // Override with application settings from portlet.xml + for (final Enumeration<String> e = config.getInitParameterNames(); e + .hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + config.getInitParameter(name)); + } + + checkProductionMode(); + checkCrossSiteProtection(); + + addonContext.init(); + } + + @Override + public void destroy() { + super.destroy(); + + addonContext.destroy(); + } + + private void checkCrossSiteProtection() { + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION, "false").equals( + "true")) { + /* + * Print an information/warning message about running with xsrf + * protection disabled + */ + getLogger().warning(WARNING_XSRF_PROTECTION_DISABLED); + } + } + + private void checkProductionMode() { + // TODO Identical code in AbstractApplicationServlet -> refactor + // Check if the application is in production mode. + // We are in production mode if productionMode=true + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) { + productionMode = true; + } + + if (!productionMode) { + /* Print an information/warning message about running in debug mode */ + // TODO Maybe we need a different message for portlets? + getLogger().warning(NOT_PRODUCTION_MODE_INFO); + } + } + + protected enum RequestType { + FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE; + } + + protected RequestType getRequestType(WrappedPortletRequest wrappedRequest) { + PortletRequest request = wrappedRequest.getPortletRequest(); + if (request instanceof RenderRequest) { + return RequestType.RENDER; + } else if (request instanceof ResourceRequest) { + ResourceRequest resourceRequest = (ResourceRequest) request; + if (ServletPortletHelper.isUIDLRequest(wrappedRequest)) { + return RequestType.UIDL; + } else if (isBrowserDetailsRequest(resourceRequest)) { + return RequestType.BROWSER_DETAILS; + } else if (ServletPortletHelper.isFileUploadRequest(wrappedRequest)) { + return RequestType.FILE_UPLOAD; + } else if (ServletPortletHelper + .isConnectorResourceRequest(wrappedRequest)) { + return RequestType.CONNECTOR_RESOURCE; + } else if (ServletPortletHelper + .isApplicationResourceRequest(wrappedRequest)) { + return RequestType.APPLICATION_RESOURCE; + } else if (isDummyRequest(resourceRequest)) { + return RequestType.DUMMY; + } else { + return RequestType.STATIC_FILE; + } + } else if (request instanceof ActionRequest) { + return RequestType.ACTION; + } else if (request instanceof EventRequest) { + return RequestType.EVENT; + } + return RequestType.UNKNOWN; + } + + private boolean isBrowserDetailsRequest(ResourceRequest request) { + return request.getResourceID() != null + && request.getResourceID().equals("browserDetails"); + } + + private boolean isDummyRequest(ResourceRequest request) { + return request.getResourceID() != null + && request.getResourceID().equals("DUMMY"); + } + + /** + * Returns true if the servlet is running in production mode. Production + * mode disables all debug facilities. + * + * @return true if in production mode, false if in debug mode + */ + public boolean isProductionMode() { + return productionMode; + } + + protected void handleRequest(PortletRequest request, + PortletResponse response) throws PortletException, IOException { + RequestTimer requestTimer = new RequestTimer(); + requestTimer.start(); + + AbstractApplicationPortletWrapper portletWrapper = new AbstractApplicationPortletWrapper( + this); + + WrappedPortletRequest wrappedRequest = createWrappedRequest(request); + + WrappedPortletResponse wrappedResponse = new WrappedPortletResponse( + response, getDeploymentConfiguration()); + + RequestType requestType = getRequestType(wrappedRequest); + + if (requestType == RequestType.UNKNOWN) { + handleUnknownRequest(request, response); + } else if (requestType == RequestType.DUMMY) { + /* + * This dummy page is used by action responses to redirect to, in + * order to prevent the boot strap code from being rendered into + * strange places such as iframes. + */ + ((ResourceResponse) response).setContentType("text/html"); + final OutputStream out = ((ResourceResponse) response) + .getPortletOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>dummy page</body></html>"); + outWriter.close(); + } else if (requestType == RequestType.STATIC_FILE) { + serveStaticResources((ResourceRequest) request, + (ResourceResponse) response); + } else { + Application application = null; + boolean transactionStarted = false; + boolean requestStarted = false; + + try { + // TODO What about PARAM_UNLOADBURST & redirectToApplication?? + + /* Find out which application this request is related to */ + application = findApplicationInstance(wrappedRequest, + requestType); + if (application == null) { + return; + } + Application.setCurrent(application); + + /* + * Get or create an application context and an application + * manager for the session + */ + PortletApplicationContext2 applicationContext = getApplicationContext(request + .getPortletSession()); + applicationContext.setResponse(response); + applicationContext.setPortletConfig(getPortletConfig()); + + PortletCommunicationManager applicationManager = applicationContext + .getApplicationManager(application); + + if (requestType == RequestType.CONNECTOR_RESOURCE) { + applicationManager.serveConnectorResource(wrappedRequest, + wrappedResponse); + return; + } + + /* Update browser information from request */ + applicationContext.getBrowser().updateRequestDetails( + wrappedRequest); + + /* + * Call application requestStart before Application.init() is + * called (bypasses the limitation in TransactionListener) + */ + if (application instanceof PortletRequestListener) { + ((PortletRequestListener) application).onRequestStart( + request, response); + requestStarted = true; + } + + /* Start the newly created application */ + startApplication(request, application, applicationContext); + + /* + * Transaction starts. Call transaction listeners. Transaction + * end is called in the finally block below. + */ + applicationContext.startTransaction(application, request); + transactionStarted = true; + + /* Notify listeners */ + + // Finds the window within the application + Root root = null; + synchronized (application) { + if (application.isRunning()) { + switch (requestType) { + case RENDER: + case ACTION: + // Both action requests and render requests are ok + // without a Root as they render the initial HTML + // and then do a second request + try { + root = application + .getRootForRequest(wrappedRequest); + } catch (RootRequiresMoreInformationException e) { + // Ignore problem and continue without root + } + break; + case BROWSER_DETAILS: + // Should not try to find a root here as the + // combined request details might change the root + break; + case FILE_UPLOAD: + // no window + break; + case APPLICATION_RESOURCE: + // use main window - should not need any window + // root = application.getRoot(); + break; + default: + root = application + .getRootForRequest(wrappedRequest); + } + // if window not found, not a problem - use null + } + } + + // TODO Should this happen before or after the transaction + // starts? + if (request instanceof RenderRequest) { + applicationContext.firePortletRenderRequest(application, + root, (RenderRequest) request, + (RenderResponse) response); + } else if (request instanceof ActionRequest) { + applicationContext.firePortletActionRequest(application, + root, (ActionRequest) request, + (ActionResponse) response); + } else if (request instanceof EventRequest) { + applicationContext.firePortletEventRequest(application, + root, (EventRequest) request, + (EventResponse) response); + } else if (request instanceof ResourceRequest) { + applicationContext.firePortletResourceRequest(application, + root, (ResourceRequest) request, + (ResourceResponse) response); + } + + /* Handle the request */ + if (requestType == RequestType.FILE_UPLOAD) { + // Root is resolved in handleFileUpload by + // PortletCommunicationManager + applicationManager.handleFileUpload(application, + wrappedRequest, wrappedResponse); + return; + } else if (requestType == RequestType.BROWSER_DETAILS) { + applicationManager.handleBrowserDetailsRequest( + wrappedRequest, wrappedResponse, application); + return; + } else if (requestType == RequestType.UIDL) { + // Handles AJAX UIDL requests + applicationManager.handleUidlRequest(wrappedRequest, + wrappedResponse, portletWrapper, root); + return; + } else { + /* + * Removes the application if it has stopped + */ + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + handleOtherRequest(wrappedRequest, wrappedResponse, + requestType, application, applicationContext, + applicationManager); + } + } catch (final SessionExpiredException e) { + // TODO Figure out a better way to deal with + // SessionExpiredExceptions + getLogger().finest("A user session has expired"); + } catch (final GeneralSecurityException e) { + // TODO Figure out a better way to deal with + // GeneralSecurityExceptions + getLogger() + .fine("General security exception, the security key was probably incorrect."); + } catch (final Throwable e) { + handleServiceException(wrappedRequest, wrappedResponse, + application, e); + } finally { + // Notifies transaction end + try { + if (transactionStarted) { + ((PortletApplicationContext2) application.getContext()) + .endTransaction(application, request); + } + } finally { + try { + if (requestStarted) { + ((PortletRequestListener) application) + .onRequestEnd(request, response); + + } + } finally { + Root.setCurrent(null); + Application.setCurrent(null); + + PortletSession session = request + .getPortletSession(false); + if (session != null) { + requestTimer.stop(getApplicationContext(session)); + } + } + } + } + } + } + + /** + * Wraps the request in a (possibly portal specific) wrapped portlet + * request. + * + * @param request + * The original PortletRequest + * @return A wrapped version of the PorletRequest + */ + protected WrappedPortletRequest createWrappedRequest(PortletRequest request) { + String portalInfo = request.getPortalContext().getPortalInfo() + .toLowerCase(); + if (portalInfo.contains("liferay")) { + return new WrappedLiferayRequest(request, + getDeploymentConfiguration()); + } else if (portalInfo.contains("gatein")) { + return new WrappedGateinRequest(request, + getDeploymentConfiguration()); + } else { + return new WrappedPortletRequest(request, + getDeploymentConfiguration()); + } + + } + + protected DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + private void handleUnknownRequest(PortletRequest request, + PortletResponse response) { + getLogger().warning("Unknown request type"); + } + + /** + * Handle a portlet request that is not for static files, UIDL or upload. + * Also render requests are handled here. + * + * This method is called after starting the application and calling portlet + * and transaction listeners. + * + * @param request + * @param response + * @param requestType + * @param application + * @param applicationContext + * @param applicationManager + * @throws PortletException + * @throws IOException + * @throws MalformedURLException + */ + private void handleOtherRequest(WrappedPortletRequest request, + WrappedResponse response, RequestType requestType, + Application application, + PortletApplicationContext2 applicationContext, + PortletCommunicationManager applicationManager) + throws PortletException, IOException, MalformedURLException { + if (requestType == RequestType.APPLICATION_RESOURCE + || requestType == RequestType.RENDER) { + if (!applicationManager.handleApplicationRequest(request, response)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + "Not found"); + } + } else if (requestType == RequestType.EVENT) { + // nothing to do, listeners do all the work + } else if (requestType == RequestType.ACTION) { + // nothing to do, listeners do all the work + } else { + throw new IllegalStateException( + "handleRequest() without anything to do - should never happen!"); + } + } + + @Override + public void processEvent(EventRequest request, EventResponse response) + throws PortletException, IOException { + handleRequest(request, response); + } + + private void serveStaticResources(ResourceRequest request, + ResourceResponse response) throws IOException, PortletException { + final String resourceID = request.getResourceID(); + final PortletContext pc = getPortletContext(); + + InputStream is = pc.getResourceAsStream(resourceID); + if (is != null) { + final String mimetype = pc.getMimeType(resourceID); + if (mimetype != null) { + response.setContentType(mimetype); + } + final OutputStream os = response.getPortletOutputStream(); + final byte buffer[] = new byte[DEFAULT_BUFFER_SIZE]; + int bytes; + while ((bytes = is.read(buffer)) >= 0) { + os.write(buffer, 0, bytes); + } + } else { + getLogger().info( + "Requested resource [" + resourceID + + "] could not be found"); + response.setProperty(ResourceResponse.HTTP_STATUS_CODE, + Integer.toString(HttpServletResponse.SC_NOT_FOUND)); + } + } + + @Override + public void processAction(ActionRequest request, ActionResponse response) + throws PortletException, IOException { + handleRequest(request, response); + } + + @Override + protected void doDispatch(RenderRequest request, RenderResponse response) + throws PortletException, IOException { + try { + // try to let super handle - it'll call methods annotated for + // handling, the default doXYZ(), or throw if a handler for the mode + // is not found + super.doDispatch(request, response); + + } catch (PortletException e) { + if (e.getCause() == null) { + // No cause interpreted as 'unknown mode' - pass that trough + // so that the application can handle + handleRequest(request, response); + + } else { + // Something else failed, pass on + throw e; + } + } + } + + @Override + public void serveResource(ResourceRequest request, ResourceResponse response) + throws PortletException, IOException { + handleRequest(request, response); + } + + boolean requestCanCreateApplication(PortletRequest request, + RequestType requestType) { + if (requestType == RequestType.UIDL && isRepaintAll(request)) { + return true; + } else if (requestType == RequestType.RENDER) { + // In most cases the first request is a render request that renders + // the HTML fragment. This should create an application instance. + return true; + } else if (requestType == RequestType.EVENT) { + // A portlet can also be sent an event even though it has not been + // rendered, e.g. portlet on one page sends an event to a portlet on + // another page and then moves the user to that page. + return true; + } + return false; + } + + private boolean isRepaintAll(PortletRequest request) { + return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null) + && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1")); + } + + private void startApplication(PortletRequest request, + Application application, PortletApplicationContext2 context) + throws PortletException, MalformedURLException { + if (!application.isRunning()) { + Locale locale = request.getLocale(); + application.setLocale(locale); + // No application URL when running inside a portlet + application.start(new ApplicationStartEvent(null, + getDeploymentConfiguration().getInitParameters(), context, + isProductionMode())); + addonContext.applicationStarted(application); + } + } + + private void endApplication(PortletRequest request, + PortletResponse response, Application application) + throws IOException { + final PortletSession session = request.getPortletSession(); + if (session != null) { + getApplicationContext(session).removeApplication(application); + } + // Do not send any redirects when running inside a portlet. + } + + private Application findApplicationInstance( + WrappedPortletRequest wrappedRequest, RequestType requestType) + throws PortletException, SessionExpiredException, + MalformedURLException { + PortletRequest request = wrappedRequest.getPortletRequest(); + + boolean requestCanCreateApplication = requestCanCreateApplication( + request, requestType); + + /* Find an existing application for this request. */ + Application application = getExistingApplication(request, + requestCanCreateApplication); + + if (application != null) { + /* + * There is an existing application. We can use this as long as the + * user not specifically requested to close or restart it. + */ + + final boolean restartApplication = (wrappedRequest + .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null); + final boolean closeApplication = (wrappedRequest + .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null); + + if (restartApplication) { + closeApplication(application, request.getPortletSession(false)); + return createApplication(request); + } else if (closeApplication) { + closeApplication(application, request.getPortletSession(false)); + return null; + } else { + return application; + } + } + + // No existing application was found + + if (requestCanCreateApplication) { + return createApplication(request); + } else { + throw new SessionExpiredException(); + } + } + + private void closeApplication(Application application, + PortletSession session) { + if (application == null) { + return; + } + + application.close(); + if (session != null) { + PortletApplicationContext2 context = getApplicationContext(session); + context.removeApplication(application); + } + } + + private Application createApplication(PortletRequest request) + throws PortletException, MalformedURLException { + Application newApplication = getNewApplication(request); + final PortletApplicationContext2 context = getApplicationContext(request + .getPortletSession()); + context.addApplication(newApplication, request.getWindowID()); + return newApplication; + } + + private Application getExistingApplication(PortletRequest request, + boolean allowSessionCreation) throws MalformedURLException, + SessionExpiredException { + + final PortletSession session = request + .getPortletSession(allowSessionCreation); + + if (session == null) { + throw new SessionExpiredException(); + } + + PortletApplicationContext2 context = getApplicationContext(session); + Application application = context.getApplicationForWindowId(request + .getWindowID()); + if (application == null) { + return null; + } + if (application.isRunning()) { + return application; + } + // application found but not running + context.removeApplication(application); + + return null; + } + + protected abstract Class<? extends Application> getApplicationClass() + throws ClassNotFoundException; + + protected Application getNewApplication(PortletRequest request) + throws PortletException { + try { + final Application application = getApplicationClass().newInstance(); + application.setRootPreserved(true); + return application; + } catch (final IllegalAccessException e) { + throw new PortletException("getNewApplication failed", e); + } catch (final InstantiationException e) { + throw new PortletException("getNewApplication failed", e); + } catch (final ClassNotFoundException e) { + throw new PortletException("getNewApplication failed", e); + } + } + + /** + * Get system messages from the current application class + * + * @return + */ + protected SystemMessages getSystemMessages() { + try { + Class<? extends Application> appCls = getApplicationClass(); + Method m = appCls.getMethod("getSystemMessages", (Class[]) null); + return (Application.SystemMessages) m.invoke(null, (Object[]) null); + } catch (ClassNotFoundException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (SecurityException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (NoSuchMethodException e) { + // This is completely ok and should be silently ignored + } catch (IllegalArgumentException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (IllegalAccessException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (InvocationTargetException e) { + // This should never happen + throw new SystemMessageException(e); + } + return Application.getSystemMessages(); + } + + private void handleServiceException(WrappedPortletRequest request, + WrappedPortletResponse response, Application application, + Throwable e) throws IOException, PortletException { + // TODO Check that this error handler is working when running inside a + // portlet + + // if this was an UIDL request, response UIDL back to client + if (getRequestType(request) == RequestType.UIDL) { + Application.SystemMessages ci = getSystemMessages(); + criticalNotification(request, response, + ci.getInternalErrorCaption(), ci.getInternalErrorMessage(), + null, ci.getInternalErrorURL()); + if (application != null) { + application.getErrorHandler() + .terminalError(new RequestError(e)); + } else { + throw new PortletException(e); + } + } else { + // Re-throw other exceptions + throw new PortletException(e); + } + + } + + @SuppressWarnings("serial") + public class RequestError implements Terminal.ErrorEvent, Serializable { + + private final Throwable throwable; + + public RequestError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Send notification to client's application. Used to notify client of + * critical errors and session expiration due to long inactivity. Server has + * no knowledge of what application client refers to. + * + * @param request + * the Portlet request instance. + * @param response + * the Portlet response to write to. + * @param caption + * for the notification + * @param message + * for the notification + * @param details + * a detail message to show in addition to the passed message. + * Currently shown directly but could be hidden behind a details + * drop down. + * @param url + * url to load after message, null for current page + * @throws IOException + * if the writing failed due to input/output error. + */ + void criticalNotification(WrappedPortletRequest request, + WrappedPortletResponse response, String caption, String message, + String details, String url) throws IOException { + + // clients JS app is still running, but server application either + // no longer exists or it might fail to perform reasonably. + // send a notification to client's application and link how + // to "restart" application. + + if (caption != null) { + caption = "\"" + caption + "\""; + } + if (details != null) { + if (message == null) { + message = details; + } else { + message += "<br/><br/>" + details; + } + } + if (message != null) { + message = "\"" + message + "\""; + } + if (url != null) { + url = "\"" + url + "\""; + } + + // Set the response type + response.setContentType("application/json; charset=UTF-8"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("for(;;);[{\"changes\":[], \"meta\" : {" + + "\"appError\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}}, \"resources\": {}, \"locales\":[]}]"); + outWriter.close(); + } + + /** + * + * Gets the application context for a PortletSession. If no context is + * currently stored in a session a new context is created and stored in the + * session. + * + * @param portletSession + * the portlet session. + * @return the application context for the session. + */ + protected PortletApplicationContext2 getApplicationContext( + PortletSession portletSession) { + return PortletApplicationContext2.getApplicationContext(portletSession); + } + + private static final Logger getLogger() { + return Logger.getLogger(AbstractApplicationPortlet.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java new file mode 100644 index 0000000000..603bc74a21 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -0,0 +1,1623 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.vaadin.Application; +import com.vaadin.Application.ApplicationStartEvent; +import com.vaadin.Application.SystemMessages; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.ThemeResource; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.AbstractCommunicationManager.Callback; +import com.vaadin.ui.Root; + +/** + * Abstract implementation of the ApplicationServlet which handles all + * communication between the client and the server. + * + * It is possible to extend this class to provide own functionality but in most + * cases this is unnecessary. + * + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.0 + */ + +@SuppressWarnings("serial") +public abstract class AbstractApplicationServlet extends HttpServlet implements + Constants { + + private static class AbstractApplicationServletWrapper implements Callback { + + private final AbstractApplicationServlet servlet; + + public AbstractApplicationServletWrapper( + AbstractApplicationServlet servlet) { + this.servlet = servlet; + } + + @Override + public void criticalNotification(WrappedRequest request, + WrappedResponse response, String cap, String msg, + String details, String outOfSyncURL) throws IOException { + servlet.criticalNotification( + WrappedHttpServletRequest.cast(request), + ((WrappedHttpServletResponse) response), cap, msg, details, + outOfSyncURL); + } + } + + // TODO Move some (all?) of the constants to a separate interface (shared + // with portlet) + + private boolean productionMode = false; + + private final String resourcePath = null; + + private int resourceCacheTime = 3600; + + private DeploymentConfiguration deploymentConfiguration = new AbstractDeploymentConfiguration( + getClass()) { + + @Override + public String getStaticFileLocation(WrappedRequest request) { + HttpServletRequest servletRequest = WrappedHttpServletRequest + .cast(request); + return AbstractApplicationServlet.this + .getStaticFilesLocation(servletRequest); + } + + @Override + public String getConfiguredWidgetset(WrappedRequest request) { + return getApplicationOrSystemProperty( + AbstractApplicationServlet.PARAMETER_WIDGETSET, + AbstractApplicationServlet.DEFAULT_WIDGETSET); + } + + @Override + public String getConfiguredTheme(WrappedRequest request) { + // Use the default + return AbstractApplicationServlet.getDefaultTheme(); + } + + @Override + public boolean isStandalone(WrappedRequest request) { + return true; + } + + @Override + public String getMimeType(String resourceName) { + return getServletContext().getMimeType(resourceName); + } + }; + + private final AddonContext addonContext = new AddonContext( + getDeploymentConfiguration()); + + /** + * Called by the servlet container to indicate to a servlet that the servlet + * is being placed into service. + * + * @param servletConfig + * the object containing the servlet's configuration and + * initialization parameters + * @throws javax.servlet.ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + @Override + public void init(javax.servlet.ServletConfig servletConfig) + throws javax.servlet.ServletException { + super.init(servletConfig); + Properties applicationProperties = getDeploymentConfiguration() + .getInitParameters(); + + // Read default parameters from server.xml + final ServletContext context = servletConfig.getServletContext(); + for (final Enumeration<String> e = context.getInitParameterNames(); e + .hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + context.getInitParameter(name)); + } + + // Override with application config from web.xml + for (final Enumeration<String> e = servletConfig + .getInitParameterNames(); e.hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + servletConfig.getInitParameter(name)); + } + + checkProductionMode(); + checkCrossSiteProtection(); + checkResourceCacheTime(); + + addonContext.init(); + } + + @Override + public void destroy() { + super.destroy(); + + addonContext.destroy(); + } + + private void checkCrossSiteProtection() { + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION, "false").equals( + "true")) { + /* + * Print an information/warning message about running with xsrf + * protection disabled + */ + getLogger().warning(WARNING_XSRF_PROTECTION_DISABLED); + } + } + + private void checkProductionMode() { + // Check if the application is in production mode. + // We are in production mode if productionMode=true + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) { + productionMode = true; + } + + if (!productionMode) { + /* Print an information/warning message about running in debug mode */ + getLogger().warning(NOT_PRODUCTION_MODE_INFO); + } + + } + + private void checkResourceCacheTime() { + // Check if the browser caching time has been set in web.xml + try { + String rct = getDeploymentConfiguration() + .getApplicationOrSystemProperty( + SERVLET_PARAMETER_RESOURCE_CACHE_TIME, "3600"); + resourceCacheTime = Integer.parseInt(rct); + } catch (NumberFormatException nfe) { + // Default is 1h + resourceCacheTime = 3600; + getLogger().warning(WARNING_RESOURCE_CACHING_TIME_NOT_NUMERIC); + } + } + + /** + * Returns true if the servlet is running in production mode. Production + * mode disables all debug facilities. + * + * @return true if in production mode, false if in debug mode + */ + public boolean isProductionMode() { + return productionMode; + } + + /** + * Returns the amount of milliseconds the browser should cache a file. + * Default is 1 hour (3600 ms). + * + * @return The amount of milliseconds files are cached in the browser + */ + public int getResourceCacheTime() { + return resourceCacheTime; + } + + /** + * Receives standard HTTP requests from the public service method and + * dispatches them. + * + * @param request + * the object that contains the request the client made of the + * servlet. + * @param response + * the object that contains the response the servlet returns to + * the client. + * @throws ServletException + * if an input or output error occurs while the servlet is + * handling the TRACE request. + * @throws IOException + * if the request for the TRACE cannot be handled. + */ + + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + service(createWrappedRequest(request), createWrappedResponse(response)); + } + + private void service(WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws ServletException, + IOException { + RequestTimer requestTimer = new RequestTimer(); + requestTimer.start(); + + AbstractApplicationServletWrapper servletWrapper = new AbstractApplicationServletWrapper( + this); + + RequestType requestType = getRequestType(request); + if (!ensureCookiesEnabled(requestType, request, response)) { + return; + } + + if (requestType == RequestType.STATIC_FILE) { + serveStaticResources(request, response); + return; + } + + Application application = null; + boolean transactionStarted = false; + boolean requestStarted = false; + + try { + // If a duplicate "close application" URL is received for an + // application that is not open, redirect to the application's main + // page. + // This is needed as e.g. Spring Security remembers the last + // URL from the application, which is the logout URL, and repeats + // it. + // We can tell apart a real onunload request from a repeated one + // based on the real one having content (at least the UIDL security + // key). + if (requestType == RequestType.UIDL + && request.getParameterMap().containsKey( + ApplicationConnection.PARAM_UNLOADBURST) + && request.getContentLength() < 1 + && getExistingApplication(request, false) == null) { + redirectToApplication(request, response); + return; + } + + // Find out which application this request is related to + application = findApplicationInstance(request, requestType); + if (application == null) { + return; + } + Application.setCurrent(application); + + /* + * Get or create a WebApplicationContext and an ApplicationManager + * for the session + */ + WebApplicationContext webApplicationContext = getApplicationContext(request + .getSession()); + CommunicationManager applicationManager = webApplicationContext + .getApplicationManager(application, this); + + if (requestType == RequestType.CONNECTOR_RESOURCE) { + applicationManager.serveConnectorResource(request, response); + return; + } + + /* Update browser information from the request */ + webApplicationContext.getBrowser().updateRequestDetails(request); + + /* + * Call application requestStart before Application.init() is called + * (bypasses the limitation in TransactionListener) + */ + if (application instanceof HttpServletRequestListener) { + ((HttpServletRequestListener) application).onRequestStart( + request, response); + requestStarted = true; + } + + // Start the application if it's newly created + startApplication(request, application, webApplicationContext); + + /* + * Transaction starts. Call transaction listeners. Transaction end + * is called in the finally block below. + */ + webApplicationContext.startTransaction(application, request); + transactionStarted = true; + + /* Handle the request */ + if (requestType == RequestType.FILE_UPLOAD) { + // Root is resolved in communication manager + applicationManager.handleFileUpload(application, request, + response); + return; + } else if (requestType == RequestType.UIDL) { + Root root = application.getRootForRequest(request); + if (root == null) { + throw new ServletException(ERROR_NO_ROOT_FOUND); + } + // Handles AJAX UIDL requests + applicationManager.handleUidlRequest(request, response, + servletWrapper, root); + return; + } else if (requestType == RequestType.BROWSER_DETAILS) { + // Browser details - not related to a specific root + applicationManager.handleBrowserDetailsRequest(request, + response, application); + return; + } + + // Removes application if it has stopped (maybe by thread or + // transactionlistener) + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + if (applicationManager.handleApplicationRequest(request, response)) { + return; + } + // TODO Should return 404 error here and not do anything more + + } catch (final SessionExpiredException e) { + // Session has expired, notify user + handleServiceSessionExpired(request, response); + } catch (final GeneralSecurityException e) { + handleServiceSecurityException(request, response); + } catch (final Throwable e) { + handleServiceException(request, response, application, e); + } finally { + // Notifies transaction end + try { + if (transactionStarted) { + ((WebApplicationContext) application.getContext()) + .endTransaction(application, request); + + } + + } finally { + try { + if (requestStarted) { + ((HttpServletRequestListener) application) + .onRequestEnd(request, response); + } + } finally { + Root.setCurrent(null); + Application.setCurrent(null); + + HttpSession session = request.getSession(false); + if (session != null) { + requestTimer.stop(getApplicationContext(session)); + } + } + } + + } + } + + private WrappedHttpServletResponse createWrappedResponse( + HttpServletResponse response) { + WrappedHttpServletResponse wrappedResponse = new WrappedHttpServletResponse( + response, getDeploymentConfiguration()); + return wrappedResponse; + } + + /** + * Create a wrapped request for a http servlet request. This method can be + * overridden if the wrapped request should have special properties. + * + * @param request + * the original http servlet request + * @return a wrapped request for the original request + */ + protected WrappedHttpServletRequest createWrappedRequest( + HttpServletRequest request) { + return new WrappedHttpServletRequest(request, + getDeploymentConfiguration()); + } + + /** + * Gets a the deployment configuration for this servlet. + * + * @return the deployment configuration + */ + protected DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + /** + * Check that cookie support is enabled in the browser. Only checks UIDL + * requests. + * + * @param requestType + * Type of the request as returned by + * {@link #getRequestType(HttpServletRequest)} + * @param request + * The request from the browser + * @param response + * The response to which an error can be written + * @return false if cookies are disabled, true otherwise + * @throws IOException + */ + private boolean ensureCookiesEnabled(RequestType requestType, + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + if (requestType == RequestType.UIDL && !isRepaintAll(request)) { + // In all other but the first UIDL request a cookie should be + // returned by the browser. + // This can be removed if cookieless mode (#3228) is supported + if (request.getRequestedSessionId() == null) { + // User has cookies disabled + criticalNotification(request, response, getSystemMessages() + .getCookiesDisabledCaption(), getSystemMessages() + .getCookiesDisabledMessage(), null, getSystemMessages() + .getCookiesDisabledURL()); + return false; + } + } + return true; + } + + /** + * Send a notification to client's application. Used to notify client of + * critical errors, session expiration and more. Server has no knowledge of + * what application client refers to. + * + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @param caption + * the notification caption + * @param message + * to notification body + * @param details + * a detail message to show in addition to the message. Currently + * shown directly below the message but could be hidden behind a + * details drop down in the future. Mainly used to give + * additional information not necessarily useful to the end user. + * @param url + * url to load when the message is dismissed. Null will reload + * the current page. + * @throws IOException + * if the writing failed due to input/output error. + */ + protected void criticalNotification(WrappedHttpServletRequest request, + HttpServletResponse response, String caption, String message, + String details, String url) throws IOException { + + if (ServletPortletHelper.isUIDLRequest(request)) { + + if (caption != null) { + caption = "\"" + JsonPaintTarget.escapeJSON(caption) + "\""; + } + if (details != null) { + if (message == null) { + message = details; + } else { + message += "<br/><br/>" + details; + } + } + + if (message != null) { + message = "\"" + JsonPaintTarget.escapeJSON(message) + "\""; + } + if (url != null) { + url = "\"" + JsonPaintTarget.escapeJSON(url) + "\""; + } + + String output = "for(;;);[{\"changes\":[], \"meta\" : {" + + "\"appError\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}}, \"resources\": {}, \"locales\":[]}]"; + writeResponse(response, "application/json; charset=UTF-8", output); + } else { + // Create an HTML reponse with the error + String output = ""; + + if (url != null) { + output += "<a href=\"" + url + "\">"; + } + if (caption != null) { + output += "<b>" + caption + "</b><br/>"; + } + if (message != null) { + output += message; + output += "<br/><br/>"; + } + + if (details != null) { + output += details; + output += "<br/><br/>"; + } + if (url != null) { + output += "</a>"; + } + writeResponse(response, "text/html; charset=UTF-8", output); + + } + + } + + /** + * Writes the response in {@code output} using the contentType given in + * {@code contentType} to the provided {@link HttpServletResponse} + * + * @param response + * @param contentType + * @param output + * Output to write (UTF-8 encoded) + * @throws IOException + */ + private void writeResponse(HttpServletResponse response, + String contentType, String output) throws IOException { + response.setContentType(contentType); + final ServletOutputStream out = response.getOutputStream(); + // Set the response type + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print(output); + outWriter.flush(); + outWriter.close(); + out.flush(); + + } + + /** + * Returns the application instance to be used for the request. If an + * existing instance is not found a new one is created or null is returned + * to indicate that the application is not available. + * + * @param request + * @param requestType + * @return + * @throws MalformedURLException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws ServletException + * @throws SessionExpiredException + */ + private Application findApplicationInstance(HttpServletRequest request, + RequestType requestType) throws MalformedURLException, + ServletException, SessionExpiredException { + + boolean requestCanCreateApplication = requestCanCreateApplication( + request, requestType); + + /* Find an existing application for this request. */ + Application application = getExistingApplication(request, + requestCanCreateApplication); + + if (application != null) { + /* + * There is an existing application. We can use this as long as the + * user not specifically requested to close or restart it. + */ + + final boolean restartApplication = (request + .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null); + final boolean closeApplication = (request + .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null); + + if (restartApplication) { + closeApplication(application, request.getSession(false)); + return createApplication(request); + } else if (closeApplication) { + closeApplication(application, request.getSession(false)); + return null; + } else { + return application; + } + } + + // No existing application was found + + if (requestCanCreateApplication) { + /* + * If the request is such that it should create a new application if + * one as not found, we do that. + */ + return createApplication(request); + } else { + /* + * The application was not found and a new one should not be + * created. Assume the session has expired. + */ + throw new SessionExpiredException(); + } + + } + + /** + * Check if the request should create an application if an existing + * application is not found. + * + * @param request + * @param requestType + * @return true if an application should be created, false otherwise + */ + boolean requestCanCreateApplication(HttpServletRequest request, + RequestType requestType) { + if (requestType == RequestType.UIDL && isRepaintAll(request)) { + /* + * UIDL request contains valid repaintAll=1 event, the user probably + * wants to initiate a new application through a custom index.html + * without using the bootstrap page. + */ + return true; + + } else if (requestType == RequestType.OTHER) { + /* + * I.e URIs that are not application resources or static (theme) + * files. + */ + return true; + + } + + return false; + } + + /** + * Gets resource path using different implementations. Required to + * supporting different servlet container implementations (application + * servers). + * + * @param servletContext + * @param path + * the resource path. + * @return the resource path. + */ + protected static String getResourcePath(ServletContext servletContext, + String path) { + String resultPath = null; + resultPath = servletContext.getRealPath(path); + if (resultPath != null) { + return resultPath; + } else { + try { + final URL url = servletContext.getResource(path); + resultPath = url.getFile(); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.INFO, + "Could not find resource path " + path, e); + } + } + return resultPath; + } + + /** + * Creates a new application and registers it into WebApplicationContext + * (aka session). This is not meant to be overridden. Override + * getNewApplication to create the application instance in a custom way. + * + * @param request + * @return + * @throws ServletException + * @throws MalformedURLException + */ + private Application createApplication(HttpServletRequest request) + throws ServletException, MalformedURLException { + Application newApplication = getNewApplication(request); + + final WebApplicationContext context = getApplicationContext(request + .getSession()); + context.addApplication(newApplication); + + return newApplication; + } + + private void handleServiceException(WrappedHttpServletRequest request, + WrappedHttpServletResponse response, Application application, + Throwable e) throws IOException, ServletException { + // if this was an UIDL request, response UIDL back to client + if (getRequestType(request) == RequestType.UIDL) { + Application.SystemMessages ci = getSystemMessages(); + criticalNotification(request, response, + ci.getInternalErrorCaption(), ci.getInternalErrorMessage(), + null, ci.getInternalErrorURL()); + if (application != null) { + application.getErrorHandler() + .terminalError(new RequestError(e)); + } else { + throw new ServletException(e); + } + } else { + // Re-throw other exceptions + throw new ServletException(e); + } + + } + + /** + * A helper method to strip away characters that might somehow be used for + * XSS attacs. Leaves at least alphanumeric characters intact. Also removes + * eg. ( and ), so values should be safe in javascript too. + * + * @param themeName + * @return + */ + protected static String stripSpecialChars(String themeName) { + StringBuilder sb = new StringBuilder(); + char[] charArray = themeName.toCharArray(); + for (int i = 0; i < charArray.length; i++) { + char c = charArray[i]; + if (!CHAR_BLACKLIST.contains(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + private static final Collection<Character> CHAR_BLACKLIST = new HashSet<Character>( + Arrays.asList(new Character[] { '&', '"', '\'', '<', '>', '(', ')', + ';' })); + + /** + * Returns the default theme. Must never return null. + * + * @return + */ + public static String getDefaultTheme() { + return DEFAULT_THEME_NAME; + } + + void handleServiceSessionExpired(WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException, + ServletException { + + if (isOnUnloadRequest(request)) { + /* + * Request was an unload request (e.g. window close event) and the + * client expects no response if it fails. + */ + return; + } + + try { + Application.SystemMessages ci = getSystemMessages(); + if (getRequestType(request) != RequestType.UIDL) { + // 'plain' http req - e.g. browser reload; + // just go ahead redirect the browser + response.sendRedirect(ci.getSessionExpiredURL()); + } else { + /* + * Invalidate session (weird to have session if we're saying + * that it's expired, and worse: portal integration will fail + * since the session is not created by the portal. + * + * Session must be invalidated before criticalNotification as it + * commits the response. + */ + request.getSession().invalidate(); + + // send uidl redirect + criticalNotification(request, response, + ci.getSessionExpiredCaption(), + ci.getSessionExpiredMessage(), null, + ci.getSessionExpiredURL()); + + } + } catch (SystemMessageException ee) { + throw new ServletException(ee); + } + + } + + private void handleServiceSecurityException( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException, + ServletException { + if (isOnUnloadRequest(request)) { + /* + * Request was an unload request (e.g. window close event) and the + * client expects no response if it fails. + */ + return; + } + + try { + Application.SystemMessages ci = getSystemMessages(); + if (getRequestType(request) != RequestType.UIDL) { + // 'plain' http req - e.g. browser reload; + // just go ahead redirect the browser + response.sendRedirect(ci.getCommunicationErrorURL()); + } else { + // send uidl redirect + criticalNotification(request, response, + ci.getCommunicationErrorCaption(), + ci.getCommunicationErrorMessage(), + INVALID_SECURITY_KEY_MSG, ci.getCommunicationErrorURL()); + /* + * Invalidate session. Portal integration will fail otherwise + * since the session is not created by the portal. + */ + request.getSession().invalidate(); + } + } catch (SystemMessageException ee) { + throw new ServletException(ee); + } + + log("Invalid security key received from " + request.getRemoteHost()); + } + + /** + * Creates a new application for the given request. + * + * @param request + * the HTTP request. + * @return A new Application instance. + * @throws ServletException + */ + protected abstract Application getNewApplication(HttpServletRequest request) + throws ServletException; + + /** + * Starts the application if it is not already running. + * + * @param request + * @param application + * @param webApplicationContext + * @throws ServletException + * @throws MalformedURLException + */ + private void startApplication(HttpServletRequest request, + Application application, WebApplicationContext webApplicationContext) + throws ServletException, MalformedURLException { + + if (!application.isRunning()) { + // Create application + final URL applicationUrl = getApplicationUrl(request); + + // Initial locale comes from the request + Locale locale = request.getLocale(); + application.setLocale(locale); + application.start(new ApplicationStartEvent(applicationUrl, + getDeploymentConfiguration().getInitParameters(), + webApplicationContext, isProductionMode())); + addonContext.applicationStarted(application); + } + } + + /** + * Check if this is a request for a static resource and, if it is, serve the + * resource to the client. + * + * @param request + * @param response + * @return true if a file was served and the request has been handled, false + * otherwise. + * @throws IOException + * @throws ServletException + */ + private boolean serveStaticResources(HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + + // FIXME What does 10 refer to? + String pathInfo = request.getPathInfo(); + if (pathInfo == null || pathInfo.length() <= 10) { + return false; + } + + if ((request.getContextPath() != null) + && (request.getRequestURI().startsWith("/VAADIN/"))) { + serveStaticResourcesInVAADIN(request.getRequestURI(), request, + response); + return true; + } else if (request.getRequestURI().startsWith( + request.getContextPath() + "/VAADIN/")) { + serveStaticResourcesInVAADIN( + request.getRequestURI().substring( + request.getContextPath().length()), request, + response); + return true; + } + + return false; + } + + /** + * Serve resources from VAADIN directory. + * + * @param filename + * The filename to serve. Should always start with /VAADIN/. + * @param request + * @param response + * @throws IOException + * @throws ServletException + */ + private void serveStaticResourcesInVAADIN(String filename, + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + final ServletContext sc = getServletContext(); + URL resourceUrl = sc.getResource(filename); + if (resourceUrl == null) { + // try if requested file is found from classloader + + // strip leading "/" otherwise stream from JAR wont work + filename = filename.substring(1); + resourceUrl = getDeploymentConfiguration().getClassLoader() + .getResource(filename); + + if (resourceUrl == null) { + // cannot serve requested file + getLogger() + .info("Requested resource [" + + filename + + "] not found from filesystem or through class loader." + + " Add widgetset and/or theme JAR to your classpath or add files to WebContent/VAADIN folder."); + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // security check: do not permit navigation out of the VAADIN + // directory + if (!isAllowedVAADINResourceUrl(request, resourceUrl)) { + getLogger() + .info("Requested resource [" + + filename + + "] not accessible in the VAADIN directory or access to it is forbidden."); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + + // Find the modification timestamp + long lastModifiedTime = 0; + URLConnection connection = null; + try { + connection = resourceUrl.openConnection(); + lastModifiedTime = connection.getLastModified(); + // Remove milliseconds to avoid comparison problems (milliseconds + // are not returned by the browser in the "If-Modified-Since" + // header). + lastModifiedTime = lastModifiedTime - lastModifiedTime % 1000; + + if (browserHasNewestVersion(request, lastModifiedTime)) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + } catch (Exception e) { + // Failed to find out last modified timestamp. Continue without it. + getLogger() + .log(Level.FINEST, + "Failed to find out last modified timestamp. Continuing without it.", + e); + } finally { + if (connection instanceof URLConnection) { + try { + // Explicitly close the input stream to prevent it + // from remaining hanging + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700 + InputStream is = connection.getInputStream(); + if (is != null) { + is.close(); + } + } catch (IOException e) { + getLogger().log(Level.INFO, + "Error closing URLConnection input stream", e); + } + } + } + + // Set type mime type if we can determine it based on the filename + final String mimetype = sc.getMimeType(filename); + if (mimetype != null) { + response.setContentType(mimetype); + } + + // Provide modification timestamp to the browser if it is known. + if (lastModifiedTime > 0) { + response.setDateHeader("Last-Modified", lastModifiedTime); + /* + * The browser is allowed to cache for 1 hour without checking if + * the file has changed. This forces browsers to fetch a new version + * when the Vaadin version is updated. This will cause more requests + * to the servlet than without this but for high volume sites the + * static files should never be served through the servlet. The + * cache timeout can be configured by setting the resourceCacheTime + * parameter in web.xml + */ + response.setHeader("Cache-Control", + "max-age= " + String.valueOf(resourceCacheTime)); + } + + // Write the resource to the client. + final OutputStream os = response.getOutputStream(); + final byte buffer[] = new byte[DEFAULT_BUFFER_SIZE]; + int bytes; + InputStream is = resourceUrl.openStream(); + while ((bytes = is.read(buffer)) >= 0) { + os.write(buffer, 0, bytes); + } + is.close(); + } + + /** + * Check whether a URL obtained from a classloader refers to a valid static + * resource in the directory VAADIN. + * + * Warning: Overriding of this method is not recommended, but is possible to + * support non-default classloaders or servers that may produce URLs + * different from the normal ones. The method prototype may change in the + * future. Care should be taken not to expose class files or other resources + * outside the VAADIN directory if the method is overridden. + * + * @param request + * @param resourceUrl + * @return + * + * @since 6.6.7 + */ + protected boolean isAllowedVAADINResourceUrl(HttpServletRequest request, + URL resourceUrl) { + if ("jar".equals(resourceUrl.getProtocol())) { + // This branch is used for accessing resources directly from the + // Vaadin JAR in development environments and in similar cases. + + // Inside a JAR, a ".." would mean a real directory named ".." so + // using it in paths should just result in the file not being found. + // However, performing a check in case some servers or class loaders + // try to normalize the path by collapsing ".." before the class + // loader sees it. + + if (!resourceUrl.getPath().contains("!/VAADIN/")) { + getLogger().info( + "Blocked attempt to access a JAR entry not starting with /VAADIN/: " + + resourceUrl); + return false; + } + getLogger().fine( + "Accepted access to a JAR entry using a class loader: " + + resourceUrl); + return true; + } else { + // Some servers such as GlassFish extract files from JARs (file:) + // and e.g. JBoss 5+ use protocols vsf: and vfsfile: . + + // Check that the URL is in a VAADIN directory and does not contain + // "/../" + if (!resourceUrl.getPath().contains("/VAADIN/") + || resourceUrl.getPath().contains("/../")) { + getLogger().info( + "Blocked attempt to access file: " + resourceUrl); + return false; + } + getLogger().fine( + "Accepted access to a file using a class loader: " + + resourceUrl); + return true; + } + } + + /** + * Checks if the browser has an up to date cached version of requested + * resource. Currently the check is performed using the "If-Modified-Since" + * header. Could be expanded if needed. + * + * @param request + * The HttpServletRequest from the browser. + * @param resourceLastModifiedTimestamp + * The timestamp when the resource was last modified. 0 if the + * last modification time is unknown. + * @return true if the If-Modified-Since header tells the cached version in + * the browser is up to date, false otherwise + */ + private boolean browserHasNewestVersion(HttpServletRequest request, + long resourceLastModifiedTimestamp) { + if (resourceLastModifiedTimestamp < 1) { + // We do not know when it was modified so the browser cannot have an + // up-to-date version + return false; + } + /* + * The browser can request the resource conditionally using an + * If-Modified-Since header. Check this against the last modification + * time. + */ + try { + // If-Modified-Since represents the timestamp of the version cached + // in the browser + long headerIfModifiedSince = request + .getDateHeader("If-Modified-Since"); + + if (headerIfModifiedSince >= resourceLastModifiedTimestamp) { + // Browser has this an up-to-date version of the resource + return true; + } + } catch (Exception e) { + // Failed to parse header. Fail silently - the browser does not have + // an up-to-date version in its cache. + } + return false; + } + + protected enum RequestType { + FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE; + } + + protected RequestType getRequestType(WrappedHttpServletRequest request) { + if (ServletPortletHelper.isFileUploadRequest(request)) { + return RequestType.FILE_UPLOAD; + } else if (ServletPortletHelper.isConnectorResourceRequest(request)) { + return RequestType.CONNECTOR_RESOURCE; + } else if (isBrowserDetailsRequest(request)) { + return RequestType.BROWSER_DETAILS; + } else if (ServletPortletHelper.isUIDLRequest(request)) { + return RequestType.UIDL; + } else if (isStaticResourceRequest(request)) { + return RequestType.STATIC_FILE; + } else if (ServletPortletHelper.isApplicationResourceRequest(request)) { + return RequestType.APPLICATION_RESOURCE; + } + return RequestType.OTHER; + + } + + private static boolean isBrowserDetailsRequest(HttpServletRequest request) { + return "POST".equals(request.getMethod()) + && request.getParameter("browserDetails") != null; + } + + private boolean isStaticResourceRequest(HttpServletRequest request) { + String pathInfo = request.getPathInfo(); + if (pathInfo == null || pathInfo.length() <= 10) { + return false; + } + + if ((request.getContextPath() != null) + && (request.getRequestURI().startsWith("/VAADIN/"))) { + return true; + } else if (request.getRequestURI().startsWith( + request.getContextPath() + "/VAADIN/")) { + return true; + } + + return false; + } + + private boolean isOnUnloadRequest(HttpServletRequest request) { + return request.getParameter(ApplicationConnection.PARAM_UNLOADBURST) != null; + } + + /** + * Get system messages from the current application class + * + * @return + */ + protected SystemMessages getSystemMessages() { + Class<? extends Application> appCls = null; + try { + appCls = getApplicationClass(); + } catch (ClassNotFoundException e) { + // Previous comment claimed that this should never happen + throw new SystemMessageException(e); + } + return getSystemMessages(appCls); + } + + public static SystemMessages getSystemMessages( + Class<? extends Application> appCls) { + try { + if (appCls != null) { + Method m = appCls + .getMethod("getSystemMessages", (Class[]) null); + return (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } + } catch (SecurityException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (NoSuchMethodException e) { + // This is completely ok and should be silently ignored + } catch (IllegalArgumentException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (IllegalAccessException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (InvocationTargetException e) { + // This should never happen + throw new SystemMessageException(e); + } + return Application.getSystemMessages(); + } + + protected abstract Class<? extends Application> getApplicationClass() + throws ClassNotFoundException; + + /** + * Return the URL from where static files, e.g. the widgetset and the theme, + * are served. In a standard configuration the VAADIN folder inside the + * returned folder is what is used for widgetsets and themes. + * + * The returned folder is usually the same as the context path and + * independent of the application. + * + * @param request + * @return The location of static resources (should contain the VAADIN + * directory). Never ends with a slash (/). + */ + protected String getStaticFilesLocation(HttpServletRequest request) { + + return getWebApplicationsStaticFileLocation(request); + } + + /** + * The default method to fetch static files location (URL). This method does + * not check for request attribute {@value #REQUEST_VAADIN_STATIC_FILE_PATH} + * + * @param request + * @return + */ + private String getWebApplicationsStaticFileLocation( + HttpServletRequest request) { + String staticFileLocation; + // if property is defined in configurations, use that + staticFileLocation = getDeploymentConfiguration() + .getApplicationOrSystemProperty(PARAMETER_VAADIN_RESOURCES, + null); + if (staticFileLocation != null) { + return staticFileLocation; + } + + // the last (but most common) option is to generate default location + // from request + + // if context is specified add it to widgetsetUrl + String ctxPath = request.getContextPath(); + + // FIXME: ctxPath.length() == 0 condition is probably unnecessary and + // might even be wrong. + + if (ctxPath.length() == 0 + && request.getAttribute("javax.servlet.include.context_path") != null) { + // include request (e.g portlet), get context path from + // attribute + ctxPath = (String) request + .getAttribute("javax.servlet.include.context_path"); + } + + // Remove heading and trailing slashes from the context path + ctxPath = removeHeadingOrTrailing(ctxPath, "/"); + + if (ctxPath.equals("")) { + return ""; + } else { + return "/" + ctxPath; + } + } + + /** + * Remove any heading or trailing "what" from the "string". + * + * @param string + * @param what + * @return + */ + private static String removeHeadingOrTrailing(String string, String what) { + while (string.startsWith(what)) { + string = string.substring(1); + } + + while (string.endsWith(what)) { + string = string.substring(0, string.length() - 1); + } + + return string; + } + + /** + * Write a redirect response to the main page of the application. + * + * @param request + * @param response + * @throws IOException + * if sending the redirect fails due to an input/output error or + * a bad application URL + */ + private void redirectToApplication(HttpServletRequest request, + HttpServletResponse response) throws IOException { + String applicationUrl = getApplicationUrl(request).toExternalForm(); + response.sendRedirect(response.encodeRedirectURL(applicationUrl)); + } + + /** + * Gets the current application URL from request. + * + * @param request + * the HTTP request. + * @throws MalformedURLException + * if the application is denied access to the persistent data + * store represented by the given URL. + */ + protected URL getApplicationUrl(HttpServletRequest request) + throws MalformedURLException { + final URL reqURL = new URL( + (request.isSecure() ? "https://" : "http://") + + request.getServerName() + + ((request.isSecure() && request.getServerPort() == 443) + || (!request.isSecure() && request + .getServerPort() == 80) ? "" : ":" + + request.getServerPort()) + + request.getRequestURI()); + String servletPath = ""; + if (request.getAttribute("javax.servlet.include.servlet_path") != null) { + // this is an include request + servletPath = request.getAttribute( + "javax.servlet.include.context_path").toString() + + request + .getAttribute("javax.servlet.include.servlet_path"); + + } else { + servletPath = request.getContextPath() + request.getServletPath(); + } + + if (servletPath.length() == 0 + || servletPath.charAt(servletPath.length() - 1) != '/') { + servletPath = servletPath + "/"; + } + URL u = new URL(reqURL, servletPath); + return u; + } + + /** + * Gets the existing application for given request. Looks for application + * instance for given request based on the requested URL. + * + * @param request + * the HTTP request. + * @param allowSessionCreation + * true if a session should be created if no session exists, + * false if no session should be created + * @return Application instance, or null if the URL does not map to valid + * application. + * @throws MalformedURLException + * if the application is denied access to the persistent data + * store represented by the given URL. + * @throws IllegalAccessException + * @throws InstantiationException + * @throws SessionExpiredException + */ + protected Application getExistingApplication(HttpServletRequest request, + boolean allowSessionCreation) throws MalformedURLException, + SessionExpiredException { + + // Ensures that the session is still valid + final HttpSession session = request.getSession(allowSessionCreation); + if (session == null) { + throw new SessionExpiredException(); + } + + WebApplicationContext context = getApplicationContext(session); + + // Gets application list for the session. + final Collection<Application> applications = context.getApplications(); + + // Search for the application (using the application URI) from the list + for (final Iterator<Application> i = applications.iterator(); i + .hasNext();) { + final Application sessionApplication = i.next(); + final String sessionApplicationPath = sessionApplication.getURL() + .getPath(); + String requestApplicationPath = getApplicationUrl(request) + .getPath(); + + if (requestApplicationPath.equals(sessionApplicationPath)) { + // Found a running application + if (sessionApplication.isRunning()) { + return sessionApplication; + } + // Application has stopped, so remove it before creating a new + // application + getApplicationContext(session).removeApplication( + sessionApplication); + break; + } + } + + // Existing application not found + return null; + } + + /** + * Ends the application. + * + * @param request + * the HTTP request. + * @param response + * the HTTP response to write to. + * @param application + * the application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(HttpServletRequest request, + HttpServletResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + + final HttpSession session = request.getSession(); + if (session != null) { + getApplicationContext(session).removeApplication(application); + } + + response.sendRedirect(response.encodeRedirectURL(logoutUrl)); + } + + /** + * Returns the path info; note that this _can_ be different than + * request.getPathInfo(). Examples where this might be useful: + * <ul> + * <li>An application runner servlet that runs different Vaadin applications + * based on an identifier.</li> + * <li>Providing a REST interface in the context root, while serving a + * Vaadin UI on a sub-URI using only one servlet (e.g. REST on + * http://example.com/foo, UI on http://example.com/foo/vaadin)</li> + * + * @param request + * @return + */ + protected String getRequestPathInfo(HttpServletRequest request) { + return request.getPathInfo(); + } + + /** + * Gets relative location of a theme resource. + * + * @param theme + * the Theme name. + * @param resource + * the Theme resource. + * @return External URI specifying the resource + */ + public String getResourceLocation(String theme, ThemeResource resource) { + + if (resourcePath == null) { + return resource.getResourceId(); + } + return resourcePath + theme + "/" + resource.getResourceId(); + } + + private boolean isRepaintAll(HttpServletRequest request) { + return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null) + && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1")); + } + + private void closeApplication(Application application, HttpSession session) { + if (application == null) { + return; + } + + application.close(); + if (session != null) { + WebApplicationContext context = getApplicationContext(session); + context.removeApplication(application); + } + } + + /** + * + * Gets the application context from an HttpSession. If no context is + * currently stored in a session a new context is created and stored in the + * session. + * + * @param session + * the HTTP session. + * @return the application context for HttpSession. + */ + protected WebApplicationContext getApplicationContext(HttpSession session) { + /* + * TODO the ApplicationContext.getApplicationContext() should be removed + * and logic moved here. Now overriding context type is possible, but + * the whole creation logic should be here. MT 1101 + */ + return WebApplicationContext.getApplicationContext(session); + } + + public class RequestError implements Terminal.ErrorEvent, Serializable { + + private final Throwable throwable; + + public RequestError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Override this method if you need to use a specialized communicaiton + * mananger implementation. + * + * @deprecated Instead of overriding this method, override + * {@link WebApplicationContext} implementation via + * {@link AbstractApplicationServlet#getApplicationContext(HttpSession)} + * method and in that customized implementation return your + * CommunicationManager in + * {@link WebApplicationContext#getApplicationManager(Application, AbstractApplicationServlet)} + * method. + * + * @param application + * @return + */ + @Deprecated + public CommunicationManager createCommunicationManager( + Application application) { + return new CommunicationManager(application); + } + + /** + * Escapes characters to html entities. An exception is made for some + * "safe characters" to keep the text somewhat readable. + * + * @param unsafe + * @return a safe string to be added inside an html tag + */ + public static final String safeEscapeForHtml(String unsafe) { + if (null == unsafe) { + return null; + } + StringBuilder safe = new StringBuilder(); + char[] charArray = unsafe.toCharArray(); + for (int i = 0; i < charArray.length; i++) { + char c = charArray[i]; + if (isSafe(c)) { + safe.append(c); + } else { + safe.append("&#"); + safe.append((int) c); + safe.append(";"); + } + } + + return safe.toString(); + } + + private static boolean isSafe(char c) { + return // + c > 47 && c < 58 || // alphanum + c > 64 && c < 91 || // A-Z + c > 96 && c < 123 // a-z + ; + } + + private static final Logger getLogger() { + return Logger.getLogger(AbstractApplicationServlet.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java new file mode 100644 index 0000000000..ba1b3cadb6 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -0,0 +1,2790 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.text.CharacterIterator; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; +import com.vaadin.Application.SystemMessages; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.Version; +import com.vaadin.annotations.JavaScript; +import com.vaadin.annotations.StyleSheet; +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.StreamVariable; +import com.vaadin.terminal.StreamVariable.StreamingEndEvent; +import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; +import com.vaadin.terminal.Terminal.ErrorEvent; +import com.vaadin.terminal.Terminal.ErrorListener; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; +import com.vaadin.terminal.gwt.server.RpcManager.RpcInvocationException; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.AbstractField; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.Root; +import com.vaadin.ui.Window; + +/** + * This is a common base class for the server-side implementations of the + * communication system between the client code (compiled with GWT into + * JavaScript) and the server side components. Its client side counterpart is + * {@link ApplicationConnection}. + * + * TODO Document better! + */ +@SuppressWarnings("serial") +public abstract class AbstractCommunicationManager implements Serializable { + + private static final String DASHDASH = "--"; + + private static final RequestHandler APP_RESOURCE_HANDLER = new ApplicationResourceHandler(); + + private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler(); + + /** + * TODO Document me! + * + * @author peholmst + */ + public interface Callback extends Serializable { + + public void criticalNotification(WrappedRequest request, + WrappedResponse response, String cap, String msg, + String details, String outOfSyncURL) throws IOException; + } + + static class UploadInterruptedException extends Exception { + public UploadInterruptedException() { + super("Upload interrupted by other thread"); + } + } + + private static String GET_PARAM_REPAINT_ALL = "repaintAll"; + + // flag used in the request to indicate that the security token should be + // written to the response + private static final String WRITE_SECURITY_TOKEN_FLAG = "writeSecurityToken"; + + /* Variable records indexes */ + public static final char VAR_BURST_SEPARATOR = '\u001d'; + + public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + + private final HashMap<Integer, ClientCache> rootToClientCache = new HashMap<Integer, ClientCache>(); + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + /* Same as in apache commons file upload library that was previously used. */ + private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; + + private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; + + /** + * The application this communication manager is used for + */ + private final Application application; + + private List<String> locales; + + private int pendingLocalesIndex; + + private int timeoutInterval = -1; + + private DragAndDropService dragAndDropService; + + private String requestThemeName; + + private int maxInactiveInterval; + + private Connector highlightedConnector; + + private Map<String, Class<?>> connectorResourceContexts = new HashMap<String, Class<?>>(); + + private Map<String, Map<String, StreamVariable>> pidToNameToStreamVariable; + + private Map<StreamVariable, String> streamVariableToSeckey; + + /** + * TODO New constructor - document me! + * + * @param application + */ + public AbstractCommunicationManager(Application application) { + this.application = application; + application.addRequestHandler(getBootstrapHandler()); + application.addRequestHandler(APP_RESOURCE_HANDLER); + application.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER); + requireLocale(application.getLocale().toString()); + } + + protected Application getApplication() { + return application; + } + + private static final int LF = "\n".getBytes()[0]; + + private static final String CRLF = "\r\n"; + + private static final String UTF8 = "UTF8"; + + private static final String GET_PARAM_HIGHLIGHT_COMPONENT = "highlightComponent"; + + private static String readLine(InputStream stream) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + int readByte = stream.read(); + while (readByte != LF) { + bout.write(readByte); + readByte = stream.read(); + } + byte[] bytes = bout.toByteArray(); + return new String(bytes, 0, bytes.length - 1, UTF8); + } + + /** + * Method used to stream content from a multipart request (either from + * servlet or portlet request) to given StreamVariable + * + * + * @param request + * @param response + * @param streamVariable + * @param owner + * @param boundary + * @throws IOException + */ + protected void doHandleSimpleMultipartFileUpload(WrappedRequest request, + WrappedResponse response, StreamVariable streamVariable, + String variableName, ClientConnector owner, String boundary) + throws IOException { + // multipart parsing, supports only one file for request, but that is + // fine for our current terminal + + final InputStream inputStream = request.getInputStream(); + + int contentLength = request.getContentLength(); + + boolean atStart = false; + boolean firstFileFieldFound = false; + + String rawfilename = "unknown"; + String rawMimeType = "application/octet-stream"; + + /* + * Read the stream until the actual file starts (empty line). Read + * filename and content type from multipart headers. + */ + while (!atStart) { + String readLine = readLine(inputStream); + contentLength -= (readLine.length() + 2); + if (readLine.startsWith("Content-Disposition:") + && readLine.indexOf("filename=") > 0) { + rawfilename = readLine.replaceAll(".*filename=", ""); + String parenthesis = rawfilename.substring(0, 1); + rawfilename = rawfilename.substring(1); + rawfilename = rawfilename.substring(0, + rawfilename.indexOf(parenthesis)); + firstFileFieldFound = true; + } else if (firstFileFieldFound && readLine.equals("")) { + atStart = true; + } else if (readLine.startsWith("Content-Type")) { + rawMimeType = readLine.split(": ")[1]; + } + } + + contentLength -= (boundary.length() + CRLF.length() + 2 + * DASHDASH.length() + 2); // 2 == CRLF + + /* + * Reads bytes from the underlying stream. Compares the read bytes to + * the boundary string and returns -1 if met. + * + * The matching happens so that if the read byte equals to the first + * char of boundary string, the stream goes to "buffering mode". In + * buffering mode bytes are read until the character does not match the + * corresponding from boundary string or the full boundary string is + * found. + * + * Note, if this is someday needed elsewhere, don't shoot yourself to + * foot and split to a top level helper class. + */ + InputStream simpleMultiPartReader = new SimpleMultiPartInputStream( + inputStream, boundary); + + /* + * Should report only the filename even if the browser sends the path + */ + final String filename = removePath(rawfilename); + final String mimeType = rawMimeType; + + try { + // TODO Shouldn't this check connectorEnabled? + if (owner == null) { + throw new UploadException( + "File upload ignored because the connector for the stream variable was not found"); + } + if (owner instanceof Component) { + if (((Component) owner).isReadOnly()) { + throw new UploadException( + "Warning: file upload ignored because the componente was read-only"); + } + } + boolean forgetVariable = streamToReceiver(simpleMultiPartReader, + streamVariable, filename, mimeType, contentLength); + if (forgetVariable) { + cleanStreamVariable(owner, variableName); + } + } catch (Exception e) { + synchronized (application) { + handleChangeVariablesError(application, (Component) owner, e, + new HashMap<String, Object>()); + } + } + sendUploadResponse(request, response); + + } + + /** + * Used to stream plain file post (aka XHR2.post(File)) + * + * @param request + * @param response + * @param streamVariable + * @param owner + * @param contentLength + * @throws IOException + */ + protected void doHandleXhrFilePost(WrappedRequest request, + WrappedResponse response, StreamVariable streamVariable, + String variableName, ClientConnector owner, int contentLength) + throws IOException { + + // These are unknown in filexhr ATM, maybe add to Accept header that + // is accessible in portlets + final String filename = "unknown"; + final String mimeType = filename; + final InputStream stream = request.getInputStream(); + try { + /* + * safe cast as in GWT terminal all variable owners are expected to + * be components. + */ + Component component = (Component) owner; + if (component.isReadOnly()) { + throw new UploadException( + "Warning: file upload ignored because the component was read-only"); + } + boolean forgetVariable = streamToReceiver(stream, streamVariable, + filename, mimeType, contentLength); + if (forgetVariable) { + cleanStreamVariable(owner, variableName); + } + } catch (Exception e) { + synchronized (application) { + handleChangeVariablesError(application, (Component) owner, e, + new HashMap<String, Object>()); + } + } + sendUploadResponse(request, response); + } + + /** + * @param in + * @param streamVariable + * @param filename + * @param type + * @param contentLength + * @return true if the streamvariable has informed that the terminal can + * forget this variable + * @throws UploadException + */ + protected final boolean streamToReceiver(final InputStream in, + StreamVariable streamVariable, String filename, String type, + int contentLength) throws UploadException { + if (streamVariable == null) { + throw new IllegalStateException( + "StreamVariable for the post not found"); + } + + final Application application = getApplication(); + + OutputStream out = null; + int totalBytes = 0; + StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( + filename, type, contentLength); + try { + boolean listenProgress; + synchronized (application) { + streamVariable.streamingStarted(startedEvent); + out = streamVariable.getOutputStream(); + listenProgress = streamVariable.listenProgress(); + } + + // Gets the output target stream + if (out == null) { + throw new NoOutputStreamException(); + } + + if (null == in) { + // No file, for instance non-existent filename in html upload + throw new NoInputStreamException(); + } + + final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE]; + int bytesReadToBuffer = 0; + while ((bytesReadToBuffer = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesReadToBuffer); + totalBytes += bytesReadToBuffer; + if (listenProgress) { + // update progress if listener set and contentLength + // received + synchronized (application) { + StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( + filename, type, contentLength, totalBytes); + streamVariable.onProgress(progressEvent); + } + } + if (streamVariable.isInterrupted()) { + throw new UploadInterruptedException(); + } + } + + // upload successful + out.close(); + StreamingEndEvent event = new StreamingEndEventImpl(filename, type, + totalBytes); + synchronized (application) { + streamVariable.streamingFinished(event); + } + + } catch (UploadInterruptedException e) { + // Download interrupted by application code + tryToCloseStream(out); + StreamingErrorEvent event = new StreamingErrorEventImpl(filename, + type, contentLength, totalBytes, e); + synchronized (application) { + streamVariable.streamingFailed(event); + } + // Note, we are not throwing interrupted exception forward as it is + // not a terminal level error like all other exception. + } catch (final Exception e) { + tryToCloseStream(out); + synchronized (application) { + StreamingErrorEvent event = new StreamingErrorEventImpl( + filename, type, contentLength, totalBytes, e); + synchronized (application) { + streamVariable.streamingFailed(event); + } + // throw exception for terminal to be handled (to be passed to + // terminalErrorHandler) + throw new UploadException(e); + } + } + return startedEvent.isDisposed(); + } + + static void tryToCloseStream(OutputStream out) { + try { + // try to close output stream (e.g. file handle) + if (out != null) { + out.close(); + } + } catch (IOException e1) { + // NOP + } + } + + /** + * Removes any possible path information from the filename and returns the + * filename. Separators / and \\ are used. + * + * @param name + * @return + */ + private static String removePath(String filename) { + if (filename != null) { + filename = filename.replaceAll("^.*[/\\\\]", ""); + } + + return filename; + } + + /** + * TODO document + * + * @param request + * @param response + * @throws IOException + */ + protected void sendUploadResponse(WrappedRequest request, + WrappedResponse response) throws IOException { + response.setContentType("text/html"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>download handled</body></html>"); + outWriter.flush(); + out.close(); + } + + /** + * Internally process a UIDL request from the client. + * + * This method calls + * {@link #handleVariables(WrappedRequest, WrappedResponse, Callback, Application, Root)} + * to process any changes to variables by the client and then repaints + * affected components using {@link #paintAfterVariableChanges()}. + * + * Also, some cleanup is done when a request arrives for an application that + * has already been closed. + * + * The method handleUidlRequest(...) in subclasses should call this method. + * + * TODO better documentation + * + * @param request + * @param response + * @param callback + * @param root + * target window for the UIDL request, can be null if target not + * found + * @throws IOException + * @throws InvalidUIDLSecurityKeyException + * @throws JSONException + */ + public void handleUidlRequest(WrappedRequest request, + WrappedResponse response, Callback callback, Root root) + throws IOException, InvalidUIDLSecurityKeyException, JSONException { + + checkWidgetsetVersion(request); + requestThemeName = request.getParameter("theme"); + maxInactiveInterval = request.getSessionMaxInactiveInterval(); + // repaint requested or session has timed out and new one is created + boolean repaintAll; + final OutputStream out; + + repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null); + // || (request.getSession().isNew()); FIXME What the h*ll is this?? + out = response.getOutputStream(); + + boolean analyzeLayouts = false; + if (repaintAll) { + // analyzing can be done only with repaintAll + analyzeLayouts = (request.getParameter(GET_PARAM_ANALYZE_LAYOUTS) != null); + + if (request.getParameter(GET_PARAM_HIGHLIGHT_COMPONENT) != null) { + String pid = request + .getParameter(GET_PARAM_HIGHLIGHT_COMPONENT); + highlightedConnector = root.getConnectorTracker().getConnector( + pid); + highlightConnector(highlightedConnector); + } + } + + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + // The rest of the process is synchronized with the application + // in order to guarantee that no parallel variable handling is + // made + synchronized (application) { + + // Finds the window within the application + if (application.isRunning()) { + // Returns if no window found + if (root == null) { + // This should not happen, no windows exists but + // application is still open. + getLogger().warning("Could not get root for application"); + return; + } + } else { + // application has been closed + endApplication(request, response, application); + return; + } + + // Change all variables based on request parameters + if (!handleVariables(request, response, callback, application, root)) { + + // var inconsistency; the client is probably out-of-sync + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod( + "getSystemMessages", (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } catch (Exception e2) { + // FIXME: Handle exception + // Not critical, but something is still wrong; print + // stacktrace + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e2); + } + if (ci != null) { + String msg = ci.getOutOfSyncMessage(); + String cap = ci.getOutOfSyncCaption(); + if (msg != null || cap != null) { + callback.criticalNotification(request, response, cap, + msg, null, ci.getOutOfSyncURL()); + // will reload page after this + return; + } + } + // No message to show, let's just repaint all. + repaintAll = true; + } + + paintAfterVariableChanges(request, response, callback, repaintAll, + outWriter, root, analyzeLayouts); + postPaint(root); + } + + outWriter.close(); + requestThemeName = null; + } + + /** + * Checks that the version reported by the client (widgetset) matches that + * of the server. + * + * @param request + */ + private void checkWidgetsetVersion(WrappedRequest request) { + String widgetsetVersion = request.getParameter("wsver"); + if (widgetsetVersion == null) { + // Only check when the widgetset version is reported. It is reported + // in the first UIDL request (not the initial request as it is a + // plain GET /) + return; + } + + if (!Version.getFullVersion().equals(widgetsetVersion)) { + getLogger().warning( + String.format(Constants.WIDGETSET_MISMATCH_INFO, + Version.getFullVersion(), widgetsetVersion)); + } + } + + /** + * Method called after the paint phase while still being synchronized on the + * application + * + * @param root + * + */ + protected void postPaint(Root root) { + // Remove connectors that have been detached from the application during + // handling of the request + root.getConnectorTracker().cleanConnectorMap(); + + if (pidToNameToStreamVariable != null) { + Iterator<String> iterator = pidToNameToStreamVariable.keySet() + .iterator(); + while (iterator.hasNext()) { + String connectorId = iterator.next(); + if (root.getConnectorTracker().getConnector(connectorId) == null) { + // Owner is no longer attached to the application + Map<String, StreamVariable> removed = pidToNameToStreamVariable + .get(connectorId); + for (String key : removed.keySet()) { + streamVariableToSeckey.remove(removed.get(key)); + } + iterator.remove(); + } + } + } + } + + protected void highlightConnector(Connector highlightedConnector) { + StringBuilder sb = new StringBuilder(); + sb.append("*** Debug details of a component: *** \n"); + sb.append("Type: "); + sb.append(highlightedConnector.getClass().getName()); + if (highlightedConnector instanceof AbstractComponent) { + AbstractComponent component = (AbstractComponent) highlightedConnector; + sb.append("\nId:"); + sb.append(highlightedConnector.getConnectorId()); + if (component.getCaption() != null) { + sb.append("\nCaption:"); + sb.append(component.getCaption()); + } + + printHighlightedComponentHierarchy(sb, component); + } + getLogger().info(sb.toString()); + } + + protected void printHighlightedComponentHierarchy(StringBuilder sb, + AbstractComponent component) { + LinkedList<Component> h = new LinkedList<Component>(); + h.add(component); + Component parent = component.getParent(); + while (parent != null) { + h.addFirst(parent); + parent = parent.getParent(); + } + + sb.append("\nComponent hierarchy:\n"); + Application application2 = component.getApplication(); + sb.append(application2.getClass().getName()); + sb.append("."); + sb.append(application2.getClass().getSimpleName()); + sb.append("("); + sb.append(application2.getClass().getSimpleName()); + sb.append(".java"); + sb.append(":1)"); + int l = 1; + for (Component component2 : h) { + sb.append("\n"); + for (int i = 0; i < l; i++) { + sb.append(" "); + } + l++; + Class<? extends Component> componentClass = component2.getClass(); + Class<?> topClass = componentClass; + while (topClass.getEnclosingClass() != null) { + topClass = topClass.getEnclosingClass(); + } + sb.append(componentClass.getName()); + sb.append("."); + sb.append(componentClass.getSimpleName()); + sb.append("("); + sb.append(topClass.getSimpleName()); + sb.append(".java:1)"); + } + } + + /** + * TODO document + * + * @param request + * @param response + * @param callback + * @param repaintAll + * @param outWriter + * @param window + * @param analyzeLayouts + * @throws PaintException + * @throws IOException + * @throws JSONException + */ + private void paintAfterVariableChanges(WrappedRequest request, + WrappedResponse response, Callback callback, boolean repaintAll, + final PrintWriter outWriter, Root root, boolean analyzeLayouts) + throws PaintException, IOException, JSONException { + + // Removes application if it has stopped during variable changes + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + openJsonMessage(outWriter, response); + + // security key + Object writeSecurityTokenFlag = request + .getAttribute(WRITE_SECURITY_TOKEN_FLAG); + + if (writeSecurityTokenFlag != null) { + outWriter.print(getSecurityKeyUIDL(request)); + } + + writeUidlResponse(request, repaintAll, outWriter, root, analyzeLayouts); + + closeJsonMessage(outWriter); + + outWriter.close(); + + } + + /** + * Gets the security key (and generates one if needed) as UIDL. + * + * @param request + * @return the security key UIDL or "" if the feature is turned off + */ + public String getSecurityKeyUIDL(WrappedRequest request) { + final String seckey = getSecurityKey(request); + if (seckey != null) { + return "\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID + + "\":\"" + seckey + "\","; + } else { + return ""; + } + } + + /** + * Gets the security key (and generates one if needed). + * + * @param request + * @return the security key + */ + protected String getSecurityKey(WrappedRequest request) { + String seckey = null; + seckey = (String) request + .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); + if (seckey == null) { + seckey = UUID.randomUUID().toString(); + request.setSessionAttribute( + ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey); + } + + return seckey; + } + + @SuppressWarnings("unchecked") + public void writeUidlResponse(WrappedRequest request, boolean repaintAll, + final PrintWriter outWriter, Root root, boolean analyzeLayouts) + throws PaintException, JSONException { + ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>(); + Application application = root.getApplication(); + // Paints components + ConnectorTracker rootConnectorTracker = root.getConnectorTracker(); + getLogger().log(Level.FINE, "* Creating response to client"); + if (repaintAll) { + getClientCache(root).clear(); + rootConnectorTracker.markAllConnectorsDirty(); + + // Reset sent locales + locales = null; + requireLocale(application.getLocale().toString()); + } + + dirtyVisibleConnectors + .addAll(getDirtyVisibleConnectors(rootConnectorTracker)); + + getLogger().log( + Level.FINE, + "Found " + dirtyVisibleConnectors.size() + + " dirty connectors to paint"); + for (ClientConnector connector : dirtyVisibleConnectors) { + if (connector instanceof Component) { + ((Component) connector).updateState(); + } + } + rootConnectorTracker.markAllConnectorsClean(); + + outWriter.print("\"changes\":["); + + List<InvalidLayout> invalidComponentRelativeSizes = null; + + JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, + !repaintAll); + legacyPaint(paintTarget, dirtyVisibleConnectors); + + if (analyzeLayouts) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes(root.getContent(), null, + null); + + // Also check any existing subwindows + if (root.getWindows() != null) { + for (Window subWindow : root.getWindows()) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes( + subWindow.getContent(), + invalidComponentRelativeSizes, null); + } + } + } + + paintTarget.close(); + outWriter.print("], "); // close changes + + // send shared state to client + + // for now, send the complete state of all modified and new + // components + + // Ideally, all this would be sent before "changes", but that causes + // complications with legacy components that create sub-components + // in their paint phase. Nevertheless, this will be processed on the + // client after component creation but before legacy UIDL + // processing. + JSONObject sharedStates = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + SharedState state = connector.getState(); + if (null != state) { + // encode and send shared state + try { + Class<? extends SharedState> stateType = connector + .getStateType(); + SharedState referenceState = null; + if (repaintAll) { + // Use an empty state object as reference for full + // repaints + try { + referenceState = stateType.newInstance(); + } catch (Exception e) { + getLogger().log( + Level.WARNING, + "Error creating reference object for state of type " + + stateType.getName()); + } + } + Object stateJson = JsonCodec.encode(state, referenceState, + stateType, root.getConnectorTracker()); + + sharedStates.put(connector.getConnectorId(), stateJson); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize shared state for connector " + + connector.getClass().getName() + " (" + + connector.getConnectorId() + "): " + + e.getMessage(), e); + } + } + } + outWriter.print("\"state\":"); + outWriter.append(sharedStates.toString()); + outWriter.print(", "); // close states + + // TODO This should be optimized. The type only needs to be + // sent once for each connector id + on refresh. Use the same cache as + // widget mapping + + JSONObject connectorTypes = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorType = paintTarget.getTag(connector); + try { + connectorTypes.put(connector.getConnectorId(), connectorType); + } catch (JSONException e) { + throw new PaintException( + "Failed to send connector type for connector " + + connector.getConnectorId() + ": " + + e.getMessage(), e); + } + } + outWriter.print("\"types\":"); + outWriter.append(connectorTypes.toString()); + outWriter.print(", "); // close states + + // Send update hierarchy information to the client. + + // This could be optimized aswell to send only info if hierarchy has + // actually changed. Much like with the shared state. Note though + // that an empty hierarchy is information aswell (e.g. change from 1 + // child to 0 children) + + outWriter.print("\"hierarchy\":"); + + JSONObject hierarchyInfo = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorId = connector.getConnectorId(); + JSONArray children = new JSONArray(); + + for (ClientConnector child : AbstractClientConnector + .getAllChildrenIterable(connector)) { + if (isVisible(child)) { + children.put(child.getConnectorId()); + } + } + try { + hierarchyInfo.put(connectorId, children); + } catch (JSONException e) { + throw new PaintException( + "Failed to send hierarchy information about " + + connectorId + " to the client: " + + e.getMessage(), e); + } + } + outWriter.append(hierarchyInfo.toString()); + outWriter.print(", "); // close hierarchy + + // send server to client RPC calls for components in the root, in call + // order + + // collect RPC calls from components in the root in the order in + // which they were performed, remove the calls from components + + LinkedList<ClientConnector> rpcPendingQueue = new LinkedList<ClientConnector>( + dirtyVisibleConnectors); + List<ClientMethodInvocation> pendingInvocations = collectPendingRpcCalls(dirtyVisibleConnectors); + + JSONArray rpcCalls = new JSONArray(); + for (ClientMethodInvocation invocation : pendingInvocations) { + // add invocation to rpcCalls + try { + JSONArray invocationJson = new JSONArray(); + invocationJson.put(invocation.getConnector().getConnectorId()); + invocationJson.put(invocation.getInterfaceName()); + invocationJson.put(invocation.getMethodName()); + JSONArray paramJson = new JSONArray(); + for (int i = 0; i < invocation.getParameterTypes().length; ++i) { + Type parameterType = invocation.getParameterTypes()[i]; + Object referenceParameter = null; + // TODO Use default values for RPC parameter types + // if (!JsonCodec.isInternalType(parameterType)) { + // try { + // referenceParameter = parameterType.newInstance(); + // } catch (Exception e) { + // logger.log(Level.WARNING, + // "Error creating reference object for parameter of type " + // + parameterType.getName()); + // } + // } + paramJson.put(JsonCodec.encode( + invocation.getParameters()[i], referenceParameter, + parameterType, root.getConnectorTracker())); + } + invocationJson.put(paramJson); + rpcCalls.put(invocationJson); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize RPC method call parameters for connector " + + invocation.getConnector().getConnectorId() + + " method " + invocation.getInterfaceName() + + "." + invocation.getMethodName() + ": " + + e.getMessage(), e); + } + + } + + if (rpcCalls.length() > 0) { + outWriter.print("\"rpc\" : "); + outWriter.append(rpcCalls.toString()); + outWriter.print(", "); // close rpc + } + + outWriter.print("\"meta\" : {"); + boolean metaOpen = false; + + if (repaintAll) { + metaOpen = true; + outWriter.write("\"repaintAll\":true"); + if (analyzeLayouts) { + outWriter.write(", \"invalidLayouts\":"); + outWriter.write("["); + if (invalidComponentRelativeSizes != null) { + boolean first = true; + for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { + if (!first) { + outWriter.write(","); + } else { + first = false; + } + invalidLayout.reportErrors(outWriter, this, System.err); + } + } + outWriter.write("]"); + } + if (highlightedConnector != null) { + outWriter.write(", \"hl\":\""); + outWriter.write(highlightedConnector.getConnectorId()); + outWriter.write("\""); + highlightedConnector = null; + } + } + + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod("getSystemMessages", + (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, (Object[]) null); + } catch (NoSuchMethodException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } catch (IllegalArgumentException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } catch (IllegalAccessException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } catch (InvocationTargetException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } + + // meta instruction for client to enable auto-forward to + // sessionExpiredURL after timer expires. + if (ci != null && ci.getSessionExpiredMessage() == null + && ci.getSessionExpiredCaption() == null + && ci.isSessionExpiredNotificationEnabled()) { + int newTimeoutInterval = getTimeoutInterval(); + if (repaintAll || (timeoutInterval != newTimeoutInterval)) { + String escapedURL = ci.getSessionExpiredURL() == null ? "" : ci + .getSessionExpiredURL().replace("/", "\\/"); + if (metaOpen) { + outWriter.write(","); + } + outWriter.write("\"timedRedirect\":{\"interval\":" + + (newTimeoutInterval + 15) + ",\"url\":\"" + + escapedURL + "\"}"); + metaOpen = true; + } + timeoutInterval = newTimeoutInterval; + } + + outWriter.print("}, \"resources\" : {"); + + // Precache custom layouts + + // TODO We should only precache the layouts that are not + // cached already (plagiate from usedPaintableTypes) + int resourceIndex = 0; + for (final Iterator<Object> i = paintTarget.getUsedResources() + .iterator(); i.hasNext();) { + final String resource = (String) i.next(); + InputStream is = null; + try { + is = getThemeResourceAsStream(root, getTheme(root), resource); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Failed to get theme resource stream.", e); + } + if (is != null) { + + outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\"" + + resource + "\" : "); + final StringBuffer layout = new StringBuffer(); + + try { + final InputStreamReader r = new InputStreamReader(is, + "UTF-8"); + final char[] buffer = new char[20000]; + int charsRead = 0; + while ((charsRead = r.read(buffer)) > 0) { + layout.append(buffer, 0, charsRead); + } + r.close(); + } catch (final java.io.IOException e) { + // FIXME: Handle exception + getLogger().log(Level.INFO, "Resource transfer failed", e); + } + outWriter.print("\"" + + JsonPaintTarget.escapeJSON(layout.toString()) + "\""); + } else { + // FIXME: Handle exception + getLogger().severe("CustomLayout not found: " + resource); + } + } + outWriter.print("}"); + + Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget + .getUsedClientConnectors(); + boolean typeMappingsOpen = false; + ClientCache clientCache = getClientCache(root); + + List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>(); + + for (Class<? extends ClientConnector> class1 : usedClientConnectors) { + if (clientCache.cache(class1)) { + // client does not know the mapping key for this type, send + // mapping to client + newConnectorTypes.add(class1); + + if (!typeMappingsOpen) { + typeMappingsOpen = true; + outWriter.print(", \"typeMappings\" : { "); + } else { + outWriter.print(" , "); + } + String canonicalName = class1.getCanonicalName(); + outWriter.print("\""); + outWriter.print(canonicalName); + outWriter.print("\" : "); + outWriter.print(getTagForType(class1)); + } + } + if (typeMappingsOpen) { + outWriter.print(" }"); + } + + boolean typeInheritanceMapOpen = false; + if (typeMappingsOpen) { + // send the whole type inheritance map if any new mappings + for (Class<? extends ClientConnector> class1 : usedClientConnectors) { + if (!ClientConnector.class.isAssignableFrom(class1 + .getSuperclass())) { + continue; + } + if (!typeInheritanceMapOpen) { + typeInheritanceMapOpen = true; + outWriter.print(", \"typeInheritanceMap\" : { "); + } else { + outWriter.print(" , "); + } + outWriter.print("\""); + outWriter.print(getTagForType(class1)); + outWriter.print("\" : "); + outWriter + .print(getTagForType((Class<? extends ClientConnector>) class1 + .getSuperclass())); + } + if (typeInheritanceMapOpen) { + outWriter.print(" }"); + } + } + + /* + * Ensure super classes come before sub classes to get script dependency + * order right. Sub class @JavaScript might assume that @JavaScript + * defined by super class is already loaded. + */ + Collections.sort(newConnectorTypes, new Comparator<Class<?>>() { + @Override + public int compare(Class<?> o1, Class<?> o2) { + // TODO optimize using Class.isAssignableFrom? + return hierarchyDepth(o1) - hierarchyDepth(o2); + } + + private int hierarchyDepth(Class<?> type) { + if (type == Object.class) { + return 0; + } else { + return hierarchyDepth(type.getSuperclass()) + 1; + } + } + }); + + List<String> scriptDependencies = new ArrayList<String>(); + List<String> styleDependencies = new ArrayList<String>(); + + for (Class<? extends ClientConnector> class1 : newConnectorTypes) { + JavaScript jsAnnotation = class1.getAnnotation(JavaScript.class); + if (jsAnnotation != null) { + for (String resource : jsAnnotation.value()) { + scriptDependencies.add(registerResource(resource, class1)); + } + } + + StyleSheet styleAnnotation = class1.getAnnotation(StyleSheet.class); + if (styleAnnotation != null) { + for (String resource : styleAnnotation.value()) { + styleDependencies.add(registerResource(resource, class1)); + } + } + } + + // Include script dependencies in output if there are any + if (!scriptDependencies.isEmpty()) { + outWriter.print(", \"scriptDependencies\": " + + new JSONArray(scriptDependencies).toString()); + } + + // Include style dependencies in output if there are any + if (!styleDependencies.isEmpty()) { + outWriter.print(", \"styleDependencies\": " + + new JSONArray(styleDependencies).toString()); + } + + // add any pending locale definitions requested by the client + printLocaleDeclarations(outWriter); + + if (dragAndDropService != null) { + dragAndDropService.printJSONResponse(outWriter); + } + + writePerformanceData(outWriter); + } + + /** + * Resolves a resource URI, registering the URI with this + * {@code AbstractCommunicationManager} if needed and returns a fully + * qualified URI. + */ + private String registerResource(String resourceUri, Class<?> context) { + try { + URI uri = new URI(resourceUri); + String protocol = uri.getScheme(); + + if ("connector".equals(protocol)) { + // Strip initial slash + String resourceName = uri.getPath().substring(1); + return registerConnectorResource(resourceName, context); + } + + if (protocol != null || uri.getHost() != null) { + return resourceUri; + } + + // Bare path interpreted as connector resource + return registerConnectorResource(resourceUri, context); + } catch (URISyntaxException e) { + getLogger().log(Level.WARNING, + "Could not parse resource url " + resourceUri, e); + return resourceUri; + } + } + + private String registerConnectorResource(String name, Class<?> context) { + synchronized (connectorResourceContexts) { + // Add to map of names accepted by serveConnectorResource + if (connectorResourceContexts.containsKey(name)) { + Class<?> oldContext = connectorResourceContexts.get(name); + if (oldContext != context) { + getLogger().warning( + "Resource " + name + " defined by both " + context + + " and " + oldContext + ". Resource from " + + oldContext + " will be used."); + } + } else { + connectorResourceContexts.put(name, context); + } + } + + return ApplicationConnection.CONNECTOR_PROTOCOL_PREFIX + "/" + name; + } + + /** + * Adds the performance timing data (used by TestBench 3) to the UIDL + * response. + */ + private void writePerformanceData(final PrintWriter outWriter) { + AbstractWebApplicationContext ctx = (AbstractWebApplicationContext) application + .getContext(); + outWriter.write(String.format(", \"timings\":[%d, %d]", + ctx.getTotalSessionTime(), ctx.getLastRequestTime())); + } + + private void legacyPaint(PaintTarget paintTarget, + ArrayList<ClientConnector> dirtyVisibleConnectors) + throws PaintException { + List<Vaadin6Component> legacyComponents = new ArrayList<Vaadin6Component>(); + for (Connector connector : dirtyVisibleConnectors) { + // All Components that want to use paintContent must implement + // Vaadin6Component + if (connector instanceof Vaadin6Component) { + legacyComponents.add((Vaadin6Component) connector); + } + } + sortByHierarchy((List) legacyComponents); + for (Vaadin6Component c : legacyComponents) { + getLogger().fine( + "Painting Vaadin6Component " + c.getClass().getName() + "@" + + Integer.toHexString(c.hashCode())); + paintTarget.startTag("change"); + final String pid = c.getConnectorId(); + paintTarget.addAttribute("pid", pid); + LegacyPaint.paint(c, paintTarget); + paintTarget.endTag("change"); + } + + } + + private void sortByHierarchy(List<Component> paintables) { + // Vaadin 6 requires parents to be painted before children as component + // containers rely on that their updateFromUIDL method has been called + // before children start calling e.g. updateCaption + Collections.sort(paintables, new Comparator<Component>() { + + @Override + public int compare(Component c1, Component c2) { + int depth1 = 0; + while (c1.getParent() != null) { + depth1++; + c1 = c1.getParent(); + } + int depth2 = 0; + while (c2.getParent() != null) { + depth2++; + c2 = c2.getParent(); + } + if (depth1 < depth2) { + return -1; + } + if (depth1 > depth2) { + return 1; + } + return 0; + } + }); + + } + + private ClientCache getClientCache(Root root) { + Integer rootId = Integer.valueOf(root.getRootId()); + ClientCache cache = rootToClientCache.get(rootId); + if (cache == null) { + cache = new ClientCache(); + rootToClientCache.put(rootId, cache); + } + return cache; + } + + /** + * Checks if the connector is visible in context. For Components, + * {@link #isVisible(Component)} is used. For other types of connectors, the + * contextual visibility of its first Component ancestor is used. If no + * Component ancestor is found, the connector is not visible. + * + * @param connector + * The connector to check + * @return <code>true</code> if the connector is visible to the client, + * <code>false</code> otherwise + */ + static boolean isVisible(ClientConnector connector) { + if (connector instanceof Component) { + return isVisible((Component) connector); + } else { + ClientConnector parent = connector.getParent(); + if (parent == null) { + return false; + } else { + return isVisible(parent); + } + } + } + + /** + * Checks if the component is visible in context, i.e. returns false if the + * child is hidden, the parent is hidden or the parent says the child should + * not be rendered (using + * {@link HasComponents#isComponentVisible(Component)} + * + * @param child + * The child to check + * @return true if the child is visible to the client, false otherwise + */ + static boolean isVisible(Component child) { + if (!child.isVisible()) { + return false; + } + + HasComponents parent = child.getParent(); + if (parent == null) { + if (child instanceof Root) { + return child.isVisible(); + } else { + return false; + } + } + + return parent.isComponentVisible(child) && isVisible(parent); + } + + private static class NullIterator<E> implements Iterator<E> { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public E next() { + return null; + } + + @Override + public void remove() { + } + + } + + /** + * Collects all pending RPC calls from listed {@link ClientConnector}s and + * clears their RPC queues. + * + * @param rpcPendingQueue + * list of {@link ClientConnector} of interest + * @return ordered list of pending RPC calls + */ + private List<ClientMethodInvocation> collectPendingRpcCalls( + List<ClientConnector> rpcPendingQueue) { + List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); + for (ClientConnector connector : rpcPendingQueue) { + List<ClientMethodInvocation> paintablePendingRpc = connector + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List<ClientMethodInvocation> oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList<ClientMethodInvocation>( + totalCalls); + + // merge two ordered comparable lists + for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { + if (paintableIndex >= paintablePendingRpc.size() + || (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc + .get(oldIndex)) + .compareTo(paintablePendingRpc + .get(paintableIndex)) <= 0)) { + pendingInvocations.add(oldPendingRpc.get(oldIndex++)); + } else { + pendingInvocations.add(paintablePendingRpc + .get(paintableIndex++)); + } + } + } + } + return pendingInvocations; + } + + protected abstract InputStream getThemeResourceAsStream(Root root, + String themeName, String resource); + + private int getTimeoutInterval() { + return maxInactiveInterval; + } + + private String getTheme(Root root) { + String themeName = root.getApplication().getThemeForRoot(root); + String requestThemeName = getRequestTheme(); + + if (requestThemeName != null) { + themeName = requestThemeName; + } + if (themeName == null) { + themeName = AbstractApplicationServlet.getDefaultTheme(); + } + return themeName; + } + + private String getRequestTheme() { + return requestThemeName; + } + + /** + * Returns false if the cross site request forgery protection is turned off. + * + * @param application + * @return false if the XSRF is turned off, true otherwise + */ + public boolean isXSRFEnabled(Application application) { + return !"true" + .equals(application + .getProperty(AbstractApplicationServlet.SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION)); + } + + /** + * TODO document + * + * If this method returns false, something was submitted that we did not + * expect; this is probably due to the client being out-of-sync and sending + * variable changes for non-existing pids + * + * @return true if successful, false if there was an inconsistency + */ + private boolean handleVariables(WrappedRequest request, + WrappedResponse response, Callback callback, + Application application2, Root root) throws IOException, + InvalidUIDLSecurityKeyException, JSONException { + boolean success = true; + + String changes = getRequestPayload(request); + if (changes != null) { + + // Manage bursts one by one + final String[] bursts = changes.split(String + .valueOf(VAR_BURST_SEPARATOR)); + + // Security: double cookie submission pattern unless disabled by + // property + if (isXSRFEnabled(application2)) { + if (bursts.length == 1 && "init".equals(bursts[0])) { + // init request; don't handle any variables, key sent in + // response. + request.setAttribute(WRITE_SECURITY_TOKEN_FLAG, true); + return true; + } else { + // ApplicationServlet has stored the security token in the + // session; check that it matched the one sent in the UIDL + String sessId = (String) request + .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); + + if (sessId == null || !sessId.equals(bursts[0])) { + throw new InvalidUIDLSecurityKeyException( + "Security key mismatch"); + } + } + + } + + for (int bi = 1; bi < bursts.length; bi++) { + // unescape any encoded separator characters in the burst + final String burst = unescapeBurst(bursts[bi]); + success &= handleBurst(request, root, burst); + + // In case that there were multiple bursts, we know that this is + // a special synchronous case for closing window. Thus we are + // not interested in sending any UIDL changes back to client. + // Still we must clear component tree between bursts to ensure + // that no removed components are updated. The painting after + // the last burst is handled normally by the calling method. + if (bi < bursts.length - 1) { + + // We will be discarding all changes + final PrintWriter outWriter = new PrintWriter( + new CharArrayWriter()); + + paintAfterVariableChanges(request, response, callback, + true, outWriter, root, false); + + } + + } + } + /* + * Note that we ignore inconsistencies while handling unload request. + * The client can't remove invalid variable changes from the burst, and + * we don't have the required logic implemented on the server side. E.g. + * a component is removed in a previous burst. + */ + return success; + } + + /** + * Processes a message burst received from the client. + * + * A burst can contain any number of RPC calls, including legacy variable + * change calls that are processed separately. + * + * Consecutive changes to the value of the same variable are combined and + * changeVariables() is only called once for them. This preserves the Vaadin + * 6 semantics for components and add-ons that do not use Vaadin 7 RPC + * directly. + * + * @param source + * @param root + * the root receiving the burst + * @param burst + * the content of the burst as a String to be parsed + * @return true if the processing of the burst was successful and there were + * no messages to non-existent components + */ + public boolean handleBurst(WrappedRequest source, Root root, + final String burst) { + boolean success = true; + try { + Set<Connector> enabledConnectors = new HashSet<Connector>(); + + List<MethodInvocation> invocations = parseInvocations( + root.getConnectorTracker(), burst); + for (MethodInvocation invocation : invocations) { + final ClientConnector connector = getConnector(root, + invocation.getConnectorId()); + + if (connector != null && connector.isConnectorEnabled()) { + enabledConnectors.add(connector); + } + } + + for (int i = 0; i < invocations.size(); i++) { + MethodInvocation invocation = invocations.get(i); + + final ClientConnector connector = getConnector(root, + invocation.getConnectorId()); + + if (connector == null) { + getLogger().log( + Level.WARNING, + "RPC call to " + invocation.getInterfaceName() + + "." + invocation.getMethodName() + + " received for connector " + + invocation.getConnectorId() + + " but no such connector could be found"); + continue; + } + + if (!enabledConnectors.contains(connector)) { + + if (invocation instanceof LegacyChangeVariablesInvocation) { + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + // TODO convert window close to a separate RPC call and + // handle above - not a variable change + + // Handle special case where window-close is called + // after the window has been removed from the + // application or the application has closed + Map<String, Object> changes = legacyInvocation + .getVariableChanges(); + if (changes.size() == 1 && changes.containsKey("close") + && Boolean.TRUE.equals(changes.get("close"))) { + // Silently ignore this + continue; + } + } + + // Connector is disabled, log a warning and move to the next + String msg = "Ignoring RPC call for disabled connector " + + connector.getClass().getName(); + if (connector instanceof Component) { + String caption = ((Component) connector).getCaption(); + if (caption != null) { + msg += ", caption=" + caption; + } + } + getLogger().warning(msg); + continue; + } + + if (invocation instanceof ServerRpcMethodInvocation) { + try { + ServerRpcManager.applyInvocation(connector, + (ServerRpcMethodInvocation) invocation); + } catch (RpcInvocationException e) { + Throwable realException = e.getCause(); + Component errorComponent = null; + if (connector instanceof Component) { + errorComponent = (Component) connector; + } + handleChangeVariablesError(root.getApplication(), + errorComponent, realException, null); + } + } else { + + // All code below is for legacy variable changes + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + Map<String, Object> changes = legacyInvocation + .getVariableChanges(); + try { + if (connector instanceof VariableOwner) { + changeVariables(source, (VariableOwner) connector, + changes); + } else { + throw new IllegalStateException( + "Received legacy variable change for " + + connector.getClass().getName() + + " (" + + connector.getConnectorId() + + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: " + + changes.keySet()); + } + } catch (Exception e) { + Component errorComponent = null; + if (connector instanceof Component) { + errorComponent = (Component) connector; + } else if (connector instanceof DragAndDropService) { + Object dropHandlerOwner = changes.get("dhowner"); + if (dropHandlerOwner instanceof Component) { + errorComponent = (Component) dropHandlerOwner; + } + } + handleChangeVariablesError(root.getApplication(), + errorComponent, e, changes); + } + } + } + } catch (JSONException e) { + getLogger().warning( + "Unable to parse RPC call from the client: " + + e.getMessage()); + // TODO or return success = false? + throw new RuntimeException(e); + } + + return success; + } + + /** + * Parse a message burst from the client into a list of MethodInvocation + * instances. + * + * @param connectorTracker + * The ConnectorTracker used to lookup connectors + * @param burst + * message string (JSON) + * @return list of MethodInvocation to perform + * @throws JSONException + */ + private List<MethodInvocation> parseInvocations( + ConnectorTracker connectorTracker, final String burst) + throws JSONException { + JSONArray invocationsJson = new JSONArray(burst); + + ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); + + MethodInvocation previousInvocation = null; + // parse JSON to MethodInvocations + for (int i = 0; i < invocationsJson.length(); ++i) { + + JSONArray invocationJson = invocationsJson.getJSONArray(i); + + MethodInvocation invocation = parseInvocation(invocationJson, + previousInvocation, connectorTracker); + if (invocation != null) { + // Can be null iff the invocation was a legacy invocation and it + // was merged with the previous one + invocations.add(invocation); + previousInvocation = invocation; + } + } + return invocations; + } + + private MethodInvocation parseInvocation(JSONArray invocationJson, + MethodInvocation previousInvocation, + ConnectorTracker connectorTracker) throws JSONException { + String connectorId = invocationJson.getString(0); + String interfaceName = invocationJson.getString(1); + String methodName = invocationJson.getString(2); + + JSONArray parametersJson = invocationJson.getJSONArray(3); + + if (LegacyChangeVariablesInvocation.isLegacyVariableChange( + interfaceName, methodName)) { + if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) { + previousInvocation = null; + } + + return parseLegacyChangeVariablesInvocation(connectorId, + interfaceName, methodName, + (LegacyChangeVariablesInvocation) previousInvocation, + parametersJson, connectorTracker); + } else { + return parseServerRpcInvocation(connectorId, interfaceName, + methodName, parametersJson, connectorTracker); + } + + } + + private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation( + String connectorId, String interfaceName, String methodName, + LegacyChangeVariablesInvocation previousInvocation, + JSONArray parametersJson, ConnectorTracker connectorTracker) + throws JSONException { + if (parametersJson.length() != 2) { + throw new JSONException( + "Invalid parameters in legacy change variables call. Expected 2, was " + + parametersJson.length()); + } + String variableName = parametersJson.getString(0); + UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType( + UidlValue.class, true, parametersJson.get(1), connectorTracker); + + Object value = uidlValue.getValue(); + + if (previousInvocation != null + && previousInvocation.getConnectorId().equals(connectorId)) { + previousInvocation.setVariableChange(variableName, value); + return null; + } else { + return new LegacyChangeVariablesInvocation(connectorId, + variableName, value); + } + } + + private ServerRpcMethodInvocation parseServerRpcInvocation( + String connectorId, String interfaceName, String methodName, + JSONArray parametersJson, ConnectorTracker connectorTracker) + throws JSONException { + ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( + connectorId, interfaceName, methodName, parametersJson.length()); + + Object[] parameters = new Object[parametersJson.length()]; + Type[] declaredRpcMethodParameterTypes = invocation.getMethod() + .getGenericParameterTypes(); + + for (int j = 0; j < parametersJson.length(); ++j) { + Object parameterValue = parametersJson.get(j); + Type parameterType = declaredRpcMethodParameterTypes[j]; + parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType, + parameterValue, connectorTracker); + } + invocation.setParameters(parameters); + return invocation; + } + + protected void changeVariables(Object source, final VariableOwner owner, + Map<String, Object> m) { + owner.changeVariables(source, m); + } + + protected ClientConnector getConnector(Root root, String connectorId) { + ClientConnector c = root.getConnectorTracker() + .getConnector(connectorId); + if (c == null + && connectorId.equals(getDragAndDropService().getConnectorId())) { + return getDragAndDropService(); + } + + return c; + } + + private DragAndDropService getDragAndDropService() { + if (dragAndDropService == null) { + dragAndDropService = new DragAndDropService(this); + } + return dragAndDropService; + } + + /** + * Reads the request data from the Request and returns it converted to an + * UTF-8 string. + * + * @param request + * @return + * @throws IOException + */ + protected String getRequestPayload(WrappedRequest request) + throws IOException { + + int requestLength = request.getContentLength(); + if (requestLength == 0) { + return null; + } + + ByteArrayOutputStream bout = requestLength <= 0 ? new ByteArrayOutputStream() + : new ByteArrayOutputStream(requestLength); + + InputStream inputStream = request.getInputStream(); + byte[] buffer = new byte[MAX_BUFFER_SIZE]; + + while (true) { + int read = inputStream.read(buffer); + if (read == -1) { + break; + } + bout.write(buffer, 0, read); + } + String result = new String(bout.toByteArray(), "utf-8"); + + return result; + } + + public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable { + private final Throwable throwable; + + public ErrorHandlerErrorEvent(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Handles an error (exception) that occurred when processing variable + * changes from the client or a failure of a file upload. + * + * For {@link AbstractField} components, + * {@link AbstractField#handleError(com.vaadin.ui.AbstractComponent.ComponentErrorEvent)} + * is called. In all other cases (or if the field does not handle the + * error), {@link ErrorListener#terminalError(ErrorEvent)} for the + * application error handler is called. + * + * @param application + * @param owner + * component that the error concerns + * @param e + * exception that occurred + * @param m + * map from variable names to values + */ + private void handleChangeVariablesError(Application application, + Component owner, Throwable t, Map<String, Object> m) { + boolean handled = false; + ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent( + owner, t, m); + + if (owner instanceof AbstractField) { + try { + handled = ((AbstractField<?>) owner).handleError(errorEvent); + } catch (Exception handlerException) { + /* + * If there is an error in the component error handler we pass + * the that error to the application error handler and continue + * processing the actual error + */ + application.getErrorHandler().terminalError( + new ErrorHandlerErrorEvent(handlerException)); + handled = false; + } + } + + if (!handled) { + application.getErrorHandler().terminalError(errorEvent); + } + + } + + /** + * Unescape encoded burst separator characters in a burst received from the + * client. This protects from separator injection attacks. + * + * @param encodedValue + * to decode + * @return decoded value + */ + protected String unescapeBurst(String encodedValue) { + final StringBuilder result = new StringBuilder(); + final StringCharacterIterator iterator = new StringCharacterIterator( + encodedValue); + char character = iterator.current(); + while (character != CharacterIterator.DONE) { + if (VAR_ESCAPE_CHARACTER == character) { + character = iterator.next(); + switch (character) { + case VAR_ESCAPE_CHARACTER + 0x30: + // escaped escape character + result.append(VAR_ESCAPE_CHARACTER); + break; + case VAR_BURST_SEPARATOR + 0x30: + // +0x30 makes these letters for easier reading + result.append((char) (character - 0x30)); + break; + case CharacterIterator.DONE: + // error + throw new RuntimeException( + "Communication error: Unexpected end of message"); + default: + // other escaped character - probably a client-server + // version mismatch + throw new RuntimeException( + "Invalid escaped character from the client - check that the widgetset and server versions match"); + } + } else { + // not a special character - add it to the result as is + result.append(character); + } + character = iterator.next(); + } + return result.toString(); + } + + /** + * Prints the queued (pending) locale definitions to a {@link PrintWriter} + * in a (UIDL) format that can be sent to the client and used there in + * formatting dates, times etc. + * + * @param outWriter + */ + private void printLocaleDeclarations(PrintWriter outWriter) { + /* + * ----------------------------- Sending Locale sensitive date + * ----------------------------- + */ + + // Send locale informations to client + outWriter.print(", \"locales\":["); + for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { + + final Locale l = generateLocale(locales.get(pendingLocalesIndex)); + // Locale name + outWriter.print("{\"name\":\"" + l.toString() + "\","); + + /* + * Month names (both short and full) + */ + final DateFormatSymbols dfs = new DateFormatSymbols(l); + final String[] short_months = dfs.getShortMonths(); + final String[] months = dfs.getMonths(); + outWriter.print("\"smn\":[\"" + + // ShortMonthNames + short_months[0] + "\",\"" + short_months[1] + "\",\"" + + short_months[2] + "\",\"" + short_months[3] + "\",\"" + + short_months[4] + "\",\"" + short_months[5] + "\",\"" + + short_months[6] + "\",\"" + short_months[7] + "\",\"" + + short_months[8] + "\",\"" + short_months[9] + "\",\"" + + short_months[10] + "\",\"" + short_months[11] + "\"" + + "],"); + outWriter.print("\"mn\":[\"" + + // MonthNames + months[0] + "\",\"" + months[1] + "\",\"" + months[2] + + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" + + months[5] + "\",\"" + months[6] + "\",\"" + months[7] + + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" + + months[10] + "\",\"" + months[11] + "\"" + "],"); + + /* + * Weekday names (both short and full) + */ + final String[] short_days = dfs.getShortWeekdays(); + final String[] days = dfs.getWeekdays(); + outWriter.print("\"sdn\":[\"" + + // ShortDayNames + short_days[1] + "\",\"" + short_days[2] + "\",\"" + + short_days[3] + "\",\"" + short_days[4] + "\",\"" + + short_days[5] + "\",\"" + short_days[6] + "\",\"" + + short_days[7] + "\"" + "],"); + outWriter.print("\"dn\":[\"" + + // DayNames + days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" + + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" + + days[7] + "\"" + "],"); + + /* + * First day of week (0 = sunday, 1 = monday) + */ + final Calendar cal = new GregorianCalendar(l); + outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); + + /* + * Date formatting (MM/DD/YYYY etc.) + */ + + DateFormat dateFormat = DateFormat.getDateTimeInstance( + DateFormat.SHORT, DateFormat.SHORT, l); + if (!(dateFormat instanceof SimpleDateFormat)) { + getLogger().warning( + "Unable to get default date pattern for locale " + + l.toString()); + dateFormat = new SimpleDateFormat(); + } + final String df = ((SimpleDateFormat) dateFormat).toPattern(); + + int timeStart = df.indexOf("H"); + if (timeStart < 0) { + timeStart = df.indexOf("h"); + } + final int ampm_first = df.indexOf("a"); + // E.g. in Korean locale AM/PM is before h:mm + // TODO should take that into consideration on client-side as well, + // now always h:mm a + if (ampm_first > 0 && ampm_first < timeStart) { + timeStart = ampm_first; + } + // Hebrew locale has time before the date + final boolean timeFirst = timeStart == 0; + String dateformat; + if (timeFirst) { + int dateStart = df.indexOf(' '); + if (ampm_first > dateStart) { + dateStart = df.indexOf(' ', ampm_first); + } + dateformat = df.substring(dateStart + 1); + } else { + dateformat = df.substring(0, timeStart - 1); + } + + outWriter.print("\"df\":\"" + dateformat.trim() + "\","); + + /* + * Time formatting (24 or 12 hour clock and AM/PM suffixes) + */ + final String timeformat = df.substring(timeStart, df.length()); + /* + * Doesn't return second or milliseconds. + * + * We use timeformat to determine 12/24-hour clock + */ + final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; + // TODO there are other possibilities as well, like 'h' in french + // (ignore them, too complicated) + final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." + : ":"; + // outWriter.print("\"tf\":\"" + timeformat + "\","); + outWriter.print("\"thc\":" + twelve_hour_clock + ","); + outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\""); + if (twelve_hour_clock) { + final String[] ampm = dfs.getAmPmStrings(); + outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] + + "\"]"); + } + outWriter.print("}"); + if (pendingLocalesIndex < locales.size() - 1) { + outWriter.print(","); + } + } + outWriter.print("]"); // Close locales + } + + /** + * Ends the Application. + * + * The browser is redirected to the Application logout URL set with + * {@link Application#setLogoutURL(String)}, or to the application URL if no + * logout URL is given. + * + * @param request + * the request instance. + * @param response + * the response to write to. + * @param application + * the Application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(WrappedRequest request, + WrappedResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + // clients JS app is still running, send a special json file to tell + // client that application has quit and where to point browser now + // Set the response type + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + openJsonMessage(outWriter, response); + outWriter.print("\"redirect\":{"); + outWriter.write("\"url\":\"" + logoutUrl + "\"}"); + closeJsonMessage(outWriter); + outWriter.flush(); + outWriter.close(); + out.flush(); + } + + protected void closeJsonMessage(PrintWriter outWriter) { + outWriter.print("}]"); + } + + /** + * Writes the opening of JSON message to be sent to client. + * + * @param outWriter + * @param response + */ + protected void openJsonMessage(PrintWriter outWriter, + WrappedResponse response) { + // Sets the response type + response.setContentType("application/json; charset=UTF-8"); + // some dirt to prevent cross site scripting + outWriter.print("for(;;);[{"); + } + + /** + * Returns dirty components which are in given window. Components in an + * invisible subtrees are omitted. + * + * @param w + * root window for which dirty components is to be fetched + * @return + */ + private ArrayList<ClientConnector> getDirtyVisibleConnectors( + ConnectorTracker connectorTracker) { + ArrayList<ClientConnector> dirtyConnectors = new ArrayList<ClientConnector>(); + for (ClientConnector c : connectorTracker.getDirtyConnectors()) { + if (isVisible(c)) { + dirtyConnectors.add(c); + } + } + + return dirtyConnectors; + } + + /** + * Queues a locale to be sent to the client (browser) for date and time + * entry etc. All locale specific information is derived from server-side + * {@link Locale} instances and sent to the client when needed, eliminating + * the need to use the {@link Locale} class and all the framework behind it + * on the client. + * + * @see Locale#toString() + * + * @param value + */ + public void requireLocale(String value) { + if (locales == null) { + locales = new ArrayList<String>(); + locales.add(application.getLocale().toString()); + pendingLocalesIndex = 0; + } + if (!locales.contains(value)) { + locales.add(value); + } + } + + /** + * Constructs a {@link Locale} instance to be sent to the client based on a + * short locale description string. + * + * @see #requireLocale(String) + * + * @param value + * @return + */ + private Locale generateLocale(String value) { + final String[] temp = value.split("_"); + if (temp.length == 1) { + return new Locale(temp[0]); + } else if (temp.length == 2) { + return new Locale(temp[0], temp[1]); + } else { + return new Locale(temp[0], temp[1], temp[2]); + } + } + + protected class InvalidUIDLSecurityKeyException extends + GeneralSecurityException { + + InvalidUIDLSecurityKeyException(String message) { + super(message); + } + + } + + private final HashMap<Class<? extends ClientConnector>, Integer> typeToKey = new HashMap<Class<? extends ClientConnector>, Integer>(); + private int nextTypeKey = 0; + + private BootstrapHandler bootstrapHandler; + + String getTagForType(Class<? extends ClientConnector> class1) { + Integer id = typeToKey.get(class1); + if (id == null) { + id = nextTypeKey++; + typeToKey.put(class1, id); + getLogger().log(Level.FINE, + "Mapping " + class1.getName() + " to " + id); + } + return id.toString(); + } + + /** + * Helper class for terminal to keep track of data that client is expected + * to know. + * + * TODO make customlayout templates (from theme) to be cached here. + */ + class ClientCache implements Serializable { + + private final Set<Object> res = new HashSet<Object>(); + + /** + * + * @param paintable + * @return true if the given class was added to cache + */ + boolean cache(Object object) { + return res.add(object); + } + + public void clear() { + res.clear(); + } + + } + + public String getStreamVariableTargetUrl(ClientConnector owner, + String name, StreamVariable value) { + /* + * We will use the same APP/* URI space as ApplicationResources but + * prefix url with UPLOAD + * + * eg. APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] + * + * SECKEY is created on each paint to make URL's unpredictable (to + * prevent CSRF attacks). + * + * NAME and PID from URI forms a key to fetch StreamVariable when + * handling post + */ + String paintableId = owner.getConnectorId(); + int rootId = owner.getRoot().getRootId(); + String key = rootId + "/" + paintableId + "/" + name; + + if (pidToNameToStreamVariable == null) { + pidToNameToStreamVariable = new HashMap<String, Map<String, StreamVariable>>(); + } + Map<String, StreamVariable> nameToStreamVariable = pidToNameToStreamVariable + .get(paintableId); + if (nameToStreamVariable == null) { + nameToStreamVariable = new HashMap<String, StreamVariable>(); + pidToNameToStreamVariable.put(paintableId, nameToStreamVariable); + } + nameToStreamVariable.put(name, value); + + if (streamVariableToSeckey == null) { + streamVariableToSeckey = new HashMap<StreamVariable, String>(); + } + String seckey = streamVariableToSeckey.get(value); + if (seckey == null) { + seckey = UUID.randomUUID().toString(); + streamVariableToSeckey.put(value, seckey); + } + + return ApplicationConnection.APP_PROTOCOL_PREFIX + + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey; + + } + + public void cleanStreamVariable(ClientConnector owner, String name) { + Map<String, StreamVariable> nameToStreamVar = pidToNameToStreamVariable + .get(owner.getConnectorId()); + nameToStreamVar.remove(name); + if (nameToStreamVar.isEmpty()) { + pidToNameToStreamVariable.remove(owner.getConnectorId()); + } + } + + /** + * Gets the bootstrap handler that should be used for generating the pages + * bootstrapping applications for this communication manager. + * + * @return the bootstrap handler to use + */ + private BootstrapHandler getBootstrapHandler() { + if (bootstrapHandler == null) { + bootstrapHandler = createBootstrapHandler(); + } + + return bootstrapHandler; + } + + protected abstract BootstrapHandler createBootstrapHandler(); + + protected boolean handleApplicationRequest(WrappedRequest request, + WrappedResponse response) throws IOException { + return application.handleRequest(request, response); + } + + public void handleBrowserDetailsRequest(WrappedRequest request, + WrappedResponse response, Application application) + throws IOException { + + // if we do not yet have a currentRoot, it should be initialized + // shortly, and we should send the initial UIDL + boolean sendUIDL = Root.getCurrent() == null; + + try { + CombinedRequest combinedRequest = new CombinedRequest(request); + + Root root = application.getRootForRequest(combinedRequest); + response.setContentType("application/json; charset=UTF-8"); + + // Use the same logic as for determined roots + BootstrapHandler bootstrapHandler = getBootstrapHandler(); + BootstrapContext context = bootstrapHandler.createContext( + combinedRequest, response, application, root.getRootId()); + + String widgetset = context.getWidgetsetName(); + String theme = context.getThemeName(); + String themeUri = bootstrapHandler.getThemeUri(context, theme); + + // TODO These are not required if it was only the init of the root + // that was delayed + JSONObject params = new JSONObject(); + params.put("widgetset", widgetset); + params.put("themeUri", themeUri); + // Root id might have changed based on e.g. window.name + params.put(ApplicationConnection.ROOT_ID_PARAMETER, + root.getRootId()); + if (sendUIDL) { + String initialUIDL = getInitialUIDL(combinedRequest, root); + params.put("uidl", initialUIDL); + } + + // NOTE! GateIn requires, for some weird reason, getOutputStream + // to be used instead of getWriter() (it seems to interpret + // application/json as a binary content type) + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + outWriter.write(params.toString()); + // NOTE GateIn requires the buffers to be flushed to work + outWriter.flush(); + out.flush(); + } catch (RootRequiresMoreInformationException e) { + // Requiring more information at this point is not allowed + // TODO handle in a better way + throw new RuntimeException(e); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Generates the initial UIDL message that can e.g. be included in a html + * page to avoid a separate round trip just for getting the UIDL. + * + * @param request + * the request that caused the initialization + * @param root + * the root for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws PaintException + * if an exception occurs while painting + * @throws JSONException + * if an exception occurs while encoding output + */ + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException { + // TODO maybe unify writeUidlResponse()? + StringWriter sWriter = new StringWriter(); + PrintWriter pWriter = new PrintWriter(sWriter); + pWriter.print("{"); + if (isXSRFEnabled(root.getApplication())) { + pWriter.print(getSecurityKeyUIDL(request)); + } + writeUidlResponse(request, true, pWriter, root, false); + pWriter.print("}"); + String initialUIDL = sWriter.toString(); + getLogger().log(Level.FINE, "Initial UIDL:" + initialUIDL); + return initialUIDL; + } + + /** + * Serve a connector resource from the classpath if the resource has + * previously been registered by calling + * {@link #registerResource(String, Class)}. Sending arbitrary files from + * the classpath is prevented by only accepting resource names that have + * explicitly been registered. Resources can currently only be registered by + * including a {@link JavaScript} or {@link StyleSheet} annotation on a + * Connector class. + * + * @param request + * @param response + * + * @throws IOException + */ + public void serveConnectorResource(WrappedRequest request, + WrappedResponse response) throws IOException { + + String pathInfo = request.getRequestPathInfo(); + // + 2 to also remove beginning and ending slashes + String resourceName = pathInfo + .substring(ApplicationConnection.CONNECTOR_RESOURCE_PREFIX + .length() + 2); + + final String mimetype = response.getDeploymentConfiguration() + .getMimeType(resourceName); + + // Security check: avoid accidentally serving from the root of the + // classpath instead of relative to the context class + if (resourceName.startsWith("/")) { + getLogger().warning( + "Connector resource request starting with / rejected: " + + resourceName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); + return; + } + + // Check that the resource name has been registered + Class<?> context; + synchronized (connectorResourceContexts) { + context = connectorResourceContexts.get(resourceName); + } + + // Security check: don't serve resource if the name hasn't been + // registered in the map + if (context == null) { + getLogger().warning( + "Connector resource request for unknown resource rejected: " + + resourceName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); + return; + } + + // Resolve file relative to the location of the context class + InputStream in = context.getResourceAsStream(resourceName); + if (in == null) { + getLogger().warning( + resourceName + " defined by " + context.getName() + + " not found. Verify that the file " + + context.getPackage().getName().replace('.', '/') + + '/' + resourceName + + " is available on the classpath."); + response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); + return; + } + + // TODO Check and set cache headers + + OutputStream out = null; + try { + if (mimetype != null) { + response.setContentType(mimetype); + } + + out = response.getOutputStream(); + + final byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE]; + + int bytesRead = 0; + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } finally { + try { + in.close(); + } catch (Exception e) { + // Do nothing + } + if (out != null) { + try { + out.close(); + } catch (Exception e) { + // Do nothing + } + } + } + } + + /** + * Handles file upload request submitted via Upload component. + * + * @param root + * The root for this request + * + * @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable) + * + * @param request + * @param response + * @throws IOException + * @throws InvalidUIDLSecurityKeyException + */ + public void handleFileUpload(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException, InvalidUIDLSecurityKeyException { + + /* + * URI pattern: APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] See + * #createReceiverUrl + */ + + String pathInfo = request.getRequestPathInfo(); + // strip away part until the data we are interested starts + int startOfData = pathInfo + .indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX) + + ServletPortletHelper.UPLOAD_URL_PREFIX.length(); + String uppUri = pathInfo.substring(startOfData); + String[] parts = uppUri.split("/", 4); // 0= rootid, 1 = cid, 2= name, 3 + // = sec key + String rootId = parts[0]; + String connectorId = parts[1]; + String variableName = parts[2]; + Root root = application.getRootById(Integer.parseInt(rootId)); + Root.setCurrent(root); + + StreamVariable streamVariable = getStreamVariable(connectorId, + variableName); + String secKey = streamVariableToSeckey.get(streamVariable); + if (secKey.equals(parts[3])) { + + ClientConnector source = getConnector(root, connectorId); + String contentType = request.getContentType(); + if (contentType.contains("boundary")) { + // Multipart requests contain boundary string + doHandleSimpleMultipartFileUpload(request, response, + streamVariable, variableName, source, + contentType.split("boundary=")[1]); + } else { + // if boundary string does not exist, the posted file is from + // XHR2.post(File) + doHandleXhrFilePost(request, response, streamVariable, + variableName, source, request.getContentLength()); + } + } else { + throw new InvalidUIDLSecurityKeyException( + "Security key in upload post did not match!"); + } + + } + + public StreamVariable getStreamVariable(String connectorId, + String variableName) { + Map<String, StreamVariable> map = pidToNameToStreamVariable + .get(connectorId); + if (map == null) { + return null; + } + StreamVariable streamVariable = map.get(variableName); + return streamVariable; + } + + /** + * Stream that extracts content from another stream until the boundary + * string is encountered. + * + * Public only for unit tests, should be considered private for all other + * purposes. + */ + public static class SimpleMultiPartInputStream extends InputStream { + + /** + * Counter of how many characters have been matched to boundary string + * from the stream + */ + int matchedCount = -1; + + /** + * Used as pointer when returning bytes after partly matched boundary + * string. + */ + int curBoundaryIndex = 0; + /** + * The byte found after a "promising start for boundary" + */ + private int bufferedByte = -1; + private boolean atTheEnd = false; + + private final char[] boundary; + + private final InputStream realInputStream; + + public SimpleMultiPartInputStream(InputStream realInputStream, + String boundaryString) { + boundary = (CRLF + DASHDASH + boundaryString).toCharArray(); + this.realInputStream = realInputStream; + } + + @Override + public int read() throws IOException { + if (atTheEnd) { + // End boundary reached, nothing more to read + return -1; + } else if (bufferedByte >= 0) { + /* Purge partially matched boundary if there was such */ + return getBuffered(); + } else if (matchedCount != -1) { + /* + * Special case where last "failed" matching ended with first + * character from boundary string + */ + return matchForBoundary(); + } else { + int fromActualStream = realInputStream.read(); + if (fromActualStream == -1) { + // unexpected end of stream + throw new IOException( + "The multipart stream ended unexpectedly"); + } + if (boundary[0] == fromActualStream) { + /* + * If matches the first character in boundary string, start + * checking if the boundary is fetched. + */ + return matchForBoundary(); + } + return fromActualStream; + } + } + + /** + * Reads the input to expect a boundary string. Expects that the first + * character has already been matched. + * + * @return -1 if the boundary was matched, else returns the first byte + * from boundary + * @throws IOException + */ + private int matchForBoundary() throws IOException { + matchedCount = 0; + /* + * Going to "buffered mode". Read until full boundary match or a + * different character. + */ + while (true) { + matchedCount++; + if (matchedCount == boundary.length) { + /* + * The whole boundary matched so we have reached the end of + * file + */ + atTheEnd = true; + return -1; + } + int fromActualStream = realInputStream.read(); + if (fromActualStream != boundary[matchedCount]) { + /* + * Did not find full boundary, cache the mismatching byte + * and start returning the partially matched boundary. + */ + bufferedByte = fromActualStream; + return getBuffered(); + } + } + } + + /** + * Returns the partly matched boundary string and the byte following + * that. + * + * @return + * @throws IOException + */ + private int getBuffered() throws IOException { + int b; + if (matchedCount == 0) { + // The boundary has been returned, return the buffered byte. + b = bufferedByte; + bufferedByte = -1; + matchedCount = -1; + } else { + b = boundary[curBoundaryIndex++]; + if (curBoundaryIndex == matchedCount) { + // The full boundary has been returned, remaining is the + // char that did not match the boundary. + + curBoundaryIndex = 0; + if (bufferedByte != boundary[0]) { + /* + * next call for getBuffered will return the + * bufferedByte that came after the partial boundary + * match + */ + matchedCount = 0; + } else { + /* + * Special case where buffered byte again matches the + * boundaryString. This could be the start of the real + * end boundary. + */ + matchedCount = 0; + bufferedByte = -1; + } + } + } + if (b == -1) { + throw new IOException("The multipart stream ended unexpectedly"); + } + return b; + } + } + + private static final Logger getLogger() { + return Logger.getLogger(AbstractCommunicationManager.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java new file mode 100644 index 0000000000..7b51712904 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java @@ -0,0 +1,143 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Constructor; +import java.util.Iterator; +import java.util.Properties; +import java.util.ServiceLoader; + +import com.vaadin.terminal.DeploymentConfiguration; + +public abstract class AbstractDeploymentConfiguration implements + DeploymentConfiguration { + + private final Class<?> systemPropertyBaseClass; + private final Properties applicationProperties = new Properties(); + private AddonContext addonContext; + + public AbstractDeploymentConfiguration(Class<?> systemPropertyBaseClass) { + this.systemPropertyBaseClass = systemPropertyBaseClass; + } + + @Override + public String getApplicationOrSystemProperty(String propertyName, + String defaultValue) { + + String val = null; + + // Try application properties + val = getApplicationProperty(propertyName); + if (val != null) { + return val; + } + + // Try system properties + val = getSystemProperty(propertyName); + if (val != null) { + return val; + } + + return defaultValue; + } + + /** + * Gets an system property value. + * + * @param parameterName + * the Name or the parameter. + * @return String value or null if not found + */ + protected String getSystemProperty(String parameterName) { + String val = null; + + String pkgName; + final Package pkg = systemPropertyBaseClass.getPackage(); + if (pkg != null) { + pkgName = pkg.getName(); + } else { + final String className = systemPropertyBaseClass.getName(); + pkgName = new String(className.toCharArray(), 0, + className.lastIndexOf('.')); + } + val = System.getProperty(pkgName + "." + parameterName); + if (val != null) { + return val; + } + + // Try lowercased system properties + val = System.getProperty(pkgName + "." + parameterName.toLowerCase()); + return val; + } + + @Override + public ClassLoader getClassLoader() { + final String classLoaderName = getApplicationOrSystemProperty( + "ClassLoader", null); + ClassLoader classLoader; + if (classLoaderName == null) { + classLoader = getClass().getClassLoader(); + } else { + try { + final Class<?> classLoaderClass = getClass().getClassLoader() + .loadClass(classLoaderName); + final Constructor<?> c = classLoaderClass + .getConstructor(new Class[] { ClassLoader.class }); + classLoader = (ClassLoader) c + .newInstance(new Object[] { getClass().getClassLoader() }); + } catch (final Exception e) { + throw new RuntimeException( + "Could not find specified class loader: " + + classLoaderName, e); + } + } + return classLoader; + } + + /** + * Gets an application property value. + * + * @param parameterName + * the Name or the parameter. + * @return String value or null if not found + */ + protected String getApplicationProperty(String parameterName) { + + String val = applicationProperties.getProperty(parameterName); + if (val != null) { + return val; + } + + // Try lower case application properties for backward compatibility with + // 3.0.2 and earlier + val = applicationProperties.getProperty(parameterName.toLowerCase()); + + return val; + } + + @Override + public Properties getInitParameters() { + return applicationProperties; + } + + @Override + public Iterator<AddonContextListener> getAddonContextListeners() { + // Called once for init and then no more, so there's no point in caching + // the instance + ServiceLoader<AddonContextListener> contextListenerLoader = ServiceLoader + .load(AddonContextListener.class, getClassLoader()); + return contextListenerLoader.iterator(); + } + + @Override + public void setAddonContext(AddonContext addonContext) { + this.addonContext = addonContext; + } + + @Override + public AddonContext getAddonContext() { + return addonContext; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java b/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java new file mode 100644 index 0000000000..d3474e736e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingEvent; + +/** + * Abstract base class for StreamingEvent implementations. + */ +@SuppressWarnings("serial") +abstract class AbstractStreamingEvent implements StreamingEvent { + private final String type; + private final String filename; + private final long contentLength; + private final long bytesReceived; + + @Override + public final String getFileName() { + return filename; + } + + @Override + public final String getMimeType() { + return type; + } + + protected AbstractStreamingEvent(String filename, String type, long length, + long bytesReceived) { + this.filename = filename; + this.type = type; + contentLength = length; + this.bytesReceived = bytesReceived; + } + + @Override + public final long getContentLength() { + return contentLength; + } + + @Override + public final long getBytesReceived() { + return bytesReceived; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java new file mode 100644 index 0000000000..3a33621d10 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java @@ -0,0 +1,268 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * Base class for web application contexts (including portlet contexts) that + * handles the common tasks. + */ +public abstract class AbstractWebApplicationContext implements + ApplicationContext, HttpSessionBindingListener, Serializable { + + protected Collection<TransactionListener> listeners = Collections + .synchronizedList(new LinkedList<TransactionListener>()); + + protected final HashSet<Application> applications = new HashSet<Application>(); + + protected WebBrowser browser = new WebBrowser(); + + protected HashMap<Application, AbstractCommunicationManager> applicationToAjaxAppMgrMap = new HashMap<Application, AbstractCommunicationManager>(); + + private long totalSessionTime = 0; + + private long lastRequestTime = -1; + + @Override + public void addTransactionListener(TransactionListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + @Override + public void removeTransactionListener(TransactionListener listener) { + listeners.remove(listener); + } + + /** + * Sends a notification that a transaction is starting. + * + * @param application + * The application associated with the transaction. + * @param request + * the HTTP or portlet request that triggered the transaction. + */ + protected void startTransaction(Application application, Object request) { + ArrayList<TransactionListener> currentListeners; + synchronized (listeners) { + currentListeners = new ArrayList<TransactionListener>(listeners); + } + for (TransactionListener listener : currentListeners) { + listener.transactionStart(application, request); + } + } + + /** + * Sends a notification that a transaction has ended. + * + * @param application + * The application associated with the transaction. + * @param request + * the HTTP or portlet request that triggered the transaction. + */ + protected void endTransaction(Application application, Object request) { + LinkedList<Exception> exceptions = null; + + ArrayList<TransactionListener> currentListeners; + synchronized (listeners) { + currentListeners = new ArrayList<TransactionListener>(listeners); + } + + for (TransactionListener listener : currentListeners) { + try { + listener.transactionEnd(application, request); + } catch (final RuntimeException t) { + if (exceptions == null) { + exceptions = new LinkedList<Exception>(); + } + exceptions.add(t); + } + } + + // If any runtime exceptions occurred, throw a combined exception + if (exceptions != null) { + final StringBuffer msg = new StringBuffer(); + for (Exception e : exceptions) { + if (msg.length() == 0) { + msg.append("\n\n--------------------------\n\n"); + } + msg.append(e.getMessage() + "\n"); + final StringWriter trace = new StringWriter(); + e.printStackTrace(new PrintWriter(trace, true)); + msg.append(trace.toString()); + } + throw new RuntimeException(msg.toString()); + } + } + + /** + * @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent) + */ + @Override + public void valueBound(HttpSessionBindingEvent arg0) { + // We are not interested in bindings + } + + /** + * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent) + */ + @Override + public void valueUnbound(HttpSessionBindingEvent event) { + // If we are going to be unbound from the session, the session must be + // closing + try { + while (!applications.isEmpty()) { + final Application app = applications.iterator().next(); + app.close(); + removeApplication(app); + } + } catch (Exception e) { + // This should never happen but is possible with rare + // configurations (e.g. robustness tests). If you have one + // thread doing HTTP socket write and another thread trying to + // remove same application here. Possible if you got e.g. session + // lifetime 1 min but socket write may take longer than 1 min. + // FIXME: Handle exception + getLogger().log(Level.SEVERE, + "Could not remove application, leaking memory.", e); + } + } + + /** + * Get the web browser associated with this application context. + * + * Because application context is related to the http session and server + * maintains one session per browser-instance, each context has exactly one + * web browser associated with it. + * + * @return + */ + public WebBrowser getBrowser() { + return browser; + } + + @Override + public Collection<Application> getApplications() { + return Collections.unmodifiableCollection(applications); + } + + protected void removeApplication(Application application) { + applications.remove(application); + applicationToAjaxAppMgrMap.remove(application); + } + + @Override + public String generateApplicationResourceURL(ApplicationResource resource, + String mapKey) { + + final String filename = resource.getFilename(); + if (filename == null) { + return ApplicationConnection.APP_PROTOCOL_PREFIX + + ApplicationConnection.APP_REQUEST_PATH + mapKey + "/"; + } else { + // #7738 At least Tomcat and JBoss refuses requests containing + // encoded slashes or backslashes in URLs. Application resource URLs + // should really be passed in another way than as part of the path + // in the future. + String encodedFileName = urlEncode(filename).replace("%2F", "/") + .replace("%5C", "\\"); + return ApplicationConnection.APP_PROTOCOL_PREFIX + + ApplicationConnection.APP_REQUEST_PATH + mapKey + "/" + + encodedFileName; + } + + } + + static String urlEncode(String filename) { + try { + return URLEncoder.encode(filename, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException( + "UTF-8 charset not available (\"this should never happen\")", + e); + } + } + + @Override + public boolean isApplicationResourceURL(URL context, String relativeUri) { + // If the relative uri is null, we are ready + if (relativeUri == null) { + return false; + } + + // Resolves the prefix + String prefix = relativeUri; + final int index = relativeUri.indexOf('/'); + if (index >= 0) { + prefix = relativeUri.substring(0, index); + } + + // Handles the resource requests + return (prefix.equals("APP")); + } + + @Override + public String getURLKey(URL context, String relativeUri) { + final int index = relativeUri.indexOf('/'); + final int next = relativeUri.indexOf('/', index + 1); + if (next < 0) { + return null; + } + return relativeUri.substring(index + 1, next); + } + + /** + * @return The total time spent servicing requests in this session. + */ + public long getTotalSessionTime() { + return totalSessionTime; + } + + /** + * Sets the time spent servicing the last request in the session and updates + * the total time spent servicing requests in this session. + * + * @param time + * the time spent in the last request. + */ + public void setLastRequestTime(long time) { + lastRequestTime = time; + totalSessionTime += time; + } + + /** + * @return the time spent servicing the last request in this session. + */ + public long getLastRequestTime() { + return lastRequestTime; + } + + private Logger getLogger() { + return Logger.getLogger(AbstractWebApplicationContext.class.getName()); + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContext.java b/server/src/com/vaadin/terminal/gwt/server/AddonContext.java new file mode 100644 index 0000000000..41e9046e22 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AddonContext.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.vaadin.Application; +import com.vaadin.event.EventRouter; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.tools.ReflectTools; + +public class AddonContext { + private static final Method APPLICATION_STARTED_METHOD = ReflectTools + .findMethod(ApplicationStartedListener.class, "applicationStarted", + ApplicationStartedEvent.class); + + private final DeploymentConfiguration deploymentConfiguration; + + private final EventRouter eventRouter = new EventRouter(); + + private List<BootstrapListener> bootstrapListeners = new ArrayList<BootstrapListener>(); + + private List<AddonContextListener> initedListeners = new ArrayList<AddonContextListener>(); + + public AddonContext(DeploymentConfiguration deploymentConfiguration) { + this.deploymentConfiguration = deploymentConfiguration; + deploymentConfiguration.setAddonContext(this); + } + + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + public void init() { + AddonContextEvent event = new AddonContextEvent(this); + Iterator<AddonContextListener> listeners = deploymentConfiguration + .getAddonContextListeners(); + while (listeners.hasNext()) { + AddonContextListener listener = listeners.next(); + listener.contextCreated(event); + initedListeners.add(listener); + } + } + + public void destroy() { + AddonContextEvent event = new AddonContextEvent(this); + for (AddonContextListener listener : initedListeners) { + listener.contextDestoryed(event); + } + } + + public void addBootstrapListener(BootstrapListener listener) { + bootstrapListeners.add(listener); + } + + public void applicationStarted(Application application) { + eventRouter.fireEvent(new ApplicationStartedEvent(this, application)); + for (BootstrapListener l : bootstrapListeners) { + application.addBootstrapListener(l); + } + } + + public void addApplicationStartedListener( + ApplicationStartedListener applicationStartListener) { + eventRouter.addListener(ApplicationStartedEvent.class, + applicationStartListener, APPLICATION_STARTED_METHOD); + } + + public void removeApplicationStartedListener( + ApplicationStartedListener applicationStartListener) { + eventRouter.removeListener(ApplicationStartedEvent.class, + applicationStartListener, APPLICATION_STARTED_METHOD); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java b/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java new file mode 100644 index 0000000000..33f681499f --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java @@ -0,0 +1,19 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventObject; + +public class AddonContextEvent extends EventObject { + + public AddonContextEvent(AddonContext source) { + super(source); + } + + public AddonContext getAddonContext() { + return (AddonContext) getSource(); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java b/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java new file mode 100644 index 0000000000..93e7df4ede --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java @@ -0,0 +1,13 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventListener; + +public interface AddonContextListener extends EventListener { + public void contextCreated(AddonContextEvent event); + + public void contextDestoryed(AddonContextEvent event); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java new file mode 100644 index 0000000000..788c48267e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.portlet.PortletConfig; +import javax.portlet.PortletException; + +import com.vaadin.Application; +import com.vaadin.terminal.gwt.server.ServletPortletHelper.ApplicationClassException; + +/** + * TODO Write documentation, fix JavaDoc tags. + * + * @author peholmst + */ +public class ApplicationPortlet2 extends AbstractApplicationPortlet { + + private Class<? extends Application> applicationClass; + + @Override + public void init(PortletConfig config) throws PortletException { + super.init(config); + try { + applicationClass = ServletPortletHelper + .getApplicationClass(getDeploymentConfiguration()); + } catch (ApplicationClassException e) { + throw new PortletException(e); + } + } + + @Override + protected Class<? extends Application> getApplicationClass() { + return applicationClass; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java new file mode 100644 index 0000000000..42726c933e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java @@ -0,0 +1,55 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; + +public class ApplicationResourceHandler implements RequestHandler { + private static final Pattern APP_RESOURCE_PATTERN = Pattern + .compile("^/?APP/(\\d+)/.*"); + + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + // Check for application resources + String requestPath = request.getRequestPathInfo(); + if (requestPath == null) { + return false; + } + Matcher resourceMatcher = APP_RESOURCE_PATTERN.matcher(requestPath); + + if (resourceMatcher.matches()) { + ApplicationResource resource = application + .getResource(resourceMatcher.group(1)); + if (resource != null) { + DownloadStream stream = resource.getStream(); + if (stream != null) { + stream.setCacheTime(resource.getCacheTime()); + stream.writeTo(response); + return true; + } + } + // We get here if the url looks like an application resource but no + // resource can be served + response.sendError(HttpServletResponse.SC_NOT_FOUND, + request.getRequestPathInfo() + " can not be found"); + return true; + } + + return false; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java new file mode 100644 index 0000000000..1af49e0da0 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java @@ -0,0 +1,78 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import com.vaadin.Application; +import com.vaadin.terminal.gwt.server.ServletPortletHelper.ApplicationClassException; + +/** + * This servlet connects a Vaadin Application to Web. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + +@SuppressWarnings("serial") +public class ApplicationServlet extends AbstractApplicationServlet { + + // Private fields + private Class<? extends Application> applicationClass; + + /** + * Called by the servlet container to indicate to a servlet that the servlet + * is being placed into service. + * + * @param servletConfig + * the object containing the servlet's configuration and + * initialization parameters + * @throws javax.servlet.ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + @Override + public void init(javax.servlet.ServletConfig servletConfig) + throws javax.servlet.ServletException { + super.init(servletConfig); + + // Loads the application class using the classloader defined in the + // deployment configuration + + try { + applicationClass = ServletPortletHelper + .getApplicationClass(getDeploymentConfiguration()); + } catch (ApplicationClassException e) { + throw new ServletException(e); + } + } + + @Override + protected Application getNewApplication(HttpServletRequest request) + throws ServletException { + + // Creates a new application instance + try { + final Application application = getApplicationClass().newInstance(); + + return application; + } catch (final IllegalAccessException e) { + throw new ServletException("getNewApplication failed", e); + } catch (final InstantiationException e) { + throw new ServletException("getNewApplication failed", e); + } catch (ClassNotFoundException e) { + throw new ServletException("getNewApplication failed", e); + } + } + + @Override + protected Class<? extends Application> getApplicationClass() + throws ClassNotFoundException { + return applicationClass; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java new file mode 100644 index 0000000000..339b88222e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventObject; + +import com.vaadin.Application; + +public class ApplicationStartedEvent extends EventObject { + private final Application application; + + public ApplicationStartedEvent(AddonContext context, + Application application) { + super(context); + this.application = application; + } + + public AddonContext getContext() { + return (AddonContext) getSource(); + } + + public Application getApplication() { + return application; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java new file mode 100644 index 0000000000..87884a0fda --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java @@ -0,0 +1,11 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventListener; + +public interface ApplicationStartedListener extends EventListener { + public void applicationStarted(ApplicationStartedEvent event); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java new file mode 100644 index 0000000000..4731a5b79f --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +public class BootstrapDom { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java new file mode 100644 index 0000000000..bcf098b5aa --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.List; + +import org.jsoup.nodes.Node; + +import com.vaadin.Application; +import com.vaadin.terminal.WrappedRequest; + +public class BootstrapFragmentResponse extends BootstrapResponse { + private final List<Node> fragmentNodes; + + public BootstrapFragmentResponse(BootstrapHandler handler, + WrappedRequest request, List<Node> fragmentNodes, + Application application, Integer rootId) { + super(handler, request, application, rootId); + this.fragmentNodes = fragmentNodes; + } + + public List<Node> getFragmentNodes() { + return fragmentNodes; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java new file mode 100644 index 0000000000..e89737337b --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java @@ -0,0 +1,570 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import org.jsoup.nodes.DataNode; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.DocumentType; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.parser.Tag; + +import com.vaadin.Application; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.Version; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.Root; + +public abstract class BootstrapHandler implements RequestHandler { + + protected class BootstrapContext implements Serializable { + + private final WrappedResponse response; + private final BootstrapFragmentResponse bootstrapResponse; + + private String widgetsetName; + private String themeName; + private String appId; + + public BootstrapContext(WrappedResponse response, + BootstrapFragmentResponse bootstrapResponse) { + this.response = response; + this.bootstrapResponse = bootstrapResponse; + } + + public WrappedResponse getResponse() { + return response; + } + + public WrappedRequest getRequest() { + return bootstrapResponse.getRequest(); + } + + public Application getApplication() { + return bootstrapResponse.getApplication(); + } + + public Integer getRootId() { + return bootstrapResponse.getRootId(); + } + + public Root getRoot() { + return bootstrapResponse.getRoot(); + } + + public String getWidgetsetName() { + if (widgetsetName == null) { + Root root = getRoot(); + if (root != null) { + widgetsetName = getWidgetsetForRoot(this); + } + } + return widgetsetName; + } + + public String getThemeName() { + if (themeName == null) { + Root root = getRoot(); + if (root != null) { + themeName = findAndEscapeThemeName(this); + } + } + return themeName; + } + + public String getAppId() { + if (appId == null) { + appId = getApplicationId(this); + } + return appId; + } + + public BootstrapFragmentResponse getBootstrapResponse() { + return bootstrapResponse; + } + + } + + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + + // TODO Should all urls be handled here? + Integer rootId = null; + try { + Root root = application.getRootForRequest(request); + if (root == null) { + writeError(response, new Throwable("No Root found")); + return true; + } + + rootId = Integer.valueOf(root.getRootId()); + } catch (RootRequiresMoreInformationException e) { + // Just keep going without rootId + } + + try { + BootstrapContext context = createContext(request, response, + application, rootId); + setupMainDiv(context); + + BootstrapFragmentResponse fragmentResponse = context + .getBootstrapResponse(); + application.modifyBootstrapResponse(fragmentResponse); + + String html = getBootstrapHtml(context); + + writeBootstrapPage(response, html); + } catch (JSONException e) { + writeError(response, e); + } + + return true; + } + + private String getBootstrapHtml(BootstrapContext context) { + WrappedRequest request = context.getRequest(); + WrappedResponse response = context.getResponse(); + DeploymentConfiguration deploymentConfiguration = request + .getDeploymentConfiguration(); + + BootstrapFragmentResponse fragmentResponse = context + .getBootstrapResponse(); + + if (deploymentConfiguration.isStandalone(request)) { + Map<String, Object> headers = new LinkedHashMap<String, Object>(); + Document document = Document.createShell(""); + BootstrapPageResponse pageResponse = new BootstrapPageResponse( + this, request, document, headers, context.getApplication(), + context.getRootId()); + List<Node> fragmentNodes = fragmentResponse.getFragmentNodes(); + Element body = document.body(); + for (Node node : fragmentNodes) { + body.appendChild(node); + } + + setupStandaloneDocument(context, pageResponse); + context.getApplication().modifyBootstrapResponse(pageResponse); + + sendBootstrapHeaders(response, headers); + + return document.outerHtml(); + } else { + StringBuilder sb = new StringBuilder(); + for (Node node : fragmentResponse.getFragmentNodes()) { + if (sb.length() != 0) { + sb.append('\n'); + } + sb.append(node.outerHtml()); + } + + return sb.toString(); + } + } + + private void sendBootstrapHeaders(WrappedResponse response, + Map<String, Object> headers) { + Set<Entry<String, Object>> entrySet = headers.entrySet(); + for (Entry<String, Object> header : entrySet) { + Object value = header.getValue(); + if (value instanceof String) { + response.setHeader(header.getKey(), (String) value); + } else if (value instanceof Long) { + response.setDateHeader(header.getKey(), + ((Long) value).longValue()); + } else { + throw new RuntimeException("Unsupported header value: " + value); + } + } + } + + private void writeBootstrapPage(WrappedResponse response, String html) + throws IOException { + response.setContentType("text/html"); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + response.getOutputStream(), "UTF-8")); + writer.append(html); + writer.close(); + } + + private void setupStandaloneDocument(BootstrapContext context, + BootstrapPageResponse response) { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + + Document document = response.getDocument(); + + DocumentType doctype = new DocumentType("html", + "-//W3C//DTD XHTML 1.0 Transitional//EN", + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd", + document.baseUri()); + document.child(0).before(doctype); + document.body().parent().attr("xmlns", "http://www.w3.org/1999/xhtml"); + + Element head = document.head(); + head.appendElement("meta").attr("http-equiv", "Content-Type") + .attr("content", "text/html; charset=utf-8"); + + // Chrome frame in all versions of IE (only if Chrome frame is + // installed) + head.appendElement("meta").attr("http-equiv", "X-UA-Compatible") + .attr("content", "chrome=1"); + + Root root = context.getRoot(); + String title = ((root == null || root.getCaption() == null) ? "" : root + .getCaption()); + head.appendElement("title").appendText(title); + + head.appendElement("style").attr("type", "text/css") + .appendText("html, body {height:100%;margin:0;}"); + + // Add favicon links + String themeName = context.getThemeName(); + if (themeName != null) { + String themeUri = getThemeUri(context, themeName); + head.appendElement("link").attr("rel", "shortcut icon") + .attr("type", "image/vnd.microsoft.icon") + .attr("href", themeUri + "/favicon.ico"); + head.appendElement("link").attr("rel", "icon") + .attr("type", "image/vnd.microsoft.icon") + .attr("href", themeUri + "/favicon.ico"); + } + + Element body = document.body(); + body.attr("scroll", "auto"); + body.addClass(ApplicationConnection.GENERATED_BODY_CLASSNAME); + } + + public BootstrapContext createContext(WrappedRequest request, + WrappedResponse response, Application application, Integer rootId) { + BootstrapContext context = new BootstrapContext(response, + new BootstrapFragmentResponse(this, request, + new ArrayList<Node>(), application, rootId)); + return context; + } + + protected String getMainDivStyle(BootstrapContext context) { + return null; + } + + /** + * Creates and returns a unique ID for the DIV where the application is to + * be rendered. + * + * @param context + * + * @return the id to use in the DOM + */ + protected abstract String getApplicationId(BootstrapContext context); + + public String getWidgetsetForRoot(BootstrapContext context) { + Root root = context.getRoot(); + WrappedRequest request = context.getRequest(); + + String widgetset = root.getApplication().getWidgetsetForRoot(root); + if (widgetset == null) { + widgetset = request.getDeploymentConfiguration() + .getConfiguredWidgetset(request); + } + + widgetset = AbstractApplicationServlet.stripSpecialChars(widgetset); + return widgetset; + } + + /** + * Method to write the div element into which that actual Vaadin application + * is rendered. + * <p> + * Override this method if you want to add some custom html around around + * the div element into which the actual Vaadin application will be + * rendered. + * + * @param context + * + * @throws IOException + * @throws JSONException + */ + private void setupMainDiv(BootstrapContext context) throws IOException, + JSONException { + String style = getMainDivStyle(context); + + /*- Add classnames; + * .v-app + * .v-app-loading + * .v-app-<simpleName for app class> + *- Additionally added from javascript: + * .v-theme-<themeName, remove non-alphanum> + */ + + String appClass = "v-app-" + + context.getApplication().getClass().getSimpleName(); + + String classNames = "v-app " + appClass; + List<Node> fragmentNodes = context.getBootstrapResponse() + .getFragmentNodes(); + + Element mainDiv = new Element(Tag.valueOf("div"), ""); + mainDiv.attr("id", context.getAppId()); + mainDiv.addClass(classNames); + if (style != null && style.length() != 0) { + mainDiv.attr("style", style); + } + mainDiv.appendElement("div").addClass("v-app-loading"); + mainDiv.appendElement("noscript") + .append("You have to enable javascript in your browser to use an application built with Vaadin."); + fragmentNodes.add(mainDiv); + + WrappedRequest request = context.getRequest(); + + DeploymentConfiguration deploymentConfiguration = request + .getDeploymentConfiguration(); + String staticFileLocation = deploymentConfiguration + .getStaticFileLocation(request); + + fragmentNodes + .add(new Element(Tag.valueOf("iframe"), "") + .attr("tabIndex", "-1") + .attr("id", "__gwt_historyFrame") + .attr("style", + "position:absolute;width:0;height:0;border:0;overflow:hidden") + .attr("src", "javascript:false")); + + String bootstrapLocation = staticFileLocation + + "/VAADIN/vaadinBootstrap.js"; + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type", + "text/javascript").attr("src", bootstrapLocation)); + Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript"); + + StringBuilder builder = new StringBuilder(); + builder.append("//<![CDATA[\n"); + builder.append("if (!window.vaadin) alert(" + + JSONObject.quote("Failed to load the bootstrap javascript: " + + bootstrapLocation) + ");\n"); + + appendMainScriptTagContents(context, builder); + + builder.append("//]]>"); + mainScriptTag.appendChild(new DataNode(builder.toString(), + mainScriptTag.baseUri())); + fragmentNodes.add(mainScriptTag); + + } + + protected void appendMainScriptTagContents(BootstrapContext context, + StringBuilder builder) throws JSONException, IOException { + JSONObject defaults = getDefaultParameters(context); + JSONObject appConfig = getApplicationParameters(context); + + boolean isDebug = !context.getApplication().isProductionMode(); + + builder.append("vaadin.setDefaults("); + appendJsonObject(builder, defaults, isDebug); + builder.append(");\n"); + + builder.append("vaadin.initApplication(\""); + builder.append(context.getAppId()); + builder.append("\","); + appendJsonObject(builder, appConfig, isDebug); + builder.append(");\n"); + } + + private static void appendJsonObject(StringBuilder builder, + JSONObject jsonObject, boolean isDebug) throws JSONException { + if (isDebug) { + builder.append(jsonObject.toString(4)); + } else { + builder.append(jsonObject.toString()); + } + } + + protected JSONObject getApplicationParameters(BootstrapContext context) + throws JSONException, PaintException { + Application application = context.getApplication(); + Integer rootId = context.getRootId(); + + JSONObject appConfig = new JSONObject(); + + if (rootId != null) { + appConfig.put(ApplicationConnection.ROOT_ID_PARAMETER, rootId); + } + + if (context.getThemeName() != null) { + appConfig.put("themeUri", + getThemeUri(context, context.getThemeName())); + } + + JSONObject versionInfo = new JSONObject(); + versionInfo.put("vaadinVersion", Version.getFullVersion()); + versionInfo.put("applicationVersion", application.getVersion()); + appConfig.put("versionInfo", versionInfo); + + appConfig.put("widgetset", context.getWidgetsetName()); + + if (rootId == null || application.isRootInitPending(rootId.intValue())) { + appConfig.put("initialPath", context.getRequest() + .getRequestPathInfo()); + + Map<String, String[]> parameterMap = context.getRequest() + .getParameterMap(); + appConfig.put("initialParams", parameterMap); + } else { + // write the initial UIDL into the config + appConfig.put("uidl", + getInitialUIDL(context.getRequest(), context.getRoot())); + } + + return appConfig; + } + + protected JSONObject getDefaultParameters(BootstrapContext context) + throws JSONException { + JSONObject defaults = new JSONObject(); + + WrappedRequest request = context.getRequest(); + Application application = context.getApplication(); + + // Get system messages + Application.SystemMessages systemMessages = AbstractApplicationServlet + .getSystemMessages(application.getClass()); + if (systemMessages != null) { + // Write the CommunicationError -message to client + JSONObject comErrMsg = new JSONObject(); + comErrMsg.put("caption", + systemMessages.getCommunicationErrorCaption()); + comErrMsg.put("message", + systemMessages.getCommunicationErrorMessage()); + comErrMsg.put("url", systemMessages.getCommunicationErrorURL()); + + defaults.put("comErrMsg", comErrMsg); + + JSONObject authErrMsg = new JSONObject(); + authErrMsg.put("caption", + systemMessages.getAuthenticationErrorCaption()); + authErrMsg.put("message", + systemMessages.getAuthenticationErrorMessage()); + authErrMsg.put("url", systemMessages.getAuthenticationErrorURL()); + + defaults.put("authErrMsg", authErrMsg); + } + + DeploymentConfiguration deploymentConfiguration = request + .getDeploymentConfiguration(); + String staticFileLocation = deploymentConfiguration + .getStaticFileLocation(request); + String widgetsetBase = staticFileLocation + "/" + + AbstractApplicationServlet.WIDGETSET_DIRECTORY_PATH; + defaults.put("widgetsetBase", widgetsetBase); + + if (!application.isProductionMode()) { + defaults.put("debug", true); + } + + if (deploymentConfiguration.isStandalone(request)) { + defaults.put("standalone", true); + } + + defaults.put("appUri", getAppUri(context)); + + return defaults; + } + + protected abstract String getAppUri(BootstrapContext context); + + /** + * Get the URI for the application theme. + * + * A portal-wide default theme is fetched from the portal shared resource + * directory (if any), other themes from the portlet. + * + * @param context + * @param themeName + * + * @return + */ + public String getThemeUri(BootstrapContext context, String themeName) { + WrappedRequest request = context.getRequest(); + final String staticFilePath = request.getDeploymentConfiguration() + .getStaticFileLocation(request); + return staticFilePath + "/" + + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName; + } + + /** + * Override if required + * + * @param context + * @return + */ + public String getThemeName(BootstrapContext context) { + return context.getApplication().getThemeForRoot(context.getRoot()); + } + + /** + * Don not override. + * + * @param context + * @return + */ + public String findAndEscapeThemeName(BootstrapContext context) { + String themeName = getThemeName(context); + if (themeName == null) { + WrappedRequest request = context.getRequest(); + themeName = request.getDeploymentConfiguration() + .getConfiguredTheme(request); + } + + // XSS preventation, theme names shouldn't contain special chars anyway. + // The servlet denies them via url parameter. + themeName = AbstractApplicationServlet.stripSpecialChars(themeName); + + return themeName; + } + + protected void writeError(WrappedResponse response, Throwable e) + throws IOException { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + e.getLocalizedMessage()); + } + + /** + * Gets the initial UIDL message to send to the client. + * + * @param request + * the originating request + * @param root + * the root for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws PaintException + * if an exception occurs while painting the components + * @throws JSONException + * if an exception occurs while formatting the output + */ + protected abstract String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException; + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java new file mode 100644 index 0000000000..d80e626cc1 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java @@ -0,0 +1,13 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventListener; + +public interface BootstrapListener extends EventListener { + public void modifyBootstrapFragment(BootstrapFragmentResponse response); + + public void modifyBootstrapPage(BootstrapPageResponse response); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java new file mode 100644 index 0000000000..802238ac62 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java @@ -0,0 +1,39 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.Map; + +import org.jsoup.nodes.Document; + +import com.vaadin.Application; +import com.vaadin.terminal.WrappedRequest; + +public class BootstrapPageResponse extends BootstrapResponse { + + private final Map<String, Object> headers; + private final Document document; + + public BootstrapPageResponse(BootstrapHandler handler, + WrappedRequest request, Document document, + Map<String, Object> headers, Application application, Integer rootId) { + super(handler, request, application, rootId); + this.headers = headers; + this.document = document; + } + + public void setHeader(String name, String value) { + headers.put(name, value); + } + + public void setDateHeader(String name, long timestamp) { + headers.put(name, Long.valueOf(timestamp)); + } + + public Document getDocument() { + return document; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java new file mode 100644 index 0000000000..88bd58593d --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java @@ -0,0 +1,45 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventObject; + +import com.vaadin.Application; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.ui.Root; + +public abstract class BootstrapResponse extends EventObject { + private final WrappedRequest request; + private final Application application; + private final Integer rootId; + + public BootstrapResponse(BootstrapHandler handler, WrappedRequest request, + Application application, Integer rootId) { + super(handler); + this.request = request; + this.application = application; + this.rootId = rootId; + } + + public BootstrapHandler getBootstrapHandler() { + return (BootstrapHandler) getSource(); + } + + public WrappedRequest getRequest() { + return request; + } + + public Application getApplication() { + return application; + } + + public Integer getRootId() { + return rootId; + } + + public Root getRoot() { + return Root.getCurrent(); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java b/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java new file mode 100644 index 0000000000..8f0c80332f --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java @@ -0,0 +1,39 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.util.Map; + +import com.vaadin.ui.AbstractComponent.ComponentErrorEvent; +import com.vaadin.ui.Component; + +@SuppressWarnings("serial") +public class ChangeVariablesErrorEvent implements ComponentErrorEvent { + + private Throwable throwable; + private Component component; + + private Map<String, Object> variableChanges; + + public ChangeVariablesErrorEvent(Component component, Throwable throwable, + Map<String, Object> variableChanges) { + this.component = component; + this.throwable = throwable; + this.variableChanges = variableChanges; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + public Component getComponent() { + return component; + } + + public Map<String, Object> getVariableChanges() { + return variableChanges; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java b/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java new file mode 100644 index 0000000000..4f74cfe4bb --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java @@ -0,0 +1,149 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.util.Collection; +import java.util.List; + +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.Extension; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.Root; + +/** + * Interface implemented by all connectors that are capable of communicating + * with the client side + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public interface ClientConnector extends Connector, RpcTarget { + /** + * Returns the list of pending server to client RPC calls and clears the + * list. + * + * @return an unmodifiable ordered list of pending server to client method + * calls (not null) + */ + public List<ClientMethodInvocation> retrievePendingRpcCalls(); + + /** + * Checks if the communicator is enabled. An enabled communicator is allowed + * to receive messages from its counter-part. + * + * @return true if the connector can receive messages, false otherwise + */ + public boolean isConnectorEnabled(); + + /** + * Returns the type of the shared state for this connector + * + * @return The type of the state. Must never return null. + */ + public Class<? extends SharedState> getStateType(); + + @Override + public ClientConnector getParent(); + + /** + * Requests that the connector should be repainted as soon as possible. + */ + public void requestRepaint(); + + /** + * Causes a repaint of this connector, and all connectors below it. + * + * This should only be used in special cases, e.g when the state of a + * descendant depends on the state of an ancestor. + */ + public void requestRepaintAll(); + + /** + * Sets the parent connector of the connector. + * + * <p> + * This method automatically calls {@link #attach()} if the connector + * becomes attached to the application, regardless of whether it was + * attached previously. Conversely, if the parent is {@code null} and the + * connector is attached to the application, {@link #detach()} is called for + * the connector. + * </p> + * <p> + * This method is rarely called directly. One of the + * {@link ComponentContainer#addComponent(Component)} or + * {@link AbstractClientConnector#addExtension(Extension)} methods are + * normally used for adding connectors to a parent and they will call this + * method implicitly. + * </p> + * + * <p> + * It is not possible to change the parent without first setting the parent + * to {@code null}. + * </p> + * + * @param parent + * the parent connector + * @throws IllegalStateException + * if a parent is given even though the connector already has a + * parent + */ + public void setParent(ClientConnector parent); + + /** + * Notifies the connector that it is connected to an application. + * + * <p> + * The caller of this method is {@link #setParent(ClientConnector)} if the + * parent is itself already attached to the application. If not, the parent + * will call the {@link #attach()} for all its children when it is attached + * to the application. This method is always called before the connector's + * data is sent to the client-side for the first time. + * </p> + * + * <p> + * The attachment logic is implemented in {@link AbstractClientConnector}. + * </p> + */ + public void attach(); + + /** + * Notifies the component that it is detached from the application. + * + * <p> + * The caller of this method is {@link #setParent(ClientConnector)} if the + * parent is in the application. When the parent is detached from the + * application it is its response to call {@link #detach()} for all the + * children and to detach itself from the terminal. + * </p> + */ + public void detach(); + + /** + * Get a read-only collection of all extensions attached to this connector. + * + * @return a collection of extensions + */ + public Collection<Extension> getExtensions(); + + /** + * Remove an extension from this connector. + * + * @param extension + * the extension to remove. + */ + public void removeExtension(Extension extension); + + /** + * Returns the root this connector is attached to + * + * @return The Root this connector is attached to or null if it is not + * attached to any Root + */ + public Root getRoot(); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java b/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java new file mode 100644 index 0000000000..64ea288665 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java @@ -0,0 +1,71 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * Internal class for keeping track of pending server to client method + * invocations for a Connector. + * + * @since 7.0 + */ +public class ClientMethodInvocation implements Serializable, + Comparable<ClientMethodInvocation> { + private final ClientConnector connector; + private final String interfaceName; + private final String methodName; + private final Object[] parameters; + private Type[] parameterTypes; + + // used for sorting calls between different connectors in the same Root + private final long sequenceNumber; + // TODO may cause problems when clustering etc. + private static long counter = 0; + + public ClientMethodInvocation(ClientConnector connector, + String interfaceName, Method method, Object[] parameters) { + this.connector = connector; + this.interfaceName = interfaceName; + methodName = method.getName(); + parameterTypes = method.getGenericParameterTypes(); + this.parameters = (null != parameters) ? parameters : new Object[0]; + sequenceNumber = ++counter; + } + + public Type[] getParameterTypes() { + return parameterTypes; + } + + public ClientConnector getConnector() { + return connector; + } + + public String getInterfaceName() { + return interfaceName; + } + + public String getMethodName() { + return methodName; + } + + public Object[] getParameters() { + return parameters; + } + + protected long getSequenceNumber() { + return sequenceNumber; + } + + @Override + public int compareTo(ClientMethodInvocation o) { + if (null == o) { + return 0; + } + return Long.signum(getSequenceNumber() - o.getSequenceNumber()); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java new file mode 100644 index 0000000000..3cc3a8cb64 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java @@ -0,0 +1,122 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.InputStream; +import java.net.URL; + +import javax.servlet.ServletContext; + +import com.vaadin.Application; +import com.vaadin.external.json.JSONException; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.ui.Root; + +/** + * Application manager processes changes and paints for single application + * instance. + * + * This class handles applications running as servlets. + * + * @see AbstractCommunicationManager + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class CommunicationManager extends AbstractCommunicationManager { + + /** + * @deprecated use {@link #CommunicationManager(Application)} instead + * @param application + * @param applicationServlet + */ + @Deprecated + public CommunicationManager(Application application, + AbstractApplicationServlet applicationServlet) { + super(application); + } + + /** + * TODO New constructor - document me! + * + * @param application + */ + public CommunicationManager(Application application) { + super(application); + } + + @Override + protected BootstrapHandler createBootstrapHandler() { + return new BootstrapHandler() { + @Override + protected String getApplicationId(BootstrapContext context) { + String appUrl = getAppUri(context); + + String appId = appUrl; + if ("".equals(appUrl)) { + appId = "ROOT"; + } + appId = appId.replaceAll("[^a-zA-Z0-9]", ""); + // Add hashCode to the end, so that it is still (sort of) + // predictable, but indicates that it should not be used in CSS + // and + // such: + int hashCode = appId.hashCode(); + if (hashCode < 0) { + hashCode = -hashCode; + } + appId = appId + "-" + hashCode; + return appId; + } + + @Override + protected String getAppUri(BootstrapContext context) { + /* Fetch relative url to application */ + // don't use server and port in uri. It may cause problems with + // some + // virtual server configurations which lose the server name + Application application = context.getApplication(); + URL url = application.getURL(); + String appUrl = url.getPath(); + if (appUrl.endsWith("/")) { + appUrl = appUrl.substring(0, appUrl.length() - 1); + } + return appUrl; + } + + @Override + public String getThemeName(BootstrapContext context) { + String themeName = context.getRequest().getParameter( + AbstractApplicationServlet.URL_PARAMETER_THEME); + if (themeName == null) { + themeName = super.getThemeName(context); + } + return themeName; + } + + @Override + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException { + return CommunicationManager.this.getInitialUIDL(request, root); + } + }; + } + + @Override + protected InputStream getThemeResourceAsStream(Root root, String themeName, + String resource) { + WebApplicationContext context = (WebApplicationContext) root + .getApplication().getContext(); + ServletContext servletContext = context.getHttpSession() + .getServletContext(); + return servletContext.getResourceAsStream("/" + + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName + + "/" + resource); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java b/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java new file mode 100644 index 0000000000..171d440796 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java @@ -0,0 +1,664 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.Vector; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.terminal.Sizeable.Unit; +import com.vaadin.ui.AbstractOrderedLayout; +import com.vaadin.ui.AbstractSplitPanel; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.Form; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.GridLayout.Area; +import com.vaadin.ui.Layout; +import com.vaadin.ui.Panel; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +@SuppressWarnings({ "serial", "deprecation" }) +public class ComponentSizeValidator implements Serializable { + + private final static int LAYERS_SHOWN = 4; + + /** + * Recursively checks given component and its subtree for invalid layout + * setups. Prints errors to std err stream. + * + * @param component + * component to check + * @return set of first level errors found + */ + public static List<InvalidLayout> validateComponentRelativeSizes( + Component component, List<InvalidLayout> errors, + InvalidLayout parent) { + + boolean invalidHeight = !checkHeights(component); + boolean invalidWidth = !checkWidths(component); + + if (invalidHeight || invalidWidth) { + InvalidLayout error = new InvalidLayout(component, invalidHeight, + invalidWidth); + if (parent != null) { + parent.addError(error); + } else { + if (errors == null) { + errors = new LinkedList<InvalidLayout>(); + } + errors.add(error); + } + parent = error; + } + + if (component instanceof Panel) { + Panel panel = (Panel) component; + errors = validateComponentRelativeSizes(panel.getContent(), errors, + parent); + } else if (component instanceof ComponentContainer) { + ComponentContainer lo = (ComponentContainer) component; + Iterator<Component> it = lo.getComponentIterator(); + while (it.hasNext()) { + errors = validateComponentRelativeSizes(it.next(), errors, + parent); + } + } else if (component instanceof Form) { + Form form = (Form) component; + if (form.getLayout() != null) { + errors = validateComponentRelativeSizes(form.getLayout(), + errors, parent); + } + if (form.getFooter() != null) { + errors = validateComponentRelativeSizes(form.getFooter(), + errors, parent); + } + } + + return errors; + } + + private static void printServerError(String msg, + Stack<ComponentInfo> attributes, boolean widthError, + PrintStream errorStream) { + StringBuffer err = new StringBuffer(); + err.append("Vaadin DEBUG\n"); + + StringBuilder indent = new StringBuilder(""); + ComponentInfo ci; + if (attributes != null) { + while (attributes.size() > LAYERS_SHOWN) { + attributes.pop(); + } + while (!attributes.empty()) { + ci = attributes.pop(); + showComponent(ci.component, ci.info, err, indent, widthError); + } + } + + err.append("Layout problem detected: "); + err.append(msg); + err.append("\n"); + err.append("Relative sizes were replaced by undefined sizes, components may not render as expected.\n"); + errorStream.println(err); + + } + + public static boolean checkHeights(Component component) { + try { + if (!hasRelativeHeight(component)) { + return true; + } + if (component instanceof Window) { + return true; + } + if (component.getParent() == null) { + return true; + } + + return parentCanDefineHeight(component); + } catch (Exception e) { + getLogger().log(Level.FINER, + "An exception occurred while validating sizes.", e); + return true; + } + } + + public static boolean checkWidths(Component component) { + try { + if (!hasRelativeWidth(component)) { + return true; + } + if (component instanceof Window) { + return true; + } + if (component.getParent() == null) { + return true; + } + + return parentCanDefineWidth(component); + } catch (Exception e) { + getLogger().log(Level.FINER, + "An exception occurred while validating sizes.", e); + return true; + } + } + + public static class InvalidLayout implements Serializable { + + private final Component component; + + private final boolean invalidHeight; + private final boolean invalidWidth; + + private final Vector<InvalidLayout> subErrors = new Vector<InvalidLayout>(); + + public InvalidLayout(Component component, boolean height, boolean width) { + this.component = component; + invalidHeight = height; + invalidWidth = width; + } + + public void addError(InvalidLayout error) { + subErrors.add(error); + } + + public void reportErrors(PrintWriter clientJSON, + AbstractCommunicationManager communicationManager, + PrintStream serverErrorStream) { + clientJSON.write("{"); + + Component parent = component.getParent(); + String paintableId = component.getConnectorId(); + + clientJSON.print("id:\"" + paintableId + "\""); + + if (invalidHeight) { + Stack<ComponentInfo> attributes = null; + String msg = ""; + // set proper error messages + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean vertical = false; + + if (ol instanceof VerticalLayout) { + vertical = true; + } + + if (vertical) { + msg = "Component with relative height inside a VerticalLayout with no height defined."; + attributes = getHeightAttributes(component); + } else { + msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined"; + attributes = getHeightAttributes(component); + } + } else if (parent instanceof GridLayout) { + msg = "At least one of the GridLayout's components in each row should have non relative height if the height of the layout is not defined."; + attributes = getHeightAttributes(component); + } else { + // default error for non sized parent issue + msg = "A component with relative height needs a parent with defined height."; + attributes = getHeightAttributes(component); + } + printServerError(msg, attributes, false, serverErrorStream); + clientJSON.print(",\"heightMsg\":\"" + msg + "\""); + } + if (invalidWidth) { + Stack<ComponentInfo> attributes = null; + String msg = ""; + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean horizontal = true; + + if (ol instanceof VerticalLayout) { + horizontal = false; + } + + if (horizontal) { + msg = "Component with relative width inside a HorizontalLayout with no width defined"; + attributes = getWidthAttributes(component); + } else { + msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined"; + attributes = getWidthAttributes(component); + } + } else if (parent instanceof GridLayout) { + msg = "At least one of the GridLayout's components in each column should have non relative width if the width of the layout is not defined."; + attributes = getWidthAttributes(component); + } else { + // default error for non sized parent issue + msg = "A component with relative width needs a parent with defined width."; + attributes = getWidthAttributes(component); + } + clientJSON.print(",\"widthMsg\":\"" + msg + "\""); + printServerError(msg, attributes, true, serverErrorStream); + } + if (subErrors.size() > 0) { + serverErrorStream.println("Sub errors >>"); + clientJSON.write(", \"subErrors\" : ["); + boolean first = true; + for (InvalidLayout subError : subErrors) { + if (!first) { + clientJSON.print(","); + } else { + first = false; + } + subError.reportErrors(clientJSON, communicationManager, + serverErrorStream); + } + clientJSON.write("]"); + serverErrorStream.println("<< Sub erros"); + } + clientJSON.write("}"); + } + } + + private static class ComponentInfo implements Serializable { + Component component; + String info; + + public ComponentInfo(Component component, String info) { + this.component = component; + this.info = info; + } + + } + + private static Stack<ComponentInfo> getHeightAttributes(Component component) { + Stack<ComponentInfo> attributes = new Stack<ComponentInfo>(); + attributes + .add(new ComponentInfo(component, getHeightString(component))); + Component parent = component.getParent(); + attributes.add(new ComponentInfo(parent, getHeightString(parent))); + + while ((parent = parent.getParent()) != null) { + attributes.add(new ComponentInfo(parent, getHeightString(parent))); + } + + return attributes; + } + + private static Stack<ComponentInfo> getWidthAttributes(Component component) { + Stack<ComponentInfo> attributes = new Stack<ComponentInfo>(); + attributes.add(new ComponentInfo(component, getWidthString(component))); + Component parent = component.getParent(); + attributes.add(new ComponentInfo(parent, getWidthString(parent))); + + while ((parent = parent.getParent()) != null) { + attributes.add(new ComponentInfo(parent, getWidthString(parent))); + } + + return attributes; + } + + private static String getWidthString(Component component) { + String width = "width: "; + if (hasRelativeWidth(component)) { + width += "RELATIVE, " + component.getWidth() + " %"; + } else if (component instanceof Window && component.getParent() == null) { + width += "MAIN WINDOW"; + } else if (component.getWidth() >= 0) { + width += "ABSOLUTE, " + component.getWidth() + " " + + component.getWidthUnits().getSymbol(); + } else { + width += "UNDEFINED"; + } + + return width; + } + + private static String getHeightString(Component component) { + String height = "height: "; + if (hasRelativeHeight(component)) { + height += "RELATIVE, " + component.getHeight() + " %"; + } else if (component instanceof Window && component.getParent() == null) { + height += "MAIN WINDOW"; + } else if (component.getHeight() > 0) { + height += "ABSOLUTE, " + component.getHeight() + " " + + component.getHeightUnits().getSymbol(); + } else { + height += "UNDEFINED"; + } + + return height; + } + + private static void showComponent(Component component, String attribute, + StringBuffer err, StringBuilder indent, boolean widthError) { + + FileLocation createLoc = creationLocations.get(component); + + FileLocation sizeLoc; + if (widthError) { + sizeLoc = widthLocations.get(component); + } else { + sizeLoc = heightLocations.get(component); + } + + err.append(indent); + indent.append(" "); + err.append("- "); + + err.append(component.getClass().getSimpleName()); + err.append("/").append(Integer.toHexString(component.hashCode())); + + if (component.getCaption() != null) { + err.append(" \""); + err.append(component.getCaption()); + err.append("\""); + } + + if (component.getDebugId() != null) { + err.append(" debugId: "); + err.append(component.getDebugId()); + } + + if (createLoc != null) { + err.append(", created at (" + createLoc.file + ":" + + createLoc.lineNumber + ")"); + + } + + if (attribute != null) { + err.append(" ("); + err.append(attribute); + if (sizeLoc != null) { + err.append(", set at (" + sizeLoc.file + ":" + + sizeLoc.lineNumber + ")"); + } + + err.append(")"); + } + err.append("\n"); + + } + + private static boolean hasNonRelativeHeightComponent( + AbstractOrderedLayout ol) { + Iterator<Component> it = ol.getComponentIterator(); + while (it.hasNext()) { + if (!hasRelativeHeight(it.next())) { + return true; + } + } + return false; + } + + public static boolean parentCanDefineHeight(Component component) { + Component parent = component.getParent(); + if (parent == null) { + // main window, valid situation + return true; + } + if (parent.getHeight() < 0) { + // Undefined height + if (parent instanceof Window) { + // Sub window with undefined size has a min-height + return true; + } + + if (parent instanceof AbstractOrderedLayout) { + boolean horizontal = true; + if (parent instanceof VerticalLayout) { + horizontal = false; + } + if (horizontal + && hasNonRelativeHeightComponent((AbstractOrderedLayout) parent)) { + return true; + } else { + return false; + } + + } else if (parent instanceof GridLayout) { + GridLayout gl = (GridLayout) parent; + Area componentArea = gl.getComponentArea(component); + boolean rowHasHeight = false; + for (int row = componentArea.getRow1(); !rowHasHeight + && row <= componentArea.getRow2(); row++) { + for (int column = 0; !rowHasHeight + && column < gl.getColumns(); column++) { + Component c = gl.getComponent(column, row); + if (c != null) { + rowHasHeight = !hasRelativeHeight(c); + } + } + } + if (!rowHasHeight) { + return false; + } else { + // Other components define row height + return true; + } + } + + if (parent instanceof Panel || parent instanceof AbstractSplitPanel + || parent instanceof TabSheet + || parent instanceof CustomComponent) { + // height undefined, we know how how component works and no + // exceptions + // TODO horiz SplitPanel ?? + return false; + } else { + // We cannot generally know if undefined component can serve + // space for children (like CustomLayout or component built by + // third party) so we assume they can + return true; + } + + } else if (hasRelativeHeight(parent)) { + // Relative height + if (parent.getParent() != null) { + return parentCanDefineHeight(parent); + } else { + return true; + } + } else { + // Absolute height + return true; + } + } + + private static boolean hasRelativeHeight(Component component) { + return (component.getHeightUnits() == Unit.PERCENTAGE && component + .getHeight() > 0); + } + + private static boolean hasNonRelativeWidthComponent(AbstractOrderedLayout ol) { + Iterator<Component> it = ol.getComponentIterator(); + while (it.hasNext()) { + if (!hasRelativeWidth(it.next())) { + return true; + } + } + return false; + } + + private static boolean hasRelativeWidth(Component paintable) { + return paintable.getWidth() > 0 + && paintable.getWidthUnits() == Unit.PERCENTAGE; + } + + public static boolean parentCanDefineWidth(Component component) { + Component parent = component.getParent(); + if (parent == null) { + // main window, valid situation + return true; + } + if (parent instanceof Window) { + // Sub window with undefined size has a min-width + return true; + } + + if (parent.getWidth() < 0) { + // Undefined width + + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean horizontal = true; + if (ol instanceof VerticalLayout) { + horizontal = false; + } + + if (!horizontal && hasNonRelativeWidthComponent(ol)) { + // valid situation, other components defined width + return true; + } else { + return false; + } + } else if (parent instanceof GridLayout) { + GridLayout gl = (GridLayout) parent; + Area componentArea = gl.getComponentArea(component); + boolean columnHasWidth = false; + for (int col = componentArea.getColumn1(); !columnHasWidth + && col <= componentArea.getColumn2(); col++) { + for (int row = 0; !columnHasWidth && row < gl.getRows(); row++) { + Component c = gl.getComponent(col, row); + if (c != null) { + columnHasWidth = !hasRelativeWidth(c); + } + } + } + if (!columnHasWidth) { + return false; + } else { + // Other components define column width + return true; + } + } else if (parent instanceof Form) { + /* + * If some other part of the form is not relative it determines + * the component width + */ + return hasNonRelativeWidthComponent((Form) parent); + } else if (parent instanceof AbstractSplitPanel + || parent instanceof TabSheet + || parent instanceof CustomComponent) { + // FIXME Could we use com.vaadin package name here and + // fail for all component containers? + // FIXME Actually this should be moved to containers so it can + // be implemented for custom containers + // TODO vertical splitpanel with another non relative component? + return false; + } else if (parent instanceof Window) { + // Sub window can define width based on caption + if (parent.getCaption() != null + && !parent.getCaption().equals("")) { + return true; + } else { + return false; + } + } else if (parent instanceof Panel) { + // TODO Panel should be able to define width based on caption + return false; + } else { + return true; + } + } else if (hasRelativeWidth(parent)) { + // Relative width + if (parent.getParent() == null) { + return true; + } + + return parentCanDefineWidth(parent); + } else { + return true; + } + + } + + private static boolean hasNonRelativeWidthComponent(Form form) { + Layout layout = form.getLayout(); + Layout footer = form.getFooter(); + + if (layout != null && !hasRelativeWidth(layout)) { + return true; + } + if (footer != null && !hasRelativeWidth(footer)) { + return true; + } + + return false; + } + + private static Map<Object, FileLocation> creationLocations = new HashMap<Object, FileLocation>(); + private static Map<Object, FileLocation> widthLocations = new HashMap<Object, FileLocation>(); + private static Map<Object, FileLocation> heightLocations = new HashMap<Object, FileLocation>(); + + public static class FileLocation implements Serializable { + public String method; + public String file; + public String className; + public String classNameSimple; + public int lineNumber; + + public FileLocation(StackTraceElement traceElement) { + file = traceElement.getFileName(); + className = traceElement.getClassName(); + classNameSimple = className + .substring(className.lastIndexOf('.') + 1); + lineNumber = traceElement.getLineNumber(); + method = traceElement.getMethodName(); + } + } + + public static void setCreationLocation(Object object) { + setLocation(creationLocations, object); + } + + public static void setWidthLocation(Object object) { + setLocation(widthLocations, object); + } + + public static void setHeightLocation(Object object) { + setLocation(heightLocations, object); + } + + private static void setLocation(Map<Object, FileLocation> map, Object object) { + StackTraceElement[] traceLines = Thread.currentThread().getStackTrace(); + for (StackTraceElement traceElement : traceLines) { + Class<?> cls; + try { + String className = traceElement.getClassName(); + if (className.startsWith("java.") + || className.startsWith("sun.")) { + continue; + } + + cls = Class.forName(className); + if (cls == ComponentSizeValidator.class || cls == Thread.class) { + continue; + } + + if (Component.class.isAssignableFrom(cls) + && !CustomComponent.class.isAssignableFrom(cls)) { + continue; + } + FileLocation cl = new FileLocation(traceElement); + map.put(object, cl); + return; + } catch (Exception e) { + // TODO Auto-generated catch block + getLogger().log(Level.FINER, + "An exception occurred while validating sizes.", e); + } + + } + } + + private static Logger getLogger() { + return Logger.getLogger(ComponentSizeValidator.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/Constants.java b/server/src/com/vaadin/terminal/gwt/server/Constants.java new file mode 100644 index 0000000000..7efb0205ac --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/Constants.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +/** + * TODO Document me! + * + * @author peholmst + * + */ +public interface Constants { + + static final String NOT_PRODUCTION_MODE_INFO = "\n" + + "=================================================================\n" + + "Vaadin is running in DEBUG MODE.\nAdd productionMode=true to web.xml " + + "to disable debug features.\nTo show debug window, add ?debug to " + + "your application URL.\n" + + "================================================================="; + + static final String WARNING_XSRF_PROTECTION_DISABLED = "\n" + + "===========================================================\n" + + "WARNING: Cross-site request forgery protection is disabled!\n" + + "==========================================================="; + + static final String WARNING_RESOURCE_CACHING_TIME_NOT_NUMERIC = "\n" + + "===========================================================\n" + + "WARNING: resourceCacheTime has been set to a non integer value " + + "in web.xml. The default of 1h will be used.\n" + + "==========================================================="; + + static final String WIDGETSET_MISMATCH_INFO = "\n" + + "=================================================================\n" + + "The widgetset in use does not seem to be built for the Vaadin\n" + + "version in use. This might cause strange problems - a\n" + + "recompile/deploy is strongly recommended.\n" + + " Vaadin version: %s\n" + + " Widgetset version: %s\n" + + "================================================================="; + + static final String URL_PARAMETER_RESTART_APPLICATION = "restartApplication"; + static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication"; + static final String URL_PARAMETER_REPAINT_ALL = "repaintAll"; + static final String URL_PARAMETER_THEME = "theme"; + + static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode"; + static final String SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION = "disable-xsrf-protection"; + static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime"; + + // Configurable parameter names + static final String PARAMETER_VAADIN_RESOURCES = "Resources"; + + static final int DEFAULT_BUFFER_SIZE = 32 * 1024; + + static final int MAX_BUFFER_SIZE = 64 * 1024; + + final String THEME_DIRECTORY_PATH = "VAADIN/themes/"; + + static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24; + + static final String WIDGETSET_DIRECTORY_PATH = "VAADIN/widgetsets/"; + + // Name of the default widget set, used if not specified in web.xml + static final String DEFAULT_WIDGETSET = "com.vaadin.terminal.gwt.DefaultWidgetSet"; + + // Widget set parameter name + static final String PARAMETER_WIDGETSET = "widgetset"; + + static final String ERROR_NO_ROOT_FOUND = "Application did not return a root for the request and did not request extra information either. Something is wrong."; + + static final String DEFAULT_THEME_NAME = "reindeer"; + + static final String INVALID_SECURITY_KEY_MSG = "Invalid security key."; + + // portal configuration parameters + static final String PORTAL_PARAMETER_VAADIN_WIDGETSET = "vaadin.widgetset"; + static final String PORTAL_PARAMETER_VAADIN_RESOURCE_PATH = "vaadin.resources.path"; + static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme"; + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java b/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java new file mode 100644 index 0000000000..efb5666efa --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java @@ -0,0 +1,313 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.vaadin.event.Transferable; +import com.vaadin.event.TransferableImpl; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.ui.dd.DragEventType; +import com.vaadin.terminal.Extension; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.ui.Component; +import com.vaadin.ui.Root; + +public class DragAndDropService implements VariableOwner, ClientConnector { + + private int lastVisitId; + + private boolean lastVisitAccepted = false; + + private DragAndDropEvent dragEvent; + + private final AbstractCommunicationManager manager; + + private AcceptCriterion acceptCriterion; + + public DragAndDropService(AbstractCommunicationManager manager) { + this.manager = manager; + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + Object owner = variables.get("dhowner"); + + // Validate drop handler owner + if (!(owner instanceof DropTarget)) { + getLogger() + .severe("DropHandler owner " + owner + + " must implement DropTarget"); + return; + } + // owner cannot be null here + + DropTarget dropTarget = (DropTarget) owner; + lastVisitId = (Integer) variables.get("visitId"); + + // request may be dropRequest or request during drag operation (commonly + // dragover or dragenter) + boolean dropRequest = isDropRequest(variables); + if (dropRequest) { + handleDropRequest(dropTarget, variables); + } else { + handleDragRequest(dropTarget, variables); + } + + } + + /** + * Handles a drop request from the VDragAndDropManager. + * + * @param dropTarget + * @param variables + */ + private void handleDropRequest(DropTarget dropTarget, + Map<String, Object> variables) { + DropHandler dropHandler = (dropTarget).getDropHandler(); + if (dropHandler == null) { + // No dropHandler returned so no drop can be performed. + getLogger().fine( + "DropTarget.getDropHandler() returned null for owner: " + + dropTarget); + return; + } + + /* + * Construct the Transferable and the DragDropDetails for the drop + * operation based on the info passed from the client widgets (drag + * source for Transferable, drop target for DragDropDetails). + */ + Transferable transferable = constructTransferable(dropTarget, variables); + TargetDetails dropData = constructDragDropDetails(dropTarget, variables); + DragAndDropEvent dropEvent = new DragAndDropEvent(transferable, + dropData); + if (dropHandler.getAcceptCriterion().accept(dropEvent)) { + dropHandler.drop(dropEvent); + } + } + + /** + * Handles a drag/move request from the VDragAndDropManager. + * + * @param dropTarget + * @param variables + */ + private void handleDragRequest(DropTarget dropTarget, + Map<String, Object> variables) { + lastVisitId = (Integer) variables.get("visitId"); + + acceptCriterion = dropTarget.getDropHandler().getAcceptCriterion(); + + /* + * Construct the Transferable and the DragDropDetails for the drag + * operation based on the info passed from the client widgets (drag + * source for Transferable, current target for DragDropDetails). + */ + Transferable transferable = constructTransferable(dropTarget, variables); + TargetDetails dragDropDetails = constructDragDropDetails(dropTarget, + variables); + + dragEvent = new DragAndDropEvent(transferable, dragDropDetails); + + lastVisitAccepted = acceptCriterion.accept(dragEvent); + } + + /** + * Construct DragDropDetails based on variables from client drop target. + * Uses DragDropDetailsTranslator if available, otherwise a default + * DragDropDetails implementation is used. + * + * @param dropTarget + * @param variables + * @return + */ + @SuppressWarnings("unchecked") + private TargetDetails constructDragDropDetails(DropTarget dropTarget, + Map<String, Object> variables) { + Map<String, Object> rawDragDropDetails = (Map<String, Object>) variables + .get("evt"); + + TargetDetails dropData = dropTarget + .translateDropTargetDetails(rawDragDropDetails); + + if (dropData == null) { + // Create a default DragDropDetails with all the raw variables + dropData = new TargetDetailsImpl(rawDragDropDetails, dropTarget); + } + + return dropData; + } + + private boolean isDropRequest(Map<String, Object> variables) { + return getRequestType(variables) == DragEventType.DROP; + } + + private DragEventType getRequestType(Map<String, Object> variables) { + int type = (Integer) variables.get("type"); + return DragEventType.values()[type]; + } + + @SuppressWarnings("unchecked") + private Transferable constructTransferable(DropTarget dropHandlerOwner, + Map<String, Object> variables) { + final Component sourceComponent = (Component) variables + .get("component"); + + variables = (Map<String, Object>) variables.get("tra"); + + Transferable transferable = null; + if (sourceComponent != null && sourceComponent instanceof DragSource) { + transferable = ((DragSource) sourceComponent) + .getTransferable(variables); + } + if (transferable == null) { + transferable = new TransferableImpl(sourceComponent, variables); + } + + return transferable; + } + + @Override + public boolean isEnabled() { + return isConnectorEnabled(); + } + + @Override + public boolean isImmediate() { + return true; + } + + void printJSONResponse(PrintWriter outWriter) throws PaintException { + if (isDirty()) { + + outWriter.print(", \"dd\":"); + + JsonPaintTarget jsonPaintTarget = new JsonPaintTarget(manager, + outWriter, false); + jsonPaintTarget.startTag("dd"); + jsonPaintTarget.addAttribute("visitId", lastVisitId); + if (acceptCriterion != null) { + jsonPaintTarget.addAttribute("accepted", lastVisitAccepted); + acceptCriterion.paintResponse(jsonPaintTarget); + } + jsonPaintTarget.endTag("dd"); + jsonPaintTarget.close(); + lastVisitId = -1; + lastVisitAccepted = false; + acceptCriterion = null; + dragEvent = null; + } + } + + private boolean isDirty() { + if (lastVisitId > 0) { + return true; + } + return false; + } + + @Override + public SharedState getState() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getConnectorId() { + return VDragAndDropManager.DD_SERVICE; + } + + @Override + public boolean isConnectorEnabled() { + // Drag'n'drop can't be disabled + return true; + } + + @Override + public List<ClientMethodInvocation> retrievePendingRpcCalls() { + return null; + } + + @Override + public RpcManager getRpcManager(Class<?> rpcInterface) { + // TODO Use rpc for drag'n'drop + return null; + } + + @Override + public Class<? extends SharedState> getStateType() { + return SharedState.class; + } + + @Override + public void requestRepaint() { + // TODO Auto-generated method stub + + } + + @Override + public ClientConnector getParent() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void requestRepaintAll() { + // TODO Auto-generated method stub + + } + + @Override + public void setParent(ClientConnector parent) { + // TODO Auto-generated method stub + + } + + @Override + public void attach() { + // TODO Auto-generated method stub + + } + + @Override + public void detach() { + // TODO Auto-generated method stub + + } + + @Override + public Collection<Extension> getExtensions() { + // TODO Auto-generated method stub + return Collections.emptySet(); + } + + @Override + public void removeExtension(Extension extension) { + // TODO Auto-generated method stub + } + + private Logger getLogger() { + return Logger.getLogger(DragAndDropService.class.getName()); + } + + @Override + public Root getRoot() { + return null; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java new file mode 100644 index 0000000000..cc12c9cc43 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java @@ -0,0 +1,417 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.google.appengine.api.datastore.Blob; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.FetchOptions.Builder; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.memcache.Expiration; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.apphosting.api.DeadlineExceededException; +import com.vaadin.service.ApplicationContext; + +/** + * ApplicationServlet to be used when deploying to Google App Engine, in + * web.xml: + * + * <pre> + * <servlet> + * <servlet-name>HelloWorld</servlet-name> + * <servlet-class>com.vaadin.terminal.gwt.server.GAEApplicationServlet</servlet-class> + * <init-param> + * <param-name>application</param-name> + * <param-value>com.vaadin.demo.HelloWorld</param-value> + * </init-param> + * </servlet> + * </pre> + * + * Session support must be enabled in appengine-web.xml: + * + * <pre> + * <sessions-enabled>true</sessions-enabled> + * </pre> + * + * Appengine datastore cleanup can be invoked by calling one of the applications + * with an additional path "/CLEAN". This can be set up as a cron-job in + * cron.xml (see appengine documentation for more information): + * + * <pre> + * <cronentries> + * <cron> + * <url>/HelloWorld/CLEAN</url> + * <description>Clean up sessions</description> + * <schedule>every 2 hours</schedule> + * </cron> + * </cronentries> + * </pre> + * + * It is recommended (but not mandatory) to extract themes and widgetsets and + * have App Engine server these statically. Extract VAADIN folder (and it's + * contents) 'next to' the WEB-INF folder, and add the following to + * appengine-web.xml: + * + * <pre> + * <static-files> + * <include path="/VAADIN/**" /> + * </static-files> + * </pre> + * + * Additional limitations: + * <ul> + * <li/>Do not change application state when serving an ApplicationResource. + * <li/>Avoid changing application state in transaction handlers, unless you're + * confident you fully understand the synchronization issues in App Engine. + * <li/>The application remains locked while uploading - no progressbar is + * possible. + * </ul> + */ +public class GAEApplicationServlet extends ApplicationServlet { + + // memcache mutex is MUTEX_BASE + sessio id + private static final String MUTEX_BASE = "_vmutex"; + + // used identify ApplicationContext in memcache and datastore + private static final String AC_BASE = "_vac"; + + // UIDL requests will attempt to gain access for this long before telling + // the client to retry + private static final int MAX_UIDL_WAIT_MILLISECONDS = 5000; + + // Tell client to retry after this delay. + // Note: currently interpreting Retry-After as ms, not sec + private static final int RETRY_AFTER_MILLISECONDS = 100; + + // Properties used in the datastore + private static final String PROPERTY_EXPIRES = "expires"; + private static final String PROPERTY_DATA = "data"; + + // path used for cleanup + private static final String CLEANUP_PATH = "/CLEAN"; + // max entities to clean at once + private static final int CLEANUP_LIMIT = 200; + // appengine session kind + private static final String APPENGINE_SESSION_KIND = "_ah_SESSION"; + // appengine session expires-parameter + private static final String PROPERTY_APPENGINE_EXPIRES = "_expires"; + + protected void sendDeadlineExceededNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + criticalNotification( + request, + response, + "Deadline Exceeded", + "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...", + "", null); + } + + protected void sendNotSerializableNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + criticalNotification( + request, + response, + "NotSerializableException", + "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", + "", getApplicationUrl(request).toString() + + "?restartApplication"); + } + + protected void sendCriticalErrorNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + criticalNotification( + request, + response, + "Critical error", + "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", + "", getApplicationUrl(request).toString() + + "?restartApplication"); + } + + @Override + protected void service(HttpServletRequest unwrappedRequest, + HttpServletResponse unwrappedResponse) throws ServletException, + IOException { + WrappedHttpServletRequest request = new WrappedHttpServletRequest( + unwrappedRequest, getDeploymentConfiguration()); + WrappedHttpServletResponse response = new WrappedHttpServletResponse( + unwrappedResponse, getDeploymentConfiguration()); + + if (isCleanupRequest(request)) { + cleanDatastore(); + return; + } + + RequestType requestType = getRequestType(request); + + if (requestType == RequestType.STATIC_FILE) { + // no locking needed, let superclass handle + super.service(request, response); + cleanSession(request); + return; + } + + if (requestType == RequestType.APPLICATION_RESOURCE) { + // no locking needed, let superclass handle + getApplicationContext(request, + MemcacheServiceFactory.getMemcacheService()); + super.service(request, response); + cleanSession(request); + return; + } + + final HttpSession session = request + .getSession(requestCanCreateApplication(request, requestType)); + if (session == null) { + handleServiceSessionExpired(request, response); + cleanSession(request); + return; + } + + boolean locked = false; + MemcacheService memcache = null; + String mutex = MUTEX_BASE + session.getId(); + memcache = MemcacheServiceFactory.getMemcacheService(); + try { + // try to get lock + long started = new Date().getTime(); + // non-UIDL requests will try indefinitely + while (requestType != RequestType.UIDL + || new Date().getTime() - started < MAX_UIDL_WAIT_MILLISECONDS) { + locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + if (locked) { + break; + } + try { + Thread.sleep(RETRY_AFTER_MILLISECONDS); + } catch (InterruptedException e) { + getLogger().finer( + "Thread.sleep() interrupted while waiting for lock. Trying again. " + + e); + } + } + + if (!locked) { + // Not locked; only UIDL can get trough here unlocked: tell + // client to retry + response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + // Note: currently interpreting Retry-After as ms, not sec + response.setHeader("Retry-After", "" + RETRY_AFTER_MILLISECONDS); + return; + } + + // de-serialize or create application context, store in session + ApplicationContext ctx = getApplicationContext(request, memcache); + + super.service(request, response); + + // serialize + started = new Date().getTime(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(ctx); + oos.flush(); + byte[] bytes = baos.toByteArray(); + + started = new Date().getTime(); + + String id = AC_BASE + session.getId(); + Date expire = new Date(started + + (session.getMaxInactiveInterval() * 1000)); + Expiration expires = Expiration.onDate(expire); + + memcache.put(id, bytes, expires); + + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Entity entity = new Entity(AC_BASE, id); + entity.setProperty(PROPERTY_EXPIRES, expire.getTime()); + entity.setProperty(PROPERTY_DATA, new Blob(bytes)); + ds.put(entity); + + } catch (DeadlineExceededException e) { + getLogger().warning("DeadlineExceeded for " + session.getId()); + sendDeadlineExceededNotification(request, response); + } catch (NotSerializableException e) { + getLogger().log(Level.SEVERE, "Not serializable!", e); + + // TODO this notification is usually not shown - should we redirect + // in some other way - can we? + sendNotSerializableNotification(request, response); + } catch (Exception e) { + getLogger().log(Level.WARNING, + "An exception occurred while servicing request.", e); + + sendCriticalErrorNotification(request, response); + } finally { + // "Next, please!" + if (locked) { + memcache.delete(mutex); + } + cleanSession(request); + } + } + + protected ApplicationContext getApplicationContext( + HttpServletRequest request, MemcacheService memcache) { + HttpSession session = request.getSession(); + String id = AC_BASE + session.getId(); + byte[] serializedAC = (byte[]) memcache.get(id); + if (serializedAC == null) { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Key key = KeyFactory.createKey(AC_BASE, id); + Entity entity = null; + try { + entity = ds.get(key); + } catch (EntityNotFoundException e) { + // Ok, we were a bit optimistic; we'll create a new one later + } + if (entity != null) { + Blob blob = (Blob) entity.getProperty(PROPERTY_DATA); + serializedAC = blob.getBytes(); + // bring it to memcache + memcache.put(AC_BASE + session.getId(), serializedAC, + Expiration.byDeltaSeconds(session + .getMaxInactiveInterval()), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + } + } + if (serializedAC != null) { + ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC); + ObjectInputStream ois; + try { + ois = new ObjectInputStream(bais); + ApplicationContext applicationContext = (ApplicationContext) ois + .readObject(); + session.setAttribute(WebApplicationContext.class.getName(), + applicationContext); + } catch (IOException e) { + getLogger().log( + Level.WARNING, + "Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created. ", e); + } catch (ClassNotFoundException e) { + getLogger().log( + Level.WARNING, + "Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created. ", e); + } + } + // will create new context if the above did not + return getApplicationContext(session); + + } + + private boolean isCleanupRequest(HttpServletRequest request) { + String path = getRequestPathInfo(request); + if (path != null && path.equals(CLEANUP_PATH)) { + return true; + } + return false; + } + + /** + * Removes the ApplicationContext from the session in order to minimize the + * data serialized to datastore and memcache. + * + * @param request + */ + private void cleanSession(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.removeAttribute(WebApplicationContext.class.getName()); + } + } + + /** + * This will look at the timestamp and delete expired persisted Vaadin and + * appengine sessions from the datastore. + * + * TODO Possible improvements include: 1. Use transactions (requires entity + * groups - overkill?) 2. Delete one-at-a-time, catch possible exception, + * continue w/ next. + */ + private void cleanDatastore() { + long expire = new Date().getTime(); + try { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + // Vaadin stuff first + { + Query q = new Query(AC_BASE); + q.setKeysOnly(); + + q.addFilter(PROPERTY_EXPIRES, + FilterOperator.LESS_THAN_OR_EQUAL, expire); + PreparedQuery pq = ds.prepare(q); + List<Entity> entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired Vaadin sessions."); + List<Key> keys = new ArrayList<Key>(); + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + } + // Also cleanup GAE sessions + { + Query q = new Query(APPENGINE_SESSION_KIND); + q.setKeysOnly(); + q.addFilter(PROPERTY_APPENGINE_EXPIRES, + FilterOperator.LESS_THAN_OR_EQUAL, expire); + PreparedQuery pq = ds.prepare(q); + List<Entity> entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired appengine sessions."); + List<Key> keys = new ArrayList<Key>(); + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + } + } catch (Exception e) { + getLogger().log(Level.WARNING, "Exception while cleaning.", e); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(GAEApplicationServlet.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java b/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java new file mode 100644 index 0000000000..d811cadf86 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java @@ -0,0 +1,54 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import javax.servlet.Filter; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext.TransactionListener; +import com.vaadin.terminal.Terminal; + +/** + * {@link Application} that implements this interface gets notified of request + * start and end by terminal. + * <p> + * Interface can be used for several helper tasks including: + * <ul> + * <li>Opening and closing database connections + * <li>Implementing {@link ThreadLocal} + * <li>Setting/Getting {@link Cookie} + * </ul> + * <p> + * Alternatives for implementing similar features are are Servlet {@link Filter} + * s and {@link TransactionListener}s in Vaadin. + * + * @since 6.2 + * @see PortletRequestListener + */ +public interface HttpServletRequestListener extends Serializable { + + /** + * This method is called before {@link Terminal} applies the request to + * Application. + * + * @param request + * @param response + */ + public void onRequestStart(HttpServletRequest request, + HttpServletResponse response); + + /** + * This method is called at the end of each request. + * + * @param request + * @param response + */ + public void onRequestEnd(HttpServletRequest request, + HttpServletResponse response); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java b/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java new file mode 100644 index 0000000000..8199bc6ada --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java @@ -0,0 +1,792 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.terminal.gwt.client.communication.JsonEncoder; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; + +/** + * Decoder for converting RPC parameters and other values from JSON in transfer + * between the client and the server and vice versa. + * + * @since 7.0 + */ +public class JsonCodec implements Serializable { + + private static Map<Class<?>, String> typeToTransportType = new HashMap<Class<?>, String>(); + + /** + * Note! This does not contain primitives. + * <p> + */ + private static Map<String, Class<?>> transportTypeToType = new HashMap<String, Class<?>>(); + + static { + registerType(String.class, JsonEncoder.VTYPE_STRING); + registerType(Connector.class, JsonEncoder.VTYPE_CONNECTOR); + registerType(Boolean.class, JsonEncoder.VTYPE_BOOLEAN); + registerType(boolean.class, JsonEncoder.VTYPE_BOOLEAN); + registerType(Integer.class, JsonEncoder.VTYPE_INTEGER); + registerType(int.class, JsonEncoder.VTYPE_INTEGER); + registerType(Float.class, JsonEncoder.VTYPE_FLOAT); + registerType(float.class, JsonEncoder.VTYPE_FLOAT); + registerType(Double.class, JsonEncoder.VTYPE_DOUBLE); + registerType(double.class, JsonEncoder.VTYPE_DOUBLE); + registerType(Long.class, JsonEncoder.VTYPE_LONG); + registerType(long.class, JsonEncoder.VTYPE_LONG); + registerType(String[].class, JsonEncoder.VTYPE_STRINGARRAY); + registerType(Object[].class, JsonEncoder.VTYPE_ARRAY); + registerType(Map.class, JsonEncoder.VTYPE_MAP); + registerType(HashMap.class, JsonEncoder.VTYPE_MAP); + registerType(List.class, JsonEncoder.VTYPE_LIST); + registerType(Set.class, JsonEncoder.VTYPE_SET); + } + + private static void registerType(Class<?> type, String transportType) { + typeToTransportType.put(type, transportType); + if (!type.isPrimitive()) { + transportTypeToType.put(transportType, type); + } + } + + public static boolean isInternalTransportType(String transportType) { + return transportTypeToType.containsKey(transportType); + } + + public static boolean isInternalType(Type type) { + if (type instanceof Class && ((Class<?>) type).isPrimitive()) { + if (type == byte.class || type == char.class) { + // Almost all primitive types are handled internally + return false; + } + // All primitive types are handled internally + return true; + } else if (type == UidlValue.class) { + // UidlValue is a special internal type wrapping type info and a + // value + return true; + } + return typeToTransportType.containsKey(getClassForType(type)); + } + + private static Class<?> getClassForType(Type type) { + if (type instanceof ParameterizedType) { + return (Class<?>) (((ParameterizedType) type).getRawType()); + } else if (type instanceof Class<?>) { + return (Class<?>) type; + } else { + return null; + } + } + + private static Class<?> getType(String transportType) { + return transportTypeToType.get(transportType); + } + + public static Object decodeInternalOrCustomType(Type targetType, + Object value, ConnectorTracker connectorTracker) + throws JSONException { + if (isInternalType(targetType)) { + return decodeInternalType(targetType, false, value, + connectorTracker); + } else { + return decodeCustomType(targetType, value, connectorTracker); + } + } + + public static Object decodeCustomType(Type targetType, Object value, + ConnectorTracker connectorTracker) throws JSONException { + if (isInternalType(targetType)) { + throw new JSONException("decodeCustomType cannot be used for " + + targetType + ", which is an internal type"); + } + + // Try to decode object using fields + if (value == JSONObject.NULL) { + return null; + } else if (targetType == byte.class || targetType == Byte.class) { + return Byte.valueOf(String.valueOf(value)); + } else if (targetType == char.class || targetType == Character.class) { + return Character.valueOf(String.valueOf(value).charAt(0)); + } else if (targetType instanceof Class<?> + && ((Class<?>) targetType).isArray()) { + // Legacy Object[] and String[] handled elsewhere, this takes care + // of generic arrays + Class<?> componentType = ((Class<?>) targetType).getComponentType(); + return decodeArray(componentType, (JSONArray) value, + connectorTracker); + } else if (targetType instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) targetType) + .getGenericComponentType(); + return decodeArray(componentType, (JSONArray) value, + connectorTracker); + } else if (targetType == JSONObject.class + || targetType == JSONArray.class) { + return value; + } else { + return decodeObject(targetType, (JSONObject) value, + connectorTracker); + } + } + + private static Object decodeArray(Type componentType, JSONArray value, + ConnectorTracker connectorTracker) throws JSONException { + Class<?> componentClass = getClassForType(componentType); + Object array = Array.newInstance(componentClass, value.length()); + for (int i = 0; i < value.length(); i++) { + Object decodedValue = decodeInternalOrCustomType(componentType, + value.get(i), connectorTracker); + Array.set(array, i, decodedValue); + } + return array; + } + + /** + * Decodes a value that is of an internal type. + * <p> + * Ensures the encoded value is of the same type as target type. + * </p> + * <p> + * Allows restricting collections so that they must be declared using + * generics. If this is used then all objects in the collection are encoded + * using the declared type. Otherwise only internal types are allowed in + * collections. + * </p> + * + * @param targetType + * The type that should be returned by this method + * @param valueAndType + * The encoded value and type array + * @param application + * A reference to the application + * @param enforceGenericsInCollections + * true if generics should be enforce, false to only allow + * internal types in collections + * @return + * @throws JSONException + */ + public static Object decodeInternalType(Type targetType, + boolean restrictToInternalTypes, Object encodedJsonValue, + ConnectorTracker connectorTracker) throws JSONException { + if (!isInternalType(targetType)) { + throw new JSONException("Type " + targetType + + " is not a supported internal type."); + } + String transportType = getInternalTransportType(targetType); + + if (encodedJsonValue == JSONObject.NULL) { + return null; + } + + // UidlValue + if (targetType == UidlValue.class) { + return decodeUidlValue((JSONArray) encodedJsonValue, + connectorTracker); + } + + // Collections + if (JsonEncoder.VTYPE_LIST.equals(transportType)) { + return decodeList(targetType, restrictToInternalTypes, + (JSONArray) encodedJsonValue, connectorTracker); + } else if (JsonEncoder.VTYPE_SET.equals(transportType)) { + return decodeSet(targetType, restrictToInternalTypes, + (JSONArray) encodedJsonValue, connectorTracker); + } else if (JsonEncoder.VTYPE_MAP.equals(transportType)) { + return decodeMap(targetType, restrictToInternalTypes, + encodedJsonValue, connectorTracker); + } + + // Arrays + if (JsonEncoder.VTYPE_ARRAY.equals(transportType)) { + + return decodeObjectArray(targetType, (JSONArray) encodedJsonValue, + connectorTracker); + + } else if (JsonEncoder.VTYPE_STRINGARRAY.equals(transportType)) { + return decodeStringArray((JSONArray) encodedJsonValue); + } + + // Special Vaadin types + + String stringValue = String.valueOf(encodedJsonValue); + + if (JsonEncoder.VTYPE_CONNECTOR.equals(transportType)) { + return connectorTracker.getConnector(stringValue); + } + + // Legacy types + + if (JsonEncoder.VTYPE_STRING.equals(transportType)) { + return stringValue; + } else if (JsonEncoder.VTYPE_INTEGER.equals(transportType)) { + return Integer.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_LONG.equals(transportType)) { + return Long.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_FLOAT.equals(transportType)) { + return Float.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_DOUBLE.equals(transportType)) { + return Double.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_BOOLEAN.equals(transportType)) { + return Boolean.valueOf(stringValue); + } + + throw new JSONException("Unknown type " + transportType); + } + + private static UidlValue decodeUidlValue(JSONArray encodedJsonValue, + ConnectorTracker connectorTracker) throws JSONException { + String type = encodedJsonValue.getString(0); + + Object decodedValue = decodeInternalType(getType(type), true, + encodedJsonValue.get(1), connectorTracker); + return new UidlValue(decodedValue); + } + + private static boolean transportTypesCompatible( + String encodedTransportType, String transportType) { + if (encodedTransportType == null) { + return false; + } + if (encodedTransportType.equals(transportType)) { + return true; + } + if (encodedTransportType.equals(JsonEncoder.VTYPE_NULL)) { + return true; + } + + return false; + } + + private static Map<Object, Object> decodeMap(Type targetType, + boolean restrictToInternalTypes, Object jsonMap, + ConnectorTracker connectorTracker) throws JSONException { + if (jsonMap instanceof JSONArray) { + // Client-side has no declared type information to determine + // encoding method for empty maps, so these are handled separately. + // See #8906. + JSONArray jsonArray = (JSONArray) jsonMap; + if (jsonArray.length() == 0) { + return new HashMap<Object, Object>(); + } + } + + if (!restrictToInternalTypes && targetType instanceof ParameterizedType) { + Type keyType = ((ParameterizedType) targetType) + .getActualTypeArguments()[0]; + Type valueType = ((ParameterizedType) targetType) + .getActualTypeArguments()[1]; + if (keyType == String.class) { + return decodeStringMap(valueType, (JSONObject) jsonMap, + connectorTracker); + } else if (keyType == Connector.class) { + return decodeConnectorMap(valueType, (JSONObject) jsonMap, + connectorTracker); + } else { + return decodeObjectMap(keyType, valueType, (JSONArray) jsonMap, + connectorTracker); + } + } else { + return decodeStringMap(UidlValue.class, (JSONObject) jsonMap, + connectorTracker); + } + } + + private static Map<Object, Object> decodeObjectMap(Type keyType, + Type valueType, JSONArray jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map<Object, Object> map = new HashMap<Object, Object>(); + + JSONArray keys = jsonMap.getJSONArray(0); + JSONArray values = jsonMap.getJSONArray(1); + + assert (keys.length() == values.length()); + + for (int i = 0; i < keys.length(); i++) { + Object key = decodeInternalOrCustomType(keyType, keys.get(i), + connectorTracker); + Object value = decodeInternalOrCustomType(valueType, values.get(i), + connectorTracker); + + map.put(key, value); + } + + return map; + } + + private static Map<Object, Object> decodeConnectorMap(Type valueType, + JSONObject jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map<Object, Object> map = new HashMap<Object, Object>(); + + for (Iterator<?> iter = jsonMap.keys(); iter.hasNext();) { + String key = (String) iter.next(); + Object value = decodeInternalOrCustomType(valueType, + jsonMap.get(key), connectorTracker); + if (valueType == UidlValue.class) { + value = ((UidlValue) value).getValue(); + } + map.put(connectorTracker.getConnector(key), value); + } + + return map; + } + + private static Map<Object, Object> decodeStringMap(Type valueType, + JSONObject jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map<Object, Object> map = new HashMap<Object, Object>(); + + for (Iterator<?> iter = jsonMap.keys(); iter.hasNext();) { + String key = (String) iter.next(); + Object value = decodeInternalOrCustomType(valueType, + jsonMap.get(key), connectorTracker); + if (valueType == UidlValue.class) { + value = ((UidlValue) value).getValue(); + } + map.put(key, value); + } + + return map; + } + + /** + * @param targetType + * @param restrictToInternalTypes + * @param typeIndex + * The index of a generic type to use to define the child type + * that should be decoded + * @param encodedValueAndType + * @param application + * @return + * @throws JSONException + */ + private static Object decodeParametrizedType(Type targetType, + boolean restrictToInternalTypes, int typeIndex, Object value, + ConnectorTracker connectorTracker) throws JSONException { + if (!restrictToInternalTypes && targetType instanceof ParameterizedType) { + Type childType = ((ParameterizedType) targetType) + .getActualTypeArguments()[typeIndex]; + // Only decode the given type + return decodeInternalOrCustomType(childType, value, + connectorTracker); + } else { + // Only UidlValue when not enforcing a given type to avoid security + // issues + UidlValue decodeInternalType = (UidlValue) decodeInternalType( + UidlValue.class, true, value, connectorTracker); + return decodeInternalType.getValue(); + } + } + + private static Object decodeEnum(Class<? extends Enum> cls, JSONObject value) { + String enumIdentifier = String.valueOf(value); + return Enum.valueOf(cls, enumIdentifier); + } + + private static String[] decodeStringArray(JSONArray jsonArray) + throws JSONException { + int length = jsonArray.length(); + List<String> tokens = new ArrayList<String>(length); + for (int i = 0; i < length; ++i) { + tokens.add(jsonArray.getString(i)); + } + return tokens.toArray(new String[tokens.size()]); + } + + private static Object[] decodeObjectArray(Type targetType, + JSONArray jsonArray, ConnectorTracker connectorTracker) + throws JSONException { + List list = decodeList(List.class, true, jsonArray, connectorTracker); + return list.toArray(new Object[list.size()]); + } + + private static List<Object> decodeList(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, + ConnectorTracker connectorTracker) throws JSONException { + List<Object> list = new ArrayList<Object>(); + for (int i = 0; i < jsonArray.length(); ++i) { + // each entry always has two elements: type and value + Object encodedValue = jsonArray.get(i); + Object decodedChild = decodeParametrizedType(targetType, + restrictToInternalTypes, 0, encodedValue, connectorTracker); + list.add(decodedChild); + } + return list; + } + + private static Set<Object> decodeSet(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, + ConnectorTracker connectorTracker) throws JSONException { + HashSet<Object> set = new HashSet<Object>(); + set.addAll(decodeList(targetType, restrictToInternalTypes, jsonArray, + connectorTracker)); + return set; + } + + /** + * Returns the name that should be used as field name in the JSON. We strip + * "set" from the setter, keeping the result - this is easy to do on both + * server and client, avoiding some issues with cASE. E.g setZIndex() + * becomes "zIndex". Also ensures that both getter and setter are present, + * returning null otherwise. + * + * @param pd + * @return the name to be used or null if both getter and setter are not + * found. + */ + static String getTransportFieldName(PropertyDescriptor pd) { + if (pd.getReadMethod() == null || pd.getWriteMethod() == null) { + return null; + } + String fieldName = pd.getWriteMethod().getName().substring(3); + fieldName = Character.toLowerCase(fieldName.charAt(0)) + + fieldName.substring(1); + return fieldName; + } + + private static Object decodeObject(Type targetType, + JSONObject serializedObject, ConnectorTracker connectorTracker) + throws JSONException { + + Class<?> targetClass = getClassForType(targetType); + if (Enum.class.isAssignableFrom(targetClass)) { + return decodeEnum(targetClass.asSubclass(Enum.class), + serializedObject); + } + + try { + Object decodedObject = targetClass.newInstance(); + for (PropertyDescriptor pd : Introspector.getBeanInfo(targetClass) + .getPropertyDescriptors()) { + + String fieldName = getTransportFieldName(pd); + if (fieldName == null) { + continue; + } + Object encodedFieldValue = serializedObject.get(fieldName); + Type fieldType = pd.getReadMethod().getGenericReturnType(); + Object decodedFieldValue = decodeInternalOrCustomType( + fieldType, encodedFieldValue, connectorTracker); + + pd.getWriteMethod().invoke(decodedObject, decodedFieldValue); + } + + return decodedObject; + } catch (IllegalArgumentException e) { + throw new JSONException(e); + } catch (IllegalAccessException e) { + throw new JSONException(e); + } catch (InvocationTargetException e) { + throw new JSONException(e); + } catch (InstantiationException e) { + throw new JSONException(e); + } catch (IntrospectionException e) { + throw new JSONException(e); + } + } + + public static Object encode(Object value, Object referenceValue, + Type valueType, ConnectorTracker connectorTracker) + throws JSONException { + + if (valueType == null) { + throw new IllegalArgumentException("type must be defined"); + } + + if (valueType instanceof WildcardType) { + throw new IllegalStateException( + "Can not serialize type with wildcard: " + valueType); + } + + if (null == value) { + return encodeNull(); + } + + if (value instanceof String[]) { + String[] array = (String[]) value; + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < array.length; ++i) { + jsonArray.put(array[i]); + } + return jsonArray; + } else if (value instanceof String) { + return value; + } else if (value instanceof Boolean) { + return value; + } else if (value instanceof Number) { + return value; + } else if (value instanceof Character) { + // Character is not a Number + return value; + } else if (value instanceof Collection) { + Collection<?> collection = (Collection<?>) value; + JSONArray jsonArray = encodeCollection(valueType, collection, + connectorTracker); + return jsonArray; + } else if (valueType instanceof Class<?> + && ((Class<?>) valueType).isArray()) { + JSONArray jsonArray = encodeArrayContents( + ((Class<?>) valueType).getComponentType(), value, + connectorTracker); + return jsonArray; + } else if (valueType instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) valueType) + .getGenericComponentType(); + JSONArray jsonArray = encodeArrayContents(componentType, value, + connectorTracker); + return jsonArray; + } else if (value instanceof Map) { + Object jsonMap = encodeMap(valueType, (Map<?, ?>) value, + connectorTracker); + return jsonMap; + } else if (value instanceof Connector) { + Connector connector = (Connector) value; + if (value instanceof Component + && !(AbstractCommunicationManager + .isVisible((Component) value))) { + return encodeNull(); + } + return connector.getConnectorId(); + } else if (value instanceof Enum) { + return encodeEnum((Enum<?>) value, connectorTracker); + } else if (value instanceof JSONArray || value instanceof JSONObject) { + return value; + } else { + // Any object that we do not know how to encode we encode by looping + // through fields + return encodeObject(value, referenceValue, connectorTracker); + } + } + + private static Object encodeNull() { + return JSONObject.NULL; + } + + private static Object encodeObject(Object value, Object referenceValue, + ConnectorTracker connectorTracker) throws JSONException { + JSONObject jsonMap = new JSONObject(); + + try { + for (PropertyDescriptor pd : Introspector.getBeanInfo( + value.getClass()).getPropertyDescriptors()) { + String fieldName = getTransportFieldName(pd); + if (fieldName == null) { + continue; + } + Method getterMethod = pd.getReadMethod(); + // We can't use PropertyDescriptor.getPropertyType() as it does + // not support generics + Type fieldType = getterMethod.getGenericReturnType(); + Object fieldValue = getterMethod.invoke(value, (Object[]) null); + boolean equals = false; + Object referenceFieldValue = null; + if (referenceValue != null) { + referenceFieldValue = getterMethod.invoke(referenceValue, + (Object[]) null); + equals = equals(fieldValue, referenceFieldValue); + } + if (!equals) { + if (jsonMap.has(fieldName)) { + throw new RuntimeException( + "Can't encode " + + value.getClass().getName() + + " as it has multiple fields with the name " + + fieldName.toLowerCase() + + ". This can happen if only casing distinguishes one property name from another."); + } + jsonMap.put( + fieldName, + encode(fieldValue, referenceFieldValue, fieldType, + connectorTracker)); + // } else { + // System.out.println("Skipping field " + fieldName + // + " of type " + fieldType.getName() + // + " for object " + value.getClass().getName() + // + " as " + fieldValue + "==" + referenceFieldValue); + } + } + } catch (Exception e) { + // TODO: Should exceptions be handled in a different way? + throw new JSONException(e); + } + return jsonMap; + } + + /** + * Compares the value with the reference. If they match, returns true. + * + * @param fieldValue + * @param referenceValue + * @return + */ + private static boolean equals(Object fieldValue, Object referenceValue) { + if (fieldValue == null) { + return referenceValue == null; + } + + if (fieldValue.equals(referenceValue)) { + return true; + } + + return false; + } + + private static String encodeEnum(Enum<?> e, + ConnectorTracker connectorTracker) throws JSONException { + return e.name(); + } + + private static JSONArray encodeArrayContents(Type componentType, + Object array, ConnectorTracker connectorTracker) + throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < Array.getLength(array); i++) { + jsonArray.put(encode(Array.get(array, i), null, componentType, + connectorTracker)); + } + return jsonArray; + } + + private static JSONArray encodeCollection(Type targetType, + Collection collection, ConnectorTracker connectorTracker) + throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (Object o : collection) { + jsonArray.put(encodeChild(targetType, 0, o, connectorTracker)); + } + return jsonArray; + } + + private static Object encodeChild(Type targetType, int typeIndex, Object o, + ConnectorTracker connectorTracker) throws JSONException { + if (targetType instanceof ParameterizedType) { + Type childType = ((ParameterizedType) targetType) + .getActualTypeArguments()[typeIndex]; + // Encode using the given type + return encode(o, null, childType, connectorTracker); + } else { + throw new JSONException("Collection is missing generics"); + } + } + + private static Object encodeMap(Type mapType, Map<?, ?> map, + ConnectorTracker connectorTracker) throws JSONException { + Type keyType, valueType; + + if (mapType instanceof ParameterizedType) { + keyType = ((ParameterizedType) mapType).getActualTypeArguments()[0]; + valueType = ((ParameterizedType) mapType).getActualTypeArguments()[1]; + } else { + throw new JSONException("Map is missing generics"); + } + + if (map.isEmpty()) { + // Client -> server encodes empty map as an empty array because of + // #8906. Do the same for server -> client to maintain symmetry. + return new JSONArray(); + } + + if (keyType == String.class) { + return encodeStringMap(valueType, map, connectorTracker); + } else if (keyType == Connector.class) { + return encodeConnectorMap(valueType, map, connectorTracker); + } else { + return encodeObjectMap(keyType, valueType, map, connectorTracker); + } + } + + private static JSONArray encodeObjectMap(Type keyType, Type valueType, + Map<?, ?> map, ConnectorTracker connectorTracker) + throws JSONException { + JSONArray keys = new JSONArray(); + JSONArray values = new JSONArray(); + + for (Entry<?, ?> entry : map.entrySet()) { + Object encodedKey = encode(entry.getKey(), null, keyType, + connectorTracker); + Object encodedValue = encode(entry.getValue(), null, valueType, + connectorTracker); + + keys.put(encodedKey); + values.put(encodedValue); + } + + return new JSONArray(Arrays.asList(keys, values)); + } + + private static JSONObject encodeConnectorMap(Type valueType, Map<?, ?> map, + ConnectorTracker connectorTracker) throws JSONException { + JSONObject jsonMap = new JSONObject(); + + for (Entry<?, ?> entry : map.entrySet()) { + Connector key = (Connector) entry.getKey(); + Object encodedValue = encode(entry.getValue(), null, valueType, + connectorTracker); + jsonMap.put(key.getConnectorId(), encodedValue); + } + + return jsonMap; + } + + private static JSONObject encodeStringMap(Type valueType, Map<?, ?> map, + ConnectorTracker connectorTracker) throws JSONException { + JSONObject jsonMap = new JSONObject(); + + for (Entry<?, ?> entry : map.entrySet()) { + String key = (String) entry.getKey(); + Object encodedValue = encode(entry.getValue(), null, valueType, + connectorTracker); + jsonMap.put(key, encodedValue); + } + + return jsonMap; + } + + /** + * Gets the transport type for the given class. Returns null if no transport + * type can be found. + * + * @param valueType + * The type that should be transported + * @return + * @throws JSONException + */ + private static String getInternalTransportType(Type valueType) { + return typeToTransportType.get(getClassForType(valueType)); + } + + private static String getCustomTransportType(Class<?> targetType) { + return targetType.getName(); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java b/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java new file mode 100644 index 0000000000..5a830ddb63 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java @@ -0,0 +1,1022 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.Vector; +import java.util.logging.Logger; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.StreamVariable; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Component; +import com.vaadin.ui.CustomLayout; + +/** + * User Interface Description Language Target. + * + * TODO document better: role of this class, UIDL format, attributes, variables, + * etc. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class JsonPaintTarget implements PaintTarget { + + /* Document type declarations */ + + private final static String UIDL_ARG_NAME = "name"; + + private final Stack<String> mOpenTags; + + private final Stack<JsonTag> openJsonTags; + + // these match each other element-wise + private final Stack<ClientConnector> openPaintables; + private final Stack<String> openPaintableTags; + + private final PrintWriter uidlBuffer; + + private boolean closed = false; + + private final AbstractCommunicationManager manager; + + private int changes = 0; + + private final Set<Object> usedResources = new HashSet<Object>(); + + private boolean customLayoutArgumentsOpen = false; + + private JsonTag tag; + + private boolean cacheEnabled = false; + + private final Set<Class<? extends ClientConnector>> usedClientConnectors = new HashSet<Class<? extends ClientConnector>>(); + + /** + * Creates a new JsonPaintTarget. + * + * @param manager + * @param outWriter + * A character-output stream. + * @param cachingRequired + * true if this is not a full repaint, i.e. caches are to be + * used. + * @throws PaintException + * if the paint operation failed. + */ + public JsonPaintTarget(AbstractCommunicationManager manager, + PrintWriter outWriter, boolean cachingRequired) + throws PaintException { + + this.manager = manager; + + // Sets the target for UIDL writing + uidlBuffer = outWriter; + + // Initialize tag-writing + mOpenTags = new Stack<String>(); + openJsonTags = new Stack<JsonTag>(); + + openPaintables = new Stack<ClientConnector>(); + openPaintableTags = new Stack<String>(); + + cacheEnabled = cachingRequired; + } + + @Override + public void startTag(String tagName) throws PaintException { + startTag(tagName, false); + } + + /** + * Prints the element start tag. + * + * <pre> + * Todo: + * Checking of input values + * + * </pre> + * + * @param tagName + * the name of the start tag. + * @throws PaintException + * if the paint operation failed. + * + */ + public void startTag(String tagName, boolean isChildNode) + throws PaintException { + // In case of null data output nothing: + if (tagName == null) { + throw new NullPointerException(); + } + + // Ensures that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + if (tag != null) { + openJsonTags.push(tag); + } + // Checks tagName and attributes here + mOpenTags.push(tagName); + + tag = new JsonTag(tagName); + + customLayoutArgumentsOpen = false; + + } + + /** + * Prints the element end tag. + * + * If the parent tag is closed before every child tag is closed an + * PaintException is raised. + * + * @param tag + * the name of the end tag. + * @throws Paintexception + * if the paint operation failed. + */ + + @Override + public void endTag(String tagName) throws PaintException { + // In case of null data output nothing: + if (tagName == null) { + throw new NullPointerException(); + } + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + if (openJsonTags.size() > 0) { + final JsonTag parent = openJsonTags.pop(); + + String lastTag = ""; + + lastTag = mOpenTags.pop(); + if (!tagName.equalsIgnoreCase(lastTag)) { + throw new PaintException("Invalid UIDL: wrong ending tag: '" + + tagName + "' expected: '" + lastTag + "'."); + } + + parent.addData(tag.getJSON()); + + tag = parent; + } else { + changes++; + uidlBuffer.print(((changes > 1) ? "," : "") + tag.getJSON()); + tag = null; + } + } + + /** + * Substitutes the XML sensitive characters with predefined XML entities. + * + * @param xml + * the String to be substituted. + * @return A new string instance where all occurrences of XML sensitive + * characters are substituted with entities. + */ + static public String escapeXML(String xml) { + if (xml == null || xml.length() <= 0) { + return ""; + } + return escapeXML(new StringBuilder(xml)).toString(); + } + + /** + * Substitutes the XML sensitive characters with predefined XML entities. + * + * @param xml + * the String to be substituted. + * @return A new StringBuilder instance where all occurrences of XML + * sensitive characters are substituted with entities. + * + */ + static StringBuilder escapeXML(StringBuilder xml) { + if (xml == null || xml.length() <= 0) { + return new StringBuilder(""); + } + + final StringBuilder result = new StringBuilder(xml.length() * 2); + + for (int i = 0; i < xml.length(); i++) { + final char c = xml.charAt(i); + final String s = toXmlChar(c); + if (s != null) { + result.append(s); + } else { + result.append(c); + } + } + return result; + } + + /** + * Escapes the given string so it can safely be used as a JSON string. + * + * @param s + * The string to escape + * @return Escaped version of the string + */ + static public String escapeJSON(String s) { + // FIXME: Move this method to another class as other classes use it + // also. + if (s == null) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + final char ch = s.charAt(i); + switch (ch) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '/': + sb.append("\\/"); + break; + default: + if (ch >= '\u0000' && ch <= '\u001F') { + final String ss = Integer.toHexString(ch); + sb.append("\\u"); + for (int k = 0; k < 4 - ss.length(); k++) { + sb.append('0'); + } + sb.append(ss.toUpperCase()); + } else { + sb.append(ch); + } + } + } + return sb.toString(); + } + + /** + * Substitutes a XML sensitive character with predefined XML entity. + * + * @param c + * the Character to be replaced with an entity. + * @return String of the entity or null if character is not to be replaced + * with an entity. + */ + private static String toXmlChar(char c) { + switch (c) { + case '&': + return "&"; // & => & + case '>': + return ">"; // > => > + case '<': + return "<"; // < => < + case '"': + return """; // " => " + case '\'': + return "'"; // ' => ' + default: + return null; + } + } + + /** + * Prints XML-escaped text. + * + * @param str + * @throws PaintException + * if the paint operation failed. + * + */ + + @Override + public void addText(String str) throws PaintException { + tag.addData("\"" + escapeJSON(str) + "\""); + } + + @Override + public void addAttribute(String name, boolean value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + (value ? "true" : "false")); + } + + @Override + public void addAttribute(String name, Resource value) throws PaintException { + if (value == null) { + throw new NullPointerException(); + } + ResourceReference reference = ResourceReference.create(value); + addAttribute(name, reference.getURL()); + } + + @Override + public void addAttribute(String name, int value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, long value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, float value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, double value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, String value) throws PaintException { + // In case of null data output nothing: + if ((value == null) || (name == null)) { + throw new NullPointerException( + "Parameters must be non-null strings"); + } + + tag.addAttribute("\"" + name + "\":\"" + escapeJSON(value) + "\""); + + if (customLayoutArgumentsOpen && "template".equals(name)) { + getUsedResources().add("layouts/" + value + ".html"); + } + + if (name.equals("locale")) { + manager.requireLocale(value); + } + + } + + @Override + public void addAttribute(String name, Component value) + throws PaintException { + final String id = value.getConnectorId(); + addAttribute(name, id); + } + + @Override + public void addAttribute(String name, Map<?, ?> value) + throws PaintException { + + StringBuilder sb = new StringBuilder(); + sb.append("\""); + sb.append(name); + sb.append("\":"); + sb.append("{"); + for (Iterator<?> it = value.keySet().iterator(); it.hasNext();) { + Object key = it.next(); + Object mapValue = value.get(key); + sb.append("\""); + if (key instanceof ClientConnector) { + sb.append(((ClientConnector) key).getConnectorId()); + } else { + sb.append(escapeJSON(key.toString())); + } + sb.append("\":"); + if (mapValue instanceof Float || mapValue instanceof Integer + || mapValue instanceof Double + || mapValue instanceof Boolean + || mapValue instanceof Alignment) { + sb.append(mapValue); + } else { + sb.append("\""); + sb.append(escapeJSON(mapValue.toString())); + sb.append("\""); + } + if (it.hasNext()) { + sb.append(","); + } + } + sb.append("}"); + + tag.addAttribute(sb.toString()); + } + + @Override + public void addAttribute(String name, Object[] values) { + // In case of null data output nothing: + if ((values == null) || (name == null)) { + throw new NullPointerException( + "Parameters must be non-null strings"); + } + final StringBuilder buf = new StringBuilder(); + buf.append("\"" + name + "\":["); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + buf.append(","); + } + buf.append("\""); + buf.append(escapeJSON(values[i].toString())); + buf.append("\""); + } + buf.append("]"); + tag.addAttribute(buf.toString()); + } + + @Override + public void addVariable(VariableOwner owner, String name, String value) + throws PaintException { + tag.addVariable(new StringVariable(owner, name, escapeJSON(value))); + } + + @Override + public void addVariable(VariableOwner owner, String name, Component value) + throws PaintException { + tag.addVariable(new StringVariable(owner, name, value.getConnectorId())); + } + + @Override + public void addVariable(VariableOwner owner, String name, int value) + throws PaintException { + tag.addVariable(new IntVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, long value) + throws PaintException { + tag.addVariable(new LongVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, float value) + throws PaintException { + tag.addVariable(new FloatVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, double value) + throws PaintException { + tag.addVariable(new DoubleVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, boolean value) + throws PaintException { + tag.addVariable(new BooleanVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, String[] value) + throws PaintException { + tag.addVariable(new ArrayVariable(owner, name, value)); + } + + /** + * Adds a upload stream type variable. + * + * TODO not converted for JSON + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void addUploadStreamVariable(VariableOwner owner, String name) + throws PaintException { + startTag("uploadstream"); + addAttribute(UIDL_ARG_NAME, name); + endTag("uploadstream"); + } + + /** + * Prints the single text section. + * + * Prints full text section. The section data is escaped + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the section data to be printed. + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void addSection(String sectionTagName, String sectionData) + throws PaintException { + tag.addData("{\"" + sectionTagName + "\":\"" + escapeJSON(sectionData) + + "\"}"); + } + + /** + * Adds XML directly to UIDL. + * + * @param xml + * the Xml to be added. + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void addUIDL(String xml) throws PaintException { + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + // Make sure that the open start tag is closed before + // anything is written. + + // Escape and write what was given + if (xml != null) { + tag.addData("\"" + escapeJSON(xml) + "\""); + } + + } + + /** + * Adds XML section with namespace. + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the section data. + * @param namespace + * the namespace to be added. + * @throws PaintException + * if the paint operation failed. + * + * @see com.vaadin.terminal.PaintTarget#addXMLSection(String, String, + * String) + */ + + @Override + public void addXMLSection(String sectionTagName, String sectionData, + String namespace) throws PaintException { + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + startTag(sectionTagName); + if (namespace != null) { + addAttribute("xmlns", namespace); + } + + if (sectionData != null) { + tag.addData("\"" + escapeJSON(sectionData) + "\""); + } + endTag(sectionTagName); + } + + /** + * Gets the UIDL already printed to stream. Paint target must be closed + * before the <code>getUIDL</code> can be called. + * + * @return the UIDL. + */ + public String getUIDL() { + if (closed) { + return uidlBuffer.toString(); + } + throw new IllegalStateException( + "Tried to read UIDL from open PaintTarget"); + } + + /** + * Closes the paint target. Paint target must be closed before the + * <code>getUIDL</code> can be called. Subsequent attempts to write to paint + * target. If the target was already closed, call to this function is + * ignored. will generate an exception. + * + * @throws PaintException + * if the paint operation failed. + */ + public void close() throws PaintException { + if (tag != null) { + uidlBuffer.write(tag.getJSON()); + } + flush(); + closed = true; + } + + /** + * Method flush. + */ + private void flush() { + uidlBuffer.flush(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.PaintTarget#startPaintable(com.vaadin.terminal + * .Paintable, java.lang.String) + */ + + @Override + public PaintStatus startPaintable(Component connector, String tagName) + throws PaintException { + boolean topLevelPaintable = openPaintables.isEmpty(); + + getLogger().fine( + "startPaintable for " + connector.getClass().getName() + "@" + + Integer.toHexString(connector.hashCode())); + startTag(tagName, true); + + openPaintables.push(connector); + openPaintableTags.push(tagName); + + addAttribute("id", connector.getConnectorId()); + + // Only paint top level paintables. All sub paintables are marked as + // queued and painted separately later. + if (!topLevelPaintable) { + return PaintStatus.CACHED; + } + + if (connector instanceof CustomLayout) { + customLayoutArgumentsOpen = true; + } + return PaintStatus.PAINTING; + } + + @Override + public void endPaintable(Component paintable) throws PaintException { + getLogger().fine( + "endPaintable for " + paintable.getClass().getName() + "@" + + Integer.toHexString(paintable.hashCode())); + + ClientConnector openPaintable = openPaintables.peek(); + if (paintable != openPaintable) { + throw new PaintException("Invalid UIDL: closing wrong paintable: '" + + paintable.getConnectorId() + "' expected: '" + + openPaintable.getConnectorId() + "'."); + } + // remove paintable from the stack + openPaintables.pop(); + String openTag = openPaintableTags.pop(); + endTag(openTag); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.PaintTarget#addCharacterData(java.lang.String ) + */ + + @Override + public void addCharacterData(String text) throws PaintException { + if (text != null) { + tag.addData(text); + } + } + + /** + * This is basically a container for UI components variables, that will be + * added at the end of JSON object. + * + * @author mattitahvonen + * + */ + class JsonTag implements Serializable { + boolean firstField = false; + + Vector<Object> variables = new Vector<Object>(); + + Vector<Object> children = new Vector<Object>(); + + Vector<Object> attr = new Vector<Object>(); + + StringBuilder data = new StringBuilder(); + + public boolean childrenArrayOpen = false; + + private boolean childNode = false; + + private boolean tagClosed = false; + + public JsonTag(String tagName) { + data.append("[\"" + tagName + "\""); + } + + private void closeTag() { + if (!tagClosed) { + data.append(attributesAsJsonObject()); + data.append(getData()); + // Writes the end (closing) tag + data.append("]"); + tagClosed = true; + } + } + + public String getJSON() { + if (!tagClosed) { + closeTag(); + } + return data.toString(); + } + + public void openChildrenArray() { + if (!childrenArrayOpen) { + // append("c : ["); + childrenArrayOpen = true; + // firstField = true; + } + } + + public void closeChildrenArray() { + // append("]"); + // firstField = false; + } + + public void setChildNode(boolean b) { + childNode = b; + } + + public boolean isChildNode() { + return childNode; + } + + public String startField() { + if (firstField) { + firstField = false; + return ""; + } else { + return ","; + } + } + + /** + * + * @param s + * json string, object or array + */ + public void addData(String s) { + children.add(s); + } + + public String getData() { + final StringBuilder buf = new StringBuilder(); + final Iterator<Object> it = children.iterator(); + while (it.hasNext()) { + buf.append(startField()); + buf.append(it.next()); + } + return buf.toString(); + } + + public void addAttribute(String jsonNode) { + attr.add(jsonNode); + } + + private String attributesAsJsonObject() { + final StringBuilder buf = new StringBuilder(); + buf.append(startField()); + buf.append("{"); + for (final Iterator<Object> iter = attr.iterator(); iter.hasNext();) { + final String element = (String) iter.next(); + buf.append(element); + if (iter.hasNext()) { + buf.append(","); + } + } + buf.append(tag.variablesAsJsonObject()); + buf.append("}"); + return buf.toString(); + } + + public void addVariable(Variable v) { + variables.add(v); + } + + private String variablesAsJsonObject() { + if (variables.size() == 0) { + return ""; + } + final StringBuilder buf = new StringBuilder(); + buf.append(startField()); + buf.append("\"v\":{"); + final Iterator<Object> iter = variables.iterator(); + while (iter.hasNext()) { + final Variable element = (Variable) iter.next(); + buf.append(element.getJsonPresentation()); + if (iter.hasNext()) { + buf.append(","); + } + } + buf.append("}"); + return buf.toString(); + } + } + + abstract class Variable implements Serializable { + + String name; + + public abstract String getJsonPresentation(); + } + + class BooleanVariable extends Variable implements Serializable { + boolean value; + + public BooleanVariable(VariableOwner owner, String name, boolean v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + (value == true ? "true" : "false"); + } + + } + + class StringVariable extends Variable implements Serializable { + String value; + + public StringVariable(VariableOwner owner, String name, String v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":\"" + value + "\""; + } + + } + + class IntVariable extends Variable implements Serializable { + int value; + + public IntVariable(VariableOwner owner, String name, int v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class LongVariable extends Variable implements Serializable { + long value; + + public LongVariable(VariableOwner owner, String name, long v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class FloatVariable extends Variable implements Serializable { + float value; + + public FloatVariable(VariableOwner owner, String name, float v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class DoubleVariable extends Variable implements Serializable { + double value; + + public DoubleVariable(VariableOwner owner, String name, double v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class ArrayVariable extends Variable implements Serializable { + String[] value; + + public ArrayVariable(VariableOwner owner, String name, String[] v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + StringBuilder sb = new StringBuilder(); + sb.append("\""); + sb.append(name); + sb.append("\":["); + for (int i = 0; i < value.length;) { + sb.append("\""); + sb.append(escapeJSON(value[i])); + sb.append("\""); + i++; + if (i < value.length) { + sb.append(","); + } + } + sb.append("]"); + return sb.toString(); + } + } + + public Set<Object> getUsedResources() { + return usedResources; + } + + @Override + @SuppressWarnings("unchecked") + public String getTag(ClientConnector clientConnector) { + Class<? extends ClientConnector> clientConnectorClass = clientConnector + .getClass(); + while (clientConnectorClass.isAnonymousClass()) { + clientConnectorClass = (Class<? extends ClientConnector>) clientConnectorClass + .getSuperclass(); + } + Class<?> clazz = clientConnectorClass; + while (!usedClientConnectors.contains(clazz) + && clazz.getSuperclass() != null + && ClientConnector.class.isAssignableFrom(clazz)) { + usedClientConnectors.add((Class<? extends ClientConnector>) clazz); + clazz = clazz.getSuperclass(); + } + return manager.getTagForType(clientConnectorClass); + } + + Collection<Class<? extends ClientConnector>> getUsedClientConnectors() { + return usedClientConnectors; + } + + @Override + public void addVariable(VariableOwner owner, String name, + StreamVariable value) throws PaintException { + String url = manager.getStreamVariableTargetUrl( + (ClientConnector) owner, name, value); + if (url != null) { + addVariable(owner, name, url); + } // else { //NOP this was just a cleanup by component } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.PaintTarget#isFullRepaint() + */ + + @Override + public boolean isFullRepaint() { + return !cacheEnabled; + } + + private static final Logger getLogger() { + return Logger.getLogger(JsonPaintTarget.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java b/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java new file mode 100644 index 0000000000..9dba05d2c1 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public class LegacyChangeVariablesInvocation extends MethodInvocation { + private Map<String, Object> variableChanges = new HashMap<String, Object>(); + + public LegacyChangeVariablesInvocation(String connectorId, + String variableName, Object value) { + super(connectorId, ApplicationConnection.UPDATE_VARIABLE_INTERFACE, + ApplicationConnection.UPDATE_VARIABLE_METHOD); + setVariableChange(variableName, value); + } + + public static boolean isLegacyVariableChange(String interfaceName, + String methodName) { + return ApplicationConnection.UPDATE_VARIABLE_METHOD + .equals(interfaceName) + && ApplicationConnection.UPDATE_VARIABLE_METHOD + .equals(methodName); + } + + public void setVariableChange(String name, Object value) { + variableChanges.put(name, value); + } + + public Map<String, Object> getVariableChanges() { + return variableChanges; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java b/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java new file mode 100644 index 0000000000..70c3add858 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class NoInputStreamException extends Exception { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java b/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java new file mode 100644 index 0000000000..e4db8453b0 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class NoOutputStreamException extends Exception { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java new file mode 100644 index 0000000000..70505ab5f9 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java @@ -0,0 +1,398 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.File; +import java.io.Serializable; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; +import javax.portlet.MimeResponse; +import javax.portlet.PortletConfig; +import javax.portlet.PortletMode; +import javax.portlet.PortletModeException; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; +import javax.portlet.PortletURL; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; +import javax.portlet.StateAwareResponse; +import javax.servlet.http.HttpSessionBindingListener; +import javax.xml.namespace.QName; + +import com.vaadin.Application; +import com.vaadin.terminal.ExternalResource; +import com.vaadin.ui.Root; + +/** + * TODO Write documentation, fix JavaDoc tags. + * + * This is automatically registered as a {@link HttpSessionBindingListener} when + * {@link PortletSession#setAttribute()} is called with the context as value. + * + * @author peholmst + */ +@SuppressWarnings("serial") +public class PortletApplicationContext2 extends AbstractWebApplicationContext { + + protected Map<Application, Set<PortletListener>> portletListeners = new HashMap<Application, Set<PortletListener>>(); + + protected transient PortletSession session; + protected transient PortletConfig portletConfig; + + protected HashMap<String, Application> portletWindowIdToApplicationMap = new HashMap<String, Application>(); + + private transient PortletResponse response; + + private final Map<String, QName> eventActionDestinationMap = new HashMap<String, QName>(); + private final Map<String, Serializable> eventActionValueMap = new HashMap<String, Serializable>(); + + private final Map<String, String> sharedParameterActionNameMap = new HashMap<String, String>(); + private final Map<String, String> sharedParameterActionValueMap = new HashMap<String, String>(); + + @Override + public File getBaseDirectory() { + String resultPath = session.getPortletContext().getRealPath("/"); + if (resultPath != null) { + return new File(resultPath); + } else { + try { + final URL url = session.getPortletContext().getResource("/"); + return new File(url.getFile()); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger() + .log(Level.INFO, + "Cannot access base directory, possible security issue " + + "with Application Server or Servlet Container", + e); + } + } + return null; + } + + protected PortletCommunicationManager getApplicationManager( + Application application) { + PortletCommunicationManager mgr = (PortletCommunicationManager) applicationToAjaxAppMgrMap + .get(application); + + if (mgr == null) { + // Creates a new manager + mgr = createPortletCommunicationManager(application); + applicationToAjaxAppMgrMap.put(application, mgr); + } + return mgr; + } + + protected PortletCommunicationManager createPortletCommunicationManager( + Application application) { + return new PortletCommunicationManager(application); + } + + public static PortletApplicationContext2 getApplicationContext( + PortletSession session) { + Object cxattr = session.getAttribute(PortletApplicationContext2.class + .getName()); + PortletApplicationContext2 cx = null; + // can be false also e.g. if old context comes from another + // classloader when using + // <private-session-attributes>false</private-session-attributes> + // and redeploying the portlet - see #7461 + if (cxattr instanceof PortletApplicationContext2) { + cx = (PortletApplicationContext2) cxattr; + } + if (cx == null) { + cx = new PortletApplicationContext2(); + session.setAttribute(PortletApplicationContext2.class.getName(), cx); + } + if (cx.session == null) { + cx.session = session; + } + return cx; + } + + @Override + protected void removeApplication(Application application) { + super.removeApplication(application); + // values() is backed by map, removes the key-value pair from the map + portletWindowIdToApplicationMap.values().remove(application); + } + + protected void addApplication(Application application, + String portletWindowId) { + applications.add(application); + portletWindowIdToApplicationMap.put(portletWindowId, application); + } + + public Application getApplicationForWindowId(String portletWindowId) { + return portletWindowIdToApplicationMap.get(portletWindowId); + } + + public PortletSession getPortletSession() { + return session; + } + + public PortletConfig getPortletConfig() { + return portletConfig; + } + + public void setPortletConfig(PortletConfig config) { + portletConfig = config; + } + + public void addPortletListener(Application app, PortletListener listener) { + Set<PortletListener> l = portletListeners.get(app); + if (l == null) { + l = new LinkedHashSet<PortletListener>(); + portletListeners.put(app, l); + } + l.add(listener); + } + + public void removePortletListener(Application app, PortletListener listener) { + Set<PortletListener> l = portletListeners.get(app); + if (l != null) { + l.remove(listener); + } + } + + public void firePortletRenderRequest(Application app, Root root, + RenderRequest request, RenderResponse response) { + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleRenderRequest(request, new RestrictedRenderResponse( + response), root); + } + } + } + + public void firePortletActionRequest(Application app, Root root, + ActionRequest request, ActionResponse response) { + String key = request.getParameter(ActionRequest.ACTION_NAME); + if (eventActionDestinationMap.containsKey(key)) { + // this action request is only to send queued portlet events + response.setEvent(eventActionDestinationMap.get(key), + eventActionValueMap.get(key)); + // cleanup + eventActionDestinationMap.remove(key); + eventActionValueMap.remove(key); + } else if (sharedParameterActionNameMap.containsKey(key)) { + // this action request is only to set shared render parameters + response.setRenderParameter(sharedParameterActionNameMap.get(key), + sharedParameterActionValueMap.get(key)); + // cleanup + sharedParameterActionNameMap.remove(key); + sharedParameterActionValueMap.remove(key); + } else { + // normal action request, notify listeners + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleActionRequest(request, response, root); + } + } + } + } + + public void firePortletEventRequest(Application app, Root root, + EventRequest request, EventResponse response) { + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleEventRequest(request, response, root); + } + } + } + + public void firePortletResourceRequest(Application app, Root root, + ResourceRequest request, ResourceResponse response) { + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleResourceRequest(request, response, root); + } + } + } + + public interface PortletListener extends Serializable { + + public void handleRenderRequest(RenderRequest request, + RenderResponse response, Root root); + + public void handleActionRequest(ActionRequest request, + ActionResponse response, Root root); + + public void handleEventRequest(EventRequest request, + EventResponse response, Root root); + + public void handleResourceRequest(ResourceRequest request, + ResourceResponse response, Root root); + } + + /** + * This is for use by {@link AbstractApplicationPortlet} only. + * + * TODO cleaner implementation, now "semi-static"! + * + * @param mimeResponse + */ + void setResponse(PortletResponse response) { + this.response = response; + } + + /** + * Creates a new action URL. + * + * @param action + * @return action URL or null if called outside a MimeRequest (outside a + * UIDL request or similar) + */ + public PortletURL generateActionURL(String action) { + PortletURL url = null; + if (response instanceof MimeResponse) { + url = ((MimeResponse) response).createActionURL(); + url.setParameter("javax.portlet.action", action); + } else { + return null; + } + return url; + } + + /** + * Sends a portlet event to the indicated destination. + * + * Internally, an action may be created and opened, as an event cannot be + * sent directly from all types of requests. + * + * The event destinations and values need to be kept in the context until + * sent. Any memory leaks if the action fails are limited to the session. + * + * Event names for events sent and received by a portlet need to be declared + * in portlet.xml . + * + * @param root + * a window in which a temporary action URL can be opened if + * necessary + * @param name + * event name + * @param value + * event value object that is Serializable and, if appropriate, + * has a valid JAXB annotation + */ + public void sendPortletEvent(Root root, QName name, Serializable value) + throws IllegalStateException { + if (response instanceof MimeResponse) { + String actionKey = "" + System.currentTimeMillis(); + while (eventActionDestinationMap.containsKey(actionKey)) { + actionKey = actionKey + "."; + } + PortletURL actionUrl = generateActionURL(actionKey); + if (actionUrl != null) { + eventActionDestinationMap.put(actionKey, name); + eventActionValueMap.put(actionKey, value); + root.getPage().open(new ExternalResource(actionUrl.toString())); + } else { + // this should never happen as we already know the response is a + // MimeResponse + throw new IllegalStateException( + "Portlet events can only be sent from a portlet request"); + } + } else if (response instanceof StateAwareResponse) { + ((StateAwareResponse) response).setEvent(name, value); + } else { + throw new IllegalStateException( + "Portlet events can only be sent from a portlet request"); + } + } + + /** + * Sets a shared portlet parameter. + * + * Internally, an action may be created and opened, as shared parameters + * cannot be set directly from all types of requests. + * + * The parameters and values need to be kept in the context until sent. Any + * memory leaks if the action fails are limited to the session. + * + * Shared parameters set or read by a portlet need to be declared in + * portlet.xml . + * + * @param root + * a window in which a temporary action URL can be opened if + * necessary + * @param name + * parameter identifier + * @param value + * parameter value + */ + public void setSharedRenderParameter(Root root, String name, String value) + throws IllegalStateException { + if (response instanceof MimeResponse) { + String actionKey = "" + System.currentTimeMillis(); + while (sharedParameterActionNameMap.containsKey(actionKey)) { + actionKey = actionKey + "."; + } + PortletURL actionUrl = generateActionURL(actionKey); + if (actionUrl != null) { + sharedParameterActionNameMap.put(actionKey, name); + sharedParameterActionValueMap.put(actionKey, value); + root.getPage().open(new ExternalResource(actionUrl.toString())); + } else { + // this should never happen as we already know the response is a + // MimeResponse + throw new IllegalStateException( + "Shared parameters can only be set from a portlet request"); + } + } else if (response instanceof StateAwareResponse) { + ((StateAwareResponse) response).setRenderParameter(name, value); + } else { + throw new IllegalStateException( + "Shared parameters can only be set from a portlet request"); + } + } + + /** + * Sets the portlet mode. This may trigger a new render request. + * + * Portlet modes used by a portlet need to be declared in portlet.xml . + * + * @param root + * a window in which the render URL can be opened if necessary + * @param portletMode + * the portlet mode to switch to + * @throws PortletModeException + * if the portlet mode is not allowed for some reason + * (configuration, permissions etc.) + */ + public void setPortletMode(Root root, PortletMode portletMode) + throws IllegalStateException, PortletModeException { + if (response instanceof MimeResponse) { + PortletURL url = ((MimeResponse) response).createRenderURL(); + url.setPortletMode(portletMode); + throw new RuntimeException("Root.open has not yet been implemented"); + // root.open(new ExternalResource(url.toString())); + } else if (response instanceof StateAwareResponse) { + ((StateAwareResponse) response).setPortletMode(portletMode); + } else { + throw new IllegalStateException( + "Portlet mode can only be changed from a portlet request"); + } + } + + private Logger getLogger() { + return Logger.getLogger(PortletApplicationContext2.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java new file mode 100644 index 0000000000..39c27d05fe --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java @@ -0,0 +1,170 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.InputStream; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletContext; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceURL; + +import com.vaadin.Application; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConfiguration; +import com.vaadin.ui.Root; + +/** + * TODO document me! + * + * @author peholmst + * + */ +@SuppressWarnings("serial") +public class PortletCommunicationManager extends AbstractCommunicationManager { + + public PortletCommunicationManager(Application application) { + super(application); + } + + @Override + protected BootstrapHandler createBootstrapHandler() { + return new BootstrapHandler() { + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + PortletRequest portletRequest = WrappedPortletRequest.cast( + request).getPortletRequest(); + if (portletRequest instanceof RenderRequest) { + return super.handleRequest(application, request, response); + } else { + return false; + } + } + + @Override + protected String getApplicationId(BootstrapContext context) { + PortletRequest portletRequest = WrappedPortletRequest.cast( + context.getRequest()).getPortletRequest(); + /* + * We need to generate a unique ID because some portals already + * create a DIV with the portlet's Window ID as the DOM ID. + */ + return "v-" + portletRequest.getWindowID(); + } + + @Override + protected String getAppUri(BootstrapContext context) { + return getRenderResponse(context).createActionURL().toString(); + } + + private RenderResponse getRenderResponse(BootstrapContext context) { + PortletResponse response = ((WrappedPortletResponse) context + .getResponse()).getPortletResponse(); + + RenderResponse renderResponse = (RenderResponse) response; + return renderResponse; + } + + @Override + protected JSONObject getDefaultParameters(BootstrapContext context) + throws JSONException { + /* + * We need this in order to get uploads to work. TODO this is + * not needed for uploads anymore, check if this is needed for + * some other things + */ + JSONObject defaults = super.getDefaultParameters(context); + + ResourceURL portletResourceUrl = getRenderResponse(context) + .createResourceURL(); + portletResourceUrl + .setResourceID(AbstractApplicationPortlet.RESOURCE_URL_ID); + defaults.put(ApplicationConfiguration.PORTLET_RESOUCE_URL_BASE, + portletResourceUrl.toString()); + + defaults.put("pathInfo", ""); + + return defaults; + } + + @Override + protected void appendMainScriptTagContents( + BootstrapContext context, StringBuilder builder) + throws JSONException, IOException { + // fixed base theme to use - all portal pages with Vaadin + // applications will load this exactly once + String portalTheme = WrappedPortletRequest + .cast(context.getRequest()) + .getPortalProperty( + AbstractApplicationPortlet.PORTAL_PARAMETER_VAADIN_THEME); + if (portalTheme != null + && !portalTheme.equals(context.getThemeName())) { + String portalThemeUri = getThemeUri(context, portalTheme); + // XSS safe - originates from portal properties + builder.append("vaadin.loadTheme('" + portalThemeUri + + "');"); + } + + super.appendMainScriptTagContents(context, builder); + } + + @Override + protected String getMainDivStyle(BootstrapContext context) { + DeploymentConfiguration deploymentConfiguration = context + .getRequest().getDeploymentConfiguration(); + return deploymentConfiguration.getApplicationOrSystemProperty( + AbstractApplicationPortlet.PORTLET_PARAMETER_STYLE, + null); + } + + @Override + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException { + return PortletCommunicationManager.this.getInitialUIDL(request, + root); + } + + @Override + protected JSONObject getApplicationParameters( + BootstrapContext context) throws JSONException, + PaintException { + JSONObject parameters = super.getApplicationParameters(context); + WrappedPortletResponse wrappedPortletResponse = (WrappedPortletResponse) context + .getResponse(); + MimeResponse portletResponse = (MimeResponse) wrappedPortletResponse + .getPortletResponse(); + ResourceURL resourceURL = portletResponse.createResourceURL(); + resourceURL.setResourceID("browserDetails"); + parameters.put("browserDetailsUrl", resourceURL.toString()); + return parameters; + } + + }; + + } + + @Override + protected InputStream getThemeResourceAsStream(Root root, String themeName, + String resource) { + PortletApplicationContext2 context = (PortletApplicationContext2) root + .getApplication().getContext(); + PortletContext portletContext = context.getPortletSession() + .getPortletContext(); + return portletContext.getResourceAsStream("/" + + AbstractApplicationPortlet.THEME_DIRECTORY_PATH + themeName + + "/" + resource); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java b/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java new file mode 100644 index 0000000000..8a30f5c1d4 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java @@ -0,0 +1,56 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.servlet.Filter; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext.TransactionListener; +import com.vaadin.terminal.Terminal; + +/** + * An {@link Application} that implements this interface gets notified of + * request start and end by the terminal. It is quite similar to the + * {@link HttpServletRequestListener}, but the parameters are Portlet specific. + * If an Application is deployed as both a Servlet and a Portlet, one most + * likely needs to implement both. + * <p> + * Only JSR 286 style Portlets are supported. + * <p> + * The interface can be used for several helper tasks including: + * <ul> + * <li>Opening and closing database connections + * <li>Implementing {@link ThreadLocal} + * <li>Inter-portlet communication + * </ul> + * <p> + * Alternatives for implementing similar features are are Servlet {@link Filter} + * s and {@link TransactionListener}s in Vaadin. + * + * @since 6.2 + * @see HttpServletRequestListener + */ +public interface PortletRequestListener extends Serializable { + + /** + * This method is called before {@link Terminal} applies the request to + * Application. + * + * @param requestData + * the {@link PortletRequest} about to change Application state + */ + public void onRequestStart(PortletRequest request, PortletResponse response); + + /** + * This method is called at the end of each request. + * + * @param requestData + * the {@link PortletRequest} + */ + public void onRequestEnd(PortletRequest request, PortletResponse response); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java b/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java new file mode 100644 index 0000000000..6c0edec466 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java @@ -0,0 +1,43 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +/** + * Times the handling of requests and stores the information as an attribute in + * the request. The timing info is later passed on to the client in the UIDL and + * the client provides JavaScript API for accessing this data from e.g. + * TestBench. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public class RequestTimer implements Serializable { + private long requestStartTime = 0; + + /** + * Starts the timing of a request. This should be called before any + * processing of the request. + */ + public void start() { + requestStartTime = System.nanoTime(); + } + + /** + * Stops the timing of a request. This should be called when all processing + * of a request has finished. + * + * @param context + */ + public void stop(AbstractWebApplicationContext context) { + // Measure and store the total handling time. This data can be + // used in TestBench 3 tests. + long time = (System.nanoTime() - requestStartTime) / 1000000; + + // The timings must be stored in the context, since a new + // RequestTimer is created for every request. + context.setLastRequestTime(time); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java b/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java new file mode 100644 index 0000000000..2104ad4b87 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java @@ -0,0 +1,67 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.Application; +import com.vaadin.shared.communication.URLReference; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.ExternalResource; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.ThemeResource; + +public class ResourceReference extends URLReference { + + private Resource resource; + + public ResourceReference(Resource resource) { + this.resource = resource; + } + + public Resource getResource() { + return resource; + } + + @Override + public String getURL() { + if (resource instanceof ExternalResource) { + return ((ExternalResource) resource).getURL(); + } else if (resource instanceof ApplicationResource) { + final ApplicationResource r = (ApplicationResource) resource; + final Application a = r.getApplication(); + if (a == null) { + throw new RuntimeException( + "An ApplicationResource (" + + r.getClass().getName() + + " must be attached to an application when it is sent to the client."); + } + final String uri = a.getRelativeLocation(r); + return uri; + } else if (resource instanceof ThemeResource) { + final String uri = "theme://" + + ((ThemeResource) resource).getResourceId(); + return uri; + } else { + throw new RuntimeException(getClass().getSimpleName() + + " does not support resources of type: " + + resource.getClass().getName()); + } + + } + + public static ResourceReference create(Resource resource) { + if (resource == null) { + return null; + } else { + return new ResourceReference(resource); + } + } + + public static Resource getResource(URLReference reference) { + if (reference == null) { + return null; + } + assert reference instanceof ResourceReference; + return ((ResourceReference) reference).getResource(); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java b/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java new file mode 100644 index 0000000000..9fdffbf9a5 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java @@ -0,0 +1,172 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.Collection; +import java.util.Locale; + +import javax.portlet.CacheControl; +import javax.portlet.PortletMode; +import javax.portlet.PortletURL; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceURL; +import javax.servlet.http.Cookie; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; + +/** + * Read-only wrapper for a {@link RenderResponse}. + * + * Only for use by {@link PortletApplicationContext} and + * {@link PortletApplicationContext2}. + */ +class RestrictedRenderResponse implements RenderResponse, Serializable { + + private RenderResponse response; + + RestrictedRenderResponse(RenderResponse response) { + this.response = response; + } + + @Override + public void addProperty(String key, String value) { + response.addProperty(key, value); + } + + @Override + public PortletURL createActionURL() { + return response.createActionURL(); + } + + @Override + public PortletURL createRenderURL() { + return response.createRenderURL(); + } + + @Override + public String encodeURL(String path) { + return response.encodeURL(path); + } + + @Override + public void flushBuffer() throws IOException { + // NOP + // TODO throw? + } + + @Override + public int getBufferSize() { + return response.getBufferSize(); + } + + @Override + public String getCharacterEncoding() { + return response.getCharacterEncoding(); + } + + @Override + public String getContentType() { + return response.getContentType(); + } + + @Override + public Locale getLocale() { + return response.getLocale(); + } + + @Override + public String getNamespace() { + return response.getNamespace(); + } + + @Override + public OutputStream getPortletOutputStream() throws IOException { + // write forbidden + return null; + } + + @Override + public PrintWriter getWriter() throws IOException { + // write forbidden + return null; + } + + @Override + public boolean isCommitted() { + return response.isCommitted(); + } + + @Override + public void reset() { + // NOP + // TODO throw? + } + + @Override + public void resetBuffer() { + // NOP + // TODO throw? + } + + @Override + public void setBufferSize(int size) { + // NOP + // TODO throw? + } + + @Override + public void setContentType(String type) { + // NOP + // TODO throw? + } + + @Override + public void setProperty(String key, String value) { + response.setProperty(key, value); + } + + @Override + public void setTitle(String title) { + response.setTitle(title); + } + + @Override + public void setNextPossiblePortletModes(Collection<PortletMode> portletModes) { + // NOP + // TODO throw? + } + + @Override + public ResourceURL createResourceURL() { + return response.createResourceURL(); + } + + @Override + public CacheControl getCacheControl() { + return response.getCacheControl(); + } + + @Override + public void addProperty(Cookie cookie) { + // NOP + // TODO throw? + } + + @Override + public void addProperty(String key, Element element) { + // NOP + // TODO throw? + } + + @Override + public Element createElement(String tagName) throws DOMException { + // NOP + return null; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/RpcManager.java b/server/src/com/vaadin/terminal/gwt/server/RpcManager.java new file mode 100644 index 0000000000..026c847e2b --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RpcManager.java @@ -0,0 +1,48 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +/** + * Server side RPC manager that can invoke methods based on RPC calls received + * from the client. + * + * @since 7.0 + */ +public interface RpcManager extends Serializable { + public void applyInvocation(ServerRpcMethodInvocation invocation) + throws RpcInvocationException; + + /** + * Wrapper exception for exceptions which occur during invocation of an RPC + * call + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0 + * + */ + public static class RpcInvocationException extends Exception { + + public RpcInvocationException() { + super(); + } + + public RpcInvocationException(String message, Throwable cause) { + super(message, cause); + } + + public RpcInvocationException(String message) { + super(message); + } + + public RpcInvocationException(Throwable cause) { + super(cause); + } + + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java b/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java new file mode 100644 index 0000000000..b280f5c6b5 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import com.vaadin.terminal.VariableOwner; + +/** + * Marker interface for server side classes that can receive RPC calls. + * + * This plays a role similar to that of {@link VariableOwner}. + * + * @since 7.0 + */ +public interface RpcTarget extends Serializable { + /** + * Returns the RPC manager instance to use when receiving calls for an RPC + * interface. + * + * @param rpcInterface + * interface for which the call was made + * @return RpcManager or null if none found for the interface + */ + public RpcManager getRpcManager(Class<?> rpcInterface); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java b/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java new file mode 100644 index 0000000000..1c7af82a36 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java @@ -0,0 +1,142 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.shared.Connector; + +/** + * Server side RPC manager that handles RPC calls coming from the client. + * + * Each {@link RpcTarget} (typically a {@link ClientConnector}) should have its + * own instance of {@link ServerRpcManager} if it wants to receive RPC calls + * from the client. + * + * @since 7.0 + */ +public class ServerRpcManager<T> implements RpcManager { + + private final T implementation; + private final Class<T> rpcInterface; + + private static final Map<Class<?>, Class<?>> boxedTypes = new HashMap<Class<?>, Class<?>>(); + static { + try { + Class<?>[] boxClasses = new Class<?>[] { Boolean.class, Byte.class, + Short.class, Character.class, Integer.class, Long.class, + Float.class, Double.class }; + for (Class<?> boxClass : boxClasses) { + Field typeField = boxClass.getField("TYPE"); + Class<?> primitiveType = (Class<?>) typeField.get(boxClass); + boxedTypes.put(primitiveType, boxClass); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Create a RPC manager for an RPC target. + * + * @param target + * RPC call target (normally a {@link Connector}) + * @param implementation + * RPC interface implementation for the target + * @param rpcInterface + * RPC interface type + */ + public ServerRpcManager(T implementation, Class<T> rpcInterface) { + this.implementation = implementation; + this.rpcInterface = rpcInterface; + } + + /** + * Invoke a method in a server side RPC target class. This method is to be + * used by the RPC framework and unit testing tools only. + * + * @param target + * non-null target of the RPC call + * @param invocation + * method invocation to perform + * @throws RpcInvocationException + */ + public static void applyInvocation(RpcTarget target, + ServerRpcMethodInvocation invocation) throws RpcInvocationException { + RpcManager manager = target.getRpcManager(invocation + .getInterfaceClass()); + if (manager != null) { + manager.applyInvocation(invocation); + } else { + getLogger() + .log(Level.WARNING, + "RPC call received for RpcTarget " + + target.getClass().getName() + + " (" + + invocation.getConnectorId() + + ") but the target has not registered any RPC interfaces"); + } + } + + /** + * Returns the RPC interface implementation for the RPC target. + * + * @return RPC interface implementation + */ + protected T getImplementation() { + return implementation; + } + + /** + * Returns the RPC interface type managed by this RPC manager instance. + * + * @return RPC interface type + */ + protected Class<T> getRpcInterface() { + return rpcInterface; + } + + /** + * Invoke a method in a server side RPC target class. This method is to be + * used by the RPC framework and unit testing tools only. + * + * @param invocation + * method invocation to perform + */ + @Override + public void applyInvocation(ServerRpcMethodInvocation invocation) + throws RpcInvocationException { + Method method = invocation.getMethod(); + Class<?>[] parameterTypes = method.getParameterTypes(); + Object[] args = new Object[parameterTypes.length]; + Object[] arguments = invocation.getParameters(); + for (int i = 0; i < args.length; i++) { + // no conversion needed for basic cases + // Class<?> type = parameterTypes[i]; + // if (type.isPrimitive()) { + // type = boxedTypes.get(type); + // } + args[i] = arguments[i]; + } + try { + method.invoke(implementation, args); + } catch (Exception e) { + throw new RpcInvocationException("Unable to invoke method " + + invocation.getMethodName() + " in " + + invocation.getInterfaceName(), e); + } + } + + private static Logger getLogger() { + return Logger.getLogger(ServerRpcManager.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java b/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java new file mode 100644 index 0000000000..ff81a27596 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java @@ -0,0 +1,113 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.ServerRpc; + +public class ServerRpcMethodInvocation extends MethodInvocation { + + private static final Map<String, Method> invocationMethodCache = new ConcurrentHashMap<String, Method>( + 128, 0.75f, 1); + + private final Method method; + + private Class<? extends ServerRpc> interfaceClass; + + public ServerRpcMethodInvocation(String connectorId, String interfaceName, + String methodName, int parameterCount) { + super(connectorId, interfaceName, methodName); + + interfaceClass = findClass(); + method = findInvocationMethod(interfaceClass, methodName, + parameterCount); + } + + private Class<? extends ServerRpc> findClass() { + try { + Class<?> rpcInterface = Class.forName(getInterfaceName()); + if (!ServerRpc.class.isAssignableFrom(rpcInterface)) { + throw new IllegalArgumentException("The interface " + + getInterfaceName() + "is not a server RPC interface."); + } + return (Class<? extends ServerRpc>) rpcInterface; + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("The server RPC interface " + + getInterfaceName() + " could not be found", e); + } finally { + + } + } + + public Class<? extends ServerRpc> getInterfaceClass() { + return interfaceClass; + } + + public Method getMethod() { + return method; + } + + /** + * Tries to find the method from the cache or alternatively by invoking + * {@link #doFindInvocationMethod(Class, String, int)} and updating the + * cache. + * + * @param targetType + * @param methodName + * @param parameterCount + * @return + */ + private Method findInvocationMethod(Class<?> targetType, String methodName, + int parameterCount) { + // TODO currently only using method name and number of parameters as the + // signature + String signature = targetType.getName() + "." + methodName + "(" + + parameterCount; + Method invocationMethod = invocationMethodCache.get(signature); + + if (invocationMethod == null) { + invocationMethod = doFindInvocationMethod(targetType, methodName, + parameterCount); + + if (invocationMethod != null) { + invocationMethodCache.put(signature, invocationMethod); + } + } + + if (invocationMethod == null) { + throw new IllegalStateException("Can't find method " + methodName + + " with " + parameterCount + " parameters in " + + targetType.getName()); + } + + return invocationMethod; + } + + /** + * Tries to find the method from the class by looping through available + * methods. + * + * @param targetType + * @param methodName + * @param parameterCount + * @return + */ + private Method doFindInvocationMethod(Class<?> targetType, + String methodName, int parameterCount) { + Method[] methods = targetType.getMethods(); + for (Method method : methods) { + Class<?>[] parameterTypes = method.getParameterTypes(); + if (method.getName().equals(methodName) + && parameterTypes.length == parameterCount) { + return method; + } + } + return null; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java new file mode 100644 index 0000000000..2a1dc31897 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java @@ -0,0 +1,120 @@ +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.Root; + +/* + @VaadinApache2LicenseForJavaFiles@ + */ + +class ServletPortletHelper implements Serializable { + public static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/"; + + public static class ApplicationClassException extends Exception { + + public ApplicationClassException(String message, Throwable cause) { + super(message, cause); + } + + public ApplicationClassException(String message) { + super(message); + } + } + + static Class<? extends Application> getApplicationClass( + DeploymentConfiguration deploymentConfiguration) + throws ApplicationClassException { + String applicationParameter = deploymentConfiguration + .getInitParameters().getProperty("application"); + String rootParameter = deploymentConfiguration.getInitParameters() + .getProperty(Application.ROOT_PARAMETER); + ClassLoader classLoader = deploymentConfiguration.getClassLoader(); + + if (applicationParameter == null) { + + // Validate the parameter value + verifyRootClass(rootParameter, classLoader); + + // Application can be used if a valid rootLayout is defined + return Application.class; + } + + try { + return (Class<? extends Application>) classLoader + .loadClass(applicationParameter); + } catch (final ClassNotFoundException e) { + throw new ApplicationClassException( + "Failed to load application class: " + applicationParameter, + e); + } + } + + private static void verifyRootClass(String className, + ClassLoader classLoader) throws ApplicationClassException { + if (className == null) { + throw new ApplicationClassException(Application.ROOT_PARAMETER + + " init parameter not defined"); + } + + // Check that the root layout class can be found + try { + Class<?> rootClass = classLoader.loadClass(className); + if (!Root.class.isAssignableFrom(rootClass)) { + throw new ApplicationClassException(className + + " does not implement Root"); + } + // Try finding a default constructor, else throw exception + rootClass.getConstructor(); + } catch (ClassNotFoundException e) { + throw new ApplicationClassException(className + + " could not be loaded", e); + } catch (SecurityException e) { + throw new ApplicationClassException("Could not access " + className + + " class", e); + } catch (NoSuchMethodException e) { + throw new ApplicationClassException(className + + " doesn't have a public no-args constructor"); + } + } + + private static boolean hasPathPrefix(WrappedRequest request, String prefix) { + String pathInfo = request.getRequestPathInfo(); + + if (pathInfo == null) { + return false; + } + + if (!prefix.startsWith("/")) { + prefix = '/' + prefix; + } + + if (pathInfo.startsWith(prefix)) { + return true; + } + + return false; + } + + public static boolean isFileUploadRequest(WrappedRequest request) { + return hasPathPrefix(request, UPLOAD_URL_PREFIX); + } + + public static boolean isConnectorResourceRequest(WrappedRequest request) { + return hasPathPrefix(request, + ApplicationConnection.CONNECTOR_RESOURCE_PREFIX + "/"); + } + + public static boolean isUIDLRequest(WrappedRequest request) { + return hasPathPrefix(request, ApplicationConnection.UIDL_REQUEST_PATH); + } + + public static boolean isApplicationResourceRequest(WrappedRequest request) { + return hasPathPrefix(request, ApplicationConnection.APP_REQUEST_PATH); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java b/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java new file mode 100644 index 0000000000..37b76de443 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class SessionExpiredException extends Exception { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java new file mode 100644 index 0000000000..0d4963bd7d --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java @@ -0,0 +1,16 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingEndEvent; + +@SuppressWarnings("serial") +final class StreamingEndEventImpl extends AbstractStreamingEvent implements + StreamingEndEvent { + + public StreamingEndEventImpl(String filename, String type, long totalBytes) { + super(filename, type, totalBytes, totalBytes); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java new file mode 100644 index 0000000000..6ab3df2789 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; + +@SuppressWarnings("serial") +final class StreamingErrorEventImpl extends AbstractStreamingEvent implements + StreamingErrorEvent { + + private final Exception exception; + + public StreamingErrorEventImpl(final String filename, final String type, + long contentLength, long bytesReceived, final Exception exception) { + super(filename, type, contentLength, bytesReceived); + this.exception = exception; + } + + @Override + public final Exception getException() { + return exception; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java new file mode 100644 index 0000000000..cfa7a1b98d --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java @@ -0,0 +1,17 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingProgressEvent; + +@SuppressWarnings("serial") +final class StreamingProgressEventImpl extends AbstractStreamingEvent implements + StreamingProgressEvent { + + public StreamingProgressEventImpl(final String filename, final String type, + long contentLength, long bytesReceived) { + super(filename, type, contentLength, bytesReceived); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java new file mode 100644 index 0000000000..274d05e111 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingStartEvent; + +@SuppressWarnings("serial") +final class StreamingStartEventImpl extends AbstractStreamingEvent implements + StreamingStartEvent { + + private boolean disposed; + + public StreamingStartEventImpl(final String filename, final String type, + long contentLength) { + super(filename, type, contentLength, 0); + } + + @Override + public void disposeStreamVariable() { + disposed = true; + } + + boolean isDisposed() { + return disposed; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java b/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java new file mode 100644 index 0000000000..d15ff8a7ef --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class SystemMessageException extends RuntimeException { + + /** + * Cause of the method exception + */ + private Throwable cause; + + /** + * Constructs a new <code>SystemMessageException</code> with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public SystemMessageException(String msg) { + super(msg); + } + + /** + * Constructs a new <code>SystemMessageException</code> with the specified + * detail message and cause. + * + * @param msg + * the detail message. + * @param cause + * the cause of the exception. + */ + public SystemMessageException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs a new <code>SystemMessageException</code> from another + * exception. + * + * @param cause + * the cause of the exception. + */ + public SystemMessageException(Throwable cause) { + this.cause = cause; + } + + /** + * @see java.lang.Throwable#getCause() + */ + @Override + public Throwable getCause() { + return cause; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java b/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java new file mode 100644 index 0000000000..5248af595e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java @@ -0,0 +1,89 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.Writer; + +import com.vaadin.Application; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; + +/** + * A {@link RequestHandler} that presents an informative page if the browser in + * use is unsupported. Recognizes Chrome Frame and allow it to be used. + * + * <p> + * This handler is usually added to the application by + * {@link AbstractCommunicationManager}. + * </p> + */ +@SuppressWarnings("serial") +public class UnsupportedBrowserHandler implements RequestHandler { + + /** Cookie used to ignore browser checks */ + public static final String FORCE_LOAD_COOKIE = "vaadinforceload=1"; + + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + + if (request.getBrowserDetails() != null) { + // Check if the browser is supported + // If Chrome Frame is available we'll assume it's ok + WebBrowser b = request.getBrowserDetails().getWebBrowser(); + if (b.isTooOldToFunctionProperly() && !b.isChromeFrameCapable()) { + // bypass if cookie set + String c = request.getHeader("Cookie"); + if (c == null || !c.contains(FORCE_LOAD_COOKIE)) { + writeBrowserTooOldPage(request, response); + return true; // request handled + } + } + } + + return false; // pass to next handler + } + + /** + * Writes a page encouraging the user to upgrade to a more current browser. + * + * @param request + * @param response + * @throws IOException + */ + protected void writeBrowserTooOldPage(WrappedRequest request, + WrappedResponse response) throws IOException { + Writer page = response.getWriter(); + WebBrowser b = request.getBrowserDetails().getWebBrowser(); + + page.write("<html><body><h1>I'm sorry, but your browser is not supported</h1>" + + "<p>The version (" + + b.getBrowserMajorVersion() + + "." + + b.getBrowserMinorVersion() + + ") of the browser you are using " + + " is outdated and not supported.</p>" + + "<p>You should <b>consider upgrading</b> to a more up-to-date browser.</p> " + + "<p>The most popular browsers are <b>" + + " <a href=\"https://www.google.com/chrome\">Chrome</a>," + + " <a href=\"http://www.mozilla.com/firefox\">Firefox</a>," + + (b.isWindows() ? " <a href=\"http://windows.microsoft.com/en-US/internet-explorer/downloads/ie\">Internet Explorer</a>," + : "") + + " <a href=\"http://www.opera.com/browser\">Opera</a>" + + " and <a href=\"http://www.apple.com/safari\">Safari</a>.</b><br/>" + + "Upgrading to the latest version of one of these <b>will make the web safer, faster and better looking.</b></p>" + + (b.isIE() ? "<script type=\"text/javascript\" src=\"http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js\"></script>" + + "<p>If you can not upgrade your browser, please consider trying <a onclick=\"CFInstall.check({mode:'overlay'});return false;\" href=\"http://www.google.com/chromeframe\">Chrome Frame</a>.</p>" + : "") // + + "<p><sub><a onclick=\"document.cookie='" + + FORCE_LOAD_COOKIE + + "';window.location.reload();return false;\" href=\"#\">Continue without updating</a> (not recommended)</sub></p>" + + "</body>\n" + "</html>"); + + page.close(); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/UploadException.java b/server/src/com/vaadin/terminal/gwt/server/UploadException.java new file mode 100644 index 0000000000..58253da0fb --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/UploadException.java @@ -0,0 +1,15 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class UploadException extends Exception { + public UploadException(Exception e) { + super("Upload failed", e); + } + + public UploadException(String msg) { + super(msg); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java new file mode 100644 index 0000000000..36c08b2ed9 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java @@ -0,0 +1,180 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.File; +import java.util.Enumeration; +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import com.vaadin.Application; + +/** + * Web application context for Vaadin applications. + * + * This is automatically added as a {@link HttpSessionBindingListener} when + * added to a {@link HttpSession}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.1 + */ +@SuppressWarnings("serial") +public class WebApplicationContext extends AbstractWebApplicationContext { + + protected transient HttpSession session; + private transient boolean reinitializingSession = false; + + /** + * Stores a reference to the currentRequest. Null it not inside a request. + */ + private transient Object currentRequest = null; + + /** + * Creates a new Web Application Context. + * + */ + protected WebApplicationContext() { + + } + + @Override + protected void startTransaction(Application application, Object request) { + currentRequest = request; + super.startTransaction(application, request); + } + + @Override + protected void endTransaction(Application application, Object request) { + super.endTransaction(application, request); + currentRequest = null; + } + + @Override + public void valueUnbound(HttpSessionBindingEvent event) { + if (!reinitializingSession) { + // Avoid closing the application if we are only reinitializing the + // session. Closing the application would cause the state to be lost + // and a new application to be created, which is not what we want. + super.valueUnbound(event); + } + } + + /** + * Discards the current session and creates a new session with the same + * contents. The purpose of this is to introduce a new session key in order + * to avoid session fixation attacks. + */ + @SuppressWarnings("unchecked") + public void reinitializeSession() { + + HttpSession oldSession = getHttpSession(); + + // Stores all attributes (security key, reference to this context + // instance) so they can be added to the new session + HashMap<String, Object> attrs = new HashMap<String, Object>(); + for (Enumeration<String> e = oldSession.getAttributeNames(); e + .hasMoreElements();) { + String name = e.nextElement(); + attrs.put(name, oldSession.getAttribute(name)); + } + + // Invalidate the current session, set flag to avoid call to + // valueUnbound + reinitializingSession = true; + oldSession.invalidate(); + reinitializingSession = false; + + // Create a new session + HttpSession newSession = ((HttpServletRequest) currentRequest) + .getSession(); + + // Restores all attributes (security key, reference to this context + // instance) + for (String name : attrs.keySet()) { + newSession.setAttribute(name, attrs.get(name)); + } + + // Update the "current session" variable + session = newSession; + } + + /** + * Gets the application context base directory. + * + * @see com.vaadin.service.ApplicationContext#getBaseDirectory() + */ + @Override + public File getBaseDirectory() { + final String realPath = ApplicationServlet.getResourcePath( + session.getServletContext(), "/"); + if (realPath == null) { + return null; + } + return new File(realPath); + } + + /** + * Gets the http-session application is running in. + * + * @return HttpSession this application context resides in. + */ + public HttpSession getHttpSession() { + return session; + } + + /** + * Gets the application context for an HttpSession. + * + * @param session + * the HTTP session. + * @return the application context for HttpSession. + */ + static public WebApplicationContext getApplicationContext( + HttpSession session) { + WebApplicationContext cx = (WebApplicationContext) session + .getAttribute(WebApplicationContext.class.getName()); + if (cx == null) { + cx = new WebApplicationContext(); + session.setAttribute(WebApplicationContext.class.getName(), cx); + } + if (cx.session == null) { + cx.session = session; + } + return cx; + } + + protected void addApplication(Application application) { + applications.add(application); + } + + /** + * Gets communication manager for an application. + * + * If this application has not been running before, a new manager is + * created. + * + * @param application + * @return CommunicationManager + */ + public CommunicationManager getApplicationManager(Application application, + AbstractApplicationServlet servlet) { + CommunicationManager mgr = (CommunicationManager) applicationToAjaxAppMgrMap + .get(application); + + if (mgr == null) { + // Creates new manager + mgr = servlet.createCommunicationManager(application); + applicationToAjaxAppMgrMap.put(application, mgr); + } + return mgr; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java b/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java new file mode 100644 index 0000000000..4b92b12b66 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java @@ -0,0 +1,462 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.Date; +import java.util.Locale; + +import com.vaadin.shared.VBrowserDetails; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.WrappedRequest; + +/** + * Class that provides information about the web browser the user is using. + * Provides information such as browser name and version, screen resolution and + * IP address. + * + * @author Vaadin Ltd. + * @version @VERSION@ + */ +public class WebBrowser implements Terminal { + + private int screenHeight = 0; + private int screenWidth = 0; + private String browserApplication = null; + private Locale locale; + private String address; + private boolean secureConnection; + private int timezoneOffset = 0; + private int rawTimezoneOffset = 0; + private int dstSavings; + private boolean dstInEffect; + private boolean touchDevice; + + private VBrowserDetails browserDetails; + private long clientServerTimeDelta; + + /** + * There is no default-theme for this terminal type. + * + * @return Always returns null. + */ + + @Override + public String getDefaultTheme() { + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Terminal#getScreenHeight() + */ + + @Override + public int getScreenHeight() { + return screenHeight; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Terminal#getScreenWidth() + */ + + @Override + public int getScreenWidth() { + return screenWidth; + } + + /** + * Get the browser user-agent string. + * + * @return The raw browser userAgent string + */ + public String getBrowserApplication() { + return browserApplication; + } + + /** + * Gets the IP-address of the web browser. If the application is running + * inside a portlet, this method will return null. + * + * @return IP-address in 1.12.123.123 -format + */ + public String getAddress() { + return address; + } + + /** Get the default locate of the browser. */ + public Locale getLocale() { + return locale; + } + + /** Is the connection made using HTTPS? */ + public boolean isSecureConnection() { + return secureConnection; + } + + /** + * Tests whether the user is using Firefox. + * + * @return true if the user is using Firefox, false if the user is not using + * Firefox or if no information on the browser is present + */ + public boolean isFirefox() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isFirefox(); + } + + /** + * Tests whether the user is using Internet Explorer. + * + * @return true if the user is using Internet Explorer, false if the user is + * not using Internet Explorer or if no information on the browser + * is present + */ + public boolean isIE() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isIE(); + } + + /** + * Tests whether the user is using Safari. + * + * @return true if the user is using Safari, false if the user is not using + * Safari or if no information on the browser is present + */ + public boolean isSafari() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isSafari(); + } + + /** + * Tests whether the user is using Opera. + * + * @return true if the user is using Opera, false if the user is not using + * Opera or if no information on the browser is present + */ + public boolean isOpera() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isOpera(); + } + + /** + * Tests whether the user is using Chrome. + * + * @return true if the user is using Chrome, false if the user is not using + * Chrome or if no information on the browser is present + */ + public boolean isChrome() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isChrome(); + } + + /** + * Tests whether the user is using Chrome Frame. + * + * @return true if the user is using Chrome Frame, false if the user is not + * using Chrome or if no information on the browser is present + */ + public boolean isChromeFrame() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isChromeFrame(); + } + + /** + * Tests whether the user's browser is Chrome Frame capable. + * + * @return true if the user can use Chrome Frame, false if the user can not + * or if no information on the browser is present + */ + public boolean isChromeFrameCapable() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isChromeFrameCapable(); + } + + /** + * Gets the major version of the browser the user is using. + * + * <p> + * Note that Internet Explorer in IE7 compatibility mode might return 8 in + * some cases even though it should return 7. + * </p> + * + * @return The major version of the browser or -1 if not known. + */ + public int getBrowserMajorVersion() { + if (browserDetails == null) { + return -1; + } + + return browserDetails.getBrowserMajorVersion(); + } + + /** + * Gets the minor version of the browser the user is using. + * + * @see #getBrowserMajorVersion() + * + * @return The minor version of the browser or -1 if not known. + */ + public int getBrowserMinorVersion() { + if (browserDetails == null) { + return -1; + } + + return browserDetails.getBrowserMinorVersion(); + } + + /** + * Tests whether the user is using Linux. + * + * @return true if the user is using Linux, false if the user is not using + * Linux or if no information on the browser is present + */ + public boolean isLinux() { + return browserDetails.isLinux(); + } + + /** + * Tests whether the user is using Mac OS X. + * + * @return true if the user is using Mac OS X, false if the user is not + * using Mac OS X or if no information on the browser is present + */ + public boolean isMacOSX() { + return browserDetails.isMacOSX(); + } + + /** + * Tests whether the user is using Windows. + * + * @return true if the user is using Windows, false if the user is not using + * Windows or if no information on the browser is present + */ + public boolean isWindows() { + return browserDetails.isWindows(); + } + + /** + * Returns the browser-reported TimeZone offset in milliseconds from GMT. + * This includes possible daylight saving adjustments, to figure out which + * TimeZone the user actually might be in, see + * {@link #getRawTimezoneOffset()}. + * + * @see WebBrowser#getRawTimezoneOffset() + * @return timezone offset in milliseconds, 0 if not available + */ + public Integer getTimezoneOffset() { + return timezoneOffset; + } + + /** + * Returns the browser-reported TimeZone offset in milliseconds from GMT + * ignoring possible daylight saving adjustments that may be in effect in + * the browser. + * <p> + * You can use this to figure out which TimeZones the user could actually be + * in by calling {@link TimeZone#getAvailableIDs(int)}. + * </p> + * <p> + * If {@link #getRawTimezoneOffset()} and {@link #getTimezoneOffset()} + * returns the same value, the browser is either in a zone that does not + * currently have daylight saving time, or in a zone that never has daylight + * saving time. + * </p> + * + * @return timezone offset in milliseconds excluding DST, 0 if not available + */ + public Integer getRawTimezoneOffset() { + return rawTimezoneOffset; + } + + /** + * Gets the difference in minutes between the browser's GMT TimeZone and + * DST. + * + * @return the amount of minutes that the TimeZone shifts when DST is in + * effect + */ + public int getDSTSavings() { + return dstSavings; + } + + /** + * Determines whether daylight savings time (DST) is currently in effect in + * the region of the browser or not. + * + * @return true if the browser resides at a location that currently is in + * DST + */ + public boolean isDSTInEffect() { + return dstInEffect; + } + + /** + * Returns the current date and time of the browser. This will not be + * entirely accurate due to varying network latencies, but should provide a + * close-enough value for most cases. Also note that the returned Date + * object uses servers default time zone, not the clients. + * + * @return the current date and time of the browser. + * @see #isDSTInEffect() + * @see #getDSTSavings() + * @see #getTimezoneOffset() + */ + public Date getCurrentDate() { + return new Date(new Date().getTime() + clientServerTimeDelta); + } + + /** + * @return true if the browser is detected to support touch events + */ + public boolean isTouchDevice() { + return touchDevice; + } + + /** + * For internal use by AbstractApplicationServlet/AbstractApplicationPortlet + * only. Updates all properties in the class according to the given + * information. + * + * @param sw + * Screen width + * @param sh + * Screen height + * @param tzo + * TimeZone offset in minutes from GMT + * @param rtzo + * raw TimeZone offset in minutes from GMT (w/o DST adjustment) + * @param dstSavings + * the difference between the raw TimeZone and DST in minutes + * @param dstInEffect + * is DST currently active in the region or not? + * @param curDate + * the current date in milliseconds since the epoch + * @param touchDevice + */ + void updateClientSideDetails(String sw, String sh, String tzo, String rtzo, + String dstSavings, String dstInEffect, String curDate, + boolean touchDevice) { + if (sw != null) { + try { + screenHeight = Integer.parseInt(sh); + screenWidth = Integer.parseInt(sw); + } catch (final NumberFormatException e) { + screenHeight = screenWidth = 0; + } + } + if (tzo != null) { + try { + // browser->java conversion: min->ms, reverse sign + timezoneOffset = -Integer.parseInt(tzo) * 60 * 1000; + } catch (final NumberFormatException e) { + timezoneOffset = 0; // default gmt+0 + } + } + if (rtzo != null) { + try { + // browser->java conversion: min->ms, reverse sign + rawTimezoneOffset = -Integer.parseInt(rtzo) * 60 * 1000; + } catch (final NumberFormatException e) { + rawTimezoneOffset = 0; // default gmt+0 + } + } + if (dstSavings != null) { + try { + // browser->java conversion: min->ms + this.dstSavings = Integer.parseInt(dstSavings) * 60 * 1000; + } catch (final NumberFormatException e) { + this.dstSavings = 0; // default no savings + } + } + if (dstInEffect != null) { + this.dstInEffect = Boolean.parseBoolean(dstInEffect); + } + if (curDate != null) { + try { + long curTime = Long.parseLong(curDate); + clientServerTimeDelta = curTime - new Date().getTime(); + } catch (final NumberFormatException e) { + clientServerTimeDelta = 0; + } + } + this.touchDevice = touchDevice; + + } + + /** + * For internal use by AbstractApplicationServlet/AbstractApplicationPortlet + * only. Updates all properties in the class according to the given + * information. + * + * @param request + * the wrapped request to read the information from + */ + void updateRequestDetails(WrappedRequest request) { + locale = request.getLocale(); + address = request.getRemoteAddr(); + secureConnection = request.isSecure(); + String agent = request.getHeader("user-agent"); + + if (agent != null) { + browserApplication = agent; + browserDetails = new VBrowserDetails(agent); + } + + if (request.getParameter("sw") != null) { + updateClientSideDetails(request.getParameter("sw"), + request.getParameter("sh"), request.getParameter("tzo"), + request.getParameter("rtzo"), request.getParameter("dstd"), + request.getParameter("dston"), + request.getParameter("curdate"), + request.getParameter("td") != null); + } + } + + /** + * Checks if the browser is so old that it simply won't work with a Vaadin + * application. Can be used to redirect to an alternative page, show + * alternative content or similar. + * + * When this method returns true chances are very high that the browser + * won't work and it does not make sense to direct the user to the Vaadin + * application. + * + * @return true if the browser won't work, false if not the browser is + * supported or might work + */ + public boolean isTooOldToFunctionProperly() { + if (browserDetails == null) { + // Don't know, so assume it will work + return false; + } + + return browserDetails.isTooOldToFunctionProperly(); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java new file mode 100644 index 0000000000..cf58f398af --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import com.vaadin.Application; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedRequest; + +/** + * Wrapper for {@link HttpServletRequest}. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedRequest + * @see WrappedHttpServletResponse + */ +public class WrappedHttpServletRequest extends HttpServletRequestWrapper + implements WrappedRequest { + + private final DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a http servlet request and associates with a deployment + * configuration + * + * @param request + * the http servlet request to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedHttpServletRequest(HttpServletRequest request, + DeploymentConfiguration deploymentConfiguration) { + super(request); + this.deploymentConfiguration = deploymentConfiguration; + } + + @Override + public String getRequestPathInfo() { + return getPathInfo(); + } + + @Override + public int getSessionMaxInactiveInterval() { + return getSession().getMaxInactiveInterval(); + } + + @Override + public Object getSessionAttribute(String name) { + return getSession().getAttribute(name); + } + + @Override + public void setSessionAttribute(String name, Object attribute) { + getSession().setAttribute(name, attribute); + } + + /** + * Gets the original, unwrapped HTTP servlet request. + * + * @return the servlet request + */ + public HttpServletRequest getHttpServletRequest() { + return this; + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + @Override + public BrowserDetails getBrowserDetails() { + return new BrowserDetails() { + @Override + public String getUriFragment() { + return null; + } + + @Override + public String getWindowName() { + return null; + } + + @Override + public WebBrowser getWebBrowser() { + WebApplicationContext context = (WebApplicationContext) Application + .getCurrent().getContext(); + return context.getBrowser(); + } + }; + } + + /** + * Helper method to get a <code>WrappedHttpServletRequest</code> from a + * <code>WrappedRequest</code>. Aside from casting, this method also takes + * care of situations where there's another level of wrapping. + * + * @param request + * a wrapped request + * @return a wrapped http servlet request + * @throws ClassCastException + * if the wrapped request doesn't wrap a http servlet request + */ + public static WrappedHttpServletRequest cast(WrappedRequest request) { + if (request instanceof CombinedRequest) { + CombinedRequest combinedRequest = (CombinedRequest) request; + request = combinedRequest.getSecondRequest(); + } + return (WrappedHttpServletRequest) request; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java new file mode 100644 index 0000000000..32b2f352a8 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java @@ -0,0 +1,75 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedResponse; + +/** + * Wrapper for {@link HttpServletResponse}. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedResponse + * @see WrappedHttpServletRequest + */ +public class WrappedHttpServletResponse extends HttpServletResponseWrapper + implements WrappedResponse { + + private DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a http servlet response and an associated deployment configuration + * + * @param response + * the http servlet response to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedHttpServletResponse(HttpServletResponse response, + DeploymentConfiguration deploymentConfiguration) { + super(response); + this.deploymentConfiguration = deploymentConfiguration; + } + + /** + * Gets the original unwrapped <code>HttpServletResponse</code> + * + * @return the unwrapped response + */ + public HttpServletResponse getHttpServletResponse() { + return this; + } + + @Override + public void setCacheTime(long milliseconds) { + doSetCacheTime(this, milliseconds); + } + + // Implementation shared with WrappedPortletResponse + static void doSetCacheTime(WrappedResponse response, long milliseconds) { + if (milliseconds <= 0) { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + } else { + response.setHeader("Cache-Control", "max-age=" + milliseconds + / 1000); + response.setDateHeader("Expires", System.currentTimeMillis() + + milliseconds); + // Required to apply caching in some Tomcats + response.setHeader("Pragma", "cache"); + } + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java new file mode 100644 index 0000000000..a3fa172034 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java @@ -0,0 +1,217 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.Map; + +import javax.portlet.ClientDataRequest; +import javax.portlet.PortletRequest; +import javax.portlet.ResourceRequest; + +import com.vaadin.Application; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * Wrapper for {@link PortletRequest} and its subclasses. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedRequest + * @see WrappedPortletResponse + */ +public class WrappedPortletRequest implements WrappedRequest { + + private final PortletRequest request; + private final DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a portlet request and an associated deployment configuration + * + * @param request + * the portlet request to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedPortletRequest(PortletRequest request, + DeploymentConfiguration deploymentConfiguration) { + this.request = request; + this.deploymentConfiguration = deploymentConfiguration; + } + + @Override + public Object getAttribute(String name) { + return request.getAttribute(name); + } + + @Override + public int getContentLength() { + try { + return ((ClientDataRequest) request).getContentLength(); + } catch (ClassCastException e) { + throw new IllegalStateException( + "Content lenght only available for ClientDataRequests"); + } + } + + @Override + public InputStream getInputStream() throws IOException { + try { + return ((ClientDataRequest) request).getPortletInputStream(); + } catch (ClassCastException e) { + throw new IllegalStateException( + "Input data only available for ClientDataRequests"); + } + } + + @Override + public String getParameter(String name) { + return request.getParameter(name); + } + + @Override + public Map<String, String[]> getParameterMap() { + return request.getParameterMap(); + } + + @Override + public void setAttribute(String name, Object o) { + request.setAttribute(name, o); + } + + @Override + public String getRequestPathInfo() { + if (request instanceof ResourceRequest) { + ResourceRequest resourceRequest = (ResourceRequest) request; + String resourceID = resourceRequest.getResourceID(); + if (AbstractApplicationPortlet.RESOURCE_URL_ID.equals(resourceID)) { + String resourcePath = resourceRequest + .getParameter(ApplicationConnection.V_RESOURCE_PATH); + return resourcePath; + } + return resourceID; + } else { + return null; + } + } + + @Override + public int getSessionMaxInactiveInterval() { + return request.getPortletSession().getMaxInactiveInterval(); + } + + @Override + public Object getSessionAttribute(String name) { + return request.getPortletSession().getAttribute(name); + } + + @Override + public void setSessionAttribute(String name, Object attribute) { + request.getPortletSession().setAttribute(name, attribute); + } + + /** + * Gets the original, unwrapped portlet request. + * + * @return the unwrapped portlet request + */ + public PortletRequest getPortletRequest() { + return request; + } + + @Override + public String getContentType() { + try { + return ((ResourceRequest) request).getContentType(); + } catch (ClassCastException e) { + throw new IllegalStateException( + "Content type only available for ResourceRequests"); + } + } + + @Override + public BrowserDetails getBrowserDetails() { + return new BrowserDetails() { + @Override + public String getUriFragment() { + return null; + } + + @Override + public String getWindowName() { + return null; + } + + @Override + public WebBrowser getWebBrowser() { + PortletApplicationContext2 context = (PortletApplicationContext2) Application + .getCurrent().getContext(); + return context.getBrowser(); + } + }; + } + + @Override + public Locale getLocale() { + return request.getLocale(); + } + + @Override + public String getRemoteAddr() { + return null; + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public String getHeader(String string) { + return null; + } + + /** + * Reads a portal property from the portal context of the wrapped request. + * + * @param name + * a string with the name of the portal property to get + * @return a string with the value of the property, or <code>null</code> if + * the property is not defined + */ + public String getPortalProperty(String name) { + return request.getPortalContext().getProperty(name); + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + /** + * Helper method to get a <code>WrappedPortlettRequest</code> from a + * <code>WrappedRequest</code>. Aside from casting, this method also takes + * care of situations where there's another level of wrapping. + * + * @param request + * a wrapped request + * @return a wrapped portlet request + * @throws ClassCastException + * if the wrapped request doesn't wrap a portlet request + */ + public static WrappedPortletRequest cast(WrappedRequest request) { + if (request instanceof CombinedRequest) { + CombinedRequest combinedRequest = (CombinedRequest) request; + request = combinedRequest.getSecondRequest(); + } + return (WrappedPortletRequest) request; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java new file mode 100644 index 0000000000..f7ecf26f3c --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java @@ -0,0 +1,111 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletResponse; +import javax.portlet.ResourceResponse; + +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedResponse; + +/** + * Wrapper for {@link PortletResponse} and its subclasses. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedResponse + * @see WrappedPortletRequest + */ +public class WrappedPortletResponse implements WrappedResponse { + private static final DateFormat HTTP_DATE_FORMAT = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); + static { + HTTP_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + private final PortletResponse response; + private DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a portlet response and an associated deployment configuration + * + * @param response + * the portlet response to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedPortletResponse(PortletResponse response, + DeploymentConfiguration deploymentConfiguration) { + this.response = response; + this.deploymentConfiguration = deploymentConfiguration; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return ((MimeResponse) response).getPortletOutputStream(); + } + + /** + * Gets the original, unwrapped portlet response. + * + * @return the unwrapped portlet response + */ + public PortletResponse getPortletResponse() { + return response; + } + + @Override + public void setContentType(String type) { + ((MimeResponse) response).setContentType(type); + } + + @Override + public PrintWriter getWriter() throws IOException { + return ((MimeResponse) response).getWriter(); + } + + @Override + public void setStatus(int responseStatus) { + response.setProperty(ResourceResponse.HTTP_STATUS_CODE, + Integer.toString(responseStatus)); + } + + @Override + public void setHeader(String name, String value) { + response.setProperty(name, value); + } + + @Override + public void setDateHeader(String name, long timestamp) { + response.setProperty(name, HTTP_DATE_FORMAT.format(new Date(timestamp))); + } + + @Override + public void setCacheTime(long milliseconds) { + WrappedHttpServletResponse.doSetCacheTime(this, milliseconds); + } + + @Override + public void sendError(int errorCode, String message) throws IOException { + setStatus(errorCode); + getWriter().write(message); + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/package.html b/server/src/com/vaadin/terminal/package.html new file mode 100644 index 0000000000..83514a0de5 --- /dev/null +++ b/server/src/com/vaadin/terminal/package.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides classes and interfaces that wrap the terminal-side functionalities +for the server-side application. (FIXME: This could be a little more descriptive and wordy.)</p> + +<h2>Package Specification</h2> + +<!-- Package spec here --> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> diff --git a/server/src/com/vaadin/tools/ReflectTools.java b/server/src/com/vaadin/tools/ReflectTools.java new file mode 100644 index 0000000000..ea2afae301 --- /dev/null +++ b/server/src/com/vaadin/tools/ReflectTools.java @@ -0,0 +1,126 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.tools; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * An util class with helpers for reflection operations. Used internally by + * Vaadin and should not be used by application developers. Subject to change at + * any time. + * + * @since 6.2 + */ +public class ReflectTools { + /** + * Locates the method in the given class. Returns null if the method is not + * found. Throws an ExceptionInInitializerError if there is a problem + * locating the method as this is mainly called from static blocks. + * + * @param cls + * Class that contains the method + * @param methodName + * The name of the method + * @param parameterTypes + * The parameter types for the method. + * @return A reference to the method + * @throws ExceptionInInitializerError + * Wraps any exception in an {@link ExceptionInInitializerError} + * so this method can be called from a static initializer. + */ + public static Method findMethod(Class<?> cls, String methodName, + Class<?>... parameterTypes) throws ExceptionInInitializerError { + try { + return cls.getDeclaredMethod(methodName, parameterTypes); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Returns the value of the java field. + * <p> + * Uses getter if present, otherwise tries to access even private fields + * directly. + * + * @param object + * The object containing the field + * @param field + * The field we want to get the value for + * @return The value of the field in the object + * @throws InvocationTargetException + * If the value could not be retrieved + * @throws IllegalAccessException + * If the value could not be retrieved + * @throws IllegalArgumentException + * If the value could not be retrieved + */ + public static Object getJavaFieldValue(Object object, + java.lang.reflect.Field field) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + PropertyDescriptor pd; + try { + pd = new PropertyDescriptor(field.getName(), object.getClass()); + Method getter = pd.getReadMethod(); + if (getter != null) { + return getter.invoke(object, (Object[]) null); + } + } catch (IntrospectionException e1) { + // Ignore this and try to get directly using the field + } + + // Try to get the value or throw an exception + if (!field.isAccessible()) { + // Try to gain access even if field is private + field.setAccessible(true); + } + return field.get(object); + } + + /** + * Sets the value of a java field. + * <p> + * Uses setter if present, otherwise tries to access even private fields + * directly. + * + * @param object + * The object containing the field + * @param field + * The field we want to set the value for + * @param value + * The value to set + * @throws IllegalAccessException + * If the value could not be assigned to the field + * @throws IllegalArgumentException + * If the value could not be assigned to the field + * @throws InvocationTargetException + * If the value could not be assigned to the field + */ + public static void setJavaFieldValue(Object object, + java.lang.reflect.Field field, Object value) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + PropertyDescriptor pd; + try { + pd = new PropertyDescriptor(field.getName(), object.getClass()); + Method setter = pd.getWriteMethod(); + if (setter != null) { + // Exceptions are thrown forward if this fails + setter.invoke(object, value); + } + } catch (IntrospectionException e1) { + // Ignore this and try to set directly using the field + } + + // Try to set the value directly to the field or throw an exception + if (!field.isAccessible()) { + // Try to gain access even if field is private + field.setAccessible(true); + } + field.set(object, value); + } +} diff --git a/server/src/com/vaadin/tools/WidgetsetCompiler.java b/server/src/com/vaadin/tools/WidgetsetCompiler.java new file mode 100644 index 0000000000..ecc1946e60 --- /dev/null +++ b/server/src/com/vaadin/tools/WidgetsetCompiler.java @@ -0,0 +1,94 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.tools; + +import java.lang.reflect.Method; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.terminal.gwt.widgetsetutils.WidgetSetBuilder; + +/** + * A wrapper for the GWT 1.6 compiler that runs the compiler in a new thread. + * + * This allows circumventing a J2SE 5.0 bug (6316197) that prevents setting the + * stack size for the main thread. Thus, larger widgetsets can be compiled. + * + * This class takes the same command line arguments as the + * com.google.gwt.dev.GWTCompiler class. The old and deprecated compiler is used + * for compatibility with GWT 1.5. + * + * A typical invocation would use e.g. the following arguments + * + * "-out WebContent/VAADIN/widgetsets com.vaadin.terminal.gwt.DefaultWidgetSet" + * + * In addition, larger memory usage settings for the VM should be used, e.g. + * + * "-Xms256M -Xmx512M -Xss8M" + * + * The source directory containing widgetset and related classes must be + * included in the classpath, as well as the gwt-dev-[platform].jar and other + * relevant JARs. + * + * @deprecated with Java 6, can use com.google.gwt.dev.Compiler directly (also + * in Eclipse plug-in etc.) + */ +@Deprecated +public class WidgetsetCompiler { + + /** + * @param args + * same arguments as for com.google.gwt.dev.Compiler + */ + public static void main(final String[] args) { + try { + // run the compiler in a different thread to enable using the + // user-set stack size + + // on Windows, the default stack size is too small for the main + // thread and cannot be changed in JRE 1.5 (see + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6316197) + + Runnable runCompiler = new Runnable() { + @Override + public void run() { + try { + // GWTCompiler.main(args); + // avoid warnings + + String wsname = args[args.length - 1]; + + // TODO expecting this is launched via eclipse WTP + // project + System.out + .println("Updating GWT module description file..."); + WidgetSetBuilder.updateWidgetSet(wsname); + System.out.println("Done."); + + System.out.println("Starting GWT compiler"); + System.setProperty("gwt.nowarn.legacy.tools", "true"); + Class<?> compilerClass = Class + .forName("com.google.gwt.dev.GWTCompiler"); + Method method = compilerClass.getDeclaredMethod("main", + String[].class); + method.invoke(null, new Object[] { args }); + } catch (Throwable thr) { + getLogger().log(Level.SEVERE, + "Widgetset compilation failed", thr); + } + } + }; + Thread runThread = new Thread(runCompiler); + runThread.start(); + runThread.join(); + System.out.println("Widgetset compilation finished"); + } catch (Throwable thr) { + getLogger().log(Level.SEVERE, "Widgetset compilation failed", thr); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(WidgetsetCompiler.class.getName()); + } +} diff --git a/server/src/com/vaadin/ui/AbsoluteLayout.java b/server/src/com/vaadin/ui/AbsoluteLayout.java new file mode 100644 index 0000000000..1c84ca2865 --- /dev/null +++ b/server/src/com/vaadin/ui/AbsoluteLayout.java @@ -0,0 +1,632 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutServerRpc; +import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +/** + * AbsoluteLayout is a layout implementation that mimics html absolute + * positioning. + * + */ +@SuppressWarnings("serial") +public class AbsoluteLayout extends AbstractLayout implements + LayoutClickNotifier { + + private AbsoluteLayoutServerRpc rpc = new AbsoluteLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(AbsoluteLayout.this, + mouseDetails, clickedConnector)); + } + }; + // Maps each component to a position + private LinkedHashMap<Component, ComponentPosition> componentToCoordinates = new LinkedHashMap<Component, ComponentPosition>(); + + /** + * Creates an AbsoluteLayout with full size. + */ + public AbsoluteLayout() { + registerRpc(rpc); + setSizeFull(); + } + + @Override + public AbsoluteLayoutState getState() { + return (AbsoluteLayoutState) super.getState(); + } + + /** + * Gets an iterator for going through all components enclosed in the + * absolute layout. + */ + @Override + public Iterator<Component> getComponentIterator() { + return componentToCoordinates.keySet().iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return componentToCoordinates.size(); + } + + /** + * Replaces one component with another one. The new component inherits the + * old components position. + */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + ComponentPosition position = getPosition(oldComponent); + removeComponent(oldComponent); + addComponent(newComponent, position); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component + * ) + */ + @Override + public void addComponent(Component c) { + addComponent(c, new ComponentPosition()); + } + + /** + * Adds a component to the layout. The component can be positioned by + * providing a string formatted in CSS-format. + * <p> + * For example the string "top:10px;left:10px" will position the component + * 10 pixels from the left and 10 pixels from the top. The identifiers: + * "top","left","right" and "bottom" can be used to specify the position. + * </p> + * + * @param c + * The component to add to the layout + * @param cssPosition + * The css position string + */ + public void addComponent(Component c, String cssPosition) { + ComponentPosition position = new ComponentPosition(); + position.setCSSString(cssPosition); + addComponent(c, position); + } + + /** + * Adds the component using the given position. Ensures the position is only + * set if the component is added correctly. + * + * @param c + * The component to add + * @param position + * The position info for the component. Must not be null. + * @throws IllegalArgumentException + * If adding the component failed + */ + private void addComponent(Component c, ComponentPosition position) + throws IllegalArgumentException { + /* + * Create position instance and add it to componentToCoordinates map. We + * need to do this before we call addComponent so the attachListeners + * can access this position. #6368 + */ + internalSetPosition(c, position); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + internalRemoveComponent(c); + throw e; + } + requestRepaint(); + } + + /** + * Removes the component from all internal data structures. Does not + * actually remove the component from the layout (this is assumed to have + * been done by the caller). + * + * @param c + * The component to remove + */ + private void internalRemoveComponent(Component c) { + componentToCoordinates.remove(c); + } + + @Override + public void updateState() { + super.updateState(); + + // This could be in internalRemoveComponent and internalSetComponent if + // Map<Connector,String> was supported. We cannot get the child + // connectorId unless the component is attached to the application so + // the String->String map cannot be populated in internal* either. + Map<String, String> connectorToPosition = new HashMap<String, String>(); + for (Iterator<Component> ci = getComponentIterator(); ci.hasNext();) { + Component c = ci.next(); + connectorToPosition.put(c.getConnectorId(), getPosition(c) + .getCSSString()); + } + getState().setConnectorToCssPosition(connectorToPosition); + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui + * .Component) + */ + @Override + public void removeComponent(Component c) { + internalRemoveComponent(c); + super.removeComponent(c); + requestRepaint(); + } + + /** + * Gets the position of a component in the layout. Returns null if component + * is not attached to the layout. + * <p> + * Note that you cannot update the position by updating this object. Call + * {@link #setPosition(Component, ComponentPosition)} with the updated + * {@link ComponentPosition} object. + * </p> + * + * @param component + * The component which position is needed + * @return An instance of ComponentPosition containing the position of the + * component, or null if the component is not enclosed in the + * layout. + */ + public ComponentPosition getPosition(Component component) { + return componentToCoordinates.get(component); + } + + /** + * Sets the position of a component in the layout. + * + * @param component + * @param position + */ + public void setPosition(Component component, ComponentPosition position) { + if (!componentToCoordinates.containsKey(component)) { + throw new IllegalArgumentException( + "Component must be a child of this layout"); + } + internalSetPosition(component, position); + } + + /** + * Updates the position for a component. Caller must ensure component is a + * child of this layout. + * + * @param component + * The component. Must be a child for this layout. Not enforced. + * @param position + * New position. Must not be null. + */ + private void internalSetPosition(Component component, + ComponentPosition position) { + componentToCoordinates.put(component, position); + requestRepaint(); + } + + /** + * The CompontPosition class represents a components position within the + * absolute layout. It contains the attributes for left, right, top and + * bottom and the units used to specify them. + */ + public class ComponentPosition implements Serializable { + + private int zIndex = -1; + private Float topValue = null; + private Float rightValue = null; + private Float bottomValue = null; + private Float leftValue = null; + + private Unit topUnits = Unit.PIXELS; + private Unit rightUnits = Unit.PIXELS; + private Unit bottomUnits = Unit.PIXELS; + private Unit leftUnits = Unit.PIXELS; + + /** + * Sets the position attributes using CSS syntax. Attributes not + * included in the string are reset to their unset states. + * + * <code><pre> + * setCSSString("top:10px;left:20%;z-index:16;"); + * </pre></code> + * + * @param css + */ + public void setCSSString(String css) { + topValue = rightValue = bottomValue = leftValue = null; + topUnits = rightUnits = bottomUnits = leftUnits = Unit.PIXELS; + zIndex = -1; + if (css == null) { + return; + } + + String[] cssProperties = css.split(";"); + for (int i = 0; i < cssProperties.length; i++) { + String[] keyValuePair = cssProperties[i].split(":"); + String key = keyValuePair[0].trim(); + if (key.equals("")) { + continue; + } + if (key.equals("z-index")) { + zIndex = Integer.parseInt(keyValuePair[1].trim()); + } else { + String value; + if (keyValuePair.length > 1) { + value = keyValuePair[1].trim(); + } else { + value = ""; + } + String symbol = value.replaceAll("[0-9\\.\\-]+", ""); + if (!symbol.equals("")) { + value = value.substring(0, value.indexOf(symbol)) + .trim(); + } + float v = Float.parseFloat(value); + Unit unit = Unit.getUnitFromSymbol(symbol); + if (key.equals("top")) { + topValue = v; + topUnits = unit; + } else if (key.equals("right")) { + rightValue = v; + rightUnits = unit; + } else if (key.equals("bottom")) { + bottomValue = v; + bottomUnits = unit; + } else if (key.equals("left")) { + leftValue = v; + leftUnits = unit; + } + } + } + requestRepaint(); + } + + /** + * Converts the internal values into a valid CSS string. + * + * @return A valid CSS string + */ + public String getCSSString() { + String s = ""; + if (topValue != null) { + s += "top:" + topValue + topUnits.getSymbol() + ";"; + } + if (rightValue != null) { + s += "right:" + rightValue + rightUnits.getSymbol() + ";"; + } + if (bottomValue != null) { + s += "bottom:" + bottomValue + bottomUnits.getSymbol() + ";"; + } + if (leftValue != null) { + s += "left:" + leftValue + leftUnits.getSymbol() + ";"; + } + if (zIndex >= 0) { + s += "z-index:" + zIndex + ";"; + } + return s; + } + + /** + * Sets the 'top' attribute; distance from the top of the component to + * the top edge of the layout. + * + * @param topValue + * The value of the 'top' attribute + * @param topUnits + * The unit of the 'top' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setTop(Float topValue, Unit topUnits) { + this.topValue = topValue; + this.topUnits = topUnits; + requestRepaint(); + } + + /** + * Sets the 'right' attribute; distance from the right of the component + * to the right edge of the layout. + * + * @param rightValue + * The value of the 'right' attribute + * @param rightUnits + * The unit of the 'right' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setRight(Float rightValue, Unit rightUnits) { + this.rightValue = rightValue; + this.rightUnits = rightUnits; + requestRepaint(); + } + + /** + * Sets the 'bottom' attribute; distance from the bottom of the + * component to the bottom edge of the layout. + * + * @param bottomValue + * The value of the 'bottom' attribute + * @param units + * The unit of the 'bottom' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setBottom(Float bottomValue, Unit bottomUnits) { + this.bottomValue = bottomValue; + this.bottomUnits = bottomUnits; + requestRepaint(); + } + + /** + * Sets the 'left' attribute; distance from the left of the component to + * the left edge of the layout. + * + * @param leftValue + * The value of the 'left' attribute + * @param units + * The unit of the 'left' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setLeft(Float leftValue, Unit leftUnits) { + this.leftValue = leftValue; + this.leftUnits = leftUnits; + requestRepaint(); + } + + /** + * Sets the 'z-index' attribute; the visual stacking order + * + * @param zIndex + * The z-index for the component. + */ + public void setZIndex(int zIndex) { + this.zIndex = zIndex; + requestRepaint(); + } + + /** + * Sets the value of the 'top' attribute; distance from the top of the + * component to the top edge of the layout. + * + * @param topValue + * The value of the 'left' attribute + */ + public void setTopValue(Float topValue) { + this.topValue = topValue; + requestRepaint(); + } + + /** + * Gets the 'top' attributes value in current units. + * + * @see #getTopUnits() + * @return The value of the 'top' attribute, null if not set + */ + public Float getTopValue() { + return topValue; + } + + /** + * Gets the 'right' attributes value in current units. + * + * @return The value of the 'right' attribute, null if not set + * @see #getRightUnits() + */ + public Float getRightValue() { + return rightValue; + } + + /** + * Sets the 'right' attribute value (distance from the right of the + * component to the right edge of the layout). Currently active units + * are maintained. + * + * @param rightValue + * The value of the 'right' attribute + * @see #setRightUnits(int) + */ + public void setRightValue(Float rightValue) { + this.rightValue = rightValue; + requestRepaint(); + } + + /** + * Gets the 'bottom' attributes value using current units. + * + * @return The value of the 'bottom' attribute, null if not set + * @see #getBottomUnits() + */ + public Float getBottomValue() { + return bottomValue; + } + + /** + * Sets the 'bottom' attribute value (distance from the bottom of the + * component to the bottom edge of the layout). Currently active units + * are maintained. + * + * @param bottomValue + * The value of the 'bottom' attribute + * @see #setBottomUnits(int) + */ + public void setBottomValue(Float bottomValue) { + this.bottomValue = bottomValue; + requestRepaint(); + } + + /** + * Gets the 'left' attributes value using current units. + * + * @return The value of the 'left' attribute, null if not set + * @see #getLeftUnits() + */ + public Float getLeftValue() { + return leftValue; + } + + /** + * Sets the 'left' attribute value (distance from the left of the + * component to the left edge of the layout). Currently active units are + * maintained. + * + * @param leftValue + * The value of the 'left' CSS-attribute + * @see #setLeftUnits(int) + */ + public void setLeftValue(Float leftValue) { + this.leftValue = leftValue; + requestRepaint(); + } + + /** + * Gets the unit for the 'top' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getTopUnits() { + return topUnits; + } + + /** + * Sets the unit for the 'top' attribute + * + * @param topUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setTopUnits(Unit topUnits) { + this.topUnits = topUnits; + requestRepaint(); + } + + /** + * Gets the unit for the 'right' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getRightUnits() { + return rightUnits; + } + + /** + * Sets the unit for the 'right' attribute + * + * @param rightUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setRightUnits(Unit rightUnits) { + this.rightUnits = rightUnits; + requestRepaint(); + } + + /** + * Gets the unit for the 'bottom' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getBottomUnits() { + return bottomUnits; + } + + /** + * Sets the unit for the 'bottom' attribute + * + * @param bottomUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setBottomUnits(Unit bottomUnits) { + this.bottomUnits = bottomUnits; + requestRepaint(); + } + + /** + * Gets the unit for the 'left' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getLeftUnits() { + return leftUnits; + } + + /** + * Sets the unit for the 'left' attribute + * + * @param leftUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setLeftUnits(Unit leftUnits) { + this.leftUnits = leftUnits; + requestRepaint(); + } + + /** + * Gets the 'z-index' attribute. + * + * @return the zIndex The z-index attribute + */ + public int getZIndex() { + return zIndex; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return getCSSString(); + } + + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java new file mode 100644 index 0000000000..e7cb38256c --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractComponent.java @@ -0,0 +1,1382 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.vaadin.Application; +import com.vaadin.event.ActionManager; +import com.vaadin.event.EventRouter; +import com.vaadin.event.MethodEventSource; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.ComponentState; +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator; +import com.vaadin.terminal.gwt.server.ResourceReference; +import com.vaadin.tools.ReflectTools; + +/** + * An abstract class that defines default implementation for the + * {@link Component} interface. Basic UI components that are not derived from an + * external component can inherit this class to easily qualify as Vaadin + * components. Most components in Vaadin do just that. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractComponent extends AbstractClientConnector + implements Component, MethodEventSource { + + /* Private members */ + + /** + * Application specific data object. The component does not use or modify + * this. + */ + private Object applicationData; + + /** + * The EventRouter used for the event model. + */ + private EventRouter eventRouter = null; + + /** + * The internal error message of the component. + */ + private ErrorMessage componentError = null; + + /** + * Locale of this component. + */ + private Locale locale; + + /** + * The component should receive focus (if {@link Focusable}) when attached. + */ + private boolean delayedFocus; + + /* Sizeable fields */ + + private float width = SIZE_UNDEFINED; + private float height = SIZE_UNDEFINED; + private Unit widthUnit = Unit.PIXELS; + private Unit heightUnit = Unit.PIXELS; + private static final Pattern sizePattern = Pattern + .compile("^(-?\\d+(\\.\\d+)?)(%|px|em|ex|in|cm|mm|pt|pc)?$"); + + private ComponentErrorHandler errorHandler = null; + + /** + * Keeps track of the Actions added to this component; the actual + * handling/notifying is delegated, usually to the containing window. + */ + private ActionManager actionManager; + + /* Constructor */ + + /** + * Constructs a new Component. + */ + public AbstractComponent() { + // ComponentSizeValidator.setCreationLocation(this); + } + + /* Get/Set component properties */ + + @Override + public void setDebugId(String id) { + getState().setDebugId(id); + } + + @Override + public String getDebugId() { + return getState().getDebugId(); + } + + /** + * Gets style for component. Multiple styles are joined with spaces. + * + * @return the component's styleValue of property style. + * @deprecated Use getStyleName() instead; renamed for consistency and to + * indicate that "style" should not be used to switch client + * side implementation, only to style the component. + */ + @Deprecated + public String getStyle() { + return getStyleName(); + } + + /** + * Sets and replaces all previous style names of the component. This method + * will trigger a {@link RepaintRequestEvent}. + * + * @param style + * the new style of the component. + * @deprecated Use setStyleName() instead; renamed for consistency and to + * indicate that "style" should not be used to switch client + * side implementation, only to style the component. + */ + @Deprecated + public void setStyle(String style) { + setStyleName(style); + } + + /* + * Gets the component's style. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + @Override + public String getStyleName() { + String s = ""; + if (getState().getStyles() != null) { + for (final Iterator<String> it = getState().getStyles().iterator(); it + .hasNext();) { + s += it.next(); + if (it.hasNext()) { + s += " "; + } + } + } + return s; + } + + /* + * Sets the component's style. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + @Override + public void setStyleName(String style) { + if (style == null || "".equals(style)) { + getState().setStyles(null); + requestRepaint(); + return; + } + if (getState().getStyles() == null) { + getState().setStyles(new ArrayList<String>()); + } + List<String> styles = getState().getStyles(); + styles.clear(); + String[] styleParts = style.split(" +"); + for (String part : styleParts) { + if (part.length() > 0) { + styles.add(part); + } + } + requestRepaint(); + } + + @Override + public void addStyleName(String style) { + if (style == null || "".equals(style)) { + return; + } + if (style.contains(" ")) { + // Split space separated style names and add them one by one. + for (String realStyle : style.split(" ")) { + addStyleName(realStyle); + } + return; + } + + if (getState().getStyles() == null) { + getState().setStyles(new ArrayList<String>()); + } + List<String> styles = getState().getStyles(); + if (!styles.contains(style)) { + styles.add(style); + requestRepaint(); + } + } + + @Override + public void removeStyleName(String style) { + if (getState().getStyles() != null) { + String[] styleParts = style.split(" +"); + for (String part : styleParts) { + if (part.length() > 0) { + getState().getStyles().remove(part); + } + } + requestRepaint(); + } + } + + /* + * Get's the component's caption. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + @Override + public String getCaption() { + return getState().getCaption(); + } + + /** + * Sets the component's caption <code>String</code>. Caption is the visible + * name of the component. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param caption + * the new caption <code>String</code> for the component. + */ + @Override + public void setCaption(String caption) { + getState().setCaption(caption); + requestRepaint(); + } + + /* + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Locale getLocale() { + if (locale != null) { + return locale; + } + HasComponents parent = getParent(); + if (parent != null) { + return parent.getLocale(); + } + final Application app = getApplication(); + if (app != null) { + return app.getLocale(); + } + return null; + } + + /** + * Sets the locale of this component. + * + * <pre> + * // Component for which the locale is meaningful + * InlineDateField date = new InlineDateField("Datum"); + * + * // German language specified with ISO 639-1 language + * // code and ISO 3166-1 alpha-2 country code. + * date.setLocale(new Locale("de", "DE")); + * + * date.setResolution(DateField.RESOLUTION_DAY); + * layout.addComponent(date); + * </pre> + * + * + * @param locale + * the locale to become this component's locale. + */ + public void setLocale(Locale locale) { + this.locale = locale; + + // FIXME: Reload value if there is a converter + requestRepaint(); + } + + /* + * Gets the component's icon resource. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public Resource getIcon() { + return ResourceReference.getResource(getState().getIcon()); + } + + /** + * Sets the component's icon. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param icon + * the icon to be shown with the component's caption. + */ + @Override + public void setIcon(Resource icon) { + getState().setIcon(ResourceReference.create(icon)); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#isEnabled() + */ + @Override + public boolean isEnabled() { + return getState().isEnabled(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#setEnabled(boolean) + */ + @Override + public void setEnabled(boolean enabled) { + if (getState().isEnabled() != enabled) { + getState().setEnabled(enabled); + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Connector#isConnectorEnabled() + */ + @Override + public boolean isConnectorEnabled() { + if (!isVisible()) { + return false; + } else if (!isEnabled()) { + return false; + } else if (!super.isConnectorEnabled()) { + return false; + } else if (!getParent().isComponentVisible(this)) { + return false; + } else { + return true; + } + } + + /* + * Tests if the component is in the immediate mode. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + public boolean isImmediate() { + return getState().isImmediate(); + } + + /** + * Sets the component's immediate mode to the specified status. This method + * will trigger a {@link RepaintRequestEvent}. + * + * @param immediate + * the boolean value specifying if the component should be in the + * immediate mode after the call. + * @see Component#isImmediate() + */ + public void setImmediate(boolean immediate) { + getState().setImmediate(immediate); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#isVisible() + */ + @Override + public boolean isVisible() { + return getState().isVisible(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#setVisible(boolean) + */ + @Override + public void setVisible(boolean visible) { + if (getState().isVisible() == visible) { + return; + } + + getState().setVisible(visible); + requestRepaint(); + if (getParent() != null) { + // Must always repaint the parent (at least the hierarchy) when + // visibility of a child component changes. + getParent().requestRepaint(); + } + } + + /** + * <p> + * Gets the component's description, used in tooltips and can be displayed + * directly in certain other components such as forms. The description can + * be used to briefly describe the state of the component to the user. The + * description string may contain certain XML tags: + * </p> + * + * <p> + * <table border=1> + * <tr> + * <td width=120><b>Tag</b></td> + * <td width=120><b>Description</b></td> + * <td width=120><b>Example</b></td> + * </tr> + * <tr> + * <td><b></td> + * <td>bold</td> + * <td><b>bold text</b></td> + * </tr> + * <tr> + * <td><i></td> + * <td>italic</td> + * <td><i>italic text</i></td> + * </tr> + * <tr> + * <td><u></td> + * <td>underlined</td> + * <td><u>underlined text</u></td> + * </tr> + * <tr> + * <td><br></td> + * <td>linebreak</td> + * <td>N/A</td> + * </tr> + * <tr> + * <td><ul><br> + * <li>item1<br> + * <li>item1<br> + * </ul></td> + * <td>item list</td> + * <td> + * <ul> + * <li>item1 + * <li>item2 + * </ul> + * </td> + * </tr> + * </table> + * </p> + * + * <p> + * These tags may be nested. + * </p> + * + * @return component's description <code>String</code> + */ + public String getDescription() { + return getState().getDescription(); + } + + /** + * Sets the component's description. See {@link #getDescription()} for more + * information on what the description is. This method will trigger a + * {@link RepaintRequestEvent}. + * + * The description is displayed as HTML/XHTML in tooltips or directly in + * certain components so care should be taken to avoid creating the + * possibility for HTML injection and possibly XSS vulnerabilities. + * + * @param description + * the new description string for the component. + */ + public void setDescription(String description) { + getState().setDescription(description); + requestRepaint(); + } + + /* + * Gets the component's parent component. Don't add a JavaDoc comment here, + * we use the default documentation from implemented interface. + */ + @Override + public HasComponents getParent() { + return (HasComponents) super.getParent(); + } + + @Override + public void setParent(ClientConnector parent) { + if (parent == null || parent instanceof HasComponents) { + super.setParent(parent); + } else { + throw new IllegalArgumentException( + "The parent of a Component must implement HasComponents, which " + + parent.getClass() + " doesn't do."); + } + } + + /** + * Returns the closest ancestor with the given type. + * <p> + * To find the Window that contains the component, use {@code Window w = + * getParent(Window.class);} + * </p> + * + * @param <T> + * The type of the ancestor + * @param parentType + * The ancestor class we are looking for + * @return The first ancestor that can be assigned to the given class. Null + * if no ancestor with the correct type could be found. + */ + public <T extends HasComponents> T findAncestor(Class<T> parentType) { + HasComponents p = getParent(); + while (p != null) { + if (parentType.isAssignableFrom(p.getClass())) { + return parentType.cast(p); + } + p = p.getParent(); + } + return null; + } + + /** + * Gets the error message for this component. + * + * @return ErrorMessage containing the description of the error state of the + * component or null, if the component contains no errors. Extending + * classes should override this method if they support other error + * message types such as validation errors or buffering errors. The + * returned error message contains information about all the errors. + */ + public ErrorMessage getErrorMessage() { + return componentError; + } + + /** + * Gets the component's error message. + * + * @link Terminal.ErrorMessage#ErrorMessage(String, int) + * + * @return the component's error message. + */ + public ErrorMessage getComponentError() { + return componentError; + } + + /** + * Sets the component's error message. The message may contain certain XML + * tags, for more information see + * + * @link Component.ErrorMessage#ErrorMessage(String, int) + * + * @param componentError + * the new <code>ErrorMessage</code> of the component. + */ + public void setComponentError(ErrorMessage componentError) { + this.componentError = componentError; + fireComponentErrorEvent(); + requestRepaint(); + } + + /* + * Tests if the component is in read-only mode. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean isReadOnly() { + return getState().isReadOnly(); + } + + /* + * Sets the component's read-only mode. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public void setReadOnly(boolean readOnly) { + getState().setReadOnly(readOnly); + requestRepaint(); + } + + /* + * Gets the parent window of the component. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Root getRoot() { + // Just make method from implemented Component interface public + return super.getRoot(); + } + + /* + * Notify the component that it's attached to a window. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void attach() { + super.attach(); + if (delayedFocus) { + focus(); + } + setActionManagerViewer(); + } + + /* + * Detach the component from application. Don't add a JavaDoc comment here, + * we use the default documentation from implemented interface. + */ + @Override + public void detach() { + super.detach(); + if (actionManager != null) { + // Remove any existing viewer. Root cast is just to make the + // compiler happy + actionManager.setViewer((Root) null); + } + } + + /** + * Sets the focus for this component if the component is {@link Focusable}. + */ + protected void focus() { + if (this instanceof Focusable) { + final Application app = getApplication(); + if (app != null) { + getRoot().setFocusedComponent((Focusable) this); + delayedFocus = false; + } else { + delayedFocus = true; + } + } + } + + /** + * Gets the application object to which the component is attached. + * + * <p> + * The method will return {@code null} if the component is not currently + * attached to an application. This is often a problem in constructors of + * regular components and in the initializers of custom composite + * components. A standard workaround is to move the problematic + * initialization to {@link #attach()}, as described in the documentation of + * the method. + * </p> + * <p> + * <b>This method is not meant to be overridden. Due to CDI requirements we + * cannot declare it as final even though it should be final.</b> + * </p> + * + * @return the parent application of the component or <code>null</code>. + * @see #attach() + */ + @Override + public Application getApplication() { + // Just make method inherited from Component interface public + return super.getApplication(); + } + + /** + * Build CSS compatible string representation of height. + * + * @return CSS height + */ + private String getCSSHeight() { + if (getHeightUnits() == Unit.PIXELS) { + return ((int) getHeight()) + getHeightUnits().getSymbol(); + } else { + return getHeight() + getHeightUnits().getSymbol(); + } + } + + /** + * Build CSS compatible string representation of width. + * + * @return CSS width + */ + private String getCSSWidth() { + if (getWidthUnits() == Unit.PIXELS) { + return ((int) getWidth()) + getWidthUnits().getSymbol(); + } else { + return getWidth() + getWidthUnits().getSymbol(); + } + } + + /** + * Returns the shared state bean with information to be sent from the server + * to the client. + * + * Subclasses should override this method and set any relevant fields of the + * state returned by super.getState(). + * + * @since 7.0 + * + * @return updated component shared state + */ + @Override + public ComponentState getState() { + return (ComponentState) super.getState(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#updateState() + */ + @Override + public void updateState() { + // TODO This logic should be on the client side and the state should + // simply be a data object with "width" and "height". + if (getHeight() >= 0 + && (getHeightUnits() != Unit.PERCENTAGE || ComponentSizeValidator + .parentCanDefineHeight(this))) { + getState().setHeight("" + getCSSHeight()); + } else { + getState().setHeight(""); + } + + if (getWidth() >= 0 + && (getWidthUnits() != Unit.PERCENTAGE || ComponentSizeValidator + .parentCanDefineWidth(this))) { + getState().setWidth("" + getCSSWidth()); + } else { + getState().setWidth(""); + } + + ErrorMessage error = getErrorMessage(); + if (null != error) { + getState().setErrorMessage(error.getFormattedHtmlMessage()); + } else { + getState().setErrorMessage(null); + } + } + + /* Documentation copied from interface */ + @Override + public void requestRepaint() { + // Invisible components (by flag in this particular component) do not + // need repaints + if (!getState().isVisible()) { + return; + } + super.requestRepaint(); + } + + /* General event framework */ + + private static final Method COMPONENT_EVENT_METHOD = ReflectTools + .findMethod(Component.Listener.class, "componentEvent", + Component.Event.class); + + /** + * <p> + * Registers a new listener with the specified activation method to listen + * events generated by this component. If the activation method does not + * have any arguments the event object will not be passed to it when it's + * called. + * </p> + * + * <p> + * This method additionally informs the event-api to route events with the + * given eventIdentifier to the components handleEvent function call. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventIdentifier + * the identifier of the event to listen for + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param target + * the object instance who owns the activation method. + * @param method + * the activation method. + * + * @since 6.2 + */ + protected void addListener(String eventIdentifier, Class<?> eventType, + Object target, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + boolean needRepaint = !eventRouter.hasListeners(eventType); + eventRouter.addListener(eventType, target, method); + + if (needRepaint) { + getState().addRegisteredEventListener(eventIdentifier); + requestRepaint(); + } + } + + /** + * Checks if the given {@link Event} type is listened for this component. + * + * @param eventType + * the event type to be checked + * @return true if a listener is registered for the given event type + */ + protected boolean hasListeners(Class<?> eventType) { + return eventRouter != null && eventRouter.hasListeners(eventType); + } + + /** + * Removes all registered listeners matching the given parameters. Since + * this method receives the event type and the listener object as + * parameters, it will unregister all <code>object</code>'s methods that are + * registered to listen to events of type <code>eventType</code> generated + * by this component. + * + * <p> + * This method additionally informs the event-api to stop routing events + * with the given eventIdentifier to the components handleEvent function + * call. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventIdentifier + * the identifier of the event to stop listening for + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + * + * @since 6.2 + */ + protected void removeListener(String eventIdentifier, Class<?> eventType, + Object target) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target); + if (!eventRouter.hasListeners(eventType)) { + getState().removeRegisteredEventListener(eventIdentifier); + requestRepaint(); + } + } + } + + /** + * <p> + * Registers a new listener with the specified activation method to listen + * events generated by this component. If the activation method does not + * have any arguments the event object will not be passed to it when it's + * called. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param target + * the object instance who owns the activation method. + * @param method + * the activation method. + */ + @Override + public void addListener(Class<?> eventType, Object target, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, target, method); + } + + /** + * <p> + * Convenience method for registering a new listener with the specified + * activation method to listen events generated by this component. If the + * activation method does not have any arguments the event object will not + * be passed to it when it's called. + * </p> + * + * <p> + * This version of <code>addListener</code> gets the name of the activation + * method as a parameter. The actual method is reflected from + * <code>object</code>, and unless exactly one match is found, + * <code>java.lang.IllegalArgumentException</code> is thrown. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * <p> + * Note: Using this method is discouraged because it cannot be checked + * during compilation. Use {@link #addListener(Class, Object, Method)} or + * {@link #addListener(com.vaadin.ui.Component.Listener)} instead. + * </p> + * + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param target + * the object instance who owns the activation method. + * @param methodName + * the name of the activation method. + */ + @Override + public void addListener(Class<?> eventType, Object target, String methodName) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, target, methodName); + } + + /** + * Removes all registered listeners matching the given parameters. Since + * this method receives the event type and the listener object as + * parameters, it will unregister all <code>object</code>'s methods that are + * registered to listen to events of type <code>eventType</code> generated + * by this component. + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + */ + @Override + public void removeListener(Class<?> eventType, Object target) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target); + } + } + + /** + * Removes one registered listener method. The given method owned by the + * given object will no longer be called when the specified events are + * generated by this component. + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * target object that has registered to listen to events of type + * <code>eventType</code> with one or more methods. + * @param method + * the method owned by <code>target</code> that's registered to + * listen to events of type <code>eventType</code>. + */ + @Override + public void removeListener(Class<?> eventType, Object target, Method method) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, method); + } + } + + /** + * <p> + * Removes one registered listener method. The given method owned by the + * given object will no longer be called when the specified events are + * generated by this component. + * </p> + * + * <p> + * This version of <code>removeListener</code> gets the name of the + * activation method as a parameter. The actual method is reflected from + * <code>target</code>, and unless exactly one match is found, + * <code>java.lang.IllegalArgumentException</code> is thrown. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + * @param methodName + * the name of the method owned by <code>target</code> that's + * registered to listen to events of type <code>eventType</code>. + */ + @Override + public void removeListener(Class<?> eventType, Object target, + String methodName) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, methodName); + } + } + + /** + * Returns all listeners that are registered for the given event type or one + * of its subclasses. + * + * @param eventType + * The type of event to return listeners for. + * @return A collection with all registered listeners. Empty if no listeners + * are found. + */ + public Collection<?> getListeners(Class<?> eventType) { + if (eventRouter == null) { + return Collections.EMPTY_LIST; + } + + return eventRouter.getListeners(eventType); + } + + /** + * Sends the event to all listeners. + * + * @param event + * the Event to be sent to all listeners. + */ + protected void fireEvent(Component.Event event) { + if (eventRouter != null) { + eventRouter.fireEvent(event); + } + + } + + /* Component event framework */ + + /* + * Registers a new listener to listen events generated by this component. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void addListener(Component.Listener listener) { + addListener(Component.Event.class, listener, COMPONENT_EVENT_METHOD); + } + + /* + * Removes a previously registered listener from this component. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Component.Listener listener) { + removeListener(Component.Event.class, listener, COMPONENT_EVENT_METHOD); + } + + /** + * Emits the component event. It is transmitted to all registered listeners + * interested in such events. + */ + protected void fireComponentEvent() { + fireEvent(new Component.Event(this)); + } + + /** + * Emits the component error event. It is transmitted to all registered + * listeners interested in such events. + */ + protected void fireComponentErrorEvent() { + fireEvent(new Component.ErrorEvent(getComponentError(), this)); + } + + /** + * Sets the data object, that can be used for any application specific data. + * The component does not use or modify this data. + * + * @param data + * the Application specific data. + * @since 3.1 + */ + public void setData(Object data) { + applicationData = data; + } + + /** + * Gets the application specific data. See {@link #setData(Object)}. + * + * @return the Application specific data set with setData function. + * @since 3.1 + */ + public Object getData() { + return applicationData; + } + + /* Sizeable and other size related methods */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getHeight() + */ + @Override + public float getHeight() { + return height; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getHeightUnits() + */ + @Override + public Unit getHeightUnits() { + return heightUnit; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getWidth() + */ + @Override + public float getWidth() { + return width; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getWidthUnits() + */ + @Override + public Unit getWidthUnits() { + return widthUnit; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(float, Unit) + */ + @Override + public void setHeight(float height, Unit unit) { + if (unit == null) { + throw new IllegalArgumentException("Unit can not be null"); + } + this.height = height; + heightUnit = unit; + requestRepaint(); + // ComponentSizeValidator.setHeightLocation(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setSizeFull() + */ + @Override + public void setSizeFull() { + setWidth(100, Unit.PERCENTAGE); + setHeight(100, Unit.PERCENTAGE); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setSizeUndefined() + */ + @Override + public void setSizeUndefined() { + setWidth(-1, Unit.PIXELS); + setHeight(-1, Unit.PIXELS); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(float, Unit) + */ + @Override + public void setWidth(float width, Unit unit) { + if (unit == null) { + throw new IllegalArgumentException("Unit can not be null"); + } + this.width = width; + widthUnit = unit; + requestRepaint(); + // ComponentSizeValidator.setWidthLocation(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(java.lang.String) + */ + @Override + public void setWidth(String width) { + Size size = parseStringSize(width); + if (size != null) { + setWidth(size.getSize(), size.getUnit()); + } else { + setWidth(-1, Unit.PIXELS); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(java.lang.String) + */ + @Override + public void setHeight(String height) { + Size size = parseStringSize(height); + if (size != null) { + setHeight(size.getSize(), size.getUnit()); + } else { + setHeight(-1, Unit.PIXELS); + } + } + + /* + * Returns array with size in index 0 unit in index 1. Null or empty string + * will produce {-1,Unit#PIXELS} + */ + private static Size parseStringSize(String s) { + if (s == null) { + return null; + } + s = s.trim(); + if ("".equals(s)) { + return null; + } + float size = 0; + Unit unit = null; + Matcher matcher = sizePattern.matcher(s); + if (matcher.find()) { + size = Float.parseFloat(matcher.group(1)); + if (size < 0) { + size = -1; + unit = Unit.PIXELS; + } else { + String symbol = matcher.group(3); + unit = Unit.getUnitFromSymbol(symbol); + } + } else { + throw new IllegalArgumentException("Invalid size argument: \"" + s + + "\" (should match " + sizePattern.pattern() + ")"); + } + return new Size(size, unit); + } + + private static class Size implements Serializable { + float size; + Unit unit; + + public Size(float size, Unit unit) { + this.size = size; + this.unit = unit; + } + + public float getSize() { + return size; + } + + public Unit getUnit() { + return unit; + } + } + + public interface ComponentErrorEvent extends Terminal.ErrorEvent { + } + + public interface ComponentErrorHandler extends Serializable { + /** + * Handle the component error + * + * @param event + * @return True if the error has been handled False, otherwise + */ + public boolean handleComponentError(ComponentErrorEvent event); + } + + /** + * Gets the error handler for the component. + * + * The error handler is dispatched whenever there is an error processing the + * data coming from the client. + * + * @return + */ + public ComponentErrorHandler getErrorHandler() { + return errorHandler; + } + + /** + * Sets the error handler for the component. + * + * The error handler is dispatched whenever there is an error processing the + * data coming from the client. + * + * If the error handler is not set, the application error handler is used to + * handle the exception. + * + * @param errorHandler + * AbstractField specific error handler + */ + public void setErrorHandler(ComponentErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Handle the component error event. + * + * @param error + * Error event to handle + * @return True if the error has been handled False, otherwise. If the error + * haven't been handled by this component, it will be handled in the + * application error handler. + */ + public boolean handleError(ComponentErrorEvent error) { + if (errorHandler != null) { + return errorHandler.handleComponentError(error); + } + return false; + + } + + /* + * Actions + */ + + /** + * Gets the {@link ActionManager} used to manage the + * {@link ShortcutListener}s added to this {@link Field}. + * + * @return the ActionManager in use + */ + protected ActionManager getActionManager() { + if (actionManager == null) { + actionManager = new ActionManager(); + setActionManagerViewer(); + } + return actionManager; + } + + /** + * Set a viewer for the action manager to be the parent sub window (if the + * component is in a window) or the root (otherwise). This is still a + * simplification of the real case as this should be handled by the parent + * VOverlay (on the client side) if the component is inside an VOverlay + * component. + */ + private void setActionManagerViewer() { + if (actionManager != null && getRoot() != null) { + // Attached and has action manager + Window w = findAncestor(Window.class); + if (w != null) { + actionManager.setViewer(w); + } else { + actionManager.setViewer(getRoot()); + } + } + + } + + public void addShortcutListener(ShortcutListener shortcut) { + getActionManager().addAction(shortcut); + } + + public void removeShortcutListener(ShortcutListener shortcut) { + if (actionManager != null) { + actionManager.removeAction(shortcut); + } + } +} diff --git a/server/src/com/vaadin/ui/AbstractComponentContainer.java b/server/src/com/vaadin/ui/AbstractComponentContainer.java new file mode 100644 index 0000000000..bc27242bb8 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractComponentContainer.java @@ -0,0 +1,351 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.terminal.gwt.server.ComponentSizeValidator; + +/** + * Extension to {@link AbstractComponent} that defines the default + * implementation for the methods in {@link ComponentContainer}. Basic UI + * components that need to contain other components inherit this class to easily + * qualify as a component container. + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractComponentContainer extends AbstractComponent + implements ComponentContainer { + + /** + * Constructs a new component container. + */ + public AbstractComponentContainer() { + super(); + } + + /** + * Removes all components from the container. This should probably be + * re-implemented in extending classes for a more powerful implementation. + */ + @Override + public void removeAllComponents() { + final LinkedList<Component> l = new LinkedList<Component>(); + + // Adds all components + for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { + l.add(i.next()); + } + + // Removes all component + for (final Iterator<Component> i = l.iterator(); i.hasNext();) { + removeComponent(i.next()); + } + } + + /* + * Moves all components from an another container into this container. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void moveComponentsFrom(ComponentContainer source) { + final LinkedList<Component> components = new LinkedList<Component>(); + for (final Iterator<Component> i = source.getComponentIterator(); i + .hasNext();) { + components.add(i.next()); + } + + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component c = i.next(); + source.removeComponent(c); + addComponent(c); + } + } + + /* Events */ + + private static final Method COMPONENT_ATTACHED_METHOD; + + private static final Method COMPONENT_DETACHED_METHOD; + + static { + try { + COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class + .getDeclaredMethod("componentAttachedToContainer", + new Class[] { ComponentAttachEvent.class }); + COMPONENT_DETACHED_METHOD = ComponentDetachListener.class + .getDeclaredMethod("componentDetachedFromContainer", + new Class[] { ComponentDetachEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractComponentContainer"); + } + } + + /* documented in interface */ + @Override + public void addListener(ComponentAttachListener listener) { + addListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + /* documented in interface */ + @Override + public void addListener(ComponentDetachListener listener) { + addListener(ComponentContainer.ComponentDetachEvent.class, listener, + COMPONENT_DETACHED_METHOD); + } + + /* documented in interface */ + @Override + public void removeListener(ComponentAttachListener listener) { + removeListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + /* documented in interface */ + @Override + public void removeListener(ComponentDetachListener listener) { + removeListener(ComponentContainer.ComponentDetachEvent.class, listener, + COMPONENT_DETACHED_METHOD); + } + + /** + * Fires the component attached event. This should be called by the + * addComponent methods after the component have been added to this + * container. + * + * @param component + * the component that has been added to this container. + */ + protected void fireComponentAttachEvent(Component component) { + fireEvent(new ComponentAttachEvent(this, component)); + } + + /** + * Fires the component detached event. This should be called by the + * removeComponent methods after the component have been removed from this + * container. + * + * @param component + * the component that has been removed from this container. + */ + protected void fireComponentDetachEvent(Component component) { + fireEvent(new ComponentDetachEvent(this, component)); + } + + /** + * This only implements the events and component parent calls. The extending + * classes must implement component list maintenance and call this method + * after component list maintenance. + * + * @see com.vaadin.ui.ComponentContainer#addComponent(Component) + */ + @Override + public void addComponent(Component c) { + if (c instanceof ComponentContainer) { + // Make sure we're not adding the component inside it's own content + for (Component parent = this; parent != null; parent = parent + .getParent()) { + if (parent == c) { + throw new IllegalArgumentException( + "Component cannot be added inside it's own content"); + } + } + } + + if (c.getParent() != null) { + // If the component already has a parent, try to remove it + ComponentContainer oldParent = (ComponentContainer) c.getParent(); + oldParent.removeComponent(c); + + } + + c.setParent(this); + fireComponentAttachEvent(c); + } + + /** + * This only implements the events and component parent calls. The extending + * classes must implement component list maintenance and call this method + * before component list maintenance. + * + * @see com.vaadin.ui.ComponentContainer#removeComponent(Component) + */ + @Override + public void removeComponent(Component c) { + if (c.getParent() == this) { + c.setParent(null); + fireComponentDetachEvent(c); + } + } + + @Override + public void setVisible(boolean visible) { + if (getState().isVisible() == visible) { + return; + } + + super.setVisible(visible); + // If the visibility state is toggled it might affect all children + // aswell, e.g. make container visible should make children visible if + // they were only hidden because the container was hidden. + requestRepaintAll(); + } + + @Override + public void setWidth(float width, Unit unit) { + /* + * child tree repaints may be needed, due to our fall back support for + * invalid relative sizes + */ + Collection<Component> dirtyChildren = null; + boolean childrenMayBecomeUndefined = false; + if (getWidth() == SIZE_UNDEFINED && width != SIZE_UNDEFINED) { + // children currently in invalid state may need repaint + dirtyChildren = getInvalidSizedChildren(false); + } else if ((width == SIZE_UNDEFINED && getWidth() != SIZE_UNDEFINED) + || (unit == Unit.PERCENTAGE + && getWidthUnits() != Unit.PERCENTAGE && !ComponentSizeValidator + .parentCanDefineWidth(this))) { + /* + * relative width children may get to invalid state if width becomes + * invalid. Width may also become invalid if units become percentage + * due to the fallback support + */ + childrenMayBecomeUndefined = true; + dirtyChildren = getInvalidSizedChildren(false); + } + super.setWidth(width, unit); + repaintChangedChildTrees(dirtyChildren, childrenMayBecomeUndefined, + false); + } + + private void repaintChangedChildTrees( + Collection<Component> invalidChildren, + boolean childrenMayBecomeUndefined, boolean vertical) { + if (childrenMayBecomeUndefined) { + Collection<Component> previouslyInvalidComponents = invalidChildren; + invalidChildren = getInvalidSizedChildren(vertical); + if (previouslyInvalidComponents != null && invalidChildren != null) { + for (Iterator<Component> iterator = invalidChildren.iterator(); iterator + .hasNext();) { + Component component = iterator.next(); + if (previouslyInvalidComponents.contains(component)) { + // still invalid don't repaint + iterator.remove(); + } + } + } + } else if (invalidChildren != null) { + Collection<Component> stillInvalidChildren = getInvalidSizedChildren(vertical); + if (stillInvalidChildren != null) { + for (Component component : stillInvalidChildren) { + // didn't become valid + invalidChildren.remove(component); + } + } + } + if (invalidChildren != null) { + repaintChildTrees(invalidChildren); + } + } + + private Collection<Component> getInvalidSizedChildren(final boolean vertical) { + HashSet<Component> components = null; + if (this instanceof Panel) { + Panel p = (Panel) this; + ComponentContainer content = p.getContent(); + boolean valid = vertical ? ComponentSizeValidator + .checkHeights(content) : ComponentSizeValidator + .checkWidths(content); + + if (!valid) { + components = new HashSet<Component>(1); + components.add(content); + } + } else { + for (Iterator<Component> componentIterator = getComponentIterator(); componentIterator + .hasNext();) { + Component component = componentIterator.next(); + boolean valid = vertical ? ComponentSizeValidator + .checkHeights(component) : ComponentSizeValidator + .checkWidths(component); + if (!valid) { + if (components == null) { + components = new HashSet<Component>(); + } + components.add(component); + } + } + } + return components; + } + + private void repaintChildTrees(Collection<Component> dirtyChildren) { + for (Component c : dirtyChildren) { + if (c instanceof ComponentContainer) { + ComponentContainer cc = (ComponentContainer) c; + cc.requestRepaintAll(); + } else { + c.requestRepaint(); + } + } + } + + @Override + public void setHeight(float height, Unit unit) { + /* + * child tree repaints may be needed, due to our fall back support for + * invalid relative sizes + */ + Collection<Component> dirtyChildren = null; + boolean childrenMayBecomeUndefined = false; + if (getHeight() == SIZE_UNDEFINED && height != SIZE_UNDEFINED) { + // children currently in invalid state may need repaint + dirtyChildren = getInvalidSizedChildren(true); + } else if ((height == SIZE_UNDEFINED && getHeight() != SIZE_UNDEFINED) + || (unit == Unit.PERCENTAGE + && getHeightUnits() != Unit.PERCENTAGE && !ComponentSizeValidator + .parentCanDefineHeight(this))) { + /* + * relative height children may get to invalid state if height + * becomes invalid. Height may also become invalid if units become + * percentage due to the fallback support. + */ + childrenMayBecomeUndefined = true; + dirtyChildren = getInvalidSizedChildren(true); + } + super.setHeight(height, unit); + repaintChangedChildTrees(dirtyChildren, childrenMayBecomeUndefined, + true); + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.HasComponents#isComponentVisible(com.vaadin.ui.Component) + */ + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/AbstractField.java b/server/src/com/vaadin/ui/AbstractField.java new file mode 100644 index 0000000000..6fe7f54df5 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractField.java @@ -0,0 +1,1657 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +import com.vaadin.data.Buffered; +import com.vaadin.data.Property; +import com.vaadin.data.Validatable; +import com.vaadin.data.Validator; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.Converter.ConversionException; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.event.Action; +import com.vaadin.event.ShortcutAction; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.terminal.AbstractErrorMessage; +import com.vaadin.terminal.CompositeErrorMessage; +import com.vaadin.terminal.ErrorMessage; + +/** + * <p> + * Abstract field component for implementing buffered property editors. The + * field may hold an internal value, or it may be connected to any data source + * that implements the {@link com.vaadin.data.Property}interface. + * <code>AbstractField</code> implements that interface itself, too, so + * accessing the Property value represented by it is straightforward. + * </p> + * + * <p> + * AbstractField also provides the {@link com.vaadin.data.Buffered} interface + * for buffering the data source value. By default the Field is in write + * through-mode and {@link #setWriteThrough(boolean)}should be called to enable + * buffering. + * </p> + * + * <p> + * The class also supports {@link com.vaadin.data.Validator validators} to make + * sure the value contained in the field is valid. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractField<T> extends AbstractComponent implements + Field<T>, Property.ReadOnlyStatusChangeListener, + Property.ReadOnlyStatusChangeNotifier, Action.ShortcutNotifier { + + /* Private members */ + + private static final Logger logger = Logger.getLogger(AbstractField.class + .getName()); + + /** + * Value of the abstract field. + */ + private T value; + + /** + * A converter used to convert from the data model type to the field type + * and vice versa. + */ + private Converter<T, Object> converter = null; + /** + * Connected data-source. + */ + private Property<?> dataSource = null; + + /** + * The list of validators. + */ + private LinkedList<Validator> validators = null; + + /** + * Auto commit mode. + */ + private boolean writeThroughMode = true; + + /** + * Reads the value from data-source, when it is not modified. + */ + private boolean readThroughMode = true; + + /** + * Flag to indicate that the field is currently committing its value to the + * datasource. + */ + private boolean committingValueToDataSource = false; + + /** + * Current source exception. + */ + private Buffered.SourceException currentBufferedSourceException = null; + + /** + * Are the invalid values allowed in fields ? + */ + private boolean invalidAllowed = true; + + /** + * Are the invalid values committed ? + */ + private boolean invalidCommitted = false; + + /** + * The error message for the exception that is thrown when the field is + * required but empty. + */ + private String requiredError = ""; + + /** + * The error message that is shown when the field value cannot be converted. + */ + private String conversionError = "Could not convert value to {0}"; + + /** + * Is automatic validation enabled. + */ + private boolean validationVisible = true; + + private boolean valueWasModifiedByDataSourceDuringCommit; + + /** + * Whether this field is currently registered as listening to events from + * its data source. + * + * @see #setPropertyDataSource(Property) + * @see #addPropertyListeners() + * @see #removePropertyListeners() + */ + private boolean isListeningToPropertyEvents = false; + + /* Component basics */ + + /* + * Paints the field. Don't add a JavaDoc comment here, we use the default + * documentation from the implemented interface. + */ + + /** + * Returns true if the error indicator be hidden when painting the component + * even when there are errors. + * + * This is a mostly internal method, but can be overridden in subclasses + * e.g. if the error indicator should also be shown for empty fields in some + * cases. + * + * @return true to hide the error indicator, false to use the normal logic + * to show it when there are errors + */ + protected boolean shouldHideErrors() { + // getErrorMessage() can still return something else than null based on + // validation etc. + return isRequired() && isEmpty() && getComponentError() == null; + } + + /** + * Returns the type of the Field. The methods <code>getValue</code> and + * <code>setValue</code> must be compatible with this type: one must be able + * to safely cast the value returned from <code>getValue</code> to the given + * type and pass any variable assignable to this type as an argument to + * <code>setValue</code>. + * + * @return the type of the Field + */ + @Override + public abstract Class<? extends T> getType(); + + /** + * The abstract field is read only also if the data source is in read only + * mode. + */ + @Override + public boolean isReadOnly() { + return super.isReadOnly() + || (dataSource != null && dataSource.isReadOnly()); + } + + /** + * Changes the readonly state and throw read-only status change events. + * + * @see com.vaadin.ui.Component#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + fireReadOnlyStatusChange(); + } + + /** + * Tests if the invalid data is committed to datasource. + * + * @see com.vaadin.data.BufferedValidatable#isInvalidCommitted() + */ + @Override + public boolean isInvalidCommitted() { + return invalidCommitted; + } + + /** + * Sets if the invalid data should be committed to datasource. + * + * @see com.vaadin.data.BufferedValidatable#setInvalidCommitted(boolean) + */ + @Override + public void setInvalidCommitted(boolean isCommitted) { + invalidCommitted = isCommitted; + } + + /* + * Saves the current value to the data source Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public void commit() throws Buffered.SourceException, InvalidValueException { + if (dataSource != null && !dataSource.isReadOnly()) { + if ((isInvalidCommitted() || isValid())) { + try { + + // Commits the value to datasource. + valueWasModifiedByDataSourceDuringCommit = false; + committingValueToDataSource = true; + getPropertyDataSource().setValue(getConvertedValue()); + } catch (final Throwable e) { + + // Sets the buffering state. + SourceException sourceException = new Buffered.SourceException( + this, e); + setCurrentBufferedSourceException(sourceException); + + // Throws the source exception. + throw sourceException; + } finally { + committingValueToDataSource = false; + } + } else { + /* An invalid value and we don't allow them, throw the exception */ + validate(); + } + } + + // The abstract field is not modified anymore + if (isModified()) { + setModified(false); + } + + // If successful, remove set the buffering state to be ok + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + + if (valueWasModifiedByDataSourceDuringCommit) { + valueWasModifiedByDataSourceDuringCommit = false; + fireValueChange(false); + } + + } + + /* + * Updates the value from the data source. Don't add a JavaDoc comment here, + * we use the default documentation from the implemented interface. + */ + @Override + public void discard() throws Buffered.SourceException { + if (dataSource != null) { + + // Gets the correct value from datasource + T newFieldValue; + try { + + // Discards buffer by overwriting from datasource + newFieldValue = convertFromDataSource(getDataSourceValue()); + + // If successful, remove set the buffering state to be ok + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + } catch (final Throwable e) { + // FIXME: What should really be done here if conversion fails? + + // Sets the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception + throw currentBufferedSourceException; + } + + final boolean wasModified = isModified(); + setModified(false); + + // If the new value differs from the previous one + if (!equals(newFieldValue, getInternalValue())) { + setInternalValue(newFieldValue); + fireValueChange(false); + } else if (wasModified) { + // If the value did not change, but the modification status did + requestRepaint(); + } + } + } + + /** + * Gets the value from the data source. This is only here because of clarity + * in the code that handles both the data model value and the field value. + * + * @return The value of the property data source + */ + private Object getDataSourceValue() { + return dataSource.getValue(); + } + + /** + * Returns the field value. This is always identical to {@link #getValue()} + * and only here because of clarity in the code that handles both the data + * model value and the field value. + * + * @return The value of the field + */ + private T getFieldValue() { + // Give the value from abstract buffers if the field if possible + if (dataSource == null || !isReadThrough() || isModified()) { + return getInternalValue(); + } + + // There is no buffered value so use whatever the data model provides + return convertFromDataSource(getDataSourceValue()); + } + + /* + * Has the field been modified since the last commit()? Don't add a JavaDoc + * comment here, we use the default documentation from the implemented + * interface. + */ + @Override + public boolean isModified() { + return getState().isModified(); + } + + private void setModified(boolean modified) { + getState().setModified(modified); + requestRepaint(); + } + + /* + * Tests if the field is in write-through mode. Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public boolean isWriteThrough() { + return writeThroughMode; + } + + /** + * Sets the field's write-through mode to the specified status. When + * switching the write-through mode on, a {@link #commit()} will be + * performed. + * + * @see #setBuffered(boolean) for an easier way to control read through and + * write through modes + * + * @param writeThrough + * Boolean value to indicate if the object should be in + * write-through mode after the call. + * @throws SourceException + * If the operation fails because of an exception is thrown by + * the data source. + * @throws InvalidValueException + * If the implicit commit operation fails because of a + * validation error. + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Override + @Deprecated + public void setWriteThrough(boolean writeThrough) + throws Buffered.SourceException, InvalidValueException { + if (writeThroughMode == writeThrough) { + return; + } + writeThroughMode = writeThrough; + if (writeThroughMode) { + commit(); + } + } + + /* + * Tests if the field is in read-through mode. Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public boolean isReadThrough() { + return readThroughMode; + } + + /** + * Sets the field's read-through mode to the specified status. When + * switching read-through mode on, the object's value is updated from the + * data source. + * + * @see #setBuffered(boolean) for an easier way to control read through and + * write through modes + * + * @param readThrough + * Boolean value to indicate if the object should be in + * read-through mode after the call. + * + * @throws SourceException + * If the operation fails because of an exception is thrown by + * the data source. The cause is included in the exception. + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Override + @Deprecated + public void setReadThrough(boolean readThrough) + throws Buffered.SourceException { + if (readThroughMode == readThrough) { + return; + } + readThroughMode = readThrough; + if (!isModified() && readThroughMode && getPropertyDataSource() != null) { + setInternalValue(convertFromDataSource(getDataSourceValue())); + fireValueChange(false); + } + } + + /** + * Sets the buffered mode of this Field. + * <p> + * When the field is in buffered mode, changes will not be committed to the + * property data source until {@link #commit()} is called. + * </p> + * <p> + * Changing buffered mode will change the read through and write through + * state for the field. + * </p> + * <p> + * Mixing calls to {@link #setBuffered(boolean)} and + * {@link #setReadThrough(boolean)} or {@link #setWriteThrough(boolean)} is + * generally a bad idea. + * </p> + * + * @param buffered + * true if buffered mode should be turned on, false otherwise + */ + @Override + public void setBuffered(boolean buffered) { + setReadThrough(!buffered); + setWriteThrough(!buffered); + } + + /** + * Checks the buffered mode of this Field. + * <p> + * This method only returns true if both read and write buffering is used. + * + * @return true if buffered mode is on, false otherwise + */ + @Override + public boolean isBuffered() { + return !isReadThrough() && !isWriteThrough(); + } + + /* Property interface implementation */ + + /** + * Returns the (field) value converted to a String using toString(). + * + * @see java.lang.Object#toString() + * @deprecated Instead use {@link #getValue()} to get the value of the + * field, {@link #getConvertedValue()} to get the field value + * converted to the data model type or + * {@link #getPropertyDataSource()} .getValue() to get the value + * of the data source. + */ + @Deprecated + @Override + public String toString() { + logger.warning("You are using AbstractField.toString() to get the value for a " + + getClass().getSimpleName() + + ". This is not recommended and will not be supported in future versions."); + final Object value = getFieldValue(); + if (value == null) { + return null; + } + return value.toString(); + } + + /** + * Gets the current value of the field. + * + * <p> + * This is the visible, modified and possible invalid value the user have + * entered to the field. + * </p> + * + * <p> + * Note that the object returned is compatible with getType(). For example, + * if the type is String, this returns Strings even when the underlying + * datasource is of some other type. In order to access the converted value, + * use {@link #getConvertedValue()} and to access the value of the property + * data source, use {@link Property#getValue()} for the property data + * source. + * </p> + * + * <p> + * Since Vaadin 7.0, no implicit conversions between other data types and + * String are performed, but a converter is used if set. + * </p> + * + * @return the current value of the field. + */ + @Override + public T getValue() { + return getFieldValue(); + } + + /** + * Sets the value of the field. + * + * @param newFieldValue + * the New value of the field. + * @throws Property.ReadOnlyException + */ + @Override + public void setValue(Object newFieldValue) + throws Property.ReadOnlyException, Converter.ConversionException { + // This check is needed as long as setValue accepts Object instead of T + if (newFieldValue != null) { + if (!getType().isAssignableFrom(newFieldValue.getClass())) { + throw new Converter.ConversionException("Value of type " + + newFieldValue.getClass() + " cannot be assigned to " + + getType().getName()); + } + } + setValue((T) newFieldValue, false); + } + + /** + * Sets the value of the field. + * + * @param newFieldValue + * the New value of the field. + * @param repaintIsNotNeeded + * True iff caller is sure that repaint is not needed. + * @throws Property.ReadOnlyException + */ + protected void setValue(T newFieldValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException, Converter.ConversionException, + InvalidValueException { + + if (!equals(newFieldValue, getInternalValue())) { + + // Read only fields can not be changed + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Repaint is needed even when the client thinks that it knows the + // new state if validity of the component may change + if (repaintIsNotNeeded + && (isRequired() || getValidators() != null || getConverter() != null)) { + repaintIsNotNeeded = false; + } + + if (!isInvalidAllowed()) { + /* + * If invalid values are not allowed the value must be validated + * before it is set. If validation fails, the + * InvalidValueException is thrown and the internal value is not + * updated. + */ + validate(newFieldValue); + } + + // Changes the value + setInternalValue(newFieldValue); + setModified(dataSource != null); + + valueWasModifiedByDataSourceDuringCommit = false; + // In write through mode , try to commit + if (isWriteThrough() && dataSource != null + && (isInvalidCommitted() || isValid())) { + try { + + // Commits the value to datasource + committingValueToDataSource = true; + getPropertyDataSource().setValue( + convertToModel(newFieldValue)); + + // The buffer is now unmodified + setModified(false); + + } catch (final Throwable e) { + + // Sets the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception + throw currentBufferedSourceException; + } finally { + committingValueToDataSource = false; + } + } + + // If successful, remove set the buffering state to be ok + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + + if (valueWasModifiedByDataSourceDuringCommit) { + /* + * Value was modified by datasource. Force repaint even if + * repaint was not requested. + */ + valueWasModifiedByDataSourceDuringCommit = repaintIsNotNeeded = false; + } + + // Fires the value change + fireValueChange(repaintIsNotNeeded); + + } + } + + private static boolean equals(Object value1, Object value2) { + if (value1 == null) { + return value2 == null; + } + return value1.equals(value2); + } + + /* External data source */ + + /** + * Gets the current data source of the field, if any. + * + * @return the current data source as a Property, or <code>null</code> if + * none defined. + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * <p> + * Sets the specified Property as the data source for the field. All + * uncommitted changes are replaced with a value from the new data source. + * </p> + * + * <p> + * If the datasource has any validators, the same validators are added to + * the field. Because the default behavior of the field is to allow invalid + * values, but not to allow committing them, this only adds visual error + * messages to fields and do not allow committing them as long as the value + * is invalid. After the value is valid, the error message is not shown and + * the commit can be done normally. + * </p> + * + * <p> + * If the data source implements + * {@link com.vaadin.data.Property.ValueChangeNotifier} and/or + * {@link com.vaadin.data.Property.ReadOnlyStatusChangeNotifier}, the field + * registers itself as a listener and updates itself according to the events + * it receives. To avoid memory leaks caused by references to a field no + * longer in use, the listener registrations are removed on + * {@link AbstractField#detach() detach} and re-added on + * {@link AbstractField#attach() attach}. + * </p> + * + * <p> + * Note: before 6.5 we actually called discard() method in the beginning of + * the method. This was removed to simplify implementation, avoid excess + * calls to backing property and to avoid odd value change events that were + * previously fired (developer expects 0-1 value change events if this + * method is called). Some complex field implementations might now need to + * override this method to do housekeeping similar to discard(). + * </p> + * + * @param newDataSource + * the new data source Property. + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + + // Saves the old value + final Object oldValue = getInternalValue(); + + // Stop listening to the old data source + removePropertyListeners(); + + // Sets the new data source + dataSource = newDataSource; + getState().setPropertyReadOnly( + dataSource == null ? false : dataSource.isReadOnly()); + + // Check if the current converter is compatible. + if (newDataSource != null + && !ConverterUtil.canConverterHandle(getConverter(), getType(), + newDataSource.getType())) { + // Changing from e.g. Number -> Double should set a new converter, + // changing from Double -> Number can keep the old one (Property + // accepts Number) + + // Set a new converter if there is a new data source and + // there is no old converter or the old is incompatible. + setConverter(newDataSource.getType()); + } + // Gets the value from source + try { + if (dataSource != null) { + T fieldValue = convertFromDataSource(getDataSourceValue()); + setInternalValue(fieldValue); + } + setModified(false); + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + } catch (final Throwable e) { + setCurrentBufferedSourceException(new Buffered.SourceException( + this, e)); + setModified(true); + } + + // Listen to new data source if possible + addPropertyListeners(); + + // Copy the validators from the data source + if (dataSource instanceof Validatable) { + final Collection<Validator> validators = ((Validatable) dataSource) + .getValidators(); + if (validators != null) { + for (final Iterator<Validator> i = validators.iterator(); i + .hasNext();) { + addValidator(i.next()); + } + } + } + + // Fires value change if the value has changed + T value = getInternalValue(); + if ((value != oldValue) + && ((value != null && !value.equals(oldValue)) || value == null)) { + fireValueChange(false); + } + } + + /** + * Retrieves a converter for the field from the converter factory defined + * for the application. Clears the converter if no application reference is + * available or if the factory returns null. + * + * @param datamodelType + * The type of the data model that we want to be able to convert + * from + */ + public void setConverter(Class<?> datamodelType) { + Converter<T, ?> c = (Converter<T, ?>) ConverterUtil.getConverter( + getType(), datamodelType, getApplication()); + setConverter(c); + } + + /** + * Convert the given value from the data source type to the UI type. + * + * @param newValue + * The data source value to convert. + * @return The converted value that is compatible with the UI type or the + * original value if its type is compatible and no converter is set. + * @throws Converter.ConversionException + * if there is no converter and the type is not compatible with + * the data source type. + */ + private T convertFromDataSource(Object newValue) { + return ConverterUtil.convertFromModel(newValue, getType(), + getConverter(), getLocale()); + } + + /** + * Convert the given value from the UI type to the data source type. + * + * @param fieldValue + * The value to convert. Typically returned by + * {@link #getFieldValue()} + * @return The converted value that is compatible with the data source type. + * @throws Converter.ConversionException + * if there is no converter and the type is not compatible with + * the data source type. + */ + private Object convertToModel(T fieldValue) + throws Converter.ConversionException { + try { + Class<?> modelType = null; + Property pd = getPropertyDataSource(); + if (pd != null) { + modelType = pd.getType(); + } else if (getConverter() != null) { + modelType = getConverter().getModelType(); + } + return ConverterUtil.convertToModel(fieldValue, + (Class<Object>) modelType, getConverter(), getLocale()); + } catch (ConversionException e) { + throw new ConversionException( + getConversionError(converter.getModelType()), e); + } + } + + /** + * Returns the conversion error with {0} replaced by the data source type. + * + * @param dataSourceType + * The type of the data source + * @return The value conversion error string with parameters replaced. + */ + protected String getConversionError(Class<?> dataSourceType) { + if (dataSourceType == null) { + return getConversionError(); + } else { + return getConversionError().replace("{0}", + dataSourceType.getSimpleName()); + } + } + + /** + * Returns the current value (as returned by {@link #getValue()}) converted + * to the data source type. + * <p> + * This returns the same as {@link AbstractField#getValue()} if no converter + * has been set. The value is not necessarily the same as the data source + * value e.g. if the field is in buffered mode and has been modified. + * </p> + * + * @return The converted value that is compatible with the data source type + */ + public Object getConvertedValue() { + return convertToModel(getFieldValue()); + } + + /** + * Sets the value of the field using a value of the data source type. The + * value given is converted to the field type and then assigned to the + * field. This will update the property data source in the same way as when + * {@link #setValue(Object)} is called. + * + * @param value + * The value to set. Must be the same type as the data source. + */ + public void setConvertedValue(Object value) { + setValue(convertFromDataSource(value)); + } + + /* Validation */ + + /** + * Adds a new validator for the field's value. All validators added to a + * field are checked each time the its value changes. + * + * @param validator + * the new validator to be added. + */ + @Override + public void addValidator(Validator validator) { + if (validators == null) { + validators = new LinkedList<Validator>(); + } + validators.add(validator); + requestRepaint(); + } + + /** + * Gets the validators of the field. + * + * @return the Unmodifiable collection that holds all validators for the + * field. + */ + @Override + public Collection<Validator> getValidators() { + if (validators == null || validators.isEmpty()) { + return null; + } + return Collections.unmodifiableCollection(validators); + } + + /** + * Removes the validator from the field. + * + * @param validator + * the validator to remove. + */ + @Override + public void removeValidator(Validator validator) { + if (validators != null) { + validators.remove(validator); + } + requestRepaint(); + } + + /** + * Removes all validators from the field. + */ + public void removeAllValidators() { + if (validators != null) { + validators.clear(); + } + requestRepaint(); + } + + /** + * Tests the current value against registered validators if the field is not + * empty. If the field is empty it is considered valid if it is not required + * and invalid otherwise. Validators are never checked for empty fields. + * + * In most cases, {@link #validate()} should be used instead of + * {@link #isValid()} to also get the error message. + * + * @return <code>true</code> if all registered validators claim that the + * current value is valid or if the field is empty and not required, + * <code>false</code> otherwise. + */ + @Override + public boolean isValid() { + + try { + validate(); + return true; + } catch (InvalidValueException e) { + return false; + } + } + + /** + * Checks the validity of the Field. + * + * A field is invalid if it is set as required (using + * {@link #setRequired(boolean)} and is empty, if one or several of the + * validators added to the field indicate it is invalid or if the value + * cannot be converted provided a converter has been set. + * + * The "required" validation is a built-in validation feature. If the field + * is required and empty this method throws an EmptyValueException with the + * error message set using {@link #setRequiredError(String)}. + * + * @see com.vaadin.data.Validatable#validate() + */ + @Override + public void validate() throws Validator.InvalidValueException { + + if (isRequired() && isEmpty()) { + throw new Validator.EmptyValueException(requiredError); + } + validate(getFieldValue()); + } + + /** + * Validates that the given value pass the validators for the field. + * <p> + * This method does not check the requiredness of the field. + * + * @param fieldValue + * The value to check + * @throws Validator.InvalidValueException + * if one or several validators fail + */ + protected void validate(T fieldValue) + throws Validator.InvalidValueException { + + Object valueToValidate = fieldValue; + + // If there is a converter we start by converting the value as we want + // to validate the converted value + if (getConverter() != null) { + try { + valueToValidate = getConverter().convertToModel(fieldValue, + getLocale()); + } catch (Exception e) { + throw new InvalidValueException( + getConversionError(getConverter().getModelType())); + } + } + + List<InvalidValueException> validationExceptions = new ArrayList<InvalidValueException>(); + if (validators != null) { + // Gets all the validation errors + for (Validator v : validators) { + try { + v.validate(valueToValidate); + } catch (final Validator.InvalidValueException e) { + validationExceptions.add(e); + } + } + } + + // If there were no errors + if (validationExceptions.isEmpty()) { + return; + } + + // If only one error occurred, throw it forwards + if (validationExceptions.size() == 1) { + throw validationExceptions.get(0); + } + + InvalidValueException[] exceptionArray = validationExceptions + .toArray(new InvalidValueException[validationExceptions.size()]); + + // Create a composite validator and include all exceptions + throw new Validator.InvalidValueException(null, exceptionArray); + } + + /** + * Fields allow invalid values by default. In most cases this is wanted, + * because the field otherwise visually forget the user input immediately. + * + * @return true iff the invalid values are allowed. + * @see com.vaadin.data.Validatable#isInvalidAllowed() + */ + @Override + public boolean isInvalidAllowed() { + return invalidAllowed; + } + + /** + * Fields allow invalid values by default. In most cases this is wanted, + * because the field otherwise visually forget the user input immediately. + * <p> + * In common setting where the user wants to assure the correctness of the + * datasource, but allow temporarily invalid contents in the field, the user + * should add the validators to datasource, that should not allow invalid + * values. The validators are automatically copied to the field when the + * datasource is set. + * </p> + * + * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean) + */ + @Override + public void setInvalidAllowed(boolean invalidAllowed) + throws UnsupportedOperationException { + this.invalidAllowed = invalidAllowed; + } + + /** + * Error messages shown by the fields are composites of the error message + * thrown by the superclasses (that is the component error message), + * validation errors and buffered source errors. + * + * @see com.vaadin.ui.AbstractComponent#getErrorMessage() + */ + @Override + public ErrorMessage getErrorMessage() { + + /* + * Check validation errors only if automatic validation is enabled. + * Empty, required fields will generate a validation error containing + * the requiredError string. For these fields the exclamation mark will + * be hidden but the error must still be sent to the client. + */ + Validator.InvalidValueException validationError = null; + if (isValidationVisible()) { + try { + validate(); + } catch (Validator.InvalidValueException e) { + if (!e.isInvisible()) { + validationError = e; + } + } + } + + // Check if there are any systems errors + final ErrorMessage superError = super.getErrorMessage(); + + // Return if there are no errors at all + if (superError == null && validationError == null + && getCurrentBufferedSourceException() == null) { + return null; + } + + // Throw combination of the error types + return new CompositeErrorMessage( + new ErrorMessage[] { + superError, + AbstractErrorMessage + .getErrorMessageForException(validationError), + AbstractErrorMessage + .getErrorMessageForException(getCurrentBufferedSourceException()) }); + + } + + /* Value change events */ + + private static final Method VALUE_CHANGE_METHOD; + + static { + try { + VALUE_CHANGE_METHOD = Property.ValueChangeListener.class + .getDeclaredMethod("valueChange", + new Class[] { Property.ValueChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractField"); + } + } + + /* + * Adds a value change listener for the field. Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public void addListener(Property.ValueChangeListener listener) { + addListener(AbstractField.ValueChangeEvent.class, listener, + VALUE_CHANGE_METHOD); + } + + /* + * Removes a value change listener from the field. Don't add a JavaDoc + * comment here, we use the default documentation from the implemented + * interface. + */ + @Override + public void removeListener(Property.ValueChangeListener listener) { + removeListener(AbstractField.ValueChangeEvent.class, listener, + VALUE_CHANGE_METHOD); + } + + /** + * Emits the value change event. The value contained in the field is + * validated before the event is created. + */ + protected void fireValueChange(boolean repaintIsNotNeeded) { + fireEvent(new AbstractField.ValueChangeEvent(this)); + if (!repaintIsNotNeeded) { + requestRepaint(); + } + } + + /* Read-only status change events */ + + private static final Method READ_ONLY_STATUS_CHANGE_METHOD; + + static { + try { + READ_ONLY_STATUS_CHANGE_METHOD = Property.ReadOnlyStatusChangeListener.class + .getDeclaredMethod( + "readOnlyStatusChange", + new Class[] { Property.ReadOnlyStatusChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractField"); + } + } + + /** + * React to read only status changes of the property by requesting a + * repaint. + * + * @see Property.ReadOnlyStatusChangeListener + */ + @Override + public void readOnlyStatusChange(Property.ReadOnlyStatusChangeEvent event) { + getState().setPropertyReadOnly(event.getProperty().isReadOnly()); + requestRepaint(); + } + + /** + * An <code>Event</code> object specifying the Property whose read-only + * status has changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ReadOnlyStatusChangeEvent extends Component.Event + implements Property.ReadOnlyStatusChangeEvent, Serializable { + + /** + * New instance of text change event. + * + * @param source + * the Source of the event. + */ + public ReadOnlyStatusChangeEvent(AbstractField source) { + super(source); + } + + /** + * Property where the event occurred. + * + * @return the Source of the event. + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + } + + /* + * Adds a read-only status change listener for the field. Don't add a + * JavaDoc comment here, we use the default documentation from the + * implemented interface. + */ + @Override + public void addListener(Property.ReadOnlyStatusChangeListener listener) { + addListener(Property.ReadOnlyStatusChangeEvent.class, listener, + READ_ONLY_STATUS_CHANGE_METHOD); + } + + /* + * Removes a read-only status change listener from the field. Don't add a + * JavaDoc comment here, we use the default documentation from the + * implemented interface. + */ + @Override + public void removeListener(Property.ReadOnlyStatusChangeListener listener) { + removeListener(Property.ReadOnlyStatusChangeEvent.class, listener, + READ_ONLY_STATUS_CHANGE_METHOD); + } + + /** + * Emits the read-only status change event. The value contained in the field + * is validated before the event is created. + */ + protected void fireReadOnlyStatusChange() { + fireEvent(new AbstractField.ReadOnlyStatusChangeEvent(this)); + } + + /** + * This method listens to data source value changes and passes the changes + * forwards. + * + * Changes are not forwarded to the listeners of the field during internal + * operations of the field to avoid duplicate notifications. + * + * @param event + * the value change event telling the data source contents have + * changed. + */ + @Override + public void valueChange(Property.ValueChangeEvent event) { + if (isReadThrough()) { + if (committingValueToDataSource) { + boolean propertyNotifiesOfTheBufferedValue = equals(event + .getProperty().getValue(), getInternalValue()); + if (!propertyNotifiesOfTheBufferedValue) { + /* + * Property (or chained property like PropertyFormatter) now + * reports different value than the one the field has just + * committed to it. In this case we respect the property + * value. + * + * Still, we don't fire value change yet, but instead + * postpone it until "commit" is done. See setValue(Object, + * boolean) and commit(). + */ + readValueFromProperty(event); + valueWasModifiedByDataSourceDuringCommit = true; + } + } else if (!isModified()) { + readValueFromProperty(event); + fireValueChange(false); + } + } + } + + private void readValueFromProperty(Property.ValueChangeEvent event) { + setInternalValue(convertFromDataSource(event.getProperty().getValue())); + } + + /** + * {@inheritDoc} + */ + @Override + public void focus() { + super.focus(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + @Override + public int getTabIndex() { + return getState().getTabIndex(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + getState().setTabIndex(tabIndex); + requestRepaint(); + } + + /** + * Returns the internal field value, which might not match the data source + * value e.g. if the field has been modified and is not in write-through + * mode. + * + * This method can be overridden by subclasses together with + * {@link #setInternalValue(Object)} to compute internal field value at + * runtime. When doing so, typically also {@link #isModified()} needs to be + * overridden and care should be taken in the management of the empty state + * and buffering support. + * + * @return internal field value + */ + protected T getInternalValue() { + return value; + } + + /** + * Sets the internal field value. This is purely used by AbstractField to + * change the internal Field value. It does not trigger valuechange events. + * It can be overridden by the inheriting classes to update all dependent + * variables. + * + * Subclasses can also override {@link #getInternalValue()} if necessary. + * + * @param newValue + * the new value to be set. + */ + protected void setInternalValue(T newValue) { + value = newValue; + if (validators != null && !validators.isEmpty()) { + requestRepaint(); + } + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + super.attach(); + + if (!isListeningToPropertyEvents) { + addPropertyListeners(); + if (!isModified() && isReadThrough()) { + // Update value from data source + discard(); + } + } + } + + @Override + public void detach() { + super.detach(); + // Stop listening to data source events on detach to avoid a potential + // memory leak. See #6155. + removePropertyListeners(); + } + + /** + * Is this field required. Required fields must filled by the user. + * + * If the field is required, it is visually indicated in the user interface. + * Furthermore, setting field to be required implicitly adds "non-empty" + * validator and thus isValid() == false or any isEmpty() fields. In those + * cases validation errors are not painted as it is obvious that the user + * must fill in the required fields. + * + * On the other hand, for the non-required fields isValid() == true if the + * field isEmpty() regardless of any attached validators. + * + * + * @return <code>true</code> if the field is required, otherwise + * <code>false</code>. + */ + @Override + public boolean isRequired() { + return getState().isRequired(); + } + + /** + * Sets the field required. Required fields must filled by the user. + * + * If the field is required, it is visually indicated in the user interface. + * Furthermore, setting field to be required implicitly adds "non-empty" + * validator and thus isValid() == false or any isEmpty() fields. In those + * cases validation errors are not painted as it is obvious that the user + * must fill in the required fields. + * + * On the other hand, for the non-required fields isValid() == true if the + * field isEmpty() regardless of any attached validators. + * + * @param required + * Is the field required. + */ + @Override + public void setRequired(boolean required) { + getState().setRequired(required); + requestRepaint(); + } + + /** + * Set the error that is show if this field is required, but empty. When + * setting requiredMessage to be "" or null, no error pop-up or exclamation + * mark is shown for a empty required field. This faults to "". Even in + * those cases isValid() returns false for empty required fields. + * + * @param requiredMessage + * Message to be shown when this field is required, but empty. + */ + @Override + public void setRequiredError(String requiredMessage) { + requiredError = requiredMessage; + requestRepaint(); + } + + @Override + public String getRequiredError() { + return requiredError; + } + + /** + * Gets the error that is shown if the field value cannot be converted to + * the data source type. + * + * @return The error that is shown if conversion of the field value fails + */ + public String getConversionError() { + return conversionError; + } + + /** + * Sets the error that is shown if the field value cannot be converted to + * the data source type. If {0} is present in the message, it will be + * replaced by the simple name of the data source type. + * + * @param valueConversionError + * Message to be shown when conversion of the value fails + */ + public void setConversionError(String valueConversionError) { + this.conversionError = valueConversionError; + requestRepaint(); + } + + /** + * Is the field empty? + * + * In general, "empty" state is same as null. As an exception, TextField + * also treats empty string as "empty". + */ + protected boolean isEmpty() { + return (getFieldValue() == null); + } + + /** + * Is automatic, visible validation enabled? + * + * If automatic validation is enabled, any validators connected to this + * component are evaluated while painting the component and potential error + * messages are sent to client. If the automatic validation is turned off, + * isValid() and validate() methods still work, but one must show the + * validation in their own code. + * + * @return True, if automatic validation is enabled. + */ + public boolean isValidationVisible() { + return validationVisible; + } + + /** + * Enable or disable automatic, visible validation. + * + * If automatic validation is enabled, any validators connected to this + * component are evaluated while painting the component and potential error + * messages are sent to client. If the automatic validation is turned off, + * isValid() and validate() methods still work, but one must show the + * validation in their own code. + * + * @param validateAutomatically + * True, if automatic validation is enabled. + */ + public void setValidationVisible(boolean validateAutomatically) { + if (validationVisible != validateAutomatically) { + requestRepaint(); + validationVisible = validateAutomatically; + } + } + + /** + * Sets the current buffered source exception. + * + * @param currentBufferedSourceException + */ + public void setCurrentBufferedSourceException( + Buffered.SourceException currentBufferedSourceException) { + this.currentBufferedSourceException = currentBufferedSourceException; + requestRepaint(); + } + + /** + * Gets the current buffered source exception. + * + * @return The current source exception + */ + protected Buffered.SourceException getCurrentBufferedSourceException() { + return currentBufferedSourceException; + } + + /** + * A ready-made {@link ShortcutListener} that focuses the given + * {@link Focusable} (usually a {@link Field}) when the keyboard shortcut is + * invoked. + * + */ + public static class FocusShortcut extends ShortcutListener { + protected Focusable focusable; + + /** + * Creates a keyboard shortcut for focusing the given {@link Focusable} + * using the shorthand notation defined in {@link ShortcutAction}. + * + * @param focusable + * to focused when the shortcut is invoked + * @param shorthandCaption + * caption with keycode and modifiers indicated + */ + public FocusShortcut(Focusable focusable, String shorthandCaption) { + super(shorthandCaption); + this.focusable = focusable; + } + + /** + * Creates a keyboard shortcut for focusing the given {@link Focusable}. + * + * @param focusable + * to focused when the shortcut is invoked + * @param keyCode + * keycode that invokes the shortcut + * @param modifiers + * modifiers required to invoke the shortcut + */ + public FocusShortcut(Focusable focusable, int keyCode, int... modifiers) { + super(null, keyCode, modifiers); + this.focusable = focusable; + } + + /** + * Creates a keyboard shortcut for focusing the given {@link Focusable}. + * + * @param focusable + * to focused when the shortcut is invoked + * @param keyCode + * keycode that invokes the shortcut + */ + public FocusShortcut(Focusable focusable, int keyCode) { + this(focusable, keyCode, null); + } + + @Override + public void handleAction(Object sender, Object target) { + focusable.focus(); + } + } + + /** + * Gets the converter used to convert the property data source value to the + * field value. + * + * @return The converter or null if none is set. + */ + public Converter<T, Object> getConverter() { + return converter; + } + + /** + * Sets the converter used to convert the field value to property data + * source type. The converter must have a presentation type that matches the + * field type. + * + * @param converter + * The new converter to use. + */ + public void setConverter(Converter<T, ?> converter) { + this.converter = (Converter<T, Object>) converter; + requestRepaint(); + } + + @Override + public AbstractFieldState getState() { + return (AbstractFieldState) super.getState(); + } + + @Override + public void updateState() { + super.updateState(); + + // Hide the error indicator if needed + getState().setHideErrors(shouldHideErrors()); + } + + /** + * Registers this as an event listener for events sent by the data source + * (if any). Does nothing if + * <code>isListeningToPropertyEvents == true</code>. + */ + private void addPropertyListeners() { + if (!isListeningToPropertyEvents) { + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) { + ((Property.ReadOnlyStatusChangeNotifier) dataSource) + .addListener(this); + } + isListeningToPropertyEvents = true; + } + } + + /** + * Stops listening to events sent by the data source (if any). Does nothing + * if <code>isListeningToPropertyEvents == false</code>. + */ + private void removePropertyListeners() { + if (isListeningToPropertyEvents) { + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource) + .removeListener(this); + } + if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) { + ((Property.ReadOnlyStatusChangeNotifier) dataSource) + .removeListener(this); + } + isListeningToPropertyEvents = false; + } + } +} diff --git a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java new file mode 100644 index 0000000000..5ec80573ab --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java @@ -0,0 +1,165 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import com.vaadin.shared.ui.JavaScriptComponentState; +import com.vaadin.terminal.JavaScriptCallbackHelper; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ui.JavaScriptWidget; + +/** + * Base class for Components with all client-side logic implemented using + * JavaScript. + * <p> + * When a new JavaScript component is initialized in the browser, the framework + * will look for a globally defined JavaScript function that will initialize the + * component. The name of the initialization function is formed by replacing . + * with _ in the name of the server-side class. If no such function is defined, + * each super class is used in turn until a match is found. The framework will + * thus first attempt with <code>com_example_MyComponent</code> for the + * server-side + * <code>com.example.MyComponent extends AbstractJavaScriptComponent</code> + * class. If MyComponent instead extends <code>com.example.SuperComponent</code> + * , then <code>com_example_SuperComponent</code> will also be attempted if + * <code>com_example_MyComponent</code> has not been defined. + * <p> + * JavaScript components have a very simple GWT widget ({@link JavaScriptWidget} + * ) just consisting of a <code>div</code> element to which the JavaScript code + * should initialize its own user interface. + * <p> + * The initialization function will be called with <code>this</code> pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + * <ul> + * <li><code>getConnectorId()</code> - returns a string with the id of the + * connector.</li> + * <li><code>getParentId([connectorId])</code> - returns a string with the id of + * the connector's parent. If <code>connectorId</code> is provided, the id of + * the parent of the corresponding connector with the passed id is returned + * instead.</li> + * <li><code>getElement([connectorId])</code> - returns the DOM Element that is + * the root of a connector's widget. <code>null</code> is returned if the + * connector can not be found or if the connector doesn't have a widget. If + * <code>connectorId</code> is not provided, the connector id of the current + * connector will be used.</li> + * <li><code>getState()</code> - returns an object corresponding to the shared + * state defined on the server. The scheme for conversion between Java and + * JavaScript types is described bellow.</li> + * <li><code>registerRpc([name, ] rpcObject)</code> - registers the + * <code>rpcObject</code> as a RPC handler. <code>rpcObject</code> should be an + * object with field containing functions for all eligible RPC functions. If + * <code>name</code> is provided, the RPC handler will only used for RPC calls + * for the RPC interface with the same fully qualified Java name. If no + * <code>name</code> is provided, the RPC handler will be used for all incoming + * RPC invocations where the RPC method name is defined as a function field in + * the handler. The scheme for conversion between Java types in the RPC + * interface definition and the JavaScript values passed as arguments to the + * handler functions is described bellow.</li> + * <li><code>getRpcProxy([name])</code> - returns an RPC proxy object. If + * <code>name</code> is provided, the proxy object will contain functions for + * all methods in the RPC interface with the same fully qualified name, provided + * a RPC handler has been registered by the server-side code. If no + * <code>name</code> is provided, the returned RPC proxy object will contain + * functions for all methods in all RPC interfaces registered for the connector + * on the server. If the same method name is present in multiple registered RPC + * interfaces, the corresponding function in the RPC proxy object will throw an + * exception when called. The scheme for conversion between Java types in the + * RPC interface and the JavaScript values that should be passed to the + * functions is described bellow.</li> + * <li><code>translateVaadinUri(uri)</code> - Translates a Vaadin URI to a URL + * that can be used in the browser. This is just way of accessing + * {@link ApplicationConnection#translateVaadinUri(String)}</li> + * </ul> + * The connector wrapper also supports these special functions: + * <ul> + * <li><code>onStateChange</code> - If the JavaScript code assigns a function to + * the field, that function is called whenever the contents of the shared state + * is changed.</li> + * <li>Any field name corresponding to a call to + * {@link #addFunction(String, JavaScriptFunction)} on the server will + * automatically be present as a function that triggers the registered function + * on the server.</li> + * <li>Any field name referred to using + * {@link #callFunction(String, Object...)} on the server will be called if a + * function has been assigned to the field.</li> + * </ul> + * <p> + * + * Values in the Shared State and in RPC calls are converted between Java and + * JavaScript using the following conventions: + * <ul> + * <li>Primitive Java numbers (byte, char, int, long, float, double) and their + * boxed types (Byte, Character, Integer, Long, Float, Double) are represented + * by JavaScript numbers.</li> + * <li>The primitive Java boolean and the boxed Boolean are represented by + * JavaScript booleans.</li> + * <li>Java Strings are represented by JavaScript strings.</li> + * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li> + * <li>Map<String, ?> in Java is represented by JavaScript object with fields + * corresponding to the map keys.</li> + * <li>Any other Java Map is represented by a JavaScript array containing two + * arrays, the first contains the keys and the second contains the values in the + * same order.</li> + * <li>A Java Bean is represented by a JavaScript object with fields + * corresponding to the bean's properties.</li> + * <li>A Java Connector is represented by a JavaScript string containing the + * connector's id.</li> + * <li>A pluggable serialization mechanism is provided for types not described + * here. Please refer to the documentation for specific types for serialization + * information.</li> + * </ul> + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractJavaScriptComponent extends AbstractComponent { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + @Override + protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) { + super.registerRpc(implementation, rpcInterfaceType); + callbackHelper.registerRpc(rpcInterfaceType); + } + + /** + * Register a {@link JavaScriptFunction} that can be called from the + * JavaScript using the provided name. A JavaScript function with the + * provided name will be added to the connector wrapper object (initially + * available as <code>this</code>). Calling that JavaScript function will + * cause the call method in the registered {@link JavaScriptFunction} to be + * invoked with the same arguments. + * + * @param functionName + * the name that should be used for client-side function + * @param function + * the {@link JavaScriptFunction} object that will be invoked + * when the JavaScript function is called + */ + protected void addFunction(String functionName, JavaScriptFunction function) { + callbackHelper.registerCallback(functionName, function); + } + + /** + * Invoke a named function that the connector JavaScript has added to the + * JavaScript connector wrapper object. The arguments should only contain + * data types that can be represented in JavaScript including primitives, + * their boxed types, arrays, String, List, Set, Map, Connector and + * JavaBeans. + * + * @param name + * the name of the function + * @param arguments + * function arguments + */ + protected void callFunction(String name, Object... arguments) { + callbackHelper.invokeCallback(name, arguments); + } + + @Override + public JavaScriptComponentState getState() { + return (JavaScriptComponentState) super.getState(); + } +} diff --git a/server/src/com/vaadin/ui/AbstractLayout.java b/server/src/com/vaadin/ui/AbstractLayout.java new file mode 100644 index 0000000000..7b3a537d06 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractLayout.java @@ -0,0 +1,77 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.shared.ui.AbstractLayoutState; +import com.vaadin.ui.Layout.MarginHandler; + +/** + * An abstract class that defines default implementation for the {@link Layout} + * interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractLayout extends AbstractComponentContainer + implements Layout, MarginHandler { + + protected MarginInfo margins = new MarginInfo(false); + + @Override + public AbstractLayoutState getState() { + return (AbstractLayoutState) super.getState(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout#setMargin(boolean) + */ + @Override + public void setMargin(boolean enabled) { + margins.setMargins(enabled); + getState().setMarginsBitmask(margins.getBitMask()); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.MarginHandler#getMargin() + */ + @Override + public MarginInfo getMargin() { + return margins; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.MarginHandler#setMargin(MarginInfo) + */ + @Override + public void setMargin(MarginInfo marginInfo) { + margins.setMargins(marginInfo); + getState().setMarginsBitmask(margins.getBitMask()); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout#setMargin(boolean, boolean, boolean, boolean) + */ + @Override + public void setMargin(boolean topEnabled, boolean rightEnabled, + boolean bottomEnabled, boolean leftEnabled) { + margins.setMargins(topEnabled, rightEnabled, bottomEnabled, leftEnabled); + getState().setMarginsBitmask(margins.getBitMask()); + requestRepaint(); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractMedia.java b/server/src/com/vaadin/ui/AbstractMedia.java new file mode 100644 index 0000000000..71b2e38ef3 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractMedia.java @@ -0,0 +1,196 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.communication.URLReference; +import com.vaadin.shared.ui.AbstractMediaState; +import com.vaadin.shared.ui.MediaControl; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.server.ResourceReference; + +/** + * Abstract base class for the HTML5 media components. + * + * @author Vaadin Ltd + */ +public abstract class AbstractMedia extends AbstractComponent { + + @Override + public AbstractMediaState getState() { + return (AbstractMediaState) super.getState(); + } + + /** + * Sets a single media file as the source of the media component. + * + * @param source + */ + public void setSource(Resource source) { + clearSources(); + + addSource(source); + } + + private void clearSources() { + getState().getSources().clear(); + getState().getSourceTypes().clear(); + } + + /** + * Adds an alternative media file to the sources list. Which of the sources + * is used is selected by the browser depending on which file formats it + * supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @param source + */ + public void addSource(Resource source) { + if (source != null) { + getState().getSources().add(new ResourceReference(source)); + getState().getSourceTypes().add(source.getMIMEType()); + requestRepaint(); + } + } + + /** + * Set multiple sources at once. Which of the sources is used is selected by + * the browser depending on which file formats it supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @param sources + */ + public void setSources(Resource... sources) { + clearSources(); + for (Resource source : sources) { + addSource(source); + } + } + + /** + * @return The sources pointed to in this media. + */ + public List<Resource> getSources() { + ArrayList<Resource> sources = new ArrayList<Resource>(); + for (URLReference ref : getState().getSources()) { + sources.add(((ResourceReference) ref).getResource()); + } + return sources; + } + + /** + * Sets whether or not the browser should show native media controls. + * + * @param showControls + */ + public void setShowControls(boolean showControls) { + getState().setShowControls(showControls); + requestRepaint(); + } + + /** + * @return true if the browser is to show native media controls. + */ + public boolean isShowControls() { + return getState().isShowControls(); + } + + /** + * Sets the alternative text to be displayed if the browser does not support + * HTML5. This text is rendered as HTML if + * {@link #setHtmlContentAllowed(boolean)} is set to true. With HTML + * rendering, this method can also be used to implement fallback to a + * flash-based player, see the <a href= + * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash" + * >Mozilla Developer Network</a> for details. + * + * @param altText + */ + public void setAltText(String altText) { + getState().setAltText(altText); + requestRepaint(); + } + + /** + * @return The text/html that is displayed when a browser doesn't support + * HTML5. + */ + public String getAltText() { + return getState().getAltText(); + } + + /** + * Set whether the alternative text ({@link #setAltText(String)}) is + * rendered as HTML or not. + * + * @param htmlContentAllowed + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + getState().setHtmlContentAllowed(htmlContentAllowed); + requestRepaint(); + } + + /** + * @return true if the alternative text ({@link #setAltText(String)}) is to + * be rendered as HTML. + */ + public boolean isHtmlContentAllowed() { + return getState().isHtmlContentAllowed(); + } + + /** + * Sets whether the media is to automatically start playback when enough + * data has been loaded. + * + * @param autoplay + */ + public void setAutoplay(boolean autoplay) { + getState().setAutoplay(autoplay); + requestRepaint(); + } + + /** + * @return true if the media is set to automatically start playback. + */ + public boolean isAutoplay() { + return getState().isAutoplay(); + } + + /** + * Set whether to mute the audio or not. + * + * @param muted + */ + public void setMuted(boolean muted) { + getState().setMuted(muted); + requestRepaint(); + } + + /** + * @return true if the audio is muted. + */ + public boolean isMuted() { + return getState().isMuted(); + } + + /** + * Pauses the media. + */ + public void pause() { + getRpcProxy(MediaControl.class).pause(); + } + + /** + * Starts playback of the media. + */ + public void play() { + getRpcProxy(MediaControl.class).play(); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java new file mode 100644 index 0000000000..0581d0a279 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -0,0 +1,383 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState.ChildComponentData; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +@SuppressWarnings("serial") +public abstract class AbstractOrderedLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier { + + private AbstractOrderedLayoutServerRpc rpc = new AbstractOrderedLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(AbstractOrderedLayout.this, + mouseDetails, clickedConnector)); + } + }; + + public static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT; + + /** + * Custom layout slots containing the components. + */ + protected LinkedList<Component> components = new LinkedList<Component>(); + + /* Child component alignments */ + + /** + * Mapping from components to alignments (horizontal + vertical). + */ + public AbstractOrderedLayout() { + registerRpc(rpc); + } + + @Override + public AbstractOrderedLayoutState getState() { + return (AbstractOrderedLayoutState) super.getState(); + } + + /** + * Add a component into this container. The component is added to the right + * or under the previous component. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + // Add to components before calling super.addComponent + // so that it is available to AttachListeners + components.add(c); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + componentAdded(c); + } + + /** + * Adds a component into this container. The component is added to the left + * or on top of the other components. + * + * @param c + * the component to be added. + */ + public void addComponentAsFirst(Component c) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + removeComponent(c); + } + components.addFirst(c); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + componentAdded(c); + + } + + /** + * Adds a component into indexed position in this container. + * + * @param c + * the component to be added. + * @param index + * the index of the component position. The components currently + * in and after the position are shifted forwards. + */ + public void addComponent(Component c, int index) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + // When c is removed, all components after it are shifted down + if (index > getComponentIndex(c)) { + index--; + } + removeComponent(c); + } + components.add(index, c); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + + componentAdded(c); + } + + private void componentRemoved(Component c) { + getState().getChildData().remove(c); + requestRepaint(); + } + + private void componentAdded(Component c) { + getState().getChildData().put(c, new ChildComponentData()); + requestRepaint(); + + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + components.remove(c); + super.removeComponent(c); + componentRemoved(c); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + @Override + public Iterator<Component> getComponentIterator() { + return components.iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component component = i.next(); + + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + + location++; + } + + if (oldLocation == -1) { + addComponent(newComponent); + } else if (newLocation == -1) { + removeComponent(oldComponent); + addComponent(newComponent, oldLocation); + } else { + // Both old and new are in the layout + if (oldLocation > newLocation) { + components.remove(oldComponent); + components.add(newLocation, oldComponent); + components.remove(newComponent); + components.add(oldLocation, newComponent); + } else { + components.remove(newComponent); + components.add(oldLocation, newComponent); + components.remove(oldComponent); + components.add(newLocation, oldComponent); + } + + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .vaadin.ui.Component, int, int) + */ + @Override + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + Alignment a = new Alignment(horizontalAlignment + verticalAlignment); + setComponentAlignment(childComponent, a); + } + + @Override + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + ChildComponentData childData = getState().getChildData().get( + childComponent); + if (childData != null) { + // Alignments are bit masks + childData.setAlignmentBitmask(alignment.getBitMask()); + requestRepaint(); + } else { + throw new IllegalArgumentException( + "Component must be added to layout before using setComponentAlignment()"); + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com + * .vaadin.ui.Component) + */ + @Override + public Alignment getComponentAlignment(Component childComponent) { + ChildComponentData childData = getState().getChildData().get( + childComponent); + if (childData == null) { + throw new IllegalArgumentException( + "The given component is not a child of this layout"); + } + + return new Alignment(childData.getAlignmentBitmask()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + @Override + public void setSpacing(boolean spacing) { + getState().setSpacing(spacing); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Override + public boolean isSpacing() { + return getState().isSpacing(); + } + + /** + * <p> + * This method is used to control how excess space in layout is distributed + * among components. Excess space may exist if layout is sized and contained + * non relatively sized components don't consume all available space. + * + * <p> + * Example how to distribute 1:3 (33%) for component1 and 2:3 (67%) for + * component2 : + * + * <code> + * layout.setExpandRatio(component1, 1);<br> + * layout.setExpandRatio(component2, 2); + * </code> + * + * <p> + * If no ratios have been set, the excess space is distributed evenly among + * all components. + * + * <p> + * Note, that width or height (depending on orientation) needs to be defined + * for this method to have any effect. + * + * @see Sizeable + * + * @param component + * the component in this layout which expand ratio is to be set + * @param ratio + */ + public void setExpandRatio(Component component, float ratio) { + ChildComponentData childData = getState().getChildData().get(component); + if (childData == null) { + throw new IllegalArgumentException( + "The given component is not a child of this layout"); + } + + childData.setExpandRatio(ratio); + requestRepaint(); + }; + + /** + * Returns the expand ratio of given component. + * + * @param component + * which expand ratios is requested + * @return expand ratio of given component, 0.0f by default. + */ + public float getExpandRatio(Component component) { + ChildComponentData childData = getState().getChildData().get(component); + if (childData == null) { + throw new IllegalArgumentException( + "The given component is not a child of this layout"); + } + + return childData.getExpandRatio(); + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + + /** + * Returns the index of the given component. + * + * @param component + * The component to look up. + * @return The index of the component or -1 if the component is not a child. + */ + public int getComponentIndex(Component component) { + return components.indexOf(component); + } + + /** + * Returns the component at the given position. + * + * @param index + * The position of the component. + * @return The component at the given index. + * @throws IndexOutOfBoundsException + * If the index is out of range. + */ + public Component getComponent(int index) throws IndexOutOfBoundsException { + return components.get(index); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractSelect.java b/server/src/com/vaadin/ui/AbstractSelect.java new file mode 100644 index 0000000000..0a97ceb649 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractSelect.java @@ -0,0 +1,2029 @@ +/* + * @VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion; +import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor; +import com.vaadin.event.dd.acceptcriteria.TargetDetailIs; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.ui.AbstractSelect.ItemCaptionMode; + +/** + * <p> + * A class representing a selection of items the user has selected in a UI. The + * set of choices is presented as a set of {@link com.vaadin.data.Item}s in a + * {@link com.vaadin.data.Container}. + * </p> + * + * <p> + * A <code>Select</code> component may be in single- or multiselect mode. + * Multiselect mode means that more than one item can be selected + * simultaneously. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +// TODO currently cannot specify type more precisely in case of multi-select +public abstract class AbstractSelect extends AbstractField<Object> implements + Container, Container.Viewer, Container.PropertySetChangeListener, + Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier, + Container.ItemSetChangeListener, Vaadin6Component { + + public enum ItemCaptionMode { + /** + * Item caption mode: Item's ID's <code>String</code> representation is + * used as caption. + */ + ID, + /** + * Item caption mode: Item's <code>String</code> representation is used + * as caption. + */ + ITEM, + /** + * Item caption mode: Index of the item is used as caption. The index + * mode can only be used with the containers implementing the + * {@link com.vaadin.data.Container.Indexed} interface. + */ + INDEX, + /** + * Item caption mode: If an Item has a caption it's used, if not, Item's + * ID's <code>String</code> representation is used as caption. <b>This + * is the default</b>. + */ + EXPLICIT_DEFAULTS_ID, + /** + * Item caption mode: Captions must be explicitly specified. + */ + EXPLICIT, + /** + * Item caption mode: Only icons are shown, captions are hidden. + */ + ICON_ONLY, + /** + * Item caption mode: Item captions are read from property specified + * with <code>setItemCaptionPropertyId</code>. + */ + PROPERTY; + } + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ID = ItemCaptionMode.ID; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ITEM = ItemCaptionMode.ITEM; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_INDEX = ItemCaptionMode.INDEX; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = ItemCaptionMode.EXPLICIT_DEFAULTS_ID; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT = ItemCaptionMode.EXPLICIT; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ICON_ONLY = ItemCaptionMode.ICON_ONLY; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_PROPERTY = ItemCaptionMode.PROPERTY; + + /** + * Interface for option filtering, used to filter options based on user + * entered value. The value is matched to the item caption. + * <code>FILTERINGMODE_OFF</code> (0) turns the filtering off. + * <code>FILTERINGMODE_STARTSWITH</code> (1) matches from the start of the + * caption. <code>FILTERINGMODE_CONTAINS</code> (1) matches anywhere in the + * caption. + */ + public interface Filtering extends Serializable { + public static final int FILTERINGMODE_OFF = 0; + public static final int FILTERINGMODE_STARTSWITH = 1; + public static final int FILTERINGMODE_CONTAINS = 2; + + /** + * Sets the option filtering mode. + * + * @param filteringMode + * the filtering mode to use + */ + public void setFilteringMode(int filteringMode); + + /** + * Gets the current filtering mode. + * + * @return the filtering mode in use + */ + public int getFilteringMode(); + + } + + /** + * Multi select modes that controls how multi select behaves. + */ + public enum MultiSelectMode { + /** + * The default behavior of the multi select mode + */ + DEFAULT, + + /** + * The previous more simple behavior of the multselect + */ + SIMPLE + } + + /** + * Is the select in multiselect mode? + */ + private boolean multiSelect = false; + + /** + * Select options. + */ + protected Container items; + + /** + * Is the user allowed to add new options? + */ + private boolean allowNewOptions; + + /** + * Keymapper used to map key values. + */ + protected KeyMapper<Object> itemIdMapper = new KeyMapper<Object>(); + + /** + * Item icons. + */ + private final HashMap<Object, Resource> itemIcons = new HashMap<Object, Resource>(); + + /** + * Item captions. + */ + private final HashMap<Object, String> itemCaptions = new HashMap<Object, String>(); + + /** + * Item caption mode. + */ + private ItemCaptionMode itemCaptionMode = ItemCaptionMode.EXPLICIT_DEFAULTS_ID; + + /** + * Item caption source property id. + */ + private Object itemCaptionPropertyId = null; + + /** + * Item icon source property id. + */ + private Object itemIconPropertyId = null; + + /** + * List of property set change event listeners. + */ + private Set<Container.PropertySetChangeListener> propertySetEventListeners = null; + + /** + * List of item set change event listeners. + */ + private Set<Container.ItemSetChangeListener> itemSetEventListeners = null; + + /** + * Item id that represents null selection of this select. + * + * <p> + * Data interface does not support nulls as item ids. Selecting the item + * identified by this id is the same as selecting no items at all. This + * setting only affects the single select mode. + * </p> + */ + private Object nullSelectionItemId = null; + + // Null (empty) selection is enabled by default + private boolean nullSelectionAllowed = true; + private NewItemHandler newItemHandler; + + // Caption (Item / Property) change listeners + CaptionChangeListener captionChangeListener; + + /* Constructors */ + + /** + * Creates an empty Select. The caption is not used. + */ + public AbstractSelect() { + setContainerDataSource(new IndexedContainer()); + } + + /** + * Creates an empty Select with caption. + */ + public AbstractSelect(String caption) { + setContainerDataSource(new IndexedContainer()); + setCaption(caption); + } + + /** + * Creates a new select that is connected to a data-source. + * + * @param caption + * the Caption of the component. + * @param dataSource + * the Container datasource to be selected from by this select. + */ + public AbstractSelect(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /** + * Creates a new select that is filled from a collection of option values. + * + * @param caption + * the Caption of this field. + * @param options + * the Collection containing the options. + */ + public AbstractSelect(String caption, Collection<?> options) { + + // Creates the options container and add given options to it + final Container c = new IndexedContainer(); + if (options != null) { + for (final Iterator<?> i = options.iterator(); i.hasNext();) { + c.addItem(i.next()); + } + } + + setCaption(caption); + setContainerDataSource(c); + } + + /* Component methods */ + + /** + * Paints the content of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // Paints select attributes + if (isMultiSelect()) { + target.addAttribute("selectmode", "multi"); + } + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + if (getNullSelectionItemId() != null) { + target.addAttribute("nullselectitem", true); + } + } + + // Constructs selected keys array + String[] selectedKeys; + if (isMultiSelect()) { + selectedKeys = new String[((Set<?>) getValue()).size()]; + } else { + selectedKeys = new String[(getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + } + + // == + // first remove all previous item/property listeners + getCaptionChangeListener().clear(); + // Paints the options and create array of selected id keys + + target.startTag("options"); + int keyIndex = 0; + // Support for external null selection item id + final Collection<?> ids = getItemIds(); + if (isNullSelectionAllowed() && getNullSelectionItemId() != null + && !ids.contains(getNullSelectionItemId())) { + final Object id = getNullSelectionItemId(); + // Paints option + target.startTag("so"); + paintItem(target, id); + if (isSelected(id)) { + selectedKeys[keyIndex++] = itemIdMapper.key(id); + } + target.endTag("so"); + } + + final Iterator<?> i = getItemIds().iterator(); + // Paints the available selection options from data source + while (i.hasNext()) { + // Gets the option attribute values + final Object id = i.next(); + if (!isNullSelectionAllowed() && id != null + && id.equals(getNullSelectionItemId())) { + // Remove item if it's the null selection item but null + // selection is not allowed + continue; + } + final String key = itemIdMapper.key(id); + // add listener for each item, to cause repaint if an item changes + getCaptionChangeListener().addNotifierForItem(id); + target.startTag("so"); + paintItem(target, id); + if (isSelected(id) && keyIndex < selectedKeys.length) { + selectedKeys[keyIndex++] = key; + } + target.endTag("so"); + } + target.endTag("options"); + // == + + // Paint variables + target.addVariable(this, "selected", selectedKeys); + if (isNewItemsAllowed()) { + target.addVariable(this, "newitem", ""); + } + + } + + protected void paintItem(PaintTarget target, Object itemId) + throws PaintException { + final String key = itemIdMapper.key(itemId); + final String caption = getItemCaption(itemId); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute("icon", icon); + } + target.addAttribute("caption", caption); + if (itemId != null && itemId.equals(getNullSelectionItemId())) { + target.addAttribute("nullselection", true); + } + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + } + } + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + // New option entered (and it is allowed) + if (isNewItemsAllowed()) { + final String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + getNewItemHandler().addNewItem(newitem); + } + } + + // Selection change + if (variables.containsKey("selected")) { + final String[] clientSideSelectedKeys = (String[]) variables + .get("selected"); + + // Multiselect mode + if (isMultiSelect()) { + + // TODO Optimize by adding repaintNotNeeded when applicable + + // Converts the key-array to id-set + final LinkedList<Object> acceptedSelections = new LinkedList<Object>(); + for (int i = 0; i < clientSideSelectedKeys.length; i++) { + final Object id = itemIdMapper + .get(clientSideSelectedKeys[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + acceptedSelections.add(id); + } + } + + if (!isNullSelectionAllowed() && acceptedSelections.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + // Limits the deselection to the set of visible items + // (non-visible items can not be deselected) + Collection<?> visibleNotSelected = getVisibleItemIds(); + if (visibleNotSelected != null) { + visibleNotSelected = new HashSet<Object>(visibleNotSelected); + // Don't remove those that will be added to preserve order + visibleNotSelected.removeAll(acceptedSelections); + + @SuppressWarnings("unchecked") + Set<Object> newsel = (Set<Object>) getValue(); + if (newsel == null) { + newsel = new LinkedHashSet<Object>(); + } else { + newsel = new LinkedHashSet<Object>(newsel); + } + newsel.removeAll(visibleNotSelected); + newsel.addAll(acceptedSelections); + setValue(newsel, true); + } + } else { + // Single select mode + if (!isNullSelectionAllowed() + && (clientSideSelectedKeys.length == 0 + || clientSideSelectedKeys[0] == null || clientSideSelectedKeys[0] == getNullSelectionItemId())) { + requestRepaint(); + return; + } + if (clientSideSelectedKeys.length == 0) { + // Allows deselection only if the deselected item is + // visible + final Object current = getValue(); + final Collection<?> visible = getVisibleItemIds(); + if (visible != null && visible.contains(current)) { + setValue(null, true); + } + } else { + final Object id = itemIdMapper + .get(clientSideSelectedKeys[0]); + if (!isNullSelectionAllowed() && id == null) { + requestRepaint(); + } else if (id != null + && id.equals(getNullSelectionItemId())) { + setValue(null, true); + } else { + setValue(id, true); + } + } + } + } + } + + /** + * TODO refine doc Setter for new item handler that is called when user adds + * new item in newItemAllowed mode. + * + * @param newItemHandler + */ + public void setNewItemHandler(NewItemHandler newItemHandler) { + this.newItemHandler = newItemHandler; + } + + /** + * TODO refine doc + * + * @return + */ + public NewItemHandler getNewItemHandler() { + if (newItemHandler == null) { + newItemHandler = new DefaultNewItemHandler(); + } + return newItemHandler; + } + + public interface NewItemHandler extends Serializable { + void addNewItem(String newItemCaption); + } + + /** + * TODO refine doc + * + * This is a default class that handles adding new items that are typed by + * user to selects container. + * + * By extending this class one may implement some logic on new item addition + * like database inserts. + * + */ + public class DefaultNewItemHandler implements NewItemHandler { + @Override + public void addNewItem(String newItemCaption) { + // Checks for readonly + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Adds new option + if (addItem(newItemCaption) != null) { + + // Sets the caption property, if used + if (getItemCaptionPropertyId() != null) { + getContainerProperty(newItemCaption, + getItemCaptionPropertyId()) + .setValue(newItemCaption); + } + if (isMultiSelect()) { + Set values = new HashSet((Collection) getValue()); + values.add(newItemCaption); + setValue(values); + } else { + setValue(newItemCaption); + } + } + } + } + + /** + * Gets the visible item ids. In Select, this returns list of all item ids, + * but can be overriden in subclasses if they paint only part of the items + * to the terminal or null if no items is visible. + */ + public Collection<?> getVisibleItemIds() { + return getItemIds(); + } + + /* Property methods */ + + /** + * Returns the type of the property. <code>getValue</code> and + * <code>setValue</code> methods must be compatible with this type: one can + * safely cast <code>getValue</code> to given type and pass any variable + * assignable to this type as a parameter to <code>setValue</code>. + * + * @return the Type of the property. + */ + @Override + public Class<?> getType() { + if (isMultiSelect()) { + return Set.class; + } else { + return Object.class; + } + } + + /** + * Gets the selected item id or in multiselect mode a set of selected ids. + * + * @see com.vaadin.ui.AbstractField#getValue() + */ + @Override + public Object getValue() { + final Object retValue = super.getValue(); + + if (isMultiSelect()) { + + // If the return value is not a set + if (retValue == null) { + return new HashSet<Object>(); + } + if (retValue instanceof Set) { + return Collections.unmodifiableSet((Set<?>) retValue); + } else if (retValue instanceof Collection) { + return new HashSet<Object>((Collection<?>) retValue); + } else { + final Set<Object> s = new HashSet<Object>(); + if (items.containsId(retValue)) { + s.add(retValue); + } + return s; + } + + } else { + return retValue; + } + } + + /** + * Sets the visible value of the property. + * + * <p> + * The value of the select is the selected item id. If the select is in + * multiselect-mode, the value is a set of selected item keys. In + * multiselect mode all collections of id:s can be assigned. + * </p> + * + * @param newValue + * the New selected item or collection of selected items. + * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object) + */ + @Override + public void setValue(Object newValue) throws Property.ReadOnlyException { + if (newValue == getNullSelectionItemId()) { + newValue = null; + } + + setValue(newValue, false); + } + + /** + * Sets the visible value of the property. + * + * <p> + * The value of the select is the selected item id. If the select is in + * multiselect-mode, the value is a set of selected item keys. In + * multiselect mode all collections of id:s can be assigned. + * </p> + * + * @param newValue + * the New selected item or collection of selected items. + * @param repaintIsNotNeeded + * True if caller is sure that repaint is not needed. + * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, + * java.lang.Boolean) + */ + @Override + protected void setValue(Object newValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException { + + if (isMultiSelect()) { + if (newValue == null) { + super.setValue(new LinkedHashSet<Object>(), repaintIsNotNeeded); + } else if (Collection.class.isAssignableFrom(newValue.getClass())) { + super.setValue(new LinkedHashSet<Object>( + (Collection<?>) newValue), repaintIsNotNeeded); + } + } else if (newValue == null || items.containsId(newValue)) { + super.setValue(newValue, repaintIsNotNeeded); + } + } + + /* Container methods */ + + /** + * Gets the item from the container with given id. If the container does not + * contain the requested item, null is returned. + * + * @param itemId + * the item id. + * @return the item from the container. + */ + @Override + public Item getItem(Object itemId) { + return items.getItem(itemId); + } + + /** + * Gets the item Id collection from the container. + * + * @return the Collection of item ids. + */ + @Override + public Collection<?> getItemIds() { + return items.getItemIds(); + } + + /** + * Gets the property Id collection from the container. + * + * @return the Collection of property ids. + */ + @Override + public Collection<?> getContainerPropertyIds() { + return items.getContainerPropertyIds(); + } + + /** + * Gets the property type. + * + * @param propertyId + * the Id identifying the property. + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + @Override + public Class<?> getType(Object propertyId) { + return items.getType(propertyId); + } + + /* + * Gets the number of items in the container. + * + * @return the Number of items in the container. + * + * @see com.vaadin.data.Container#size() + */ + @Override + public int size() { + return items.size(); + } + + /** + * Tests, if the collection contains an item with given id. + * + * @param itemId + * the Id the of item to be tested. + */ + @Override + public boolean containsId(Object itemId) { + if (itemId != null) { + return items.containsId(itemId); + } else { + return false; + } + } + + /** + * Gets the Property identified by the given itemId and propertyId from the + * Container + * + * @see com.vaadin.data.Container#getContainerProperty(Object, Object) + */ + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + return items.getContainerProperty(itemId, propertyId); + } + + /** + * Adds the new property to all items. Adds a property with given id, type + * and default value to all items in the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + + final boolean retval = items.addContainerProperty(propertyId, type, + defaultValue); + if (retval && !(items instanceof Container.PropertySetChangeNotifier)) { + firePropertySetChange(); + } + return retval; + } + + /** + * Removes all items from the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + + final boolean retval = items.removeAllItems(); + itemIdMapper.removeAll(); + if (retval) { + setValue(null); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + } + return retval; + } + + /** + * Creates a new item into container with container managed id. The id of + * the created new item is returned. The item can be fetched with getItem() + * method. if the creation fails, null is returned. + * + * @return the Id of the created item or null in case of failure. + * @see com.vaadin.data.Container#addItem() + */ + @Override + public Object addItem() throws UnsupportedOperationException { + + final Object retval = items.addItem(); + if (retval != null + && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /** + * Create a new item into container. The created new item is returned and + * ready for setting property values. if the creation fails, null is + * returned. In case the container already contains the item, null is + * returned. + * + * This functionality is optional. If the function is unsupported, it always + * returns null. + * + * @param itemId + * the Identification of the item to be created. + * @return the Created item with the given id, or null in case of failure. + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + + final Item retval = items.addItem(itemId); + if (retval != null + && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + + unselect(itemId); + final boolean retval = items.removeItem(itemId); + itemIdMapper.remove(itemId); + if (retval && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /** + * Removes the property from all items. Removes a property with given id + * from all the items in the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object) + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + + final boolean retval = items.removeContainerProperty(propertyId); + if (retval && !(items instanceof Container.PropertySetChangeNotifier)) { + firePropertySetChange(); + } + return retval; + } + + /* Container.Viewer methods */ + + /** + * Sets the Container that serves as the data source of the viewer. + * + * As a side-effect the fields value (selection) is set to null due old + * selection not necessary exists in new Container. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + * + * @param newDataSource + * the new data source. + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + + getCaptionChangeListener().clear(); + + if (items != newDataSource) { + + // Removes listeners from the old datasource + if (items != null) { + if (items instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) items) + .removeListener(this); + } + if (items instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) items) + .removeListener(this); + } + } + + // Assigns new data source + items = newDataSource; + + // Clears itemIdMapper also + itemIdMapper.removeAll(); + + // Adds listeners + if (items != null) { + if (items instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) items).addListener(this); + } + if (items instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) items) + .addListener(this); + } + } + + /* + * We expect changing the data source should also clean value. See + * #810, #4607, #5281 + */ + setValue(null); + + requestRepaint(); + + } + } + + /** + * Gets the viewing data-source container. + * + * @see com.vaadin.data.Container.Viewer#getContainerDataSource() + */ + @Override + public Container getContainerDataSource() { + return items; + } + + /* Select attributes */ + + /** + * Is the select in multiselect mode? In multiselect mode + * + * @return the Value of property multiSelect. + */ + public boolean isMultiSelect() { + return multiSelect; + } + + /** + * Sets the multiselect mode. Setting multiselect mode false may lose + * selection information: if selected items set contains one or more + * selected items, only one of the selected items is kept as selected. + * + * Subclasses of AbstractSelect can choose not to support changing the + * multiselect mode, and may throw {@link UnsupportedOperationException}. + * + * @param multiSelect + * the New value of property multiSelect. + */ + public void setMultiSelect(boolean multiSelect) { + if (multiSelect && getNullSelectionItemId() != null) { + throw new IllegalStateException( + "Multiselect and NullSelectionItemId can not be set at the same time."); + } + if (multiSelect != this.multiSelect) { + + // Selection before mode change + final Object oldValue = getValue(); + + this.multiSelect = multiSelect; + + // Convert the value type + if (multiSelect) { + final Set<Object> s = new HashSet<Object>(); + if (oldValue != null) { + s.add(oldValue); + } + setValue(s); + } else { + final Set<?> s = (Set<?>) oldValue; + if (s == null || s.isEmpty()) { + setValue(null); + } else { + // Set the single select to contain only the first + // selected value in the multiselect + setValue(s.iterator().next()); + } + } + + requestRepaint(); + } + } + + /** + * Does the select allow adding new options by the user. If true, the new + * options can be added to the Container. The text entered by the user is + * used as id. Note that data-source must allow adding new items. + * + * @return True if additions are allowed. + */ + public boolean isNewItemsAllowed() { + + return allowNewOptions; + } + + /** + * Enables or disables possibility to add new options by the user. + * + * @param allowNewOptions + * the New value of property allowNewOptions. + */ + public void setNewItemsAllowed(boolean allowNewOptions) { + + // Only handle change requests + if (this.allowNewOptions != allowNewOptions) { + + this.allowNewOptions = allowNewOptions; + + requestRepaint(); + } + } + + /** + * Override the caption of an item. Setting caption explicitly overrides id, + * item and index captions. + * + * @param itemId + * the id of the item to be recaptioned. + * @param caption + * the New caption. + */ + public void setItemCaption(Object itemId, String caption) { + if (itemId != null) { + itemCaptions.put(itemId, caption); + requestRepaint(); + } + } + + /** + * Gets the caption of an item. The caption is generated as specified by the + * item caption mode. See <code>setItemCaptionMode()</code> for more + * details. + * + * @param itemId + * the id of the item to be queried. + * @return the caption for specified item. + */ + public String getItemCaption(Object itemId) { + + // Null items can not be found + if (itemId == null) { + return null; + } + + String caption = null; + + switch (getItemCaptionMode()) { + + case ID: + caption = itemId.toString(); + break; + + case INDEX: + if (items instanceof Container.Indexed) { + caption = String.valueOf(((Container.Indexed) items) + .indexOfId(itemId)); + } else { + caption = "ERROR: Container is not indexed"; + } + break; + + case ITEM: + final Item i = getItem(itemId); + if (i != null) { + caption = i.toString(); + } + break; + + case EXPLICIT: + caption = itemCaptions.get(itemId); + break; + + case EXPLICIT_DEFAULTS_ID: + caption = itemCaptions.get(itemId); + if (caption == null) { + caption = itemId.toString(); + } + break; + + case PROPERTY: + final Property<?> p = getContainerProperty(itemId, + getItemCaptionPropertyId()); + if (p != null) { + Object value = p.getValue(); + if (value != null) { + caption = value.toString(); + } + } + break; + } + + // All items must have some captions + return caption != null ? caption : ""; + } + + /** + * Sets tqhe icon for an item. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param icon + * the icon to use or null. + */ + public void setItemIcon(Object itemId, Resource icon) { + if (itemId != null) { + if (icon == null) { + itemIcons.remove(itemId); + } else { + itemIcons.put(itemId, icon); + } + requestRepaint(); + } + } + + /** + * Gets the item icon. + * + * @param itemId + * the id of the item to be assigned an icon. + * @return the icon for the item or null, if not specified. + */ + public Resource getItemIcon(Object itemId) { + final Resource explicit = itemIcons.get(itemId); + if (explicit != null) { + return explicit; + } + + if (getItemIconPropertyId() == null) { + return null; + } + + final Property<?> ip = getContainerProperty(itemId, + getItemIconPropertyId()); + if (ip == null) { + return null; + } + final Object icon = ip.getValue(); + if (icon instanceof Resource) { + return (Resource) icon; + } + + return null; + } + + /** + * Sets the item caption mode. + * + * <p> + * The mode can be one of the following ones: + * <ul> + * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items + * Id-objects <code>toString</code> is used as item caption. If caption is + * explicitly specified, it overrides the id-caption. + * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects + * <code>toString</code> is used as item caption.</li> + * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects + * <code>toString</code> is used as item caption.</li> + * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used + * as item caption. The index mode can only be used with the containers + * implementing <code>Container.Indexed</code> interface.</li> + * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be + * explicitly specified.</li> + * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read + * from property, that must be specified with + * <code>setItemCaptionPropertyId</code>.</li> + * </ul> + * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default + * mode. + * </p> + * + * @param mode + * the One of the modes listed above. + */ + public void setItemCaptionMode(ItemCaptionMode mode) { + if (mode != null) { + itemCaptionMode = mode; + requestRepaint(); + } + } + + /** + * Gets the item caption mode. + * + * <p> + * The mode can be one of the following ones: + * <ul> + * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items + * Id-objects <code>toString</code> is used as item caption. If caption is + * explicitly specified, it overrides the id-caption. + * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects + * <code>toString</code> is used as item caption.</li> + * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects + * <code>toString</code> is used as item caption.</li> + * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used + * as item caption. The index mode can only be used with the containers + * implementing <code>Container.Indexed</code> interface.</li> + * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be + * explicitly specified.</li> + * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read + * from property, that must be specified with + * <code>setItemCaptionPropertyId</code>.</li> + * </ul> + * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default + * mode. + * </p> + * + * @return the One of the modes listed above. + */ + public ItemCaptionMode getItemCaptionMode() { + return itemCaptionMode; + } + + /** + * Sets the item caption property. + * + * <p> + * Setting the id to a existing property implicitly sets the item caption + * mode to <code>ITEM_CAPTION_MODE_PROPERTY</code>. If the object is in + * <code>ITEM_CAPTION_MODE_PROPERTY</code> mode, setting caption property id + * null resets the item caption mode to + * <code>ITEM_CAPTION_EXPLICIT_DEFAULTS_ID</code>. + * </p> + * <p> + * Note that the type of the property used for caption must be String + * </p> + * <p> + * Setting the property id to null disables this feature. The id is null by + * default + * </p> + * . + * + * @param propertyId + * the id of the property. + * + */ + public void setItemCaptionPropertyId(Object propertyId) { + if (propertyId != null) { + itemCaptionPropertyId = propertyId; + setItemCaptionMode(ITEM_CAPTION_MODE_PROPERTY); + requestRepaint(); + } else { + itemCaptionPropertyId = null; + if (getItemCaptionMode() == ITEM_CAPTION_MODE_PROPERTY) { + setItemCaptionMode(ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID); + } + requestRepaint(); + } + } + + /** + * Gets the item caption property. + * + * @return the Id of the property used as item caption source. + */ + public Object getItemCaptionPropertyId() { + return itemCaptionPropertyId; + } + + /** + * Sets the item icon property. + * + * <p> + * If the property id is set to a valid value, each item is given an icon + * got from the given property of the items. The type of the property must + * be assignable to Resource. + * </p> + * + * <p> + * Note : The icons set with <code>setItemIcon</code> function override the + * icons from the property. + * </p> + * + * <p> + * Setting the property id to null disables this feature. The id is null by + * default + * </p> + * . + * + * @param propertyId + * the id of the property that specifies icons for items or null + * @throws IllegalArgumentException + * If the propertyId is not in the container or is not of a + * valid type + */ + public void setItemIconPropertyId(Object propertyId) + throws IllegalArgumentException { + if (propertyId == null) { + itemIconPropertyId = null; + } else if (!getContainerPropertyIds().contains(propertyId)) { + throw new IllegalArgumentException( + "Property id not found in the container"); + } else if (Resource.class.isAssignableFrom(getType(propertyId))) { + itemIconPropertyId = propertyId; + } else { + throw new IllegalArgumentException( + "Property type must be assignable to Resource"); + } + requestRepaint(); + } + + /** + * Gets the item icon property. + * + * <p> + * If the property id is set to a valid value, each item is given an icon + * got from the given property of the items. The type of the property must + * be assignable to Icon. + * </p> + * + * <p> + * Note : The icons set with <code>setItemIcon</code> function override the + * icons from the property. + * </p> + * + * <p> + * Setting the property id to null disables this feature. The id is null by + * default + * </p> + * . + * + * @return the Id of the property containing the item icons. + */ + public Object getItemIconPropertyId() { + return itemIconPropertyId; + } + + /** + * Tests if an item is selected. + * + * <p> + * In single select mode testing selection status of the item identified by + * {@link #getNullSelectionItemId()} returns true if the value of the + * property is null. + * </p> + * + * @param itemId + * the Id the of the item to be tested. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public boolean isSelected(Object itemId) { + if (itemId == null) { + return false; + } + if (isMultiSelect()) { + return ((Set<?>) getValue()).contains(itemId); + } else { + final Object value = getValue(); + return itemId.equals(value == null ? getNullSelectionItemId() + : value); + } + } + + /** + * Selects an item. + * + * <p> + * In single select mode selecting item identified by + * {@link #getNullSelectionItemId()} sets the value of the property to null. + * </p> + * + * @param itemId + * the identifier of Item to be selected. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public void select(Object itemId) { + if (!isMultiSelect()) { + setValue(itemId); + } else if (!isSelected(itemId) && itemId != null + && items.containsId(itemId)) { + final Set<Object> s = new HashSet<Object>((Set<?>) getValue()); + s.add(itemId); + setValue(s); + } + } + + /** + * Unselects an item. + * + * @param itemId + * the identifier of the Item to be unselected. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public void unselect(Object itemId) { + if (isSelected(itemId)) { + if (isMultiSelect()) { + final Set<Object> s = new HashSet<Object>((Set<?>) getValue()); + s.remove(itemId); + setValue(s); + } else { + setValue(null); + } + } + } + + /** + * Notifies this listener that the Containers contents has changed. + * + * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent) + */ + @Override + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + firePropertySetChange(); + } + + /** + * Adds a new Property set change listener for this Container. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + @Override + public void addListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners == null) { + propertySetEventListeners = new LinkedHashSet<Container.PropertySetChangeListener>(); + } + propertySetEventListeners.add(listener); + } + + /** + * Removes a previously registered Property set change listener. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners != null) { + propertySetEventListeners.remove(listener); + if (propertySetEventListeners.isEmpty()) { + propertySetEventListeners = null; + } + } + } + + /** + * Adds an Item set change listener for the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + @Override + public void addListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners == null) { + itemSetEventListeners = new LinkedHashSet<Container.ItemSetChangeListener>(); + } + itemSetEventListeners.add(listener); + } + + /** + * Removes the Item set change listener from the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners != null) { + itemSetEventListeners.remove(listener); + if (itemSetEventListeners.isEmpty()) { + itemSetEventListeners = null; + } + } + } + + @Override + public Collection<?> getListeners(Class<?> eventType) { + if (Container.ItemSetChangeEvent.class.isAssignableFrom(eventType)) { + if (itemSetEventListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(itemSetEventListeners); + } + } else if (Container.PropertySetChangeEvent.class + .isAssignableFrom(eventType)) { + if (propertySetEventListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertySetEventListeners); + } + } + + return super.getListeners(eventType); + } + + /** + * Lets the listener know a Containers Item set has changed. + * + * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent) + */ + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + // Clears the item id mapping table + itemIdMapper.removeAll(); + + // Notify all listeners + fireItemSetChange(); + } + + /** + * Fires the property set change event. + */ + protected void firePropertySetChange() { + if (propertySetEventListeners != null + && !propertySetEventListeners.isEmpty()) { + final Container.PropertySetChangeEvent event = new PropertySetChangeEvent(); + final Object[] listeners = propertySetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Container.PropertySetChangeListener) listeners[i]) + .containerPropertySetChange(event); + } + } + requestRepaint(); + } + + /** + * Fires the item set change event. + */ + protected void fireItemSetChange() { + if (itemSetEventListeners != null && !itemSetEventListeners.isEmpty()) { + final Container.ItemSetChangeEvent event = new ItemSetChangeEvent(); + final Object[] listeners = itemSetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Container.ItemSetChangeListener) listeners[i]) + .containerItemSetChange(event); + } + } + requestRepaint(); + } + + /** + * Implementation of item set change event. + */ + private class ItemSetChangeEvent implements Serializable, + Container.ItemSetChangeEvent { + + /** + * Gets the Property where the event occurred. + * + * @see com.vaadin.data.Container.ItemSetChangeEvent#getContainer() + */ + @Override + public Container getContainer() { + return AbstractSelect.this; + } + + } + + /** + * Implementation of property set change event. + */ + private class PropertySetChangeEvent implements + Container.PropertySetChangeEvent, Serializable { + + /** + * Retrieves the Container whose contents have been modified. + * + * @see com.vaadin.data.Container.PropertySetChangeEvent#getContainer() + */ + @Override + public Container getContainer() { + return AbstractSelect.this; + } + + } + + /** + * For multi-selectable fields, also an empty collection of values is + * considered to be an empty field. + * + * @see AbstractField#isEmpty(). + */ + @Override + protected boolean isEmpty() { + if (!multiSelect) { + return super.isEmpty(); + } else { + Object value = getValue(); + return super.isEmpty() + || (value instanceof Collection && ((Collection<?>) value) + .isEmpty()); + } + } + + /** + * Allow or disallow empty selection by the user. If the select is in + * single-select mode, you can make an item represent the empty selection by + * calling <code>setNullSelectionItemId()</code>. This way you can for + * instance set an icon and caption for the null selection item. + * + * @param nullSelectionAllowed + * whether or not to allow empty selection + * @see #setNullSelectionItemId(Object) + * @see #isNullSelectionAllowed() + */ + public void setNullSelectionAllowed(boolean nullSelectionAllowed) { + if (nullSelectionAllowed != this.nullSelectionAllowed) { + this.nullSelectionAllowed = nullSelectionAllowed; + requestRepaint(); + } + } + + /** + * Checks if null empty selection is allowed by the user. + * + * @return whether or not empty selection is allowed + * @see #setNullSelectionAllowed(boolean) + */ + public boolean isNullSelectionAllowed() { + return nullSelectionAllowed; + } + + /** + * Returns the item id that represents null value of this select in single + * select mode. + * + * <p> + * Data interface does not support nulls as item ids. Selecting the item + * identified by this id is the same as selecting no items at all. This + * setting only affects the single select mode. + * </p> + * + * @return the Object Null value item id. + * @see #setNullSelectionItemId(Object) + * @see #isSelected(Object) + * @see #select(Object) + */ + public Object getNullSelectionItemId() { + return nullSelectionItemId; + } + + /** + * Sets the item id that represents null value of this select. + * + * <p> + * Data interface does not support nulls as item ids. Selecting the item + * identified by this id is the same as selecting no items at all. This + * setting only affects the single select mode. + * </p> + * + * @param nullSelectionItemId + * the nullSelectionItemId to set. + * @see #getNullSelectionItemId() + * @see #isSelected(Object) + * @see #select(Object) + */ + public void setNullSelectionItemId(Object nullSelectionItemId) { + if (nullSelectionItemId != null && isMultiSelect()) { + throw new IllegalStateException( + "Multiselect and NullSelectionItemId can not be set at the same time."); + } + this.nullSelectionItemId = nullSelectionItemId; + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.AbstractField#attach() + */ + @Override + public void attach() { + super.attach(); + } + + /** + * Detaches the component from application. + * + * @see com.vaadin.ui.AbstractComponent#detach() + */ + @Override + public void detach() { + getCaptionChangeListener().clear(); + super.detach(); + } + + // Caption change listener + protected CaptionChangeListener getCaptionChangeListener() { + if (captionChangeListener == null) { + captionChangeListener = new CaptionChangeListener(); + } + return captionChangeListener; + } + + /** + * This is a listener helper for Item and Property changes that should cause + * a repaint. It should be attached to all items that are displayed, and the + * default implementation does this in paintContent(). Especially + * "lazyloading" components should take care to add and remove listeners as + * appropriate. Call addNotifierForItem() for each painted item (and + * remember to clear). + * + * NOTE: singleton, use getCaptionChangeListener(). + * + */ + protected class CaptionChangeListener implements + Item.PropertySetChangeListener, Property.ValueChangeListener { + + // TODO clean this up - type is either Item.PropertySetChangeNotifier or + // Property.ValueChangeNotifier + HashSet<Object> captionChangeNotifiers = new HashSet<Object>(); + + public void addNotifierForItem(Object itemId) { + switch (getItemCaptionMode()) { + case ITEM: + final Item i = getItem(itemId); + if (i == null) { + return; + } + if (i instanceof Item.PropertySetChangeNotifier) { + ((Item.PropertySetChangeNotifier) i) + .addListener(getCaptionChangeListener()); + captionChangeNotifiers.add(i); + } + Collection<?> pids = i.getItemPropertyIds(); + if (pids != null) { + for (Iterator<?> it = pids.iterator(); it.hasNext();) { + Property<?> p = i.getItemProperty(it.next()); + if (p != null + && p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addListener(getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + } + + } + break; + case PROPERTY: + final Property<?> p = getContainerProperty(itemId, + getItemCaptionPropertyId()); + if (p != null && p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addListener(getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + break; + + } + } + + public void clear() { + for (Iterator<Object> it = captionChangeNotifiers.iterator(); it + .hasNext();) { + Object notifier = it.next(); + if (notifier instanceof Item.PropertySetChangeNotifier) { + ((Item.PropertySetChangeNotifier) notifier) + .removeListener(getCaptionChangeListener()); + } else { + ((Property.ValueChangeNotifier) notifier) + .removeListener(getCaptionChangeListener()); + } + } + captionChangeNotifiers.clear(); + } + + @Override + public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) { + requestRepaint(); + } + + @Override + public void itemPropertySetChange( + com.vaadin.data.Item.PropertySetChangeEvent event) { + requestRepaint(); + } + + } + + /** + * Criterion which accepts a drop only if the drop target is (one of) the + * given Item identifier(s). Criterion can be used only on a drop targets + * that extends AbstractSelect like {@link Table} and {@link Tree}. The + * target and identifiers of valid Items are given in constructor. + * + * @since 6.3 + */ + public static class TargetItemIs extends AbstractItemSetCriterion { + + /** + * @param select + * the select implementation that is used as a drop target + * @param itemId + * the identifier(s) that are valid drop locations + */ + public TargetItemIs(AbstractSelect select, Object... itemId) { + super(select, itemId); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + if (dropTargetData.getTarget() != select) { + return false; + } + return itemIds.contains(dropTargetData.getItemIdOver()); + } + + } + + /** + * Abstract helper class to implement item id based criterion. + * + * Note, inner class used not to open itemIdMapper for public access. + * + * @since 6.3 + * + */ + private static abstract class AbstractItemSetCriterion extends + ClientSideCriterion { + protected final Collection<Object> itemIds = new HashSet<Object>(); + protected AbstractSelect select; + + public AbstractItemSetCriterion(AbstractSelect select, Object... itemId) { + if (itemIds == null || select == null) { + throw new IllegalArgumentException( + "Accepted item identifiers must be accepted."); + } + Collections.addAll(itemIds, itemId); + this.select = select; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + String[] keys = new String[itemIds.size()]; + int i = 0; + for (Object itemId : itemIds) { + String key = select.itemIdMapper.key(itemId); + keys[i++] = key; + } + target.addAttribute("keys", keys); + target.addAttribute("s", select); + } + + } + + /** + * This criterion accepts a only a {@link Transferable} that contains given + * Item (practically its identifier) from a specific AbstractSelect. + * + * @since 6.3 + */ + public static class AcceptItem extends AbstractItemSetCriterion { + + /** + * @param select + * the select from which the item id's are checked + * @param itemId + * the item identifier(s) of the select that are accepted + */ + public AcceptItem(AbstractSelect select, Object... itemId) { + super(select, itemId); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + DataBoundTransferable transferable = (DataBoundTransferable) dragEvent + .getTransferable(); + if (transferable.getSourceComponent() != select) { + return false; + } + return itemIds.contains(transferable.getItemId()); + } + + /** + * A simple accept criterion which ensures that {@link Transferable} + * contains an {@link Item} (or actually its identifier). In other words + * the criterion check that drag is coming from a {@link Container} like + * {@link Tree} or {@link Table}. + */ + public static final ClientSideCriterion ALL = new ContainsDataFlavor( + "itemId"); + + } + + /** + * TargetDetails implementation for subclasses of {@link AbstractSelect} + * that implement {@link DropTarget}. + * + * @since 6.3 + */ + public class AbstractSelectTargetDetails extends TargetDetailsImpl { + + /** + * The item id over which the drag event happened. + */ + protected Object idOver; + + /** + * Constructor that automatically converts itemIdOver key to + * corresponding item Id + * + */ + protected AbstractSelectTargetDetails(Map<String, Object> rawVariables) { + super(rawVariables, (DropTarget) AbstractSelect.this); + // eagar fetch itemid, mapper may be emptied + String keyover = (String) getData("itemIdOver"); + if (keyover != null) { + idOver = itemIdMapper.get(keyover); + } + } + + /** + * If the drag operation is currently over an {@link Item}, this method + * returns the identifier of that {@link Item}. + * + */ + public Object getItemIdOver() { + return idOver; + } + + /** + * Returns a detailed vertical location where the drop happened on Item. + */ + public VerticalDropLocation getDropLocation() { + String detail = (String) getData("detail"); + if (detail == null) { + return null; + } + return VerticalDropLocation.valueOf(detail); + } + + } + + /** + * An accept criterion to accept drops only on a specific vertical location + * of an item. + * <p> + * This accept criterion is currently usable in Tree and Table + * implementations. + */ + public static class VerticalLocationIs extends TargetDetailIs { + public static VerticalLocationIs TOP = new VerticalLocationIs( + VerticalDropLocation.TOP); + public static VerticalLocationIs BOTTOM = new VerticalLocationIs( + VerticalDropLocation.BOTTOM); + public static VerticalLocationIs MIDDLE = new VerticalLocationIs( + VerticalDropLocation.MIDDLE); + + private VerticalLocationIs(VerticalDropLocation l) { + super("detail", l.name()); + } + } + + /** + * Implement this interface and pass it to Tree.setItemDescriptionGenerator + * or Table.setItemDescriptionGenerator to generate mouse over descriptions + * ("tooltips") for the rows and cells in Table or for the items in Tree. + */ + public interface ItemDescriptionGenerator extends Serializable { + + /** + * Called by Table when a cell (and row) is painted or a item is painted + * in Tree + * + * @param source + * The source of the generator, the Tree or Table the + * generator is attached to + * @param itemId + * The itemId of the painted cell + * @param propertyId + * The propertyId of the cell, null when getting row + * description + * @return The description or "tooltip" of the item. + */ + public String generateDescription(Component source, Object itemId, + Object propertyId); + } +} diff --git a/server/src/com/vaadin/ui/AbstractSplitPanel.java b/server/src/com/vaadin/ui/AbstractSplitPanel.java new file mode 100644 index 0000000000..90dc38ff65 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractSplitPanel.java @@ -0,0 +1,521 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; + +import com.vaadin.event.ComponentEventListener; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.tools.ReflectTools; + +/** + * AbstractSplitPanel. + * + * <code>AbstractSplitPanel</code> is base class for a component container that + * can contain two components. The components are split by a divider element. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.5 + */ +public abstract class AbstractSplitPanel extends AbstractComponentContainer { + + // TODO use Unit in AbstractSplitPanelState and remove these + private Unit posUnit; + private Unit posMinUnit; + private Unit posMaxUnit; + + private AbstractSplitPanelRpc rpc = new AbstractSplitPanelRpc() { + + @Override + public void splitterClick(MouseEventDetails mouseDetails) { + fireEvent(new SplitterClickEvent(AbstractSplitPanel.this, + mouseDetails)); + } + + @Override + public void setSplitterPosition(float position) { + getSplitterState().setPosition(position); + } + }; + + public AbstractSplitPanel() { + registerRpc(rpc); + setSplitPosition(50, Unit.PERCENTAGE, false); + setSplitPositionLimits(0, Unit.PERCENTAGE, 100, Unit.PERCENTAGE); + } + + /** + * Modifiable and Serializable Iterator for the components, used by + * {@link AbstractSplitPanel#getComponentIterator()}. + */ + private class ComponentIterator implements Iterator<Component>, + Serializable { + + int i = 0; + + @Override + public boolean hasNext() { + if (i < getComponentCount()) { + return true; + } + return false; + } + + @Override + public Component next() { + if (!hasNext()) { + return null; + } + i++; + if (i == 1) { + return (getFirstComponent() == null ? getSecondComponent() + : getFirstComponent()); + } else if (i == 2) { + return getSecondComponent(); + } + return null; + } + + @Override + public void remove() { + if (i == 1) { + if (getFirstComponent() != null) { + setFirstComponent(null); + i = 0; + } else { + setSecondComponent(null); + } + } else if (i == 2) { + setSecondComponent(null); + } + } + } + + /** + * Add a component into this container. The component is added to the right + * or under the previous component. + * + * @param c + * the component to be added. + */ + + @Override + public void addComponent(Component c) { + if (getFirstComponent() == null) { + setFirstComponent(c); + } else if (getSecondComponent() == null) { + setSecondComponent(c); + } else { + throw new UnsupportedOperationException( + "Split panel can contain only two components"); + } + } + + /** + * Sets the first component of this split panel. Depending on the direction + * the first component is shown at the top or to the left. + * + * @param c + * The component to use as first component + */ + public void setFirstComponent(Component c) { + if (getFirstComponent() == c) { + // Nothing to do + return; + } + + if (getFirstComponent() != null) { + // detach old + removeComponent(getFirstComponent()); + } + getState().setFirstChild(c); + if (c != null) { + super.addComponent(c); + } + + requestRepaint(); + } + + /** + * Sets the second component of this split panel. Depending on the direction + * the second component is shown at the bottom or to the left. + * + * @param c + * The component to use as first component + */ + public void setSecondComponent(Component c) { + if (getSecondComponent() == c) { + // Nothing to do + return; + } + + if (getSecondComponent() != null) { + // detach old + removeComponent(getSecondComponent()); + } + getState().setSecondChild(c); + if (c != null) { + super.addComponent(c); + } + requestRepaint(); + } + + /** + * Gets the first component of this split panel. Depending on the direction + * this is either the component shown at the top or to the left. + * + * @return the first component of this split panel + */ + public Component getFirstComponent() { + return (Component) getState().getFirstChild(); + } + + /** + * Gets the second component of this split panel. Depending on the direction + * this is either the component shown at the top or to the left. + * + * @return the second component of this split panel + */ + public Component getSecondComponent() { + return (Component) getState().getSecondChild(); + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + + @Override + public void removeComponent(Component c) { + super.removeComponent(c); + if (c == getFirstComponent()) { + getState().setFirstChild(null); + } else if (c == getSecondComponent()) { + getState().setSecondChild(null); + } + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components (zero, one or two) + */ + + @Override + public int getComponentCount() { + int count = 0; + if (getFirstComponent() != null) { + count++; + } + if (getSecondComponent() != null) { + count++; + } + return count; + } + + /* Documented in superclass */ + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + if (oldComponent == getFirstComponent()) { + setFirstComponent(newComponent); + } else if (oldComponent == getSecondComponent()) { + setSecondComponent(newComponent); + } + requestRepaint(); + } + + /** + * Moves the position of the splitter. + * + * @param pos + * the new size of the first region in the unit that was last + * used (default is percentage). Fractions are only allowed when + * unit is percentage. + */ + public void setSplitPosition(float pos) { + setSplitPosition(pos, posUnit, false); + } + + /** + * Moves the position of the splitter. + * + * @param pos + * the new size of the region in the unit that was last used + * (default is percentage). Fractions are only allowed when unit + * is percentage. + * + * @param reverse + * if set to true the split splitter position is measured by the + * second region else it is measured by the first region + */ + public void setSplitPosition(float pos, boolean reverse) { + setSplitPosition(pos, posUnit, reverse); + } + + /** + * Moves the position of the splitter with given position and unit. + * + * @param pos + * the new size of the first region. Fractions are only allowed + * when unit is percentage. + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + */ + public void setSplitPosition(float pos, Unit unit) { + setSplitPosition(pos, unit, false); + } + + /** + * Moves the position of the splitter with given position and unit. + * + * @param pos + * the new size of the first region. Fractions are only allowed + * when unit is percentage. + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + * @param reverse + * if set to true the split splitter position is measured by the + * second region else it is measured by the first region + * + */ + public void setSplitPosition(float pos, Unit unit, boolean reverse) { + if (unit != Unit.PERCENTAGE && unit != Unit.PIXELS) { + throw new IllegalArgumentException( + "Only percentage and pixel units are allowed"); + } + if (unit != Unit.PERCENTAGE) { + pos = Math.round(pos); + } + SplitterState splitterState = getSplitterState(); + splitterState.setPosition(pos); + splitterState.setPositionUnit(unit.getSymbol()); + splitterState.setPositionReversed(reverse); + posUnit = unit; + + requestRepaint(); + } + + /** + * Returns the current position of the splitter, in + * {@link #getSplitPositionUnit()} units. + * + * @return position of the splitter + */ + public float getSplitPosition() { + return getSplitterState().getPosition(); + } + + /** + * Returns the unit of position of the splitter + * + * @return unit of position of the splitter + */ + public Unit getSplitPositionUnit() { + return posUnit; + } + + /** + * Sets the minimum split position to the given position and unit. If the + * split position is reversed, maximum and minimum are also reversed. + * + * @param pos + * the minimum position of the split + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + * Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS + */ + public void setMinSplitPosition(int pos, Unit unit) { + setSplitPositionLimits(pos, unit, getSplitterState().getMaxPosition(), + posMaxUnit); + } + + /** + * Returns the current minimum position of the splitter, in + * {@link #getMinSplitPositionUnit()} units. + * + * @return the minimum position of the splitter + */ + public float getMinSplitPosition() { + return getSplitterState().getMinPosition(); + } + + /** + * Returns the unit of the minimum position of the splitter. + * + * @return the unit of the minimum position of the splitter + */ + public Unit getMinSplitPositionUnit() { + return posMinUnit; + } + + /** + * Sets the maximum split position to the given position and unit. If the + * split position is reversed, maximum and minimum are also reversed. + * + * @param pos + * the maximum position of the split + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + * Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS + */ + public void setMaxSplitPosition(float pos, Unit unit) { + setSplitPositionLimits(getSplitterState().getMinPosition(), posMinUnit, + pos, unit); + } + + /** + * Returns the current maximum position of the splitter, in + * {@link #getMaxSplitPositionUnit()} units. + * + * @return the maximum position of the splitter + */ + public float getMaxSplitPosition() { + return getSplitterState().getMaxPosition(); + } + + /** + * Returns the unit of the maximum position of the splitter + * + * @return the unit of the maximum position of the splitter + */ + public Unit getMaxSplitPositionUnit() { + return posMaxUnit; + } + + /** + * Sets the maximum and minimum position of the splitter. If the split + * position is reversed, maximum and minimum are also reversed. + * + * @param minPos + * the new minimum position + * @param minPosUnit + * the unit (from {@link Sizeable}) in which the minimum position + * is given. + * @param maxPos + * the new maximum position + * @param maxPosUnit + * the unit (from {@link Sizeable}) in which the maximum position + * is given. + */ + private void setSplitPositionLimits(float minPos, Unit minPosUnit, + float maxPos, Unit maxPosUnit) { + if ((minPosUnit != Unit.PERCENTAGE && minPosUnit != Unit.PIXELS) + || (maxPosUnit != Unit.PERCENTAGE && maxPosUnit != Unit.PIXELS)) { + throw new IllegalArgumentException( + "Only percentage and pixel units are allowed"); + } + + SplitterState state = getSplitterState(); + + state.setMinPosition(minPos); + state.setMinPositionUnit(minPosUnit.getSymbol()); + posMinUnit = minPosUnit; + + state.setMaxPosition(maxPos); + state.setMaxPositionUnit(maxPosUnit.getSymbol()); + posMaxUnit = maxPosUnit; + + requestRepaint(); + } + + /** + * Lock the SplitPanels position, disabling the user from dragging the split + * handle. + * + * @param locked + * Set <code>true</code> if locked, <code>false</code> otherwise. + */ + public void setLocked(boolean locked) { + getSplitterState().setLocked(locked); + requestRepaint(); + } + + /** + * Is the SplitPanel handle locked (user not allowed to change split + * position by dragging). + * + * @return <code>true</code> if locked, <code>false</code> otherwise. + */ + public boolean isLocked() { + return getSplitterState().isLocked(); + } + + /** + * <code>SplitterClickListener</code> interface for listening for + * <code>SplitterClickEvent</code> fired by a <code>SplitPanel</code>. + * + * @see SplitterClickEvent + * @since 6.2 + */ + public interface SplitterClickListener extends ComponentEventListener { + + public static final Method clickMethod = ReflectTools.findMethod( + SplitterClickListener.class, "splitterClick", + SplitterClickEvent.class); + + /** + * SplitPanel splitter has been clicked + * + * @param event + * SplitterClickEvent event. + */ + public void splitterClick(SplitterClickEvent event); + } + + public class SplitterClickEvent extends ClickEvent { + + public SplitterClickEvent(Component source, + MouseEventDetails mouseEventDetails) { + super(source, mouseEventDetails); + } + + } + + public void addListener(SplitterClickListener listener) { + addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + SplitterClickEvent.class, listener, + SplitterClickListener.clickMethod); + } + + public void removeListener(SplitterClickListener listener) { + removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + SplitterClickEvent.class, listener); + } + + @Override + public AbstractSplitPanelState getState() { + return (AbstractSplitPanelState) super.getState(); + } + + private SplitterState getSplitterState() { + return getState().getSplitterState(); + } +} diff --git a/server/src/com/vaadin/ui/AbstractTextField.java b/server/src/com/vaadin/ui/AbstractTextField.java new file mode 100644 index 0000000000..2326c07d97 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractTextField.java @@ -0,0 +1,674 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.event.FieldEvents.TextChangeEvent; +import com.vaadin.event.FieldEvents.TextChangeListener; +import com.vaadin.event.FieldEvents.TextChangeNotifier; +import com.vaadin.shared.ui.textfield.AbstractTextFieldState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +public abstract class AbstractTextField extends AbstractField<String> implements + BlurNotifier, FocusNotifier, TextChangeNotifier, Vaadin6Component { + + /** + * Null representation. + */ + private String nullRepresentation = "null"; + /** + * Is setting to null from non-null value allowed by setting with null + * representation . + */ + private boolean nullSettingAllowed = false; + /** + * The text content when the last messages to the server was sent. Cleared + * when value is changed. + */ + private String lastKnownTextContent; + + /** + * The position of the cursor when the last message to the server was sent. + */ + private int lastKnownCursorPosition; + + /** + * Flag indicating that a text change event is pending to be triggered. + * Cleared by {@link #setInternalValue(Object)} and when the event is fired. + */ + private boolean textChangeEventPending; + + private boolean isFiringTextChangeEvent = false; + + private TextChangeEventMode textChangeEventMode = TextChangeEventMode.LAZY; + + private final int DEFAULT_TEXTCHANGE_TIMEOUT = 400; + + private int textChangeEventTimeout = DEFAULT_TEXTCHANGE_TIMEOUT; + + /** + * Temporarily holds the new selection position. Cleared on paint. + */ + private int selectionPosition = -1; + + /** + * Temporarily holds the new selection length. + */ + private int selectionLength; + + /** + * Flag used to determine whether we are currently handling a state change + * triggered by a user. Used to properly fire text change event before value + * change event triggered by the client side. + */ + private boolean changingVariables; + + protected AbstractTextField() { + super(); + } + + @Override + public AbstractTextFieldState getState() { + return (AbstractTextFieldState) super.getState(); + } + + @Override + public void updateState() { + super.updateState(); + + String value = getValue(); + if (value == null) { + value = getNullRepresentation(); + } + getState().setText(value); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + + if (selectionPosition != -1) { + target.addAttribute("selpos", selectionPosition); + target.addAttribute("sellen", selectionLength); + selectionPosition = -1; + } + + if (hasListeners(TextChangeEvent.class)) { + target.addAttribute(VTextField.ATTR_TEXTCHANGE_EVENTMODE, + getTextChangeEventMode().toString()); + target.addAttribute(VTextField.ATTR_TEXTCHANGE_TIMEOUT, + getTextChangeTimeout()); + if (lastKnownTextContent != null) { + /* + * The field has be repainted for some reason (e.g. caption, + * size, stylename), but the value has not been changed since + * the last text change event. Let the client side know about + * the value the server side knows. Client side may then ignore + * the actual value, depending on its state. + */ + target.addAttribute( + VTextField.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS, true); + } + } + + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + changingVariables = true; + + try { + + if (variables.containsKey(VTextField.VAR_CURSOR)) { + Integer object = (Integer) variables.get(VTextField.VAR_CURSOR); + lastKnownCursorPosition = object.intValue(); + } + + if (variables.containsKey(VTextField.VAR_CUR_TEXT)) { + /* + * NOTE, we might want to develop this further so that on a + * value change event the whole text content don't need to be + * sent from the client to server. Just "commit" the value from + * currentText to the value. + */ + handleInputEventTextChange(variables); + } + + // Sets the text + if (variables.containsKey("text") && !isReadOnly()) { + + // Only do the setting if the string representation of the value + // has been updated + String newValue = (String) variables.get("text"); + + // server side check for max length + if (getMaxLength() != -1 && newValue.length() > getMaxLength()) { + newValue = newValue.substring(0, getMaxLength()); + } + final String oldValue = getValue(); + if (newValue != null + && (oldValue == null || isNullSettingAllowed()) + && newValue.equals(getNullRepresentation())) { + newValue = null; + } + if (newValue != oldValue + && (newValue == null || !newValue.equals(oldValue))) { + boolean wasModified = isModified(); + setValue(newValue, true); + + // If the modified status changes, or if we have a + // formatter, repaint is needed after all. + if (wasModified != isModified()) { + requestRepaint(); + } + } + } + firePendingTextChangeEvent(); + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } finally { + changingVariables = false; + + } + + } + + @Override + public Class<String> getType() { + return String.class; + } + + /** + * Gets the null-string representation. + * + * <p> + * The null-valued strings are represented on the user interface by + * replacing the null value with this string. If the null representation is + * set null (not 'null' string), painting null value throws exception. + * </p> + * + * <p> + * The default value is string 'null'. + * </p> + * + * @return the String Textual representation for null strings. + * @see TextField#isNullSettingAllowed() + */ + public String getNullRepresentation() { + return nullRepresentation; + } + + /** + * Is setting nulls with null-string representation allowed. + * + * <p> + * If this property is true, writing null-representation string to text + * field always sets the field value to real null. If this property is + * false, null setting is not made, but the null values are maintained. + * Maintenance of null-values is made by only converting the textfield + * contents to real null, if the text field matches the null-string + * representation and the current value of the field is null. + * </p> + * + * <p> + * By default this setting is false + * </p> + * + * @return boolean Should the null-string represenation be always converted + * to null-values. + * @see TextField#getNullRepresentation() + */ + public boolean isNullSettingAllowed() { + return nullSettingAllowed; + } + + /** + * Sets the null-string representation. + * + * <p> + * The null-valued strings are represented on the user interface by + * replacing the null value with this string. If the null representation is + * set null (not 'null' string), painting null value throws exception. + * </p> + * + * <p> + * The default value is string 'null' + * </p> + * + * @param nullRepresentation + * Textual representation for null strings. + * @see TextField#setNullSettingAllowed(boolean) + */ + public void setNullRepresentation(String nullRepresentation) { + this.nullRepresentation = nullRepresentation; + requestRepaint(); + } + + /** + * Sets the null conversion mode. + * + * <p> + * If this property is true, writing null-representation string to text + * field always sets the field value to real null. If this property is + * false, null setting is not made, but the null values are maintained. + * Maintenance of null-values is made by only converting the textfield + * contents to real null, if the text field matches the null-string + * representation and the current value of the field is null. + * </p> + * + * <p> + * By default this setting is false. + * </p> + * + * @param nullSettingAllowed + * Should the null-string representation always be converted to + * null-values. + * @see TextField#getNullRepresentation() + */ + public void setNullSettingAllowed(boolean nullSettingAllowed) { + this.nullSettingAllowed = nullSettingAllowed; + requestRepaint(); + } + + @Override + protected boolean isEmpty() { + return super.isEmpty() || getValue().length() == 0; + } + + /** + * Returns the maximum number of characters in the field. Value -1 is + * considered unlimited. Terminal may however have some technical limits. + * + * @return the maxLength + */ + public int getMaxLength() { + return getState().getMaxLength(); + } + + /** + * Sets the maximum number of characters in the field. Value -1 is + * considered unlimited. Terminal may however have some technical limits. + * + * @param maxLength + * the maxLength to set + */ + public void setMaxLength(int maxLength) { + getState().setMaxLength(maxLength); + requestRepaint(); + } + + /** + * Gets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @return the number of columns in the editor. + */ + public int getColumns() { + return getState().getColumns(); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + getState().setColumns(columns); + requestRepaint(); + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return getState().getInputPrompt(); + } + + /** + * Sets the input prompt - a textual prompt that is displayed when the field + * would otherwise be empty, to prompt the user for input. + * + * @param inputPrompt + */ + public void setInputPrompt(String inputPrompt) { + getState().setInputPrompt(inputPrompt); + requestRepaint(); + } + + /* ** Text Change Events ** */ + + private void firePendingTextChangeEvent() { + if (textChangeEventPending && !isFiringTextChangeEvent) { + isFiringTextChangeEvent = true; + textChangeEventPending = false; + try { + fireEvent(new TextChangeEventImpl(this)); + } finally { + isFiringTextChangeEvent = false; + } + } + } + + @Override + protected void setInternalValue(String newValue) { + if (changingVariables && !textChangeEventPending) { + + /* + * TODO check for possible (minor?) issue (not tested) + * + * -field with e.g. PropertyFormatter. + * + * -TextChangeListener and it changes value. + * + * -if formatter again changes the value, do we get an extra + * simulated text change event ? + */ + + /* + * Fire a "simulated" text change event before value change event if + * change is coming from the client side. + * + * Iff there is both value change and textChangeEvent in same + * variable burst, it is a text field in non immediate mode and the + * text change event "flushed" queued value change event. In this + * case textChangeEventPending flag is already on and text change + * event will be fired after the value change event. + */ + if (newValue == null && lastKnownTextContent != null + && !lastKnownTextContent.equals(getNullRepresentation())) { + // Value was changed from something to null representation + lastKnownTextContent = getNullRepresentation(); + textChangeEventPending = true; + } else if (newValue != null + && !newValue.toString().equals(lastKnownTextContent)) { + // Value was changed to something else than null representation + lastKnownTextContent = newValue.toString(); + textChangeEventPending = true; + } + firePendingTextChangeEvent(); + } + + super.setInternalValue(newValue); + } + + @Override + public void setValue(Object newValue) throws ReadOnlyException { + super.setValue(newValue); + /* + * Make sure w reset lastKnownTextContent field on value change. The + * clearing must happen here as well because TextChangeListener can + * revert the original value. Client must respect the value in this + * case. AbstractField optimizes value change if the existing value is + * reset. Also we need to force repaint if the flag is on. + */ + if (lastKnownTextContent != null) { + lastKnownTextContent = null; + requestRepaint(); + } + } + + private void handleInputEventTextChange(Map<String, Object> variables) { + /* + * TODO we could vastly optimize the communication of values by using + * some sort of diffs instead of always sending the whole text content. + * Also on value change events we could use the mechanism. + */ + String object = (String) variables.get(VTextField.VAR_CUR_TEXT); + lastKnownTextContent = object; + textChangeEventPending = true; + } + + /** + * Sets the mode how the TextField triggers {@link TextChangeEvent}s. + * + * @param inputEventMode + * the new mode + * + * @see TextChangeEventMode + */ + public void setTextChangeEventMode(TextChangeEventMode inputEventMode) { + textChangeEventMode = inputEventMode; + requestRepaint(); + } + + /** + * @return the mode used to trigger {@link TextChangeEvent}s. + */ + public TextChangeEventMode getTextChangeEventMode() { + return textChangeEventMode; + } + + /** + * Different modes how the TextField can trigger {@link TextChangeEvent}s. + */ + public enum TextChangeEventMode { + + /** + * An event is triggered on each text content change, most commonly key + * press events. + */ + EAGER, + /** + * Each text change event in the UI causes the event to be communicated + * to the application after a timeout. The length of the timeout can be + * controlled with {@link TextField#setInputEventTimeout(int)}. Only the + * last input event is reported to the server side if several text + * change events happen during the timeout. + * <p> + * In case of a {@link ValueChangeEvent} the schedule is not kept + * strictly. Before a {@link ValueChangeEvent} a {@link TextChangeEvent} + * is triggered if the text content has changed since the previous + * TextChangeEvent regardless of the schedule. + */ + TIMEOUT, + /** + * An event is triggered when there is a pause of text modifications. + * The length of the pause can be modified with + * {@link TextField#setInputEventTimeout(int)}. Like with the + * {@link #TIMEOUT} mode, an event is forced before + * {@link ValueChangeEvent}s, even if the user did not keep a pause + * while entering the text. + * <p> + * This is the default mode. + */ + LAZY + } + + @Override + public void addListener(TextChangeListener listener) { + addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, + listener, TextChangeListener.EVENT_METHOD); + } + + @Override + public void removeListener(TextChangeListener listener) { + removeListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, + listener); + } + + /** + * The text change timeout modifies how often text change events are + * communicated to the application when {@link #getTextChangeEventMode()} is + * {@link TextChangeEventMode#LAZY} or {@link TextChangeEventMode#TIMEOUT}. + * + * + * @see #getTextChangeEventMode() + * + * @param timeout + * the timeout in milliseconds + */ + public void setTextChangeTimeout(int timeout) { + textChangeEventTimeout = timeout; + requestRepaint(); + } + + /** + * Gets the timeout used to fire {@link TextChangeEvent}s when the + * {@link #getTextChangeEventMode()} is {@link TextChangeEventMode#LAZY} or + * {@link TextChangeEventMode#TIMEOUT}. + * + * @return the timeout value in milliseconds + */ + public int getTextChangeTimeout() { + return textChangeEventTimeout; + } + + public class TextChangeEventImpl extends TextChangeEvent { + private String curText; + private int cursorPosition; + + private TextChangeEventImpl(final AbstractTextField tf) { + super(tf); + curText = tf.getCurrentTextContent(); + cursorPosition = tf.getCursorPosition(); + } + + @Override + public AbstractTextField getComponent() { + return (AbstractTextField) super.getComponent(); + } + + @Override + public String getText() { + return curText; + } + + @Override + public int getCursorPosition() { + return cursorPosition; + } + + } + + /** + * Gets the current (or the last known) text content in the field. + * <p> + * Note the text returned by this method is not necessary the same that is + * returned by the {@link #getValue()} method. The value is updated when the + * terminal fires a value change event via e.g. blurring the field or by + * pressing enter. The value returned by this method is updated also on + * {@link TextChangeEvent}s. Due to this high dependency to the terminal + * implementation this method is (at least at this point) not published. + * + * @return the text which is currently displayed in the field. + */ + private String getCurrentTextContent() { + if (lastKnownTextContent != null) { + return lastKnownTextContent; + } else { + Object text = getValue(); + if (text == null) { + return getNullRepresentation(); + } + return text.toString(); + } + } + + /** + * Selects all text in the field. + * + * @since 6.4 + */ + public void selectAll() { + String text = getValue() == null ? "" : getValue().toString(); + setSelectionRange(0, text.length()); + } + + /** + * Sets the range of text to be selected. + * + * As a side effect the field will become focused. + * + * @since 6.4 + * + * @param pos + * the position of the first character to be selected + * @param length + * the number of characters to be selected + */ + public void setSelectionRange(int pos, int length) { + selectionPosition = pos; + selectionLength = length; + focus(); + requestRepaint(); + } + + /** + * Sets the cursor position in the field. As a side effect the field will + * become focused. + * + * @since 6.4 + * + * @param pos + * the position for the cursor + * */ + public void setCursorPosition(int pos) { + setSelectionRange(pos, 0); + lastKnownCursorPosition = pos; + } + + /** + * Returns the last known cursor position of the field. + * + * <p> + * Note that due to the client server nature or the GWT terminal, Vaadin + * cannot provide the exact value of the cursor position in most situations. + * The value is updated only when the client side terminal communicates to + * TextField, like on {@link ValueChangeEvent}s and {@link TextChangeEvent} + * s. This may change later if a deep push integration is built to Vaadin. + * + * @return the cursor position + */ + public int getCursorPosition() { + return lastKnownCursorPosition; + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + +} diff --git a/server/src/com/vaadin/ui/Accordion.java b/server/src/com/vaadin/ui/Accordion.java new file mode 100644 index 0000000000..b937c7bc2b --- /dev/null +++ b/server/src/com/vaadin/ui/Accordion.java @@ -0,0 +1,19 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * An accordion is a component similar to a {@link TabSheet}, but with a + * vertical orientation and the selected component presented between tabs. + * + * Closable tabs are not supported by the accordion. + * + * The {@link Accordion} can be styled with the .v-accordion, .v-accordion-item, + * .v-accordion-item-first and .v-accordion-item-caption styles. + * + * @see TabSheet + */ +public class Accordion extends TabSheet { + +} diff --git a/server/src/com/vaadin/ui/Alignment.java b/server/src/com/vaadin/ui/Alignment.java new file mode 100644 index 0000000000..0d73da8504 --- /dev/null +++ b/server/src/com/vaadin/ui/Alignment.java @@ -0,0 +1,158 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.shared.ui.AlignmentInfo.Bits; + +/** + * Class containing information about alignment of a component. Use the + * pre-instantiated classes. + */ +@SuppressWarnings("serial") +public final class Alignment implements Serializable { + + public static final Alignment TOP_RIGHT = new Alignment(Bits.ALIGNMENT_TOP + + Bits.ALIGNMENT_RIGHT); + public static final Alignment TOP_LEFT = new Alignment(Bits.ALIGNMENT_TOP + + Bits.ALIGNMENT_LEFT); + public static final Alignment TOP_CENTER = new Alignment(Bits.ALIGNMENT_TOP + + Bits.ALIGNMENT_HORIZONTAL_CENTER); + public static final Alignment MIDDLE_RIGHT = new Alignment( + Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_RIGHT); + public static final Alignment MIDDLE_LEFT = new Alignment( + Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_LEFT); + public static final Alignment MIDDLE_CENTER = new Alignment( + Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_HORIZONTAL_CENTER); + public static final Alignment BOTTOM_RIGHT = new Alignment( + Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_RIGHT); + public static final Alignment BOTTOM_LEFT = new Alignment( + Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_LEFT); + public static final Alignment BOTTOM_CENTER = new Alignment( + Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_HORIZONTAL_CENTER); + + private final int bitMask; + + public Alignment(int bitMask) { + this.bitMask = bitMask; + } + + /** + * Returns a bitmask representation of the alignment value. Used internally + * by terminal. + * + * @return the bitmask representation of the alignment value + */ + public int getBitMask() { + return bitMask; + } + + /** + * Checks if component is aligned to the top of the available space. + * + * @return true if aligned top + */ + public boolean isTop() { + return (bitMask & Bits.ALIGNMENT_TOP) == Bits.ALIGNMENT_TOP; + } + + /** + * Checks if component is aligned to the bottom of the available space. + * + * @return true if aligned bottom + */ + public boolean isBottom() { + return (bitMask & Bits.ALIGNMENT_BOTTOM) == Bits.ALIGNMENT_BOTTOM; + } + + /** + * Checks if component is aligned to the left of the available space. + * + * @return true if aligned left + */ + public boolean isLeft() { + return (bitMask & Bits.ALIGNMENT_LEFT) == Bits.ALIGNMENT_LEFT; + } + + /** + * Checks if component is aligned to the right of the available space. + * + * @return true if aligned right + */ + public boolean isRight() { + return (bitMask & Bits.ALIGNMENT_RIGHT) == Bits.ALIGNMENT_RIGHT; + } + + /** + * Checks if component is aligned middle (vertically center) of the + * available space. + * + * @return true if aligned bottom + */ + public boolean isMiddle() { + return (bitMask & Bits.ALIGNMENT_VERTICAL_CENTER) == Bits.ALIGNMENT_VERTICAL_CENTER; + } + + /** + * Checks if component is aligned center (horizontally) of the available + * space. + * + * @return true if aligned center + */ + public boolean isCenter() { + return (bitMask & Bits.ALIGNMENT_HORIZONTAL_CENTER) == Bits.ALIGNMENT_HORIZONTAL_CENTER; + } + + /** + * Returns string representation of vertical alignment. + * + * @return vertical alignment as CSS value + */ + public String getVerticalAlignment() { + if (isBottom()) { + return "bottom"; + } else if (isMiddle()) { + return "middle"; + } + return "top"; + } + + /** + * Returns string representation of horizontal alignment. + * + * @return horizontal alignment as CSS value + */ + public String getHorizontalAlignment() { + if (isRight()) { + return "right"; + } else if (isCenter()) { + return "center"; + } + return "left"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (obj.getClass() != this.getClass())) { + return false; + } + Alignment a = (Alignment) obj; + return bitMask == a.bitMask; + } + + @Override + public int hashCode() { + return bitMask; + } + + @Override + public String toString() { + return String.valueOf(bitMask); + } + +} diff --git a/server/src/com/vaadin/ui/Audio.java b/server/src/com/vaadin/ui/Audio.java new file mode 100644 index 0000000000..ac2ee869a6 --- /dev/null +++ b/server/src/com/vaadin/ui/Audio.java @@ -0,0 +1,55 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.terminal.Resource; + +/** + * The Audio component translates into an HTML5 <audio> element and as + * such is only supported in browsers that support HTML5 media markup. Browsers + * that do not support HTML5 display the text or HTML set by calling + * {@link #setAltText(String)}. + * + * A flash-player fallback can be implemented by setting HTML content allowed ( + * {@link #setHtmlContentAllowed(boolean)} and calling + * {@link #setAltText(String)} with the flash player markup. An example of flash + * fallback can be found at the <a href= + * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash" + * >Mozilla Developer Network</a>. + * + * Multiple sources can be specified. Which of the sources is used is selected + * by the browser depending on which file formats it supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @author Vaadin Ltd + * @since 6.7.0 + */ +public class Audio extends AbstractMedia { + + public Audio() { + this("", null); + } + + /** + * @param caption + * The caption of the audio component. + */ + public Audio(String caption) { + this(caption, null); + } + + /** + * @param caption + * The caption of the audio component + * @param source + * The audio file to play. + */ + public Audio(String caption, Resource source) { + setCaption(caption); + setSource(source); + setShowControls(true); + } +} diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java new file mode 100644 index 0000000000..0cb667d527 --- /dev/null +++ b/server/src/com/vaadin/ui/Button.java @@ -0,0 +1,539 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.vaadin.event.Action; +import com.vaadin.event.FieldEvents; +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.ShortcutAction; +import com.vaadin.event.ShortcutAction.KeyCode; +import com.vaadin.event.ShortcutAction.ModifierKey; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.button.ButtonServerRpc; +import com.vaadin.shared.ui.button.ButtonState; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.Component.Focusable; + +/** + * A generic button component. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Button extends AbstractComponent implements + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Focusable, + Action.ShortcutNotifier { + + private ButtonServerRpc rpc = new ButtonServerRpc() { + + @Override + public void click(MouseEventDetails mouseEventDetails) { + fireClick(mouseEventDetails); + } + + @Override + public void disableOnClick() { + // Could be optimized so the button is not repainted because of + // this (client side has already disabled the button) + setEnabled(false); + } + }; + + FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) { + + @Override + protected void fireEvent(Event event) { + Button.this.fireEvent(event); + } + }; + + /** + * Creates a new push button. + */ + public Button() { + registerRpc(rpc); + registerRpc(focusBlurRpc); + } + + /** + * Creates a new push button with the given caption. + * + * @param caption + * the Button caption. + */ + public Button(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new push button with a click listener. + * + * @param caption + * the Button caption. + * @param listener + * the Button click listener. + */ + public Button(String caption, ClickListener listener) { + this(caption); + addListener(listener); + } + + /** + * Click event. This event is thrown, when the button is clicked. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class ClickEvent extends Component.Event { + + private final MouseEventDetails details; + + /** + * New instance of text change event. + * + * @param source + * the Source of the event. + */ + public ClickEvent(Component source) { + super(source); + details = null; + } + + /** + * Constructor with mouse details + * + * @param source + * The source where the click took place + * @param details + * Details about the mouse click + */ + public ClickEvent(Component source, MouseEventDetails details) { + super(source); + this.details = details; + } + + /** + * Gets the Button where the event occurred. + * + * @return the Source of the event. + */ + public Button getButton() { + return (Button) getSource(); + } + + /** + * Returns the mouse position (x coordinate) when the click took place. + * The position is relative to the browser client area. + * + * @return The mouse cursor x position or -1 if unknown + */ + public int getClientX() { + if (null != details) { + return details.getClientX(); + } else { + return -1; + } + } + + /** + * Returns the mouse position (y coordinate) when the click took place. + * The position is relative to the browser client area. + * + * @return The mouse cursor y position or -1 if unknown + */ + public int getClientY() { + if (null != details) { + return details.getClientY(); + } else { + return -1; + } + } + + /** + * Returns the relative mouse position (x coordinate) when the click + * took place. The position is relative to the clicked component. + * + * @return The mouse cursor x position relative to the clicked layout + * component or -1 if no x coordinate available + */ + public int getRelativeX() { + if (null != details) { + return details.getRelativeX(); + } else { + return -1; + } + } + + /** + * Returns the relative mouse position (y coordinate) when the click + * took place. The position is relative to the clicked component. + * + * @return The mouse cursor y position relative to the clicked layout + * component or -1 if no y coordinate available + */ + public int getRelativeY() { + if (null != details) { + return details.getRelativeY(); + } else { + return -1; + } + } + + /** + * Checks if the Alt key was down when the mouse event took place. + * + * @return true if Alt was down when the event occured, false otherwise + * or if unknown + */ + public boolean isAltKey() { + if (null != details) { + return details.isAltKey(); + } else { + return false; + } + } + + /** + * Checks if the Ctrl key was down when the mouse event took place. + * + * @return true if Ctrl was pressed when the event occured, false + * otherwise or if unknown + */ + public boolean isCtrlKey() { + if (null != details) { + return details.isCtrlKey(); + } else { + return false; + } + } + + /** + * Checks if the Meta key was down when the mouse event took place. + * + * @return true if Meta was pressed when the event occured, false + * otherwise or if unknown + */ + public boolean isMetaKey() { + if (null != details) { + return details.isMetaKey(); + } else { + return false; + } + } + + /** + * Checks if the Shift key was down when the mouse event took place. + * + * @return true if Shift was pressed when the event occured, false + * otherwise or if unknown + */ + public boolean isShiftKey() { + if (null != details) { + return details.isShiftKey(); + } else { + return false; + } + } + } + + /** + * Interface for listening for a {@link ClickEvent} fired by a + * {@link Component}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ClickListener extends Serializable { + + public static final Method BUTTON_CLICK_METHOD = ReflectTools + .findMethod(ClickListener.class, "buttonClick", + ClickEvent.class); + + /** + * Called when a {@link Button} has been clicked. A reference to the + * button is given by {@link ClickEvent#getButton()}. + * + * @param event + * An event containing information about the click. + */ + public void buttonClick(ClickEvent event); + + } + + /** + * Adds the button click listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ClickListener listener) { + addListener(ClickEvent.class, listener, + ClickListener.BUTTON_CLICK_METHOD); + } + + /** + * Removes the button click listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEvent.class, listener, + ClickListener.BUTTON_CLICK_METHOD); + } + + /** + * Simulates a button click, notifying all server-side listeners. + * + * No action is taken is the button is disabled. + */ + public void click() { + if (isEnabled() && !isReadOnly()) { + fireClick(); + } + } + + /** + * Fires a click event to all listeners without any event details. + * + * In subclasses, override {@link #fireClick(MouseEventDetails)} instead of + * this method. + */ + protected void fireClick() { + fireEvent(new Button.ClickEvent(this)); + } + + /** + * Fires a click event to all listeners. + * + * @param details + * MouseEventDetails from which keyboard modifiers and other + * information about the mouse click can be obtained. If the + * button was clicked by a keyboard event, some of the fields may + * be empty/undefined. + */ + protected void fireClick(MouseEventDetails details) { + fireEvent(new Button.ClickEvent(this, details)); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + /* + * Actions + */ + + protected ClickShortcut clickShortcut; + + /** + * Makes it possible to invoke a click on this button by pressing the given + * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/> + * The shortcut is global (bound to the containing Window). + * + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut, null for + * none + */ + public void setClickShortcut(int keyCode, int... modifiers) { + if (clickShortcut != null) { + removeShortcutListener(clickShortcut); + } + clickShortcut = new ClickShortcut(this, keyCode, modifiers); + addShortcutListener(clickShortcut); + getState().setClickShortcutKeyCode(clickShortcut.getKeyCode()); + } + + /** + * Removes the keyboard shortcut previously set with + * {@link #setClickShortcut(int, int...)}. + */ + public void removeClickShortcut() { + if (clickShortcut != null) { + removeShortcutListener(clickShortcut); + clickShortcut = null; + getState().setClickShortcutKeyCode(0); + } + } + + /** + * A {@link ShortcutListener} specifically made to define a keyboard + * shortcut that invokes a click on the given button. + * + */ + public static class ClickShortcut extends ShortcutListener { + protected Button button; + + /** + * Creates a keyboard shortcut for clicking the given button using the + * shorthand notation defined in {@link ShortcutAction}. + * + * @param button + * to be clicked when the shortcut is invoked + * @param shorthandCaption + * the caption with shortcut keycode and modifiers indicated + */ + public ClickShortcut(Button button, String shorthandCaption) { + super(shorthandCaption); + this.button = button; + } + + /** + * Creates a keyboard shortcut for clicking the given button using the + * given {@link KeyCode} and {@link ModifierKey}s. + * + * @param button + * to be clicked when the shortcut is invoked + * @param keyCode + * KeyCode to react to + * @param modifiers + * optional modifiers for shortcut + */ + public ClickShortcut(Button button, int keyCode, int... modifiers) { + super(null, keyCode, modifiers); + this.button = button; + } + + /** + * Creates a keyboard shortcut for clicking the given button using the + * given {@link KeyCode}. + * + * @param button + * to be clicked when the shortcut is invoked + * @param keyCode + * KeyCode to react to + */ + public ClickShortcut(Button button, int keyCode) { + this(button, keyCode, null); + } + + @Override + public void handleAction(Object sender, Object target) { + button.click(); + } + } + + /** + * Determines if a button is automatically disabled when clicked. See + * {@link #setDisableOnClick(boolean)} for details. + * + * @return true if the button is disabled when clicked, false otherwise + */ + public boolean isDisableOnClick() { + return getState().isDisableOnClick(); + } + + /** + * Determines if a button is automatically disabled when clicked. If this is + * set to true the button will be automatically disabled when clicked, + * typically to prevent (accidental) extra clicks on a button. + * + * @param disableOnClick + * true to disable button when it is clicked, false otherwise + */ + public void setDisableOnClick(boolean disableOnClick) { + getState().setDisableOnClick(disableOnClick); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + @Override + public int getTabIndex() { + return getState().getTabIndex(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + getState().setTabIndex(tabIndex); + requestRepaint(); + } + + @Override + public void focus() { + // Overridden only to make public + super.focus(); + } + + @Override + public ButtonState getState() { + return (ButtonState) super.getState(); + } + + /** + * Set whether the caption text is rendered as HTML or not. You might need + * to retheme button to allow higher content than the original text style. + * + * If set to true, the captions are passed to the browser as html and the + * developer is responsible for ensuring no harmful html is used. If set to + * false, the content is passed to the browser as plain text. + * + * @param htmlContentAllowed + * <code>true</code> if caption is rendered as HTML, + * <code>false</code> otherwise + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + if (getState().isHtmlContentAllowed() != htmlContentAllowed) { + getState().setHtmlContentAllowed(htmlContentAllowed); + requestRepaint(); + } + } + + /** + * Return HTML rendering setting + * + * @return <code>true</code> if the caption text is to be rendered as HTML, + * <code>false</code> otherwise + */ + public boolean isHtmlContentAllowed() { + return getState().isHtmlContentAllowed(); + } + +} diff --git a/server/src/com/vaadin/ui/CheckBox.java b/server/src/com/vaadin/ui/CheckBox.java new file mode 100644 index 0000000000..30ac9b4626 --- /dev/null +++ b/server/src/com/vaadin/ui/CheckBox.java @@ -0,0 +1,141 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc; +import com.vaadin.shared.ui.checkbox.CheckBoxState; + +public class CheckBox extends AbstractField<Boolean> { + + private CheckBoxServerRpc rpc = new CheckBoxServerRpc() { + + @Override + public void setChecked(boolean checked, + MouseEventDetails mouseEventDetails) { + if (isReadOnly()) { + return; + } + + final Boolean oldValue = getValue(); + final Boolean newValue = checked; + + if (!newValue.equals(oldValue)) { + // The event is only sent if the switch state is changed + setValue(newValue); + } + + } + }; + + FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) { + @Override + protected void fireEvent(Event event) { + CheckBox.this.fireEvent(event); + } + }; + + /** + * Creates a new checkbox. + */ + public CheckBox() { + registerRpc(rpc); + registerRpc(focusBlurRpc); + setValue(Boolean.FALSE); + } + + /** + * Creates a new checkbox with a set caption. + * + * @param caption + * the Checkbox caption. + */ + public CheckBox(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new checkbox with a caption and a set initial state. + * + * @param caption + * the caption of the checkbox + * @param initialState + * the initial state of the checkbox + */ + public CheckBox(String caption, boolean initialState) { + this(caption); + setValue(initialState); + } + + /** + * Creates a new checkbox that is connected to a boolean property. + * + * @param state + * the Initial state of the switch-button. + * @param dataSource + */ + public CheckBox(String caption, Property<?> dataSource) { + this(caption); + setPropertyDataSource(dataSource); + } + + @Override + public Class<Boolean> getType() { + return Boolean.class; + } + + @Override + public CheckBoxState getState() { + return (CheckBoxState) super.getState(); + } + + @Override + protected void setInternalValue(Boolean newValue) { + super.setInternalValue(newValue); + if (newValue == null) { + newValue = false; + } + getState().setChecked(newValue); + } + + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + /** + * Get the boolean value of the button state. + * + * @return True iff the button is pressed down or checked. + * + * @deprecated Use {@link #getValue()} instead and, if needed, handle null + * values. + */ + @Deprecated + public boolean booleanValue() { + Boolean value = getValue(); + return (null == value) ? false : value.booleanValue(); + } +} diff --git a/server/src/com/vaadin/ui/ComboBox.java b/server/src/com/vaadin/ui/ComboBox.java new file mode 100644 index 0000000000..6286dad124 --- /dev/null +++ b/server/src/com/vaadin/ui/ComboBox.java @@ -0,0 +1,116 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.combobox.VFilterSelect; + +/** + * A filtering dropdown single-select. Suitable for newItemsAllowed, but it's + * turned of by default to avoid mistakes. Items are filtered based on user + * input, and loaded dynamically ("lazy-loading") from the server. You can turn + * on newItemsAllowed and change filtering mode (and also turn it off), but you + * can not turn on multi-select mode. + * + */ +@SuppressWarnings("serial") +public class ComboBox extends Select { + + private String inputPrompt = null; + + /** + * If text input is not allowed, the ComboBox behaves like a pretty + * NativeSelect - the user can not enter any text and clicking the text + * field opens the drop down with options + */ + private boolean textInputAllowed = true; + + public ComboBox() { + setNewItemsAllowed(false); + } + + public ComboBox(String caption, Collection<?> options) { + super(caption, options); + setNewItemsAllowed(false); + } + + public ComboBox(String caption, Container dataSource) { + super(caption, dataSource); + setNewItemsAllowed(false); + } + + public ComboBox(String caption) { + super(caption); + setNewItemsAllowed(false); + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return inputPrompt; + } + + /** + * Sets the input prompt - a textual prompt that is displayed when the + * select would otherwise be empty, to prompt the user for input. + * + * @param inputPrompt + * the desired input prompt, or null to disable + */ + public void setInputPrompt(String inputPrompt) { + this.inputPrompt = inputPrompt; + requestRepaint(); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (inputPrompt != null) { + target.addAttribute("prompt", inputPrompt); + } + super.paintContent(target); + + if (!textInputAllowed) { + target.addAttribute(VFilterSelect.ATTR_NO_TEXT_INPUT, true); + } + } + + /** + * Sets whether it is possible to input text into the field or whether the + * field area of the component is just used to show what is selected. By + * disabling text input, the comboBox will work in the same way as a + * {@link NativeSelect} + * + * @see #isTextInputAllowed() + * + * @param textInputAllowed + * true to allow entering text, false to just show the current + * selection + */ + public void setTextInputAllowed(boolean textInputAllowed) { + this.textInputAllowed = textInputAllowed; + requestRepaint(); + } + + /** + * Returns true if the user can enter text into the field to either filter + * the selections or enter a new value if {@link #isNewItemsAllowed()} + * returns true. If text input is disabled, the comboBox will work in the + * same way as a {@link NativeSelect} + * + * @return + */ + public boolean isTextInputAllowed() { + return textInputAllowed; + } + +} diff --git a/server/src/com/vaadin/ui/Component.java b/server/src/com/vaadin/ui/Component.java new file mode 100644 index 0000000000..a2c257ab68 --- /dev/null +++ b/server/src/com/vaadin/ui/Component.java @@ -0,0 +1,1047 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.EventListener; +import java.util.EventObject; +import java.util.Locale; + +import com.vaadin.Application; +import com.vaadin.event.FieldEvents; +import com.vaadin.shared.ComponentState; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * {@code Component} is the top-level interface that is and must be implemented + * by all Vaadin components. {@code Component} is paired with + * {@link AbstractComponent}, which provides a default implementation for all + * the methods defined in this interface. + * + * <p> + * Components are laid out in the user interface hierarchically. The layout is + * managed by layout components, or more generally by components that implement + * the {@link ComponentContainer} interface. Such a container is the + * <i>parent</i> of the contained components. + * </p> + * + * <p> + * The {@link #getParent()} method allows retrieving the parent component of a + * component. While there is a {@link #setParent(Component) setParent()}, you + * rarely need it as you normally add components with the + * {@link ComponentContainer#addComponent(Component) addComponent()} method of + * the layout or other {@code ComponentContainer}, which automatically sets the + * parent. + * </p> + * + * <p> + * A component becomes <i>attached</i> to an application (and the + * {@link #attach()} is called) when it or one of its parents is attached to the + * main window of the application through its containment hierarchy. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Component extends ClientConnector, Sizeable, Serializable { + + /** + * Gets all user-defined CSS style names of a component. If the component + * has multiple style names defined, the return string is a space-separated + * list of style names. Built-in style names defined in Vaadin or GWT are + * not returned. + * + * <p> + * The style names are returned only in the basic form in which they were + * added; each user-defined style name shows as two CSS style class names in + * the rendered HTML: one as it was given and one prefixed with the + * component-specific style name. Only the former is returned. + * </p> + * + * @return the style name or a space-separated list of user-defined style + * names of the component + * @see #setStyleName(String) + * @see #addStyleName(String) + * @see #removeStyleName(String) + */ + public String getStyleName(); + + /** + * Sets one or more user-defined style names of the component, replacing any + * previous user-defined styles. Multiple styles can be specified as a + * space-separated list of style names. The style names must be valid CSS + * class names and should not conflict with any built-in style names in + * Vaadin or GWT. + * + * <pre> + * Label label = new Label("This text has a lot of style"); + * label.setStyleName("myonestyle myotherstyle"); + * </pre> + * + * <p> + * Each style name will occur in two versions: one as specified and one that + * is prefixed with the style name of the component. For example, if you + * have a {@code Button} component and give it "{@code mystyle}" style, the + * component will have both "{@code mystyle}" and "{@code v-button-mystyle}" + * styles. You could then style the component either with: + * </p> + * + * <pre> + * .myonestyle {background: blue;} + * </pre> + * + * <p> + * or + * </p> + * + * <pre> + * .v-button-myonestyle {background: blue;} + * </pre> + * + * <p> + * It is normally a good practice to use {@link #addStyleName(String) + * addStyleName()} rather than this setter, as different software + * abstraction layers can then add their own styles without accidentally + * removing those defined in other layers. + * </p> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. + * </p> + * + * @param style + * the new style or styles of the component as a space-separated + * list + * @see #getStyleName() + * @see #addStyleName(String) + * @see #removeStyleName(String) + */ + public void setStyleName(String style); + + /** + * Adds a style name to component. The style name will be rendered as a HTML + * class name, which can be used in a CSS definition. + * + * <pre> + * Label label = new Label("This text has style"); + * label.addStyleName("mystyle"); + * </pre> + * + * <p> + * Each style name will occur in two versions: one as specified and one that + * is prefixed wil the style name of the component. For example, if you have + * a {@code Button} component and give it "{@code mystyle}" style, the + * component will have both "{@code mystyle}" and "{@code v-button-mystyle}" + * styles. You could then style the component either with: + * </p> + * + * <pre> + * .mystyle {font-style: italic;} + * </pre> + * + * <p> + * or + * </p> + * + * <pre> + * .v-button-mystyle {font-style: italic;} + * </pre> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. + * </p> + * + * @param style + * the new style to be added to the component + * @see #getStyleName() + * @see #setStyleName(String) + * @see #removeStyleName(String) + */ + public void addStyleName(String style); + + /** + * Removes one or more style names from component. Multiple styles can be + * specified as a space-separated list of style names. + * + * <p> + * The parameter must be a valid CSS style name. Only user-defined style + * names added with {@link #addStyleName(String) addStyleName()} or + * {@link #setStyleName(String) setStyleName()} can be removed; built-in + * style names defined in Vaadin or GWT can not be removed. + * </p> + * + * * This method will trigger a {@link RepaintRequestEvent}. + * + * @param style + * the style name or style names to be removed + * @see #getStyleName() + * @see #setStyleName(String) + * @see #addStyleName(String) + */ + public void removeStyleName(String style); + + /** + * Tests whether the component is enabled or not. A user can not interact + * with disabled components. Disabled components are rendered in a style + * that indicates the status, usually in gray color. Children of a disabled + * component are also disabled. Components are enabled by default. + * + * <p> + * As a security feature, all updates for disabled components are blocked on + * the server-side. + * </p> + * + * <p> + * Note that this method only returns the status of the component and does + * not take parents into account. Even though this method returns true the + * component can be disabled to the user if a parent is disabled. + * </p> + * + * @return <code>true</code> if the component and its parent are enabled, + * <code>false</code> otherwise. + * @see VariableOwner#isEnabled() + */ + public boolean isEnabled(); + + /** + * Enables or disables the component. The user can not interact disabled + * components, which are shown with a style that indicates the status, + * usually shaded in light gray color. Components are enabled by default. + * + * <pre> + * Button enabled = new Button("Enabled"); + * enabled.setEnabled(true); // The default + * layout.addComponent(enabled); + * + * Button disabled = new Button("Disabled"); + * disabled.setEnabled(false); + * layout.addComponent(disabled); + * </pre> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent} for the component + * and, if it is a {@link ComponentContainer}, for all its children + * recursively. + * </p> + * + * @param enabled + * a boolean value specifying if the component should be enabled + * or not + */ + public void setEnabled(boolean enabled); + + /** + * Tests the <i>visibility</i> property of the component. + * + * <p> + * Visible components are drawn in the user interface, while invisible ones + * are not. The effect is not merely a cosmetic CSS change - no information + * about an invisible component will be sent to the client. The effect is + * thus the same as removing the component from its parent. Making a + * component invisible through this property can alter the positioning of + * other components. + * </p> + * + * <p> + * A component is visible only if all its parents are also visible. This is + * not checked by this method though, so even if this method returns true, + * the component can be hidden from the user because a parent is set to + * invisible. + * </p> + * + * @return <code>true</code> if the component has been set to be visible in + * the user interface, <code>false</code> if not + * @see #setVisible(boolean) + * @see #attach() + */ + public boolean isVisible(); + + /** + * Sets the visibility of the component. + * + * <p> + * Visible components are drawn in the user interface, while invisible ones + * are not. The effect is not merely a cosmetic CSS change - no information + * about an invisible component will be sent to the client. The effect is + * thus the same as removing the component from its parent. + * </p> + * + * <pre> + * TextField readonly = new TextField("Read-Only"); + * readonly.setValue("You can't see this!"); + * readonly.setVisible(false); + * layout.addComponent(readonly); + * </pre> + * + * <p> + * A component is visible only if all of its parents are also visible. If a + * component is explicitly set to be invisible, changes in the visibility of + * its parents will not change the visibility of the component. + * </p> + * + * @param visible + * the boolean value specifying if the component should be + * visible after the call or not. + * @see #isVisible() + */ + public void setVisible(boolean visible); + + /** + * Gets the parent component of the component. + * + * <p> + * Components can be nested but a component can have only one parent. A + * component that contains other components, that is, can be a parent, + * should usually inherit the {@link ComponentContainer} interface. + * </p> + * + * @return the parent component + */ + @Override + public HasComponents getParent(); + + /** + * Tests whether the component is in the read-only mode. The user can not + * change the value of a read-only component. As only {@link Field} + * components normally have a value that can be input or changed by the + * user, this is mostly relevant only to field components, though not + * restricted to them. + * + * <p> + * Notice that the read-only mode only affects whether the user can change + * the <i>value</i> of the component; it is possible to, for example, scroll + * a read-only table. + * </p> + * + * <p> + * The method will return {@code true} if the component or any of its + * parents is in the read-only mode. + * </p> + * + * @return <code>true</code> if the component or any of its parents is in + * read-only mode, <code>false</code> if not. + * @see #setReadOnly(boolean) + */ + public boolean isReadOnly(); + + /** + * Sets the read-only mode of the component to the specified mode. The user + * can not change the value of a read-only component. + * + * <p> + * As only {@link Field} components normally have a value that can be input + * or changed by the user, this is mostly relevant only to field components, + * though not restricted to them. + * </p> + * + * <p> + * Notice that the read-only mode only affects whether the user can change + * the <i>value</i> of the component; it is possible to, for example, scroll + * a read-only table. + * </p> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. + * </p> + * + * @param readOnly + * a boolean value specifying whether the component is put + * read-only mode or not + */ + public void setReadOnly(boolean readOnly); + + /** + * Gets the caption of the component. + * + * <p> + * See {@link #setCaption(String)} for a detailed description of the + * caption. + * </p> + * + * @return the caption of the component or {@code null} if the caption is + * not set. + * @see #setCaption(String) + */ + public String getCaption(); + + /** + * Sets the caption of the component. + * + * <p> + * A <i>caption</i> is an explanatory textual label accompanying a user + * interface component, usually shown above, left of, or inside the + * component. <i>Icon</i> (see {@link #setIcon(Resource) setIcon()} is + * closely related to caption and is usually displayed horizontally before + * or after it, depending on the component and the containing layout. + * </p> + * + * <p> + * The caption can usually also be given as the first parameter to a + * constructor, though some components do not support it. + * </p> + * + * <pre> + * RichTextArea area = new RichTextArea(); + * area.setCaption("You can edit stuff here"); + * area.setValue("<h1>Helpful Heading</h1>" + * + "<p>All this is for you to edit.</p>"); + * </pre> + * + * <p> + * The contents of a caption are automatically quoted, so no raw XHTML can + * be rendered in a caption. The validity of the used character encoding, + * usually UTF-8, is not checked. + * </p> + * + * <p> + * The caption of a component is, by default, managed and displayed by the + * layout component or component container in which the component is placed. + * For example, the {@link VerticalLayout} component shows the captions + * left-aligned above the contained components, while the {@link FormLayout} + * component shows the captions on the left side of the vertically laid + * components, with the captions and their associated components + * left-aligned in their own columns. The {@link CustomComponent} does not + * manage the caption of its composition root, so if the root component has + * a caption, it will not be rendered. Some components, such as + * {@link Button} and {@link Panel}, manage the caption themselves and + * display it inside the component. + * </p> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. A + * reimplementation should call the superclass implementation. + * </p> + * + * @param caption + * the new caption for the component. If the caption is + * {@code null}, no caption is shown and it does not normally + * take any space + */ + public void setCaption(String caption); + + /** + * Gets the icon resource of the component. + * + * <p> + * See {@link #setIcon(Resource)} for a detailed description of the icon. + * </p> + * + * @return the icon resource of the component or {@code null} if the + * component has no icon + * @see #setIcon(Resource) + */ + public Resource getIcon(); + + /** + * Sets the icon of the component. + * + * <p> + * An icon is an explanatory graphical label accompanying a user interface + * component, usually shown above, left of, or inside the component. Icon is + * closely related to caption (see {@link #setCaption(String) setCaption()}) + * and is usually displayed horizontally before or after it, depending on + * the component and the containing layout. + * </p> + * + * <p> + * The image is loaded by the browser from a resource, typically a + * {@link com.vaadin.terminal.ThemeResource}. + * </p> + * + * <pre> + * // Component with an icon from a custom theme + * TextField name = new TextField("Name"); + * name.setIcon(new ThemeResource("icons/user.png")); + * layout.addComponent(name); + * + * // Component with an icon from another theme ('runo') + * Button ok = new Button("OK"); + * ok.setIcon(new ThemeResource("../runo/icons/16/ok.png")); + * layout.addComponent(ok); + * </pre> + * + * <p> + * The icon of a component is, by default, managed and displayed by the + * layout component or component container in which the component is placed. + * For example, the {@link VerticalLayout} component shows the icons + * left-aligned above the contained components, while the {@link FormLayout} + * component shows the icons on the left side of the vertically laid + * components, with the icons and their associated components left-aligned + * in their own columns. The {@link CustomComponent} does not manage the + * icon of its composition root, so if the root component has an icon, it + * will not be rendered. + * </p> + * + * <p> + * An icon will be rendered inside an HTML element that has the + * {@code v-icon} CSS style class. The containing layout may enclose an icon + * and a caption inside elements related to the caption, such as + * {@code v-caption} . + * </p> + * + * This method will trigger a {@link RepaintRequestEvent}. + * + * @param icon + * the icon of the component. If null, no icon is shown and it + * does not normally take any space. + * @see #getIcon() + * @see #setCaption(String) + */ + public void setIcon(Resource icon); + + /** + * Gets the Root the component is attached to. + * + * <p> + * If the component is not attached to a Root through a component + * containment hierarchy, <code>null</code> is returned. + * </p> + * + * @return the Root of the component or <code>null</code> if it is not + * attached to a Root + */ + @Override + public Root getRoot(); + + /** + * Gets the application object to which the component is attached. + * + * <p> + * The method will return {@code null} if the component is not currently + * attached to an application. + * </p> + * + * <p> + * Getting a null value is often a problem in constructors of regular + * components and in the initializers of custom composite components. A + * standard workaround is to use {@link Application#getCurrent()} to + * retrieve the application instance that the current request relates to. + * Another way is to move the problematic initialization to + * {@link #attach()}, as described in the documentation of the method. + * </p> + * + * @return the parent application of the component or <code>null</code>. + * @see #attach() + */ + public Application getApplication(); + + /** + * {@inheritDoc} + * + * <p> + * Reimplementing the {@code attach()} method is useful for tasks that need + * to get a reference to the parent, window, or application object with the + * {@link #getParent()}, {@link #getRoot()}, and {@link #getApplication()} + * methods. A component does not yet know these objects in the constructor, + * so in such case, the methods will return {@code null}. For example, the + * following is invalid: + * </p> + * + * <pre> + * public class AttachExample extends CustomComponent { + * public AttachExample() { + * // ERROR: We can't access the application object yet. + * ClassResource r = new ClassResource("smiley.jpg", getApplication()); + * Embedded image = new Embedded("Image:", r); + * setCompositionRoot(image); + * } + * } + * </pre> + * + * <p> + * Adding a component to an application triggers calling the + * {@link #attach()} method for the component. Correspondingly, removing a + * component from a container triggers calling the {@link #detach()} method. + * If the parent of an added component is already connected to the + * application, the {@code attach()} is called immediately from + * {@link #setParent(Component)}. + * </p> + * <p> + * This method must call {@link Root#componentAttached(Component)} to let + * the Root know that a new Component has been attached. + * </p> + * + * + * <pre> + * public class AttachExample extends CustomComponent { + * public AttachExample() { + * } + * + * @Override + * public void attach() { + * super.attach(); // Must call. + * + * // Now we know who ultimately owns us. + * ClassResource r = new ClassResource("smiley.jpg", getApplication()); + * Embedded image = new Embedded("Image:", r); + * setCompositionRoot(image); + * } + * } + * </pre> + */ + @Override + public void attach(); + + /** + * Gets the locale of the component. + * + * <p> + * If a component does not have a locale set, the locale of its parent is + * returned, and so on. Eventually, if no parent has locale set, the locale + * of the application is returned. If the application does not have a locale + * set, it is determined by <code>Locale.getDefault()</code>. + * </p> + * + * <p> + * As the component must be attached before its locale can be acquired, + * using this method in the internationalization of component captions, etc. + * is generally not feasible. For such use case, we recommend using an + * otherwise acquired reference to the application locale. + * </p> + * + * @return Locale of this component or {@code null} if the component and + * none of its parents has a locale set and the component is not yet + * attached to an application. + */ + public Locale getLocale(); + + /** + * Returns the current shared state bean for the component. The state (or + * changes to it) is communicated from the server to the client. + * + * Subclasses can use a more specific return type for this method. + * + * @return The state object for the component + * + * @since 7.0 + */ + @Override + public ComponentState getState(); + + /** + * Called before the shared state is sent to the client. Gives the component + * an opportunity to set computed/dynamic state values e.g. state values + * that depend on other component features. + * <p> + * This method must not alter the component hierarchy in any way. + * </p> + * + * @since 7.0 + */ + public void updateState(); + + /** + * Adds an unique id for component that get's transferred to terminal for + * testing purposes. Keeping identifiers unique is the responsibility of the + * programmer. + * + * @param id + * An alphanumeric id + */ + public void setDebugId(String id); + + /** + * Get's currently set debug identifier + * + * @return current debug id, null if not set + */ + public String getDebugId(); + + /* Component event framework */ + + /** + * Superclass of all component originated events. + * + * <p> + * Events are the basis of all user interaction handling in Vaadin. To + * handle events, you provide a listener object that receives the events of + * the particular event type. + * </p> + * + * <pre> + * Button button = new Button("Click Me!"); + * button.addListener(new Button.ClickListener() { + * public void buttonClick(ClickEvent event) { + * getWindow().showNotification("Thank You!"); + * } + * }); + * layout.addComponent(button); + * </pre> + * + * <p> + * Notice that while each of the event types have their corresponding + * listener types; the listener interfaces are not required to inherit the + * {@code Component.Listener} interface. + * </p> + * + * @see Component.Listener + */ + @SuppressWarnings("serial") + public class Event extends EventObject { + + /** + * Constructs a new event with the specified source component. + * + * @param source + * the source component of the event + */ + public Event(Component source) { + super(source); + } + + /** + * Gets the component where the event occurred. + * + * @return the source component of the event + */ + public Component getComponent() { + return (Component) getSource(); + } + } + + /** + * Listener interface for receiving <code>Component.Event</code>s. + * + * <p> + * Listener interfaces are the basis of all user interaction handling in + * Vaadin. You have or create a listener object that receives the events. + * All event types have their corresponding listener types; they are not, + * however, required to inherit the {@code Component.Listener} interface, + * and they rarely do so. + * </p> + * + * <p> + * This generic listener interface is useful typically when you wish to + * handle events from different component types in a single listener method + * ({@code componentEvent()}. If you handle component events in an anonymous + * listener class, you normally use the component specific listener class, + * such as {@link com.vaadin.ui.Button.ClickEvent}. + * </p> + * + * <pre> + * class Listening extends CustomComponent implements Listener { + * Button ok; // Stored for determining the source of an event + * + * Label status; // For displaying info about the event + * + * public Listening() { + * VerticalLayout layout = new VerticalLayout(); + * + * // Some miscellaneous component + * TextField name = new TextField("Say it all here"); + * name.addListener(this); + * name.setImmediate(true); + * layout.addComponent(name); + * + * // Handle button clicks as generic events instead + * // of Button.ClickEvent events + * ok = new Button("OK"); + * ok.addListener(this); + * layout.addComponent(ok); + * + * // For displaying information about an event + * status = new Label(""); + * layout.addComponent(status); + * + * setCompositionRoot(layout); + * } + * + * public void componentEvent(Event event) { + * // Act according to the source of the event + * if (event.getSource() == ok + * && event.getClass() == Button.ClickEvent.class) + * getWindow().showNotification("Click!"); + * + * // Display source component and event class names + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); + * } + * } + * + * Listening listening = new Listening(); + * layout.addComponent(listening); + * </pre> + * + * @see Component#addListener(Listener) + */ + public interface Listener extends EventListener, Serializable { + + /** + * Notifies the listener of a component event. + * + * <p> + * As the event can typically come from one of many source components, + * you may need to differentiate between the event source by component + * reference, class, etc. + * </p> + * + * <pre> + * public void componentEvent(Event event) { + * // Act according to the source of the event + * if (event.getSource() == ok && event.getClass() == Button.ClickEvent.class) + * getWindow().showNotification("Click!"); + * + * // Display source component and event class names + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); + * } + * </pre> + * + * @param event + * the event that has occured. + */ + public void componentEvent(Component.Event event); + } + + /** + * Registers a new (generic) component event listener for the component. + * + * <pre> + * class Listening extends CustomComponent implements Listener { + * // Stored for determining the source of an event + * Button ok; + * + * Label status; // For displaying info about the event + * + * public Listening() { + * VerticalLayout layout = new VerticalLayout(); + * + * // Some miscellaneous component + * TextField name = new TextField("Say it all here"); + * name.addListener(this); + * name.setImmediate(true); + * layout.addComponent(name); + * + * // Handle button clicks as generic events instead + * // of Button.ClickEvent events + * ok = new Button("OK"); + * ok.addListener(this); + * layout.addComponent(ok); + * + * // For displaying information about an event + * status = new Label(""); + * layout.addComponent(status); + * + * setCompositionRoot(layout); + * } + * + * public void componentEvent(Event event) { + * // Act according to the source of the event + * if (event.getSource() == ok) + * getWindow().showNotification("Click!"); + * + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); + * } + * } + * + * Listening listening = new Listening(); + * layout.addComponent(listening); + * </pre> + * + * @param listener + * the new Listener to be registered. + * @see Component.Event + * @see #removeListener(Listener) + */ + public void addListener(Component.Listener listener); + + /** + * Removes a previously registered component event listener from this + * component. + * + * @param listener + * the listener to be removed. + * @see #addListener(Listener) + */ + public void removeListener(Component.Listener listener); + + /** + * Class of all component originated error events. + * + * <p> + * The component error event is normally fired by + * {@link AbstractComponent#setComponentError(ErrorMessage)}. The component + * errors are set by the framework in some situations and can be set by user + * code. They are indicated in a component with an error indicator. + * </p> + */ + @SuppressWarnings("serial") + public class ErrorEvent extends Event { + + private final ErrorMessage message; + + /** + * Constructs a new event with a specified source component. + * + * @param message + * the error message. + * @param component + * the source component. + */ + public ErrorEvent(ErrorMessage message, Component component) { + super(component); + this.message = message; + } + + /** + * Gets the error message. + * + * @return the error message. + */ + public ErrorMessage getErrorMessage() { + return message; + } + } + + /** + * Listener interface for receiving <code>Component.Errors</code>s. + */ + public interface ErrorListener extends EventListener, Serializable { + + /** + * Notifies the listener of a component error. + * + * @param event + * the event that has occured. + */ + public void componentError(Component.ErrorEvent event); + } + + /** + * A sub-interface implemented by components that can obtain input focus. + * This includes all {@link Field} components as well as some other + * components, such as {@link Upload}. + * + * <p> + * Focus can be set with {@link #focus()}. This interface does not provide + * an accessor that would allow finding out the currently focused component; + * focus information can be acquired for some (but not all) {@link Field} + * components through the {@link com.vaadin.event.FieldEvents.FocusListener} + * and {@link com.vaadin.event.FieldEvents.BlurListener} interfaces. + * </p> + * + * @see FieldEvents + */ + public interface Focusable extends Component { + + /** + * Sets the focus to this component. + * + * <pre> + * Form loginBox = new Form(); + * loginBox.setCaption("Login"); + * layout.addComponent(loginBox); + * + * // Create the first field which will be focused + * TextField username = new TextField("User name"); + * loginBox.addField("username", username); + * + * // Set focus to the user name + * username.focus(); + * + * TextField password = new TextField("Password"); + * loginBox.addField("password", password); + * + * Button login = new Button("Login"); + * loginBox.getFooter().addComponent(login); + * </pre> + * + * <p> + * Notice that this interface does not provide an accessor that would + * allow finding out the currently focused component. Focus information + * can be acquired for some (but not all) {@link Field} components + * through the {@link com.vaadin.event.FieldEvents.FocusListener} and + * {@link com.vaadin.event.FieldEvents.BlurListener} interfaces. + * </p> + * + * @see com.vaadin.event.FieldEvents + * @see com.vaadin.event.FieldEvents.FocusEvent + * @see com.vaadin.event.FieldEvents.FocusListener + * @see com.vaadin.event.FieldEvents.BlurEvent + * @see com.vaadin.event.FieldEvents.BlurListener + */ + public void focus(); + + /** + * Gets the <i>tabulator index</i> of the {@code Focusable} component. + * + * @return tab index set for the {@code Focusable} component + * @see #setTabIndex(int) + */ + public int getTabIndex(); + + /** + * Sets the <i>tabulator index</i> of the {@code Focusable} component. + * The tab index property is used to specify the order in which the + * fields are focused when the user presses the Tab key. Components with + * a defined tab index are focused sequentially first, and then the + * components with no tab index. + * + * <pre> + * Form loginBox = new Form(); + * loginBox.setCaption("Login"); + * layout.addComponent(loginBox); + * + * // Create the first field which will be focused + * TextField username = new TextField("User name"); + * loginBox.addField("username", username); + * + * // Set focus to the user name + * username.focus(); + * + * TextField password = new TextField("Password"); + * loginBox.addField("password", password); + * + * Button login = new Button("Login"); + * loginBox.getFooter().addComponent(login); + * + * // An additional component which natural focus order would + * // be after the button. + * CheckBox remember = new CheckBox("Remember me"); + * loginBox.getFooter().addComponent(remember); + * + * username.setTabIndex(1); + * password.setTabIndex(2); + * remember.setTabIndex(3); // Different than natural place + * login.setTabIndex(4); + * </pre> + * + * <p> + * After all focusable user interface components are done, the browser + * can begin again from the component with the smallest tab index, or it + * can take the focus out of the page, for example, to the location bar. + * </p> + * + * <p> + * If the tab index is not set (is set to zero), the default tab order + * is used. The order is somewhat browser-dependent, but generally + * follows the HTML structure of the page. + * </p> + * + * <p> + * A negative value means that the component is completely removed from + * the tabulation order and can not be reached by pressing the Tab key + * at all. + * </p> + * + * @param tabIndex + * the tab order of this component. Indexes usually start + * from 1. Zero means that default tab order should be used. + * A negative value means that the field should not be + * included in the tabbing sequence. + * @see #getTabIndex() + */ + public void setTabIndex(int tabIndex); + + } + +} diff --git a/server/src/com/vaadin/ui/ComponentContainer.java b/server/src/com/vaadin/ui/ComponentContainer.java new file mode 100644 index 0000000000..8182d54b56 --- /dev/null +++ b/server/src/com/vaadin/ui/ComponentContainer.java @@ -0,0 +1,222 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +/** + * Extension to the {@link Component} interface which adds to it the capacity to + * contain other components. All UI elements that can have child elements + * implement this interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ComponentContainer extends HasComponents { + + /** + * Adds the component into this container. + * + * @param c + * the component to be added. + */ + public void addComponent(Component c); + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + public void removeComponent(Component c); + + /** + * Removes all components from this container. + */ + public void removeAllComponents(); + + /** + * Replaces the component in the container with another one without changing + * position. + * + * <p> + * This method replaces component with another one is such way that the new + * component overtakes the position of the old component. If the old + * component is not in the container, the new component is added to the + * container. If the both component are already in the container, their + * positions are swapped. Component attach and detach events should be taken + * care as with add and remove. + * </p> + * + * @param oldComponent + * the old component that will be replaced. + * @param newComponent + * the new component to be replaced. + */ + public void replaceComponent(Component oldComponent, Component newComponent); + + /** + * Gets the number of children this {@link ComponentContainer} has. This + * must be symmetric with what {@link #getComponentIterator()} returns. + * + * @return The number of child components this container has. + * @since 7.0.0 + */ + public int getComponentCount(); + + /** + * Moves all components from an another container into this container. The + * components are removed from <code>source</code>. + * + * @param source + * the container which contains the components that are to be + * moved to this container. + */ + public void moveComponentsFrom(ComponentContainer source); + + /** + * Listens the component attach events. + * + * @param listener + * the listener to add. + */ + public void addListener(ComponentAttachListener listener); + + /** + * Stops the listening component attach events. + * + * @param listener + * the listener to removed. + */ + public void removeListener(ComponentAttachListener listener); + + /** + * Listens the component detach events. + */ + public void addListener(ComponentDetachListener listener); + + /** + * Stops the listening component detach events. + */ + public void removeListener(ComponentDetachListener listener); + + /** + * Component attach listener interface. + */ + public interface ComponentAttachListener extends Serializable { + + /** + * A new component is attached to container. + * + * @param event + * the component attach event. + */ + public void componentAttachedToContainer(ComponentAttachEvent event); + } + + /** + * Component detach listener interface. + */ + public interface ComponentDetachListener extends Serializable { + + /** + * A component has been detached from container. + * + * @param event + * the component detach event. + */ + public void componentDetachedFromContainer(ComponentDetachEvent event); + } + + /** + * Component attach event sent when a component is attached to container. + */ + @SuppressWarnings("serial") + public class ComponentAttachEvent extends Component.Event { + + private final Component component; + + /** + * Creates a new attach event. + * + * @param container + * the component container the component has been detached + * to. + * @param attachedComponent + * the component that has been attached. + */ + public ComponentAttachEvent(ComponentContainer container, + Component attachedComponent) { + super(container); + component = attachedComponent; + } + + /** + * Gets the component container. + * + * @param the + * component container. + */ + public ComponentContainer getContainer() { + return (ComponentContainer) getSource(); + } + + /** + * Gets the attached component. + * + * @param the + * attach component. + */ + public Component getAttachedComponent() { + return component; + } + } + + /** + * Component detach event sent when a component is detached from container. + */ + @SuppressWarnings("serial") + public class ComponentDetachEvent extends Component.Event { + + private final Component component; + + /** + * Creates a new detach event. + * + * @param container + * the component container the component has been detached + * from. + * @param detachedComponent + * the component that has been detached. + */ + public ComponentDetachEvent(ComponentContainer container, + Component detachedComponent) { + super(container); + component = detachedComponent; + } + + /** + * Gets the component container. + * + * @param the + * component container. + */ + public ComponentContainer getContainer() { + return (ComponentContainer) getSource(); + } + + /** + * Gets the detached component. + * + * @return the detached component. + */ + public Component getDetachedComponent() { + return component; + } + } + +} diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java new file mode 100644 index 0000000000..e3d1bf86db --- /dev/null +++ b/server/src/com/vaadin/ui/ConnectorTracker.java @@ -0,0 +1,320 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * A class which takes care of book keeping of {@link ClientConnector}s for a + * Root. + * <p> + * Provides {@link #getConnector(String)} which can be used to lookup a + * connector from its id. This is for framework use only and should not be + * needed in applications. + * </p> + * <p> + * Tracks which {@link ClientConnector}s are dirty so they can be updated to the + * client when the following response is sent. A connector is dirty when an + * operation has been performed on it on the server and as a result of this + * operation new information needs to be sent to its {@link ServerConnector}. + * </p> + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public class ConnectorTracker implements Serializable { + + private final HashMap<String, ClientConnector> connectorIdToConnector = new HashMap<String, ClientConnector>(); + private Set<ClientConnector> dirtyConnectors = new HashSet<ClientConnector>(); + + private Root root; + + /** + * Gets a logger for this class + * + * @return A logger instance for logging within this class + * + */ + public static Logger getLogger() { + return Logger.getLogger(ConnectorTracker.class.getName()); + } + + /** + * Creates a new ConnectorTracker for the given root. A tracker is always + * attached to a root and the root cannot be changed during the lifetime of + * a {@link ConnectorTracker}. + * + * @param root + * The root to attach to. Cannot be null. + */ + public ConnectorTracker(Root root) { + this.root = root; + } + + /** + * Register the given connector. + * <p> + * The lookup method {@link #getConnector(String)} only returns registered + * connectors. + * </p> + * + * @param connector + * The connector to register. + */ + public void registerConnector(ClientConnector connector) { + String connectorId = connector.getConnectorId(); + ClientConnector previouslyRegistered = connectorIdToConnector + .get(connectorId); + if (previouslyRegistered == null) { + connectorIdToConnector.put(connectorId, connector); + getLogger().fine( + "Registered " + connector.getClass().getSimpleName() + " (" + + connectorId + ")"); + } else if (previouslyRegistered != connector) { + throw new RuntimeException("A connector with id " + connectorId + + " is already registered!"); + } else { + getLogger().warning( + "An already registered connector was registered again: " + + connector.getClass().getSimpleName() + " (" + + connectorId + ")"); + } + + } + + /** + * Unregister the given connector. + * + * <p> + * The lookup method {@link #getConnector(String)} only returns registered + * connectors. + * </p> + * + * @param connector + * The connector to unregister + */ + public void unregisterConnector(ClientConnector connector) { + String connectorId = connector.getConnectorId(); + if (!connectorIdToConnector.containsKey(connectorId)) { + getLogger().warning( + "Tried to unregister " + + connector.getClass().getSimpleName() + " (" + + connectorId + ") which is not registered"); + return; + } + if (connectorIdToConnector.get(connectorId) != connector) { + throw new RuntimeException("The given connector with id " + + connectorId + + " is not the one that was registered for that id"); + } + + getLogger().fine( + "Unregistered " + connector.getClass().getSimpleName() + " (" + + connectorId + ")"); + connectorIdToConnector.remove(connectorId); + } + + /** + * Gets a connector by its id. + * + * @param connectorId + * The connector id to look for + * @return The connector with the given id or null if no connector has the + * given id + */ + public ClientConnector getConnector(String connectorId) { + return connectorIdToConnector.get(connectorId); + } + + /** + * Cleans the connector map from all connectors that are no longer attached + * to the application. This should only be called by the framework. + */ + public void cleanConnectorMap() { + // remove detached components from paintableIdMap so they + // can be GC'ed + Iterator<String> iterator = connectorIdToConnector.keySet().iterator(); + + while (iterator.hasNext()) { + String connectorId = iterator.next(); + ClientConnector connector = connectorIdToConnector.get(connectorId); + if (getRootForConnector(connector) != root) { + // If connector is no longer part of this root, + // remove it from the map. If it is re-attached to the + // application at some point it will be re-added through + // registerConnector(connector) + + // This code should never be called as cleanup should take place + // in detach() + getLogger() + .warning( + "cleanConnectorMap unregistered connector " + + getConnectorAndParentInfo(connector) + + "). This should have been done when the connector was detached."); + iterator.remove(); + } + } + + } + + /** + * Finds the root that the connector is attached to. + * + * @param connector + * The connector to lookup + * @return The root the connector is attached to or null if it is not + * attached to any root. + */ + private Root getRootForConnector(ClientConnector connector) { + if (connector == null) { + return null; + } + if (connector instanceof Component) { + return ((Component) connector).getRoot(); + } + + return getRootForConnector(connector.getParent()); + } + + /** + * Mark the connector as dirty. + * + * @see #getDirtyConnectors() + * + * @param connector + * The connector that should be marked clean. + */ + public void markDirty(ClientConnector connector) { + if (getLogger().isLoggable(Level.FINE)) { + if (!dirtyConnectors.contains(connector)) { + getLogger().fine( + getConnectorAndParentInfo(connector) + " " + + "is now dirty"); + } + } + + dirtyConnectors.add(connector); + } + + /** + * Mark the connector as clean. + * + * @param connector + * The connector that should be marked clean. + */ + public void markClean(ClientConnector connector) { + if (getLogger().isLoggable(Level.FINE)) { + if (dirtyConnectors.contains(connector)) { + getLogger().fine( + getConnectorAndParentInfo(connector) + " " + + "is no longer dirty"); + } + } + + dirtyConnectors.remove(connector); + } + + /** + * Returns {@link #getConnectorString(ClientConnector)} for the connector + * and its parent (if it has a parent). + * + * @param connector + * The connector + * @return A string describing the connector and its parent + */ + private String getConnectorAndParentInfo(ClientConnector connector) { + String message = getConnectorString(connector); + if (connector.getParent() != null) { + message += " (parent: " + getConnectorString(connector.getParent()) + + ")"; + } + return message; + } + + /** + * Returns a string with the connector name and id. Useful mostly for + * debugging and logging. + * + * @param connector + * The connector + * @return A string that describes the connector + */ + private String getConnectorString(ClientConnector connector) { + if (connector == null) { + return "(null)"; + } + + String connectorId; + try { + connectorId = connector.getConnectorId(); + } catch (RuntimeException e) { + // This happens if the connector is not attached to the application. + // SHOULD not happen in this case but theoretically can. + connectorId = "@" + Integer.toHexString(connector.hashCode()); + } + return connector.getClass().getName() + "(" + connectorId + ")"; + } + + /** + * Mark all connectors in this root as dirty. + */ + public void markAllConnectorsDirty() { + markConnectorsDirtyRecursively(root); + getLogger().fine("All connectors are now dirty"); + } + + /** + * Mark all connectors in this root as clean. + */ + public void markAllConnectorsClean() { + dirtyConnectors.clear(); + getLogger().fine("All connectors are now clean"); + } + + /** + * Marks all visible connectors dirty, starting from the given connector and + * going downwards in the hierarchy. + * + * @param c + * The component to start iterating downwards from + */ + private void markConnectorsDirtyRecursively(ClientConnector c) { + if (c instanceof Component && !((Component) c).isVisible()) { + return; + } + markDirty(c); + for (ClientConnector child : AbstractClientConnector + .getAllChildrenIterable(c)) { + markConnectorsDirtyRecursively(child); + } + } + + /** + * Returns a collection of all connectors which have been marked as dirty. + * <p> + * The state and pending RPC calls for dirty connectors are sent to the + * client in the following request. + * </p> + * + * @return A collection of all dirty connectors for this root. This list may + * contain invisible connectors. + */ + public Collection<ClientConnector> getDirtyConnectors() { + return dirtyConnectors; + } + +} diff --git a/server/src/com/vaadin/ui/CssLayout.java b/server/src/com/vaadin/ui/CssLayout.java new file mode 100644 index 0000000000..356f0a3843 --- /dev/null +++ b/server/src/com/vaadin/ui/CssLayout.java @@ -0,0 +1,308 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.csslayout.CssLayoutServerRpc; +import com.vaadin.shared.ui.csslayout.CssLayoutState; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +/** + * CssLayout is a layout component that can be used in browser environment only. + * It simply renders components and their captions into a same div element. + * Component layout can then be adjusted with css. + * <p> + * In comparison to {@link HorizontalLayout} and {@link VerticalLayout} + * <ul> + * <li>rather similar server side api + * <li>no spacing, alignment or expand ratios + * <li>much simpler DOM that can be styled by skilled web developer + * <li>no abstraction of browser differences (developer must ensure that the + * result works properly on each browser) + * <li>different kind of handling for relative sizes (that are set from server + * side) (*) + * <li>noticeably faster rendering time in some situations as we rely more on + * the browser's rendering engine. + * </ul> + * <p> + * With {@link CustomLayout} one can often achieve similar results (good looking + * layouts with web technologies), but with CustomLayout developer needs to work + * with fixed templates. + * <p> + * By extending CssLayout one can also inject some css rules straight to child + * components using {@link #getCss(Component)}. + * + * <p> + * (*) Relative sizes (set from server side) are treated bit differently than in + * other layouts in Vaadin. In cssLayout the size is calculated relatively to + * CSS layouts content area which is pretty much as in html and css. In other + * layouts the size of component is calculated relatively to the "slot" given by + * layout. + * <p> + * Also note that client side framework in Vaadin modifies inline style + * properties width and height. This happens on each update to component. If one + * wants to set component sizes with CSS, component must have undefined size on + * server side (which is not the default for all components) and the size must + * be defined with class styles - not by directly injecting width and height. + * + * @since 6.1 brought in from "FastLayouts" incubator project + * + */ +public class CssLayout extends AbstractLayout implements LayoutClickNotifier { + + private CssLayoutServerRpc rpc = new CssLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(CssLayout.this, + mouseDetails, clickedConnector)); + } + }; + /** + * Custom layout slots containing the components. + */ + protected LinkedList<Component> components = new LinkedList<Component>(); + + public CssLayout() { + registerRpc(rpc); + } + + /** + * Add a component into this container. The component is added to the right + * or under the previous component. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + // Add to components before calling super.addComponent + // so that it is available to AttachListeners + components.add(c); + try { + super.addComponent(c); + requestRepaint(); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + } + + /** + * Adds a component into this container. The component is added to the left + * or on top of the other components. + * + * @param c + * the component to be added. + */ + public void addComponentAsFirst(Component c) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + removeComponent(c); + } + components.addFirst(c); + try { + super.addComponent(c); + requestRepaint(); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + } + + /** + * Adds a component into indexed position in this container. + * + * @param c + * the component to be added. + * @param index + * the index of the component position. The components currently + * in and after the position are shifted forwards. + */ + public void addComponent(Component c, int index) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + // When c is removed, all components after it are shifted down + if (index > getComponentIndex(c)) { + index--; + } + removeComponent(c); + } + components.add(index, c); + try { + super.addComponent(c); + requestRepaint(); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + components.remove(c); + super.removeComponent(c); + requestRepaint(); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + @Override + public Iterator<Component> getComponentIterator() { + return components.iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + @Override + public void updateState() { + super.updateState(); + getState().getChildCss().clear(); + for (Iterator<Component> ci = getComponentIterator(); ci.hasNext();) { + Component child = ci.next(); + String componentCssString = getCss(child); + if (componentCssString != null) { + getState().getChildCss().put(child, componentCssString); + } + + } + } + + @Override + public CssLayoutState getState() { + return (CssLayoutState) super.getState(); + } + + /** + * Returns styles to be applied to given component. Override this method to + * inject custom style rules to components. + * + * <p> + * Note that styles are injected over previous styles before actual child + * rendering. Previous styles are not cleared, but overridden. + * + * <p> + * Note that one most often achieves better code style, by separating + * styling to theme (with custom theme and {@link #addStyleName(String)}. + * With own custom styles it is also very easy to break browser + * compatibility. + * + * @param c + * the component + * @return css rules to be applied to component + */ + protected String getCss(Component c) { + return null; + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component component = i.next(); + + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + + location++; + } + + if (oldLocation == -1) { + addComponent(newComponent); + } else if (newLocation == -1) { + removeComponent(oldComponent); + addComponent(newComponent, oldLocation); + } else { + if (oldLocation > newLocation) { + components.remove(oldComponent); + components.add(newLocation, oldComponent); + components.remove(newComponent); + components.add(oldLocation, newComponent); + } else { + components.remove(newComponent); + components.add(oldLocation, newComponent); + components.remove(oldComponent); + components.add(newLocation, oldComponent); + } + + requestRepaint(); + } + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + + /** + * Returns the index of the given component. + * + * @param component + * The component to look up. + * @return The index of the component or -1 if the component is not a child. + */ + public int getComponentIndex(Component component) { + return components.indexOf(component); + } + + /** + * Returns the component at the given position. + * + * @param index + * The position of the component. + * @return The component at the given index. + * @throws IndexOutOfBoundsException + * If the index is out of range. + */ + public Component getComponent(int index) throws IndexOutOfBoundsException { + return components.get(index); + } + +} diff --git a/server/src/com/vaadin/ui/CustomComponent.java b/server/src/com/vaadin/ui/CustomComponent.java new file mode 100644 index 0000000000..40b5dcd636 --- /dev/null +++ b/server/src/com/vaadin/ui/CustomComponent.java @@ -0,0 +1,189 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * Custom component provides simple implementation of Component interface for + * creation of new UI components by composition of existing components. + * <p> + * The component is used by inheriting the CustomComponent class and setting + * composite root inside the Custom component. The composite root itself can + * contain more components, but their interfaces are hidden from the users. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CustomComponent extends AbstractComponentContainer { + + /** + * The root component implementing the custom component. + */ + private Component root = null; + + /** + * Constructs a new custom component. + * + * <p> + * The component is implemented by wrapping the methods of the composition + * root component given as parameter. The composition root must be set + * before the component can be used. + * </p> + */ + public CustomComponent() { + // expand horizontally by default + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Constructs a new custom component. + * + * <p> + * The component is implemented by wrapping the methods of the composition + * root component given as parameter. The composition root must not be null + * and can not be changed after the composition. + * </p> + * + * @param compositionRoot + * the root of the composition component tree. + */ + public CustomComponent(Component compositionRoot) { + this(); + setCompositionRoot(compositionRoot); + } + + /** + * Returns the composition root. + * + * @return the Component Composition root. + */ + protected Component getCompositionRoot() { + return root; + } + + /** + * Sets the compositions root. + * <p> + * The composition root must be set to non-null value before the component + * can be used. The composition root can only be set once. + * </p> + * + * @param compositionRoot + * the root of the composition component tree. + */ + protected void setCompositionRoot(Component compositionRoot) { + if (compositionRoot != root) { + if (root != null) { + // remove old component + super.removeComponent(root); + } + if (compositionRoot != null) { + // set new component + super.addComponent(compositionRoot); + } + root = compositionRoot; + requestRepaint(); + } + } + + /* Basic component features ------------------------------------------ */ + + private class ComponentIterator implements Iterator<Component>, + Serializable { + boolean first = getCompositionRoot() != null; + + @Override + public boolean hasNext() { + return first; + } + + @Override + public Component next() { + first = false; + return root; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components (zero or one) + */ + @Override + public int getComponentCount() { + return (root != null ? 1 : 0); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component, + * com.vaadin.ui.Component) + */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. Use + * {@link CustomComponent#setCompositionRoot(Component)} to set + * CustomComponents "child". + * + * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component) + */ + @Override + public void addComponent(Component c) { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.AbstractComponentContainer#moveComponentsFrom(com.vaadin.ui.ComponentContainer) + */ + @Override + public void moveComponentsFrom(ComponentContainer source) { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.AbstractComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component) + */ + @Override + public void removeComponent(Component c) { + throw new UnsupportedOperationException(); + } + +} diff --git a/server/src/com/vaadin/ui/CustomField.java b/server/src/com/vaadin/ui/CustomField.java new file mode 100644 index 0000000000..ab3797a58c --- /dev/null +++ b/server/src/com/vaadin/ui/CustomField.java @@ -0,0 +1,237 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; + +import com.vaadin.data.Property; + +/** + * A {@link Field} whose UI content can be constructed by the user, enabling the + * creation of e.g. form fields by composing Vaadin components. Customization of + * both the visual presentation and the logic of the field is possible. + * + * Subclasses must implement {@link #getType()} and {@link #initContent()}. + * + * Most custom fields can simply compose a user interface that calls the methods + * {@link #setInternalValue(Object)} and {@link #getInternalValue()} when + * necessary. + * + * It is also possible to override {@link #validate()}, + * {@link #setInternalValue(Object)}, {@link #commit()}, + * {@link #setPropertyDataSource(Property)}, {@link #isEmpty()} and other logic + * of the field. Methods overriding {@link #setInternalValue(Object)} should + * also call the corresponding superclass method. + * + * @param <T> + * field value type + * + * @since 7.0 + */ +public abstract class CustomField<T> extends AbstractField<T> implements + ComponentContainer { + + /** + * The root component implementing the custom component. + */ + private Component root = null; + + /** + * Constructs a new custom field. + * + * <p> + * The component is implemented by wrapping the methods of the composition + * root component given as parameter. The composition root must be set + * before the component can be used. + * </p> + */ + public CustomField() { + // expand horizontally by default + setWidth(100, Unit.PERCENTAGE); + } + + /** + * Constructs the content and notifies it that the {@link CustomField} is + * attached to a window. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + // First call super attach to notify all children (none if content has + // not yet been created) + super.attach(); + + // If the content has not yet been created, we create and attach it at + // this point. + if (root == null) { + // Ensure content is created and its parent is set. + // The getContent() call creates the content and attaches the + // content + fireComponentAttachEvent(getContent()); + } + } + + /** + * Returns the content (UI) of the custom component. + * + * @return Component + */ + protected Component getContent() { + if (null == root) { + root = initContent(); + root.setParent(this); + } + return root; + } + + /** + * Create the content component or layout for the field. Subclasses of + * {@link CustomField} should implement this method. + * + * Note that this method is called when the CustomField is attached to a + * layout or when {@link #getContent()} is called explicitly for the first + * time. It is only called once for a {@link CustomField}. + * + * @return {@link Component} representing the UI of the CustomField + */ + protected abstract Component initContent(); + + // Size related methods + // TODO might not be necessary to override but following the pattern from + // AbstractComponentContainer + + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + requestRepaintAll(); + } + + @Override + public void setWidth(float height, Unit unit) { + super.setWidth(height, unit); + requestRepaintAll(); + } + + // ComponentContainer methods + + private class ComponentIterator implements Iterator<Component>, + Serializable { + boolean first = (root != null); + + @Override + public boolean hasNext() { + return first; + } + + @Override + public Component next() { + first = false; + return getContent(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + @Override + public int getComponentCount() { + return (null != getContent()) ? 1 : 0; + } + + /** + * Fires the component attached event. This should be called by the + * addComponent methods after the component have been added to this + * container. + * + * @param component + * the component that has been added to this container. + */ + protected void fireComponentAttachEvent(Component component) { + fireEvent(new ComponentAttachEvent(this, component)); + } + + // TODO remove these methods when ComponentContainer interface is cleaned up + + @Override + public void addComponent(Component c) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeComponent(Component c) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAllComponents() { + throw new UnsupportedOperationException(); + } + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + throw new UnsupportedOperationException(); + } + + @Override + public void moveComponentsFrom(ComponentContainer source) { + throw new UnsupportedOperationException(); + } + + private static final Method COMPONENT_ATTACHED_METHOD; + + static { + try { + COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class + .getDeclaredMethod("componentAttachedToContainer", + new Class[] { ComponentAttachEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in CustomField"); + } + } + + @Override + public void addListener(ComponentAttachListener listener) { + addListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + @Override + public void removeListener(ComponentAttachListener listener) { + removeListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + @Override + public void addListener(ComponentDetachListener listener) { + // content never detached + } + + @Override + public void removeListener(ComponentDetachListener listener) { + // content never detached + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } +} diff --git a/server/src/com/vaadin/ui/CustomLayout.java b/server/src/com/vaadin/ui/CustomLayout.java new file mode 100644 index 0000000000..d7830603f0 --- /dev/null +++ b/server/src/com/vaadin/ui/CustomLayout.java @@ -0,0 +1,329 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.vaadin.shared.ui.customlayout.CustomLayoutState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.server.JsonPaintTarget; + +/** + * <p> + * A container component with freely designed layout and style. The layout + * consists of items with textually represented locations. Each item contains + * one sub-component, which can be any Vaadin component, such as a layout. The + * adapter and theme are responsible for rendering the layout with a given style + * by placing the items in the defined locations. + * </p> + * + * <p> + * The placement of the locations is not fixed - different themes can define the + * locations in a way that is suitable for them. One typical example would be to + * create visual design for a web site as a custom layout: the visual design + * would define locations for "menu", "body", and "title", for example. The + * layout would then be implemented as an XHTML template for each theme. + * </p> + * + * <p> + * The default theme handles the styles that are not defined by drawing the + * subcomponents just as in OrderedLayout. + * </p> + * + * @author Vaadin Ltd. + * @author Duy B. Vo (<a + * href="mailto:devduy@gmail.com?subject=Vaadin">devduy@gmail.com</a>) + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CustomLayout extends AbstractLayout implements Vaadin6Component { + + private static final int BUFFER_SIZE = 10000; + + /** + * Custom layout slots containing the components. + */ + private final HashMap<String, Component> slots = new HashMap<String, Component>(); + + /** + * Default constructor only used by subclasses. Subclasses are responsible + * for setting the appropriate fields. Either + * {@link #setTemplateName(String)}, that makes layout fetch the template + * from theme, or {@link #setTemplateContents(String)}. + */ + protected CustomLayout() { + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Constructs a custom layout with the template given in the stream. + * + * @param templateStream + * Stream containing template data. Must be using UTF-8 encoding. + * To use a String as a template use for instance new + * ByteArrayInputStream("<template>".getBytes()). + * @param streamLength + * Length of the templateStream + * @throws IOException + */ + public CustomLayout(InputStream templateStream) throws IOException { + this(); + initTemplateContentsFromInputStream(templateStream); + } + + /** + * Constructor for custom layout with given template name. Template file is + * fetched from "<theme>/layout/<templateName>". + */ + public CustomLayout(String template) { + this(); + setTemplateName(template); + } + + protected void initTemplateContentsFromInputStream( + InputStream templateStream) throws IOException { + InputStreamReader reader = new InputStreamReader(templateStream, + "UTF-8"); + StringBuilder b = new StringBuilder(BUFFER_SIZE); + + char[] cbuf = new char[BUFFER_SIZE]; + int offset = 0; + + while (true) { + int nrRead = reader.read(cbuf, offset, BUFFER_SIZE); + b.append(cbuf, 0, nrRead); + if (nrRead < BUFFER_SIZE) { + break; + } + } + + setTemplateContents(b.toString()); + } + + @Override + public CustomLayoutState getState() { + return (CustomLayoutState) super.getState(); + } + + /** + * Adds the component into this container to given location. If the location + * is already populated, the old component is removed. + * + * @param c + * the component to be added. + * @param location + * the location of the component. + */ + public void addComponent(Component c, String location) { + final Component old = slots.get(location); + if (old != null) { + removeComponent(old); + } + slots.put(location, c); + getState().getChildLocations().put(c, location); + c.setParent(this); + fireComponentAttachEvent(c); + requestRepaint(); + } + + /** + * Adds the component into this container. The component is added without + * specifying the location (empty string is then used as location). Only one + * component can be added to the default "" location and adding more + * components into that location overwrites the old components. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + this.addComponent(c, ""); + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + if (c == null) { + return; + } + slots.values().remove(c); + getState().getChildLocations().remove(c); + super.removeComponent(c); + requestRepaint(); + } + + /** + * Removes the component from this container from given location. + * + * @param location + * the Location identifier of the component. + */ + public void removeComponent(String location) { + this.removeComponent(slots.get(location)); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + @Override + public Iterator<Component> getComponentIterator() { + return slots.values().iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return slots.values().size(); + } + + /** + * Gets the child-component by its location. + * + * @param location + * the name of the location where the requested component + * resides. + * @return the Component in the given location or null if not found. + */ + public Component getComponent(String location) { + return slots.get(location); + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + String oldLocation = null; + String newLocation = null; + for (final Iterator<String> i = slots.keySet().iterator(); i.hasNext();) { + final String location = i.next(); + final Component component = slots.get(location); + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + } + + if (oldLocation == null) { + addComponent(newComponent); + } else if (newLocation == null) { + removeComponent(oldLocation); + addComponent(newComponent, oldLocation); + } else { + slots.put(newLocation, oldComponent); + slots.put(oldLocation, newComponent); + getState().getChildLocations().put(newComponent, oldLocation); + getState().getChildLocations().put(oldComponent, newLocation); + requestRepaint(); + } + } + + /** Get the name of the template */ + public String getTemplateName() { + return getState().getTemplateName(); + } + + /** Get the contents of the template */ + public String getTemplateContents() { + return getState().getTemplateContents(); + } + + /** + * Set the name of the template used to draw custom layout. + * + * With GWT-adapter, the template with name 'templatename' is loaded from + * VAADIN/themes/themename/layouts/templatename.html. If the theme has not + * been set (with Application.setTheme()), themename is 'default'. + * + * @param templateName + */ + public void setTemplateName(String templateName) { + getState().setTemplateName(templateName); + getState().setTemplateContents(null); + requestRepaint(); + } + + /** + * Set the contents of the template used to draw the custom layout. + * + * @param templateContents + */ + public void setTemplateContents(String templateContents) { + getState().setTemplateContents(templateContents); + getState().setTemplateName(null); + requestRepaint(); + } + + /** + * Although most layouts support margins, CustomLayout does not. The + * behaviour of this layout is determined almost completely by the actual + * template. + * + * @throws UnsupportedOperationException + */ + @Override + public void setMargin(boolean enabled) { + throw new UnsupportedOperationException( + "CustomLayout does not support margins."); + } + + /** + * Although most layouts support margins, CustomLayout does not. The + * behaviour of this layout is determined almost completely by the actual + * template. + * + * @throws UnsupportedOperationException + */ + @Override + public void setMargin(boolean topEnabled, boolean rightEnabled, + boolean bottomEnabled, boolean leftEnabled) { + throw new UnsupportedOperationException( + "CustomLayout does not support margins."); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Nothing to see here + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + // Workaround to make the CommunicationManager read the template file + // and send it to the client + String templateName = getState().getTemplateName(); + if (templateName != null && templateName.length() != 0) { + Set<Object> usedResources = ((JsonPaintTarget) target) + .getUsedResources(); + String resourceName = "layouts/" + templateName + ".html"; + usedResources.add(resourceName); + } + } + +} diff --git a/server/src/com/vaadin/ui/DateField.java b/server/src/com/vaadin/ui/DateField.java new file mode 100644 index 0000000000..d0a22f3c29 --- /dev/null +++ b/server/src/com/vaadin/ui/DateField.java @@ -0,0 +1,869 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import com.vaadin.data.Property; +import com.vaadin.data.Validator; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.event.FieldEvents; +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.datefield.VDateField; + +/** + * <p> + * A date editor component that can be bound to any {@link Property} that is + * compatible with <code>java.util.Date</code>. + * </p> + * <p> + * Since <code>DateField</code> extends <code>AbstractField</code> it implements + * the {@link com.vaadin.data.Buffered}interface. + * </p> + * <p> + * A <code>DateField</code> is in write-through mode by default, so + * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)}must be called to + * enable buffering. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class DateField extends AbstractField<Date> implements + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Vaadin6Component { + + /** + * Resolutions for DateFields + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ + public enum Resolution { + SECOND(Calendar.SECOND), MINUTE(Calendar.MINUTE), HOUR( + Calendar.HOUR_OF_DAY), DAY(Calendar.DAY_OF_MONTH), MONTH( + Calendar.MONTH), YEAR(Calendar.YEAR); + + private int calendarField; + + private Resolution(int calendarField) { + this.calendarField = calendarField; + } + + /** + * Returns the field in {@link Calendar} that corresponds to this + * resolution. + * + * @return one of the field numbers used by Calendar + */ + public int getCalendarField() { + return calendarField; + } + + /** + * Returns the resolutions that are higher or equal to the given + * resolution, starting from the given resolution. In other words + * passing DAY to this methods returns DAY,MONTH,YEAR + * + * @param r + * The resolution to start from + * @return An iterable for the resolutions higher or equal to r + */ + public static Iterable<Resolution> getResolutionsHigherOrEqualTo( + Resolution r) { + List<Resolution> resolutions = new ArrayList<DateField.Resolution>(); + Resolution[] values = Resolution.values(); + for (int i = r.ordinal(); i < values.length; i++) { + resolutions.add(values[i]); + } + return resolutions; + } + + /** + * Returns the resolutions that are lower than the given resolution, + * starting from the given resolution. In other words passing DAY to + * this methods returns HOUR,MINUTE,SECOND. + * + * @param r + * The resolution to start from + * @return An iterable for the resolutions lower than r + */ + public static List<Resolution> getResolutionsLowerThan(Resolution r) { + List<Resolution> resolutions = new ArrayList<DateField.Resolution>(); + Resolution[] values = Resolution.values(); + for (int i = r.ordinal() - 1; i >= 0; i--) { + resolutions.add(values[i]); + } + return resolutions; + } + }; + + /** + * Resolution identifier: seconds. + * + * @deprecated Use {@link Resolution#SECOND} + */ + @Deprecated + public static final Resolution RESOLUTION_SEC = Resolution.SECOND; + + /** + * Resolution identifier: minutes. + * + * @deprecated Use {@link Resolution#MINUTE} + */ + @Deprecated + public static final Resolution RESOLUTION_MIN = Resolution.MINUTE; + + /** + * Resolution identifier: hours. + * + * @deprecated Use {@link Resolution#HOUR} + */ + @Deprecated + public static final Resolution RESOLUTION_HOUR = Resolution.HOUR; + + /** + * Resolution identifier: days. + * + * @deprecated Use {@link Resolution#DAY} + */ + @Deprecated + public static final Resolution RESOLUTION_DAY = Resolution.DAY; + + /** + * Resolution identifier: months. + * + * @deprecated Use {@link Resolution#MONTH} + */ + @Deprecated + public static final Resolution RESOLUTION_MONTH = Resolution.MONTH; + + /** + * Resolution identifier: years. + * + * @deprecated Use {@link Resolution#YEAR} + */ + @Deprecated + public static final Resolution RESOLUTION_YEAR = Resolution.YEAR; + + /** + * Specified smallest modifiable unit for the date field. + */ + private Resolution resolution = Resolution.DAY; + + /** + * The internal calendar to be used in java.utl.Date conversions. + */ + private transient Calendar calendar; + + /** + * Overridden format string + */ + private String dateFormat; + + private boolean lenient = false; + + private String dateString = null; + + /** + * Was the last entered string parsable? If this flag is false, datefields + * internal validator does not pass. + */ + private boolean uiHasValidDateString = true; + + /** + * Determines if week numbers are shown in the date selector. + */ + private boolean showISOWeekNumbers = false; + + private String currentParseErrorMessage; + + private String defaultParseErrorMessage = "Date format not recognized"; + + private TimeZone timeZone = null; + + private static Map<Resolution, String> variableNameForResolution = new HashMap<DateField.Resolution, String>(); + { + variableNameForResolution.put(Resolution.SECOND, "sec"); + variableNameForResolution.put(Resolution.MINUTE, "min"); + variableNameForResolution.put(Resolution.HOUR, "hour"); + variableNameForResolution.put(Resolution.DAY, "day"); + variableNameForResolution.put(Resolution.MONTH, "month"); + variableNameForResolution.put(Resolution.YEAR, "year"); + } + + /* Constructors */ + + /** + * Constructs an empty <code>DateField</code> with no caption. + */ + public DateField() { + } + + /** + * Constructs an empty <code>DateField</code> with caption. + * + * @param caption + * the caption of the datefield. + */ + public DateField(String caption) { + setCaption(caption); + } + + /** + * Constructs a new <code>DateField</code> that's bound to the specified + * <code>Property</code> and has the given caption <code>String</code>. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param dataSource + * the Property to be edited with this editor. + */ + public DateField(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a new <code>DateField</code> that's bound to the specified + * <code>Property</code> and has no caption. + * + * @param dataSource + * the Property to be edited with this editor. + */ + public DateField(Property dataSource) throws IllegalArgumentException { + if (!Date.class.isAssignableFrom(dataSource.getType())) { + throw new IllegalArgumentException("Can't use " + + dataSource.getType().getName() + + " typed property as datasource"); + } + + setPropertyDataSource(dataSource); + } + + /** + * Constructs a new <code>DateField</code> with the given caption and + * initial text contents. The editor constructed this way will not be bound + * to a Property unless + * {@link com.vaadin.data.Property.Viewer#setPropertyDataSource(Property)} + * is called to bind it. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param value + * the Date value. + */ + public DateField(String caption, Date value) { + setValue(value); + setCaption(caption); + } + + /* Component basic features */ + + /* + * Paints this component. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // Adds the locale as attribute + final Locale l = getLocale(); + if (l != null) { + target.addAttribute("locale", l.toString()); + } + + if (getDateFormat() != null) { + target.addAttribute("format", dateFormat); + } + + if (!isLenient()) { + target.addAttribute("strict", true); + } + + target.addAttribute(VDateField.WEEK_NUMBERS, isShowISOWeekNumbers()); + target.addAttribute("parsable", uiHasValidDateString); + /* + * TODO communicate back the invalid date string? E.g. returning back to + * app or refresh. + */ + + // Gets the calendar + final Calendar calendar = getCalendar(); + final Date currentDate = getValue(); + + // Only paint variables for the resolution and up, e.g. Resolution DAY + // paints DAY,MONTH,YEAR + for (Resolution res : Resolution + .getResolutionsHigherOrEqualTo(resolution)) { + int value = -1; + if (currentDate != null) { + value = calendar.get(res.getCalendarField()); + if (res == Resolution.MONTH) { + // Calendar month is zero based + value++; + } + } + target.addVariable(this, variableNameForResolution.get(res), value); + } + } + + @Override + protected boolean shouldHideErrors() { + return super.shouldHideErrors() && uiHasValidDateString; + } + + /* + * Invoked when a variable of the component changes. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + if (!isReadOnly() + && (variables.containsKey("year") + || variables.containsKey("month") + || variables.containsKey("day") + || variables.containsKey("hour") + || variables.containsKey("min") + || variables.containsKey("sec") + || variables.containsKey("msec") || variables + .containsKey("dateString"))) { + + // Old and new dates + final Date oldDate = getValue(); + Date newDate = null; + + // this enables analyzing invalid input on the server + final String newDateString = (String) variables.get("dateString"); + dateString = newDateString; + + // Gets the new date in parts + boolean hasChanges = false; + Map<Resolution, Integer> calendarFieldChanges = new HashMap<DateField.Resolution, Integer>(); + + for (Resolution r : Resolution + .getResolutionsHigherOrEqualTo(resolution)) { + // Only handle what the client is allowed to send. The same + // resolutions that are painted + String variableName = variableNameForResolution.get(r); + + if (variables.containsKey(variableName)) { + Integer value = (Integer) variables.get(variableName); + if (r == Resolution.MONTH) { + // Calendar MONTH is zero based + value--; + } + if (value >= 0) { + hasChanges = true; + calendarFieldChanges.put(r, value); + } + } + } + + // If no new variable values were received, use the previous value + if (!hasChanges) { + newDate = null; + } else { + // Clone the calendar for date operation + final Calendar cal = getCalendar(); + + // Update the value based on the received info + // Must set in this order to avoid invalid dates (or wrong + // dates if lenient is true) in calendar + for (int r = Resolution.YEAR.ordinal(); r >= 0; r--) { + Resolution res = Resolution.values()[r]; + if (calendarFieldChanges.containsKey(res)) { + + // Field resolution should be included. Others are + // skipped so that client can not make unexpected + // changes (e.g. day change even though resolution is + // year). + Integer newValue = calendarFieldChanges.get(res); + cal.set(res.getCalendarField(), newValue); + } + } + newDate = cal.getTime(); + } + + if (newDate == null && dateString != null && !"".equals(dateString)) { + try { + Date parsedDate = handleUnparsableDateString(dateString); + setValue(parsedDate, true); + + /* + * Ensure the value is sent to the client if the value is + * set to the same as the previous (#4304). Does not repaint + * if handleUnparsableDateString throws an exception. In + * this case the invalid text remains in the DateField. + */ + requestRepaint(); + } catch (Converter.ConversionException e) { + + /* + * Datefield now contains some text that could't be parsed + * into date. + */ + if (oldDate != null) { + /* + * Set the logic value to null. + */ + setValue(null); + /* + * Reset the dateString (overridden to null by setValue) + */ + dateString = newDateString; + } + + /* + * Saves the localized message of parse error. This can be + * overridden in handleUnparsableDateString. The message + * will later be used to show a validation error. + */ + currentParseErrorMessage = e.getLocalizedMessage(); + + /* + * The value of the DateField should be null if an invalid + * value has been given. Not using setValue() since we do + * not want to cause the client side value to change. + */ + uiHasValidDateString = false; + + /* + * Because of our custom implementation of isValid(), that + * also checks the parsingSucceeded flag, we must also + * notify the form (if this is used in one) that the + * validity of this field has changed. + * + * Normally fields validity doesn't change without value + * change and form depends on this implementation detail. + */ + notifyFormOfValidityChange(); + requestRepaint(); + } + } else if (newDate != oldDate + && (newDate == null || !newDate.equals(oldDate))) { + setValue(newDate, true); // Don't require a repaint, client + // updates itself + } else if (!uiHasValidDateString) { // oldDate == + // newDate == null + // Empty value set, previously contained unparsable date string, + // clear related internal fields + setValue(null); + } + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } + + /** + * This method is called to handle a non-empty date string from the client + * if the client could not parse it as a Date. + * + * By default, a Converter.ConversionException is thrown, and the current + * value is not modified. + * + * This can be overridden to handle conversions, to return null (equivalent + * to empty input), to throw an exception or to fire an event. + * + * @param dateString + * @return parsed Date + * @throws Converter.ConversionException + * to keep the old value and indicate an error + */ + protected Date handleUnparsableDateString(String dateString) + throws Converter.ConversionException { + currentParseErrorMessage = null; + throw new Converter.ConversionException(getParseErrorMessage()); + } + + /* Property features */ + + /* + * Gets the edited property's type. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + @Override + public Class<Date> getType() { + return Date.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, boolean) + */ + @Override + protected void setValue(Date newValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException { + + /* + * First handle special case when the client side component have a date + * string but value is null (e.g. unparsable date string typed in by the + * user). No value changes should happen, but we need to do some + * internal housekeeping. + */ + if (newValue == null && !uiHasValidDateString) { + /* + * Side-effects of setInternalValue clears possible previous strings + * and flags about invalid input. + */ + setInternalValue(null); + + /* + * Due to DateField's special implementation of isValid(), + * datefields validity may change although the logical value does + * not change. This is an issue for Form which expects that validity + * of Fields cannot change unless actual value changes. + * + * So we check if this field is inside a form and the form has + * registered this as a field. In this case we repaint the form. + * Without this hacky solution the form might not be able to clean + * validation errors etc. We could avoid this by firing an extra + * value change event, but feels like at least as bad solution as + * this. + */ + notifyFormOfValidityChange(); + requestRepaint(); + return; + } + + super.setValue(newValue, repaintIsNotNeeded); + } + + /** + * Detects if this field is used in a Form (logically) and if so, notifies + * it (by repainting it) that the validity of this field might have changed. + */ + private void notifyFormOfValidityChange() { + Component parenOfDateField = getParent(); + boolean formFound = false; + while (parenOfDateField != null || formFound) { + if (parenOfDateField instanceof Form) { + Form f = (Form) parenOfDateField; + Collection<?> visibleItemProperties = f.getItemPropertyIds(); + for (Object fieldId : visibleItemProperties) { + Field<?> field = f.getField(fieldId); + if (field == this) { + /* + * this datefield is logically in a form. Do the same + * thing as form does in its value change listener that + * it registers to all fields. + */ + f.requestRepaint(); + formFound = true; + break; + } + } + } + if (formFound) { + break; + } + parenOfDateField = parenOfDateField.getParent(); + } + } + + @Override + protected void setInternalValue(Date newValue) { + // Also set the internal dateString + if (newValue != null) { + dateString = newValue.toString(); + } else { + dateString = null; + } + + if (!uiHasValidDateString) { + // clear component error and parsing flag + setComponentError(null); + uiHasValidDateString = true; + currentParseErrorMessage = null; + } + + super.setInternalValue(newValue); + } + + /** + * Gets the resolution. + * + * @return int + */ + public Resolution getResolution() { + return resolution; + } + + /** + * Sets the resolution of the DateField. + * + * The default resolution is {@link Resolution#DAY} since Vaadin 7.0. + * + * @param resolution + * the resolution to set. + */ + public void setResolution(Resolution resolution) { + this.resolution = resolution; + requestRepaint(); + } + + /** + * Returns new instance calendar used in Date conversions. + * + * Returns new clone of the calendar object initialized using the the + * current date (if available) + * + * If this is no calendar is assigned the <code>Calendar.getInstance</code> + * is used. + * + * @return the Calendar. + * @see #setCalendar(Calendar) + */ + private Calendar getCalendar() { + + // Makes sure we have an calendar instance + if (calendar == null) { + calendar = Calendar.getInstance(); + // Start by a zeroed calendar to avoid having values for lower + // resolution variables e.g. time when resolution is day + for (Resolution r : Resolution.getResolutionsLowerThan(resolution)) { + calendar.set(r.getCalendarField(), 0); + } + calendar.set(Calendar.MILLISECOND, 0); + } + + // Clone the instance + final Calendar newCal = (Calendar) calendar.clone(); + + // Assigns the current time tom calendar. + final Date currentDate = getValue(); + if (currentDate != null) { + newCal.setTime(currentDate); + } + + final TimeZone currentTimeZone = getTimeZone(); + if (currentTimeZone != null) { + newCal.setTimeZone(currentTimeZone); + } + + return newCal; + } + + /** + * Sets formatting used by some component implementations. See + * {@link SimpleDateFormat} for format details. + * + * By default it is encouraged to used default formatting defined by Locale, + * but due some JVM bugs it is sometimes necessary to use this method to + * override formatting. See Vaadin issue #2200. + * + * @param dateFormat + * the dateFormat to set + * + * @see com.vaadin.ui.AbstractComponent#setLocale(Locale)) + */ + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + requestRepaint(); + } + + /** + * Returns a format string used to format date value on client side or null + * if default formatting from {@link Component#getLocale()} is used. + * + * @return the dateFormat + */ + public String getDateFormat() { + return dateFormat; + } + + /** + * Specifies whether or not date/time interpretation in component is to be + * lenient. + * + * @see Calendar#setLenient(boolean) + * @see #isLenient() + * + * @param lenient + * true if the lenient mode is to be turned on; false if it is to + * be turned off. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + requestRepaint(); + } + + /** + * Returns whether date/time interpretation is to be lenient. + * + * @see #setLenient(boolean) + * + * @return true if the interpretation mode of this calendar is lenient; + * false otherwise. + */ + public boolean isLenient() { + return lenient; + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + /** + * Checks whether ISO 8601 week numbers are shown in the date selector. + * + * @return true if week numbers are shown, false otherwise. + */ + public boolean isShowISOWeekNumbers() { + return showISOWeekNumbers; + } + + /** + * Sets the visibility of ISO 8601 week numbers in the date selector. ISO + * 8601 defines that a week always starts with a Monday so the week numbers + * are only shown if this is the case. + * + * @param showWeekNumbers + * true if week numbers should be shown, false otherwise. + */ + public void setShowISOWeekNumbers(boolean showWeekNumbers) { + showISOWeekNumbers = showWeekNumbers; + requestRepaint(); + } + + /** + * Validates the current value against registered validators if the field is + * not empty. Note that DateField is considered empty (value == null) and + * invalid if it contains text typed in by the user that couldn't be parsed + * into a Date value. + * + * @see com.vaadin.ui.AbstractField#validate() + */ + @Override + public void validate() throws InvalidValueException { + /* + * To work properly in form we must throw exception if there is + * currently a parsing error in the datefield. Parsing error is kind of + * an internal validator. + */ + if (!uiHasValidDateString) { + throw new UnparsableDateString(currentParseErrorMessage); + } + super.validate(); + } + + /** + * Return the error message that is shown if the user inputted value can't + * be parsed into a Date object. If + * {@link #handleUnparsableDateString(String)} is overridden and it throws a + * custom exception, the message returned by + * {@link Exception#getLocalizedMessage()} will be used instead of the value + * returned by this method. + * + * @see #setParseErrorMessage(String) + * + * @return the error message that the DateField uses when it can't parse the + * textual input from user to a Date object + */ + public String getParseErrorMessage() { + return defaultParseErrorMessage; + } + + /** + * Sets the default error message used if the DateField cannot parse the + * text input by user to a Date field. Note that if the + * {@link #handleUnparsableDateString(String)} method is overridden, the + * localized message from its exception is used. + * + * @see #getParseErrorMessage() + * @see #handleUnparsableDateString(String) + * @param parsingErrorMessage + */ + public void setParseErrorMessage(String parsingErrorMessage) { + defaultParseErrorMessage = parsingErrorMessage; + } + + /** + * Sets the time zone used by this date field. The time zone is used to + * convert the absolute time in a Date object to a logical time displayed in + * the selector and to convert the select time back to a Date object. + * + * If no time zone has been set, the current default time zone returned by + * {@code TimeZone.getDefault()} is used. + * + * @see #getTimeZone() + * @param timeZone + * the time zone to use for time calculations. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + requestRepaint(); + } + + /** + * Gets the time zone used by this field. The time zone is used to convert + * the absolute time in a Date object to a logical time displayed in the + * selector and to convert the select time back to a Date object. + * + * If {@code null} is returned, the current default time zone returned by + * {@code TimeZone.getDefault()} is used. + * + * @return the current time zone + */ + public TimeZone getTimeZone() { + return timeZone; + } + + public static class UnparsableDateString extends + Validator.InvalidValueException { + + public UnparsableDateString(String message) { + super(message); + } + + } +} diff --git a/server/src/com/vaadin/ui/DefaultFieldFactory.java b/server/src/com/vaadin/ui/DefaultFieldFactory.java new file mode 100644 index 0000000000..e17f08c1c6 --- /dev/null +++ b/server/src/com/vaadin/ui/DefaultFieldFactory.java @@ -0,0 +1,146 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * This class contains a basic implementation for both {@link FormFieldFactory} + * and {@link TableFieldFactory}. The class is singleton, use {@link #get()} + * method to get reference to the instance. + * + * <p> + * There are also some static helper methods available for custom built field + * factories. + * + */ +public class DefaultFieldFactory implements FormFieldFactory, TableFieldFactory { + + private static final DefaultFieldFactory instance = new DefaultFieldFactory(); + + /** + * Singleton method to get an instance of DefaultFieldFactory. + * + * @return an instance of DefaultFieldFactory + */ + public static DefaultFieldFactory get() { + return instance; + } + + protected DefaultFieldFactory() { + } + + @Override + public Field<?> createField(Item item, Object propertyId, + Component uiContext) { + Class<?> type = item.getItemProperty(propertyId).getType(); + Field<?> field = createFieldByPropertyType(type); + field.setCaption(createCaptionByPropertyId(propertyId)); + return field; + } + + @Override + public Field<?> createField(Container container, Object itemId, + Object propertyId, Component uiContext) { + Property<?> containerProperty = container.getContainerProperty(itemId, + propertyId); + Class<?> type = containerProperty.getType(); + Field<?> field = createFieldByPropertyType(type); + field.setCaption(createCaptionByPropertyId(propertyId)); + return field; + } + + /** + * If name follows method naming conventions, convert the name to spaced + * upper case text. For example, convert "firstName" to "First Name" + * + * @param propertyId + * @return the formatted caption string + */ + public static String createCaptionByPropertyId(Object propertyId) { + String name = propertyId.toString(); + if (name.length() > 0) { + + int dotLocation = name.lastIndexOf('.'); + if (dotLocation > 0 && dotLocation < name.length() - 1) { + name = name.substring(dotLocation + 1); + } + if (name.indexOf(' ') < 0 + && name.charAt(0) == Character.toLowerCase(name.charAt(0)) + && name.charAt(0) != Character.toUpperCase(name.charAt(0))) { + StringBuffer out = new StringBuffer(); + out.append(Character.toUpperCase(name.charAt(0))); + int i = 1; + + while (i < name.length()) { + int j = i; + for (; j < name.length(); j++) { + char c = name.charAt(j); + if (Character.toLowerCase(c) != c + && Character.toUpperCase(c) == c) { + break; + } + } + if (j == name.length()) { + out.append(name.substring(i)); + } else { + out.append(name.substring(i, j)); + out.append(" " + name.charAt(j)); + } + i = j + 1; + } + + name = out.toString(); + } + } + return name; + } + + /** + * Creates fields based on the property type. + * <p> + * The default field type is {@link TextField}. Other field types generated + * by this method: + * <p> + * <b>Boolean</b>: {@link CheckBox}.<br/> + * <b>Date</b>: {@link DateField}(resolution: day).<br/> + * <b>Item</b>: {@link Form}. <br/> + * <b>default field type</b>: {@link TextField}. + * <p> + * + * @param type + * the type of the property + * @return the most suitable generic {@link Field} for given type + */ + public static Field<?> createFieldByPropertyType(Class<?> type) { + // Null typed properties can not be edited + if (type == null) { + return null; + } + + // Item field + if (Item.class.isAssignableFrom(type)) { + return new Form(); + } + + // Date field + if (Date.class.isAssignableFrom(type)) { + final DateField df = new DateField(); + df.setResolution(DateField.RESOLUTION_DAY); + return df; + } + + // Boolean field + if (Boolean.class.isAssignableFrom(type)) { + return new CheckBox(); + } + + return new TextField(); + } + +} diff --git a/server/src/com/vaadin/ui/DragAndDropWrapper.java b/server/src/com/vaadin/ui/DragAndDropWrapper.java new file mode 100644 index 0000000000..67229a45fe --- /dev/null +++ b/server/src/com/vaadin/ui/DragAndDropWrapper.java @@ -0,0 +1,407 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.vaadin.event.Transferable; +import com.vaadin.event.TransferableImpl; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.HorizontalDropLocation; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.StreamVariable; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper; + +@SuppressWarnings("serial") +public class DragAndDropWrapper extends CustomComponent implements DropTarget, + DragSource, Vaadin6Component { + + public class WrapperTransferable extends TransferableImpl { + + private Html5File[] files; + + public WrapperTransferable(Component sourceComponent, + Map<String, Object> rawVariables) { + super(sourceComponent, rawVariables); + Integer fc = (Integer) rawVariables.get("filecount"); + if (fc != null) { + files = new Html5File[fc]; + for (int i = 0; i < fc; i++) { + Html5File file = new Html5File( + (String) rawVariables.get("fn" + i), // name + (Integer) rawVariables.get("fs" + i), // size + (String) rawVariables.get("ft" + i)); // mime + String id = (String) rawVariables.get("fi" + i); + files[i] = file; + receivers.put(id, file); + requestRepaint(); // paint Receivers + } + } + } + + /** + * The component in wrapper that is being dragged or null if the + * transferable is not a component (most likely an html5 drag). + * + * @return + */ + public Component getDraggedComponent() { + Component object = (Component) getData("component"); + return object; + } + + /** + * @return the mouse down event that started the drag and drop operation + */ + public MouseEventDetails getMouseDownEvent() { + return MouseEventDetails.deSerialize((String) getData("mouseDown")); + } + + public Html5File[] getFiles() { + return files; + } + + public String getText() { + String data = (String) getData("Text"); // IE, html5 + if (data == null) { + // check for "text/plain" (webkit) + data = (String) getData("text/plain"); + } + return data; + } + + public String getHtml() { + String data = (String) getData("Html"); // IE, html5 + if (data == null) { + // check for "text/plain" (webkit) + data = (String) getData("text/html"); + } + return data; + } + + } + + private Map<String, Html5File> receivers = new HashMap<String, Html5File>(); + + public class WrapperTargetDetails extends TargetDetailsImpl { + + public WrapperTargetDetails(Map<String, Object> rawDropData) { + super(rawDropData, DragAndDropWrapper.this); + } + + /** + * @return the absolute position of wrapper on the page + */ + public Integer getAbsoluteLeft() { + return (Integer) getData("absoluteLeft"); + } + + /** + * + * @return the absolute position of wrapper on the page + */ + public Integer getAbsoluteTop() { + return (Integer) getData("absoluteTop"); + } + + /** + * @return details about the actual event that caused the event details. + * Practically mouse move or mouse up. + */ + public MouseEventDetails getMouseEvent() { + return MouseEventDetails + .deSerialize((String) getData("mouseEvent")); + } + + /** + * @return a detail about the drags vertical position over the wrapper. + */ + public VerticalDropLocation getVerticalDropLocation() { + return VerticalDropLocation + .valueOf((String) getData("verticalLocation")); + } + + /** + * @return a detail about the drags horizontal position over the + * wrapper. + */ + public HorizontalDropLocation getHorizontalDropLocation() { + return HorizontalDropLocation + .valueOf((String) getData("horizontalLocation")); + } + + /** + * @deprecated use {@link #getVerticalDropLocation()} instead + */ + @Deprecated + public VerticalDropLocation verticalDropLocation() { + return getVerticalDropLocation(); + } + + /** + * @deprecated use {@link #getHorizontalDropLocation()} instead + */ + @Deprecated + public HorizontalDropLocation horizontalDropLocation() { + return getHorizontalDropLocation(); + } + + } + + public enum DragStartMode { + /** + * {@link DragAndDropWrapper} does not start drag events at all + */ + NONE, + /** + * The component on which the drag started will be shown as drag image. + */ + COMPONENT, + /** + * The whole wrapper is used as a drag image when dragging. + */ + WRAPPER, + /** + * The whole wrapper is used to start an HTML5 drag. + * + * NOTE: In Internet Explorer 6 to 8, this prevents user interactions + * with the wrapper's contents. For example, clicking a button inside + * the wrapper will no longer work. + */ + HTML5, + } + + private final Map<String, Object> html5DataFlavors = new LinkedHashMap<String, Object>(); + private DragStartMode dragStartMode = DragStartMode.NONE; + + /** + * Wraps given component in a {@link DragAndDropWrapper}. + * + * @param root + * the component to be wrapped + */ + public DragAndDropWrapper(Component root) { + super(root); + } + + /** + * Sets data flavors available in the DragAndDropWrapper is used to start an + * HTML5 style drags. Most commonly the "Text" flavor should be set. + * Multiple data types can be set. + * + * @param type + * the string identifier of the drag "payload". E.g. "Text" or + * "text/html" + * @param value + * the value + */ + public void setHTML5DataFlavor(String type, Object value) { + html5DataFlavors.put(type, value); + requestRepaint(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute(VDragAndDropWrapper.DRAG_START_MODE, + dragStartMode.ordinal()); + if (getDropHandler() != null) { + getDropHandler().getAcceptCriterion().paint(target); + } + if (receivers != null && receivers.size() > 0) { + for (Iterator<Entry<String, Html5File>> it = receivers.entrySet() + .iterator(); it.hasNext();) { + Entry<String, com.vaadin.ui.Html5File> entry = it.next(); + String id = entry.getKey(); + Html5File html5File = entry.getValue(); + if (html5File.getStreamVariable() != null) { + target.addVariable(this, "rec-" + id, new ProxyReceiver( + html5File)); + // these are cleaned from receivers once the upload has + // started + } else { + // instructs the client side not to send the file + target.addVariable(this, "rec-" + id, (String) null); + // forget the file from subsequent paints + it.remove(); + } + } + } + target.addAttribute(VDragAndDropWrapper.HTML5_DATA_FLAVORS, + html5DataFlavors); + } + + private DropHandler dropHandler; + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + requestRepaint(); + } + + @Override + public TargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new WrapperTargetDetails(clientVariables); + } + + @Override + public Transferable getTransferable(final Map<String, Object> rawVariables) { + return new WrapperTransferable(this, rawVariables); + } + + public void setDragStartMode(DragStartMode dragStartMode) { + this.dragStartMode = dragStartMode; + requestRepaint(); + } + + public DragStartMode getDragStartMode() { + return dragStartMode; + } + + final class ProxyReceiver implements StreamVariable { + + private Html5File file; + + public ProxyReceiver(Html5File file) { + this.file = file; + } + + private boolean listenProgressOfUploadedFile; + + @Override + public OutputStream getOutputStream() { + if (file.getStreamVariable() == null) { + return null; + } + return file.getStreamVariable().getOutputStream(); + } + + @Override + public boolean listenProgress() { + return file.getStreamVariable().listenProgress(); + } + + @Override + public void onProgress(StreamingProgressEvent event) { + file.getStreamVariable().onProgress( + new ReceivingEventWrapper(event)); + } + + @Override + public void streamingStarted(StreamingStartEvent event) { + listenProgressOfUploadedFile = file.getStreamVariable() != null; + if (listenProgressOfUploadedFile) { + file.getStreamVariable().streamingStarted( + new ReceivingEventWrapper(event)); + } + // no need tell to the client about this receiver on next paint + receivers.remove(file); + // let the terminal GC the streamvariable and not to accept other + // file uploads to this variable + event.disposeStreamVariable(); + } + + @Override + public void streamingFinished(StreamingEndEvent event) { + if (listenProgressOfUploadedFile) { + file.getStreamVariable().streamingFinished( + new ReceivingEventWrapper(event)); + } + } + + @Override + public void streamingFailed(final StreamingErrorEvent event) { + if (listenProgressOfUploadedFile) { + file.getStreamVariable().streamingFailed( + new ReceivingEventWrapper(event)); + } + } + + @Override + public boolean isInterrupted() { + return file.getStreamVariable().isInterrupted(); + } + + /* + * With XHR2 file posts we can't provide as much information from the + * terminal as with multipart request. This helper class wraps the + * terminal event and provides the lacking information from the + * Html5File. + */ + class ReceivingEventWrapper implements StreamingErrorEvent, + StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent { + + private StreamingEvent wrappedEvent; + + ReceivingEventWrapper(StreamingEvent e) { + wrappedEvent = e; + } + + @Override + public String getMimeType() { + return file.getType(); + } + + @Override + public String getFileName() { + return file.getFileName(); + } + + @Override + public long getContentLength() { + return file.getFileSize(); + } + + public StreamVariable getReceiver() { + return ProxyReceiver.this; + } + + @Override + public Exception getException() { + if (wrappedEvent instanceof StreamingErrorEvent) { + return ((StreamingErrorEvent) wrappedEvent).getException(); + } + return null; + } + + @Override + public long getBytesReceived() { + return wrappedEvent.getBytesReceived(); + } + + /** + * Calling this method has no effect. DD files are receive only once + * anyway. + */ + @Override + public void disposeStreamVariable() { + + } + } + + } + +} diff --git a/server/src/com/vaadin/ui/Embedded.java b/server/src/com/vaadin/ui/Embedded.java new file mode 100644 index 0000000000..6088c5aa66 --- /dev/null +++ b/server/src/com/vaadin/ui/Embedded.java @@ -0,0 +1,531 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.embedded.EmbeddedServerRpc; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.embedded.EmbeddedConnector; + +/** + * Component for embedding external objects. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Embedded extends AbstractComponent implements Vaadin6Component { + + /** + * General object type. + */ + public static final int TYPE_OBJECT = 0; + + /** + * Image types. + */ + public static final int TYPE_IMAGE = 1; + + /** + * Browser ("iframe") type. + */ + public static final int TYPE_BROWSER = 2; + + /** + * Type of the object. + */ + private int type = TYPE_OBJECT; + + /** + * Source of the embedded object. + */ + private Resource source = null; + + /** + * Generic object attributes. + */ + private String mimeType = null; + + private String standby = null; + + /** + * Hash of object parameters. + */ + private final Map<String, String> parameters = new HashMap<String, String>(); + + /** + * Applet or other client side runnable properties. + */ + private String codebase = null; + + private String codetype = null; + + private String classId = null; + + private String archive = null; + + private String altText; + + private EmbeddedServerRpc rpc = new EmbeddedServerRpc() { + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Embedded.this, mouseDetails)); + } + }; + + /** + * Creates a new empty Embedded object. + */ + public Embedded() { + registerRpc(rpc); + } + + /** + * Creates a new empty Embedded object with caption. + * + * @param caption + */ + public Embedded(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new Embedded object whose contents is loaded from given + * resource. The dimensions are assumed if possible. The type is guessed + * from resource. + * + * @param caption + * @param source + * the Source of the embedded object. + */ + public Embedded(String caption, Resource source) { + this(caption); + setSource(source); + } + + /** + * Invoked when the component state should be painted. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + switch (type) { + case TYPE_IMAGE: + target.addAttribute("type", "image"); + break; + case TYPE_BROWSER: + target.addAttribute("type", "browser"); + break; + default: + break; + } + + if (getSource() != null) { + target.addAttribute("src", getSource()); + } + + if (mimeType != null && !"".equals(mimeType)) { + target.addAttribute("mimetype", mimeType); + } + if (classId != null && !"".equals(classId)) { + target.addAttribute("classid", classId); + } + if (codebase != null && !"".equals(codebase)) { + target.addAttribute("codebase", codebase); + } + if (codetype != null && !"".equals(codetype)) { + target.addAttribute("codetype", codetype); + } + if (standby != null && !"".equals(standby)) { + target.addAttribute("standby", standby); + } + if (archive != null && !"".equals(archive)) { + target.addAttribute("archive", archive); + } + if (altText != null && !"".equals(altText)) { + target.addAttribute(EmbeddedConnector.ALTERNATE_TEXT, altText); + } + + // Params + for (final Iterator<String> i = getParameterNames(); i.hasNext();) { + target.startTag("embeddedparam"); + final String key = i.next(); + target.addAttribute("name", key); + target.addAttribute("value", getParameter(key)); + target.endTag("embeddedparam"); + } + } + + /** + * Sets this component's "alt-text", that is, an alternate text that can be + * presented instead of this component's normal content, for accessibility + * purposes. Does not work when {@link #setType(int)} has been called with + * {@link #TYPE_BROWSER}. + * + * @param altText + * A short, human-readable description of this component's + * content. + * @since 6.8 + */ + public void setAlternateText(String altText) { + if (altText != this.altText + || (altText != null && !altText.equals(this.altText))) { + this.altText = altText; + requestRepaint(); + } + } + + /** + * Gets this component's "alt-text". + * + * @see #setAlternateText(String) + */ + public String getAlternateText() { + return altText; + } + + /** + * Sets an object parameter. Parameters are optional information, and they + * are passed to the instantiated object. Parameters are are stored as name + * value pairs. This overrides the previous value assigned to this + * parameter. + * + * @param name + * the name of the parameter. + * @param value + * the value of the parameter. + */ + public void setParameter(String name, String value) { + parameters.put(name, value); + requestRepaint(); + } + + /** + * Gets the value of an object parameter. Parameters are optional + * information, and they are passed to the instantiated object. Parameters + * are are stored as name value pairs. + * + * @return the Value of parameter or null if not found. + */ + public String getParameter(String name) { + return parameters.get(name); + } + + /** + * Removes an object parameter from the list. + * + * @param name + * the name of the parameter to remove. + */ + public void removeParameter(String name) { + parameters.remove(name); + requestRepaint(); + } + + /** + * Gets the embedded object parameter names. + * + * @return the Iterator of parameters names. + */ + public Iterator<String> getParameterNames() { + return parameters.keySet().iterator(); + } + + /** + * This attribute specifies the base path used to resolve relative URIs + * specified by the classid, data, and archive attributes. When absent, its + * default value is the base URI of the current document. + * + * @return the code base. + */ + public String getCodebase() { + return codebase; + } + + /** + * Gets the MIME-Type of the code. + * + * @return the MIME-Type of the code. + */ + public String getCodetype() { + return codetype; + } + + /** + * Gets the MIME-Type of the object. + * + * @return the MIME-Type of the object. + */ + public String getMimeType() { + return mimeType; + } + + /** + * This attribute specifies a message that a user agent may render while + * loading the object's implementation and data. + * + * @return The text displayed when loading + */ + public String getStandby() { + return standby; + } + + /** + * This attribute specifies the base path used to resolve relative URIs + * specified by the classid, data, and archive attributes. When absent, its + * default value is the base URI of the current document. + * + * @param codebase + * The base path + */ + public void setCodebase(String codebase) { + if (codebase != this.codebase + || (codebase != null && !codebase.equals(this.codebase))) { + this.codebase = codebase; + requestRepaint(); + } + } + + /** + * This attribute specifies the content type of data expected when + * downloading the object specified by classid. This attribute is optional + * but recommended when classid is specified since it allows the user agent + * to avoid loading information for unsupported content types. When absent, + * it defaults to the value of the type attribute. + * + * @param codetype + * the codetype to set. + */ + public void setCodetype(String codetype) { + if (codetype != this.codetype + || (codetype != null && !codetype.equals(this.codetype))) { + this.codetype = codetype; + requestRepaint(); + } + } + + /** + * Sets the mimeType, the MIME-Type of the object. + * + * @param mimeType + * the mimeType to set. + */ + public void setMimeType(String mimeType) { + if (mimeType != this.mimeType + || (mimeType != null && !mimeType.equals(this.mimeType))) { + this.mimeType = mimeType; + if ("application/x-shockwave-flash".equals(mimeType)) { + /* + * Automatically add wmode transparent as we use lots of + * floating layers in Vaadin. If developers need better flash + * performance, they can override this value programmatically + * back to "window" (the defautl). + */ + if (getParameter("wmode") == null) { + setParameter("wmode", "transparent"); + } + } + requestRepaint(); + } + } + + /** + * This attribute specifies a message that a user agent may render while + * loading the object's implementation and data. + * + * @param standby + * The text to display while loading + */ + public void setStandby(String standby) { + if (standby != this.standby + || (standby != null && !standby.equals(this.standby))) { + this.standby = standby; + requestRepaint(); + } + } + + /** + * This attribute may be used to specify the location of an object's + * implementation via a URI. + * + * @return the classid. + */ + public String getClassId() { + return classId; + } + + /** + * This attribute may be used to specify the location of an object's + * implementation via a URI. + * + * @param classId + * the classId to set. + */ + public void setClassId(String classId) { + if (classId != this.classId + || (classId != null && !classId.equals(this.classId))) { + this.classId = classId; + requestRepaint(); + } + } + + /** + * Gets the resource contained in the embedded object. + * + * @return the Resource + */ + public Resource getSource() { + return source; + } + + /** + * Gets the type of the embedded object. + * <p> + * This can be one of the following: + * <ul> + * <li>TYPE_OBJECT <i>(This is the default)</i> + * <li>TYPE_IMAGE + * </ul> + * </p> + * + * @return the type. + */ + public int getType() { + return type; + } + + /** + * Sets the object source resource. The dimensions are assumed if possible. + * The type is guessed from resource. + * + * @param source + * the source to set. + */ + public void setSource(Resource source) { + if (source != null && !source.equals(this.source)) { + this.source = source; + final String mt = source.getMIMEType(); + + if (mimeType == null) { + mimeType = mt; + } + + if (mt.equals("image/svg+xml")) { + type = TYPE_OBJECT; + } else if ((mt.substring(0, mt.indexOf("/")) + .equalsIgnoreCase("image"))) { + type = TYPE_IMAGE; + } else { + // Keep previous type + } + requestRepaint(); + } + } + + /** + * Sets the object type. + * <p> + * This can be one of the following: + * <ul> + * <li>TYPE_OBJECT <i>(This is the default)</i> + * <li>TYPE_IMAGE + * <li>TYPE_BROWSER + * </ul> + * </p> + * + * @param type + * the type to set. + */ + public void setType(int type) { + if (type != TYPE_OBJECT && type != TYPE_IMAGE && type != TYPE_BROWSER) { + throw new IllegalArgumentException("Unsupported type"); + } + if (type != this.type) { + this.type = type; + requestRepaint(); + } + } + + /** + * This attribute may be used to specify a space-separated list of URIs for + * archives containing resources relevant to the object, which may include + * the resources specified by the classid and data attributes. Preloading + * archives will generally result in reduced load times for objects. + * Archives specified as relative URIs should be interpreted relative to the + * codebase attribute. + * + * @return Space-separated list of URIs with resources relevant to the + * object + */ + public String getArchive() { + return archive; + } + + /** + * This attribute may be used to specify a space-separated list of URIs for + * archives containing resources relevant to the object, which may include + * the resources specified by the classid and data attributes. Preloading + * archives will generally result in reduced load times for objects. + * Archives specified as relative URIs should be interpreted relative to the + * codebase attribute. + * + * @param archive + * Space-separated list of URIs with resources relevant to the + * object + */ + public void setArchive(String archive) { + if (archive != this.archive + || (archive != null && !archive.equals(this.archive))) { + this.archive = archive; + requestRepaint(); + } + } + + /** + * Add a click listener to the component. The listener is called whenever + * the user clicks inside the component. Depending on the content the event + * may be blocked and in that case no event is fired. + * + * Use {@link #removeListener(ClickListener)} to remove the listener. + * + * @param listener + * The listener to add + */ + public void addListener(ClickListener listener) { + addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, ClickEvent.class, + listener, ClickListener.clickMethod); + } + + /** + * Remove a click listener from the component. The listener should earlier + * have been added using {@link #addListener(ClickListener)}. + * + * @param listener + * The listener to remove + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + ClickEvent.class, listener); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + +} diff --git a/server/src/com/vaadin/ui/Field.java b/server/src/com/vaadin/ui/Field.java new file mode 100644 index 0000000000..6dc40d192f --- /dev/null +++ b/server/src/com/vaadin/ui/Field.java @@ -0,0 +1,97 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.BufferedValidatable; +import com.vaadin.data.Property; +import com.vaadin.ui.Component.Focusable; + +/** + * TODO document + * + * @author Vaadin Ltd. + * + * @param T + * the type of values in the field, which might not be the same type + * as that of the data source if converters are used + * + * @author IT Mill Ltd. + */ +public interface Field<T> extends Component, BufferedValidatable, Property<T>, + Property.ValueChangeNotifier, Property.ValueChangeListener, + Property.Editor, Focusable { + + /** + * Is this field required. + * + * Required fields must filled by the user. + * + * @return <code>true</code> if the field is required,otherwise + * <code>false</code>. + * @since 3.1 + */ + public boolean isRequired(); + + /** + * Sets the field required. Required fields must filled by the user. + * + * @param required + * Is the field required. + * @since 3.1 + */ + public void setRequired(boolean required); + + /** + * Sets the error message to be displayed if a required field is empty. + * + * @param requiredMessage + * Error message. + * @since 5.2.6 + */ + public void setRequiredError(String requiredMessage); + + /** + * Gets the error message that is to be displayed if a required field is + * empty. + * + * @return Error message. + * @since 5.2.6 + */ + public String getRequiredError(); + + /** + * An <code>Event</code> object specifying the Field whose value has been + * changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("serial") + public static class ValueChangeEvent extends Component.Event implements + Property.ValueChangeEvent { + + /** + * Constructs a new event object with the specified source field object. + * + * @param source + * the field that caused the event. + */ + public ValueChangeEvent(Field source) { + super(source); + } + + /** + * Gets the Property which triggered the event. + * + * @return the Source Property of the event. + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + } +} diff --git a/server/src/com/vaadin/ui/Form.java b/server/src/com/vaadin/ui/Form.java new file mode 100644 index 0000000000..fbc4d5a8e6 --- /dev/null +++ b/server/src/com/vaadin/ui/Form.java @@ -0,0 +1,1420 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import com.vaadin.data.Buffered; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Validatable; +import com.vaadin.data.Validator; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.fieldgroup.FieldGroup; +import com.vaadin.data.util.BeanItem; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.Action.ShortcutNotifier; +import com.vaadin.event.ActionManager; +import com.vaadin.shared.ui.form.FormState; +import com.vaadin.terminal.AbstractErrorMessage; +import com.vaadin.terminal.CompositeErrorMessage; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.UserError; +import com.vaadin.terminal.Vaadin6Component; + +/** + * Form component provides easy way of creating and managing sets fields. + * + * <p> + * <code>Form</code> is a container for fields implementing {@link Field} + * interface. It provides support for any layouts and provides buffering + * interface for easy connection of commit and discard buttons. All the form + * fields can be customized by adding validators, setting captions and icons, + * setting immediateness, etc. Also direct mechanism for replacing existing + * fields with selections is given. + * </p> + * + * <p> + * <code>Form</code> provides customizable editor for classes implementing + * {@link com.vaadin.data.Item} interface. Also the form itself implements this + * interface for easier connectivity to other items. To use the form as editor + * for an item, just connect the item to form with + * {@link Form#setItemDataSource(Item)}. If only a part of the item needs to be + * edited, {@link Form#setItemDataSource(Item,Collection)} can be used instead. + * After the item has been connected to the form, the automatically created + * fields can be customized and new fields can be added. If you need to connect + * a class that does not implement {@link com.vaadin.data.Item} interface, most + * properties of any class following bean pattern, can be accessed trough + * {@link com.vaadin.data.util.BeanItem}. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @deprecated Use {@link FieldGroup} instead of {@link Form} for more + * flexibility. + */ +@Deprecated +public class Form extends AbstractField<Object> implements Item.Editor, + Buffered, Item, Validatable, Action.Notifier, HasComponents, + Vaadin6Component { + + private Object propertyValue; + + /** + * Item connected to this form as datasource. + */ + private Item itemDatasource; + + /** + * Ordered list of property ids in this editor. + */ + private final LinkedList<Object> propertyIds = new LinkedList<Object>(); + + /** + * Current buffered source exception. + */ + private Buffered.SourceException currentBufferedSourceException = null; + + /** + * Is the form in write trough mode. + */ + private boolean writeThrough = true; + + /** + * Is the form in read trough mode. + */ + private boolean readThrough = true; + + /** + * Mapping from propertyName to corresponding field. + */ + private final HashMap<Object, Field<?>> fields = new HashMap<Object, Field<?>>(); + + /** + * Form may act as an Item, its own properties are stored here. + */ + private final HashMap<Object, Property<?>> ownProperties = new HashMap<Object, Property<?>>(); + + /** + * Field factory for this form. + */ + private FormFieldFactory fieldFactory; + + /** + * Visible item properties. + */ + private Collection<?> visibleItemProperties; + + /** + * Form needs to repaint itself if child fields value changes due possible + * change in form validity. + * + * TODO introduce ValidityChangeEvent (#6239) and start using it instead. + * See e.g. DateField#notifyFormOfValidityChange(). + */ + private final ValueChangeListener fieldValueChangeListener = new ValueChangeListener() { + @Override + public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) { + requestRepaint(); + } + }; + + /** + * If this is true, commit implicitly calls setValidationVisible(true). + */ + private boolean validationVisibleOnCommit = true; + + // special handling for gridlayout; remember initial cursor pos + private int gridlayoutCursorX = -1; + private int gridlayoutCursorY = -1; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. Note that the extended AbstractField is a + * {@link ShortcutNotifier} and has a actionManager that delegates actions + * to the containing window. This one does not delegate. + */ + private ActionManager ownActionManager = new ActionManager(this); + + /** + * Constructs a new form with default layout. + * + * <p> + * By default the form uses {@link FormLayout}. + * </p> + */ + public Form() { + this(null); + setValidationVisible(false); + } + + /** + * Constructs a new form with given {@link Layout}. + * + * @param formLayout + * the layout of the form. + */ + public Form(Layout formLayout) { + this(formLayout, DefaultFieldFactory.get()); + } + + /** + * Constructs a new form with given {@link Layout} and + * {@link FormFieldFactory}. + * + * @param formLayout + * the layout of the form. + * @param fieldFactory + * the FieldFactory of the form. + */ + public Form(Layout formLayout, FormFieldFactory fieldFactory) { + super(); + setLayout(formLayout); + setFooter(null); + setFormFieldFactory(fieldFactory); + setValidationVisible(false); + setWidth(100, UNITS_PERCENTAGE); + } + + @Override + public FormState getState() { + return (FormState) super.getState(); + } + + /* Documented in interface */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (ownActionManager != null) { + ownActionManager.paintActions(null, target); + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Actions + if (ownActionManager != null) { + ownActionManager.handleActions(variables, this); + } + } + + /** + * The error message of a Form is the error of the first field with a + * non-empty error. + * + * Empty error messages of the contained fields are skipped, because an + * empty error indicator would be confusing to the user, especially if there + * are errors that have something to display. This is also the reason why + * the calculation of the error message is separate from validation, because + * validation fails also on empty errors. + */ + @Override + public ErrorMessage getErrorMessage() { + + // Reimplement the checking of validation error by using + // getErrorMessage() recursively instead of validate(). + ErrorMessage validationError = null; + if (isValidationVisible()) { + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + Object f = fields.get(i.next()); + if (f instanceof AbstractComponent) { + AbstractComponent field = (AbstractComponent) f; + + validationError = field.getErrorMessage(); + if (validationError != null) { + // Show caption as error for fields with empty errors + if ("".equals(validationError.toString())) { + validationError = new UserError(field.getCaption()); + } + break; + } else if (f instanceof Field && !((Field<?>) f).isValid()) { + // Something is wrong with the field, but no proper + // error is given. Generate one. + validationError = new UserError(field.getCaption()); + break; + } + } + } + } + + // Return if there are no errors at all + if (getComponentError() == null && validationError == null + && currentBufferedSourceException == null) { + return null; + } + + // Throw combination of the error types + return new CompositeErrorMessage( + new ErrorMessage[] { + getComponentError(), + validationError, + AbstractErrorMessage + .getErrorMessageForException(currentBufferedSourceException) }); + } + + /** + * Controls the making validation visible implicitly on commit. + * + * Having commit() call setValidationVisible(true) implicitly is the default + * behaviour. You can disable the implicit setting by setting this property + * as false. + * + * It is useful, because you usually want to start with the form free of + * errors and only display them after the user clicks Ok. You can disable + * the implicit setting by setting this property as false. + * + * @param makeVisible + * If true (default), validation is made visible when commit() is + * called. If false, the visibility is left as it is. + */ + public void setValidationVisibleOnCommit(boolean makeVisible) { + validationVisibleOnCommit = makeVisible; + } + + /** + * Is validation made automatically visible on commit? + * + * See setValidationVisibleOnCommit(). + * + * @return true if validation is made automatically visible on commit. + */ + public boolean isValidationVisibleOnCommit() { + return validationVisibleOnCommit; + } + + /* + * Commit changes to the data source Don't add a JavaDoc comment here, we + * use the default one from the interface. + */ + @Override + public void commit() throws Buffered.SourceException, InvalidValueException { + + LinkedList<SourceException> problems = null; + + // Only commit on valid state if so requested + if (!isInvalidCommitted() && !isValid()) { + /* + * The values are not ok and we are told not to commit invalid + * values + */ + if (validationVisibleOnCommit) { + setValidationVisible(true); + } + + // Find the first invalid value and throw the exception + validate(); + } + + // Try to commit all + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + try { + final Field<?> f = (fields.get(i.next())); + // Commit only non-readonly fields. + if (!f.isReadOnly()) { + f.commit(); + } + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList<SourceException>(); + } + problems.add(e); + } + } + + // No problems occurred + if (problems == null) { + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + return; + } + + // Commit problems + final Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (final Iterator<SourceException> i = problems.iterator(); i + .hasNext();) { + causes[index++] = i.next(); + } + final Buffered.SourceException e = new Buffered.SourceException(this, + causes); + currentBufferedSourceException = e; + requestRepaint(); + throw e; + } + + /* + * Discards local changes and refresh values from the data source Don't add + * a JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void discard() throws Buffered.SourceException { + + LinkedList<SourceException> problems = null; + + // Try to discard all changes + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + try { + (fields.get(i.next())).discard(); + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList<SourceException>(); + } + problems.add(e); + } + } + + // No problems occurred + if (problems == null) { + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + return; + } + + // Discards problems occurred + final Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (final Iterator<SourceException> i = problems.iterator(); i + .hasNext();) { + causes[index++] = i.next(); + } + final Buffered.SourceException e = new Buffered.SourceException(this, + causes); + currentBufferedSourceException = e; + requestRepaint(); + throw e; + } + + /* + * Is the object modified but not committed? Don't add a JavaDoc comment + * here, we use the default one from the interface. + */ + @Override + public boolean isModified() { + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + final Field<?> f = fields.get(i.next()); + if (f != null && f.isModified()) { + return true; + } + + } + return false; + } + + /* + * Is the editor in a read-through mode? Don't add a JavaDoc comment here, + * we use the default one from the interface. + */ + @Override + @Deprecated + public boolean isReadThrough() { + return readThrough; + } + + /* + * Is the editor in a write-through mode? Don't add a JavaDoc comment here, + * we use the default one from the interface. + */ + @Override + @Deprecated + public boolean isWriteThrough() { + return writeThrough; + } + + /* + * Sets the editor's read-through mode to the specified status. Don't add a + * JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void setReadThrough(boolean readThrough) { + if (readThrough != this.readThrough) { + this.readThrough = readThrough; + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setReadThrough(readThrough); + } + } + } + + /* + * Sets the editor's read-through mode to the specified status. Don't add a + * JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void setWriteThrough(boolean writeThrough) throws SourceException, + InvalidValueException { + if (writeThrough != this.writeThrough) { + this.writeThrough = writeThrough; + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setWriteThrough(writeThrough); + } + } + } + + /** + * Adds a new property to form and create corresponding field. + * + * @see com.vaadin.data.Item#addItemProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) { + + // Checks inputs + if (id == null || property == null) { + throw new NullPointerException("Id and property must be non-null"); + } + + // Checks that the property id is not reserved + if (propertyIds.contains(id)) { + return false; + } + + propertyIds.add(id); + ownProperties.put(id, property); + + // Gets suitable field + final Field<?> field = fieldFactory.createField(this, id, this); + if (field == null) { + return false; + } + + // Configures the field + bindPropertyToField(id, property, field); + + // Register and attach the created field + addField(id, field); + + return true; + } + + /** + * Registers the field with the form and adds the field to the form layout. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * <p> + * This field is added to the layout using the + * {@link #attachField(Object, Field)} method. + * </p> + * + * @param propertyId + * the Property id the the field. + * @param field + * the field which should be added to the form. + */ + public void addField(Object propertyId, Field<?> field) { + registerField(propertyId, field); + attachField(propertyId, field); + requestRepaint(); + } + + /** + * Register the field with the form. All registered fields are validated + * when the form is validated and also committed when the form is committed. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * + * @param propertyId + * the Property id of the field. + * @param field + * the Field that should be registered + */ + private void registerField(Object propertyId, Field<?> field) { + if (propertyId == null || field == null) { + return; + } + + fields.put(propertyId, field); + field.addListener(fieldValueChangeListener); + if (!propertyIds.contains(propertyId)) { + // adding a field directly + propertyIds.addLast(propertyId); + } + + // Update the read and write through status and immediate to match the + // form. + // Should this also include invalidCommitted (#3993)? + field.setReadThrough(readThrough); + field.setWriteThrough(writeThrough); + if (isImmediate() && field instanceof AbstractComponent) { + ((AbstractComponent) field).setImmediate(true); + } + } + + /** + * Adds the field to the form layout. + * <p> + * The field is added to the form layout in the default position (the + * position used by {@link Layout#addComponent(Component)}. If the + * underlying layout is a {@link CustomLayout} the field is added to the + * CustomLayout location given by the string representation of the property + * id using {@link CustomLayout#addComponent(Component, String)}. + * </p> + * + * <p> + * Override this method to control how the fields are added to the layout. + * </p> + * + * @param propertyId + * @param field + */ + protected void attachField(Object propertyId, Field field) { + if (propertyId == null || field == null) { + return; + } + + Layout layout = getLayout(); + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(field, propertyId.toString()); + } else { + layout.addComponent(field); + } + + } + + /** + * The property identified by the property id. + * + * <p> + * The property data source of the field specified with property id is + * returned. If there is a (with specified property id) having no data + * source, the field is returned instead of the data source. + * </p> + * + * @see com.vaadin.data.Item#getItemProperty(Object) + */ + @Override + public Property<?> getItemProperty(Object id) { + final Field<?> field = fields.get(id); + if (field == null) { + // field does not exist or it is not (yet) created for this property + return ownProperties.get(id); + } + final Property<?> property = field.getPropertyDataSource(); + + if (property != null) { + return property; + } else { + return field; + } + } + + /** + * Gets the field identified by the propertyid. + * + * @param propertyId + * the id of the property. + */ + public Field<?> getField(Object propertyId) { + return fields.get(propertyId); + } + + /* Documented in interface */ + @Override + public Collection<?> getItemPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Removes the property and corresponding field from the form. + * + * @see com.vaadin.data.Item#removeItemProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) { + ownProperties.remove(id); + + final Field<?> field = fields.get(id); + + if (field != null) { + propertyIds.remove(id); + fields.remove(id); + detachField(field); + field.removeListener(fieldValueChangeListener); + return true; + } + + return false; + } + + /** + * Called when a form field is detached from a Form. Typically when a new + * Item is assigned to Form via {@link #setItemDataSource(Item)}. + * <p> + * Override this method to control how the fields are removed from the + * layout. + * </p> + * + * @param field + * the field to be detached from the forms layout. + */ + protected void detachField(final Field field) { + Component p = field.getParent(); + if (p instanceof ComponentContainer) { + ((ComponentContainer) p).removeComponent(field); + } + } + + /** + * Removes all properties and fields from the form. + * + * @return the Success of the operation. Removal of all fields succeeded if + * (and only if) the return value is <code>true</code>. + */ + public boolean removeAllProperties() { + final Object[] properties = propertyIds.toArray(); + boolean success = true; + + for (int i = 0; i < properties.length; i++) { + if (!removeItemProperty(properties[i])) { + success = false; + } + } + + return success; + } + + /* Documented in the interface */ + @Override + public Item getItemDataSource() { + return itemDatasource; + } + + /** + * Sets the item datasource for the form. + * + * <p> + * Setting item datasource clears any fields, the form might contain and + * adds all the properties as fields to the form. + * </p> + * + * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item) + */ + @Override + public void setItemDataSource(Item newDataSource) { + setItemDataSource(newDataSource, + newDataSource != null ? newDataSource.getItemPropertyIds() + : null); + } + + /** + * Set the item datasource for the form, but limit the form contents to + * specified properties of the item. + * + * <p> + * Setting item datasource clears any fields, the form might contain and + * adds the specified the properties as fields to the form, in the specified + * order. + * </p> + * + * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item) + */ + public void setItemDataSource(Item newDataSource, Collection<?> propertyIds) { + + if (getLayout() instanceof GridLayout) { + GridLayout gl = (GridLayout) getLayout(); + if (gridlayoutCursorX == -1) { + // first setItemDataSource, remember initial cursor + gridlayoutCursorX = gl.getCursorX(); + gridlayoutCursorY = gl.getCursorY(); + } else { + // restore initial cursor + gl.setCursorX(gridlayoutCursorX); + gl.setCursorY(gridlayoutCursorY); + } + } + + // Removes all fields first from the form + removeAllProperties(); + + // Sets the datasource + itemDatasource = newDataSource; + + // If the new datasource is null, just set null datasource + if (itemDatasource == null) { + requestRepaint(); + return; + } + + // Adds all the properties to this form + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + final Object id = i.next(); + final Property<?> property = itemDatasource.getItemProperty(id); + if (id != null && property != null) { + final Field<?> f = fieldFactory.createField(itemDatasource, id, + this); + if (f != null) { + bindPropertyToField(id, property, f); + addField(id, f); + } + } + } + } + + /** + * Binds an item property to a field. The default behavior is to bind + * property straight to Field. If Property.Viewer type property (e.g. + * PropertyFormatter) is already set for field, the property is bound to + * that Property.Viewer. + * + * @param propertyId + * @param property + * @param field + * @since 6.7.3 + */ + protected void bindPropertyToField(final Object propertyId, + final Property property, final Field field) { + // check if field has a property that is Viewer set. In that case we + // expect developer has e.g. PropertyFormatter that he wishes to use and + // assign the property to the Viewer instead. + boolean hasFilterProperty = field.getPropertyDataSource() != null + && (field.getPropertyDataSource() instanceof Property.Viewer); + if (hasFilterProperty) { + ((Property.Viewer) field.getPropertyDataSource()) + .setPropertyDataSource(property); + } else { + field.setPropertyDataSource(property); + } + } + + /** + * Gets the layout of the form. + * + * <p> + * By default form uses <code>OrderedLayout</code> with <code>form</code> + * -style. + * </p> + * + * @return the Layout of the form. + */ + public Layout getLayout() { + return (Layout) getState().getLayout(); + } + + /** + * Sets the layout of the form. + * + * <p> + * If set to null then Form uses a FormLayout by default. + * </p> + * + * @param layout + * the layout of the form. + */ + public void setLayout(Layout layout) { + + // Use orderedlayout by default + if (layout == null) { + layout = new FormLayout(); + } + + // reset cursor memory + gridlayoutCursorX = -1; + gridlayoutCursorY = -1; + + // Move fields from previous layout + if (getLayout() != null) { + final Object[] properties = propertyIds.toArray(); + for (int i = 0; i < properties.length; i++) { + Field<?> f = getField(properties[i]); + detachField(f); + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(f, + properties[i].toString()); + } else { + layout.addComponent(f); + } + } + + getLayout().setParent(null); + } + + // Replace the previous layout + layout.setParent(this); + getState().setLayout(layout); + + // Hierarchy has changed so we need to repaint (this could be a + // hierarchy repaint only) + requestRepaint(); + } + + /** + * Sets the form field to be selectable from static list of changes. + * + * <p> + * The list values and descriptions are given as array. The value-array must + * contain the current value of the field and the lengths of the arrays must + * match. Null values are not supported. + * </p> + * + * Note: since Vaadin 7.0, returns an {@link AbstractSelect} instead of a + * {@link Select}. + * + * @param propertyId + * the id of the property. + * @param values + * @param descriptions + * @return the select property generated + */ + public AbstractSelect replaceWithSelect(Object propertyId, Object[] values, + Object[] descriptions) { + + // Checks the parameters + if (propertyId == null || values == null || descriptions == null) { + throw new NullPointerException("All parameters must be non-null"); + } + if (values.length != descriptions.length) { + throw new IllegalArgumentException( + "Value and description list are of different size"); + } + + // Gets the old field + final Field<?> oldField = fields.get(propertyId); + if (oldField == null) { + throw new IllegalArgumentException("Field with given propertyid '" + + propertyId.toString() + "' can not be found."); + } + final Object value = oldField.getPropertyDataSource() == null ? oldField + .getValue() : oldField.getPropertyDataSource().getValue(); + + // Checks that the value exists and check if the select should + // be forced in multiselect mode + boolean found = false; + boolean isMultiselect = false; + for (int i = 0; i < values.length && !found; i++) { + if (values[i] == value + || (value != null && value.equals(values[i]))) { + found = true; + } + } + if (value != null && !found) { + if (value instanceof Collection) { + for (final Iterator<?> it = ((Collection<?>) value).iterator(); it + .hasNext();) { + final Object val = it.next(); + found = false; + for (int i = 0; i < values.length && !found; i++) { + if (values[i] == val + || (val != null && val.equals(values[i]))) { + found = true; + } + } + if (!found) { + throw new IllegalArgumentException( + "Currently selected value '" + val + + "' of property '" + + propertyId.toString() + + "' was not found"); + } + } + isMultiselect = true; + } else { + throw new IllegalArgumentException("Current value '" + value + + "' of property '" + propertyId.toString() + + "' was not found"); + } + } + + // Creates the new field matching to old field parameters + final AbstractSelect newField = isMultiselect ? new ListSelect() + : new Select(); + newField.setCaption(oldField.getCaption()); + newField.setReadOnly(oldField.isReadOnly()); + newField.setReadThrough(oldField.isReadThrough()); + newField.setWriteThrough(oldField.isWriteThrough()); + + // Creates the options list + newField.addContainerProperty("desc", String.class, ""); + newField.setItemCaptionPropertyId("desc"); + for (int i = 0; i < values.length; i++) { + Object id = values[i]; + final Item item; + if (id == null) { + id = newField.addItem(); + item = newField.getItem(id); + newField.setNullSelectionItemId(id); + } else { + item = newField.addItem(id); + } + + if (item != null) { + item.getItemProperty("desc").setValue( + descriptions[i].toString()); + } + } + + // Sets the property data source + final Property<?> property = oldField.getPropertyDataSource(); + oldField.setPropertyDataSource(null); + newField.setPropertyDataSource(property); + + // Replaces the old field with new one + getLayout().replaceComponent(oldField, newField); + fields.put(propertyId, newField); + newField.addListener(fieldValueChangeListener); + oldField.removeListener(fieldValueChangeListener); + + return newField; + } + + /** + * Checks the validity of the Form and all of its fields. + * + * @see com.vaadin.data.Validatable#validate() + */ + @Override + public void validate() throws InvalidValueException { + super.validate(); + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).validate(); + } + } + + /** + * Checks the validabtable object accept invalid values. + * + * @see com.vaadin.data.Validatable#isInvalidAllowed() + */ + @Override + public boolean isInvalidAllowed() { + return true; + } + + /** + * Should the validabtable object accept invalid values. + * + * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean) + */ + @Override + public void setInvalidAllowed(boolean invalidValueAllowed) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Sets the component's to read-only mode to the specified state. + * + * @see com.vaadin.ui.Component#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setReadOnly(readOnly); + } + } + + /** + * Sets the field factory used by this Form to genarate Fields for + * properties. + * + * {@link FormFieldFactory} is used to create fields for form properties. + * {@link DefaultFieldFactory} is used by default. + * + * @param fieldFactory + * the new factory used to create the fields. + * @see Field + * @see FormFieldFactory + */ + public void setFormFieldFactory(FormFieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + } + + /** + * Get the field factory of the form. + * + * @return the FormFieldFactory Factory used to create the fields. + */ + public FormFieldFactory getFormFieldFactory() { + return fieldFactory; + } + + /** + * Gets the field type. + * + * @see com.vaadin.ui.AbstractField#getType() + */ + @Override + public Class<?> getType() { + if (getPropertyDataSource() != null) { + return getPropertyDataSource().getType(); + } + return Object.class; + } + + /** + * Sets the internal value. + * + * This is relevant when the Form is used as Field. + * + * @see com.vaadin.ui.AbstractField#setInternalValue(java.lang.Object) + */ + @Override + protected void setInternalValue(Object newValue) { + // Stores the old value + final Object oldValue = propertyValue; + + // Sets the current Value + super.setInternalValue(newValue); + propertyValue = newValue; + + // Ignores form updating if data object has not changed. + if (oldValue != newValue) { + setFormDataSource(newValue, getVisibleItemProperties()); + } + } + + /** + * Gets the first focusable field in form. If there are enabled, + * non-read-only fields, the first one of them is returned. Otherwise, the + * field for the first property (or null if none) is returned. + * + * @return the Field. + */ + private Field<?> getFirstFocusableField() { + if (getItemPropertyIds() != null) { + for (Object id : getItemPropertyIds()) { + if (id != null) { + Field<?> field = getField(id); + if (field.isEnabled() && !field.isReadOnly()) { + return field; + } + } + } + // fallback: first field if none of the fields is enabled and + // writable + Object id = getItemPropertyIds().iterator().next(); + if (id != null) { + return getField(id); + } + } + return null; + } + + /** + * Updates the internal form datasource. + * + * Method setFormDataSource. + * + * @param data + * @param properties + */ + protected void setFormDataSource(Object data, Collection<?> properties) { + + // If data is an item use it. + Item item = null; + if (data instanceof Item) { + item = (Item) data; + } else if (data != null) { + item = new BeanItem<Object>(data); + } + + // Sets the datasource to form + if (item != null && properties != null) { + // Shows only given properties + this.setItemDataSource(item, properties); + } else { + // Shows all properties + this.setItemDataSource(item); + } + } + + /** + * Returns the visibleProperties. + * + * @return the Collection of visible Item properites. + */ + public Collection<?> getVisibleItemProperties() { + return visibleItemProperties; + } + + /** + * Sets the visibleProperties. + * + * @param visibleProperties + * the visibleProperties to set. + */ + public void setVisibleItemProperties(Collection<?> visibleProperties) { + visibleItemProperties = visibleProperties; + Object value = getValue(); + if (value == null) { + value = itemDatasource; + } + setFormDataSource(value, getVisibleItemProperties()); + } + + /** + * Sets the visibleProperties. + * + * @param visibleProperties + * the visibleProperties to set. + */ + public void setVisibleItemProperties(Object[] visibleProperties) { + LinkedList<Object> v = new LinkedList<Object>(); + for (int i = 0; i < visibleProperties.length; i++) { + v.add(visibleProperties[i]); + } + setVisibleItemProperties(v); + } + + /** + * Focuses the first field in the form. + * + * @see com.vaadin.ui.Component.Focusable#focus() + */ + @Override + public void focus() { + final Field<?> f = getFirstFocusableField(); + if (f != null) { + f.focus(); + } + } + + /** + * Sets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + super.setTabIndex(tabIndex); + for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) { + (getField(i.next())).setTabIndex(tabIndex); + } + } + + /** + * Setting the form to be immediate also sets all the fields of the form to + * the same state. + */ + @Override + public void setImmediate(boolean immediate) { + super.setImmediate(immediate); + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractComponent) { + ((AbstractComponent) f).setImmediate(immediate); + } + } + } + + /** Form is empty if all of its fields are empty. */ + @Override + protected boolean isEmpty() { + + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractField) { + if (!((AbstractField<?>) f).isEmpty()) { + return false; + } + } + } + + return true; + } + + /** + * Adding validators directly to form is not supported. + * + * Add the validators to form fields instead. + */ + @Override + public void addValidator(Validator validator) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a layout that is rendered below normal form contents. This area + * can be used for example to include buttons related to form contents. + * + * @return layout rendered below normal form contents. + */ + public Layout getFooter() { + return (Layout) getState().getFooter(); + } + + /** + * Sets the layout that is rendered below normal form contents. Setting this + * to null will cause an empty HorizontalLayout to be rendered in the + * footer. + * + * @param footer + * the new footer layout + */ + public void setFooter(Layout footer) { + if (getFooter() != null) { + getFooter().setParent(null); + } + if (footer == null) { + footer = new HorizontalLayout(); + } + + getState().setFooter(footer); + footer.setParent(this); + + // Hierarchy has changed so we need to repaint (this could be a + // hierarchy repaint only) + requestRepaint(); + + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (getParent() != null && !getParent().isEnabled()) { + // some ancestor still disabled, don't update children + return; + } else { + getLayout().requestRepaintAll(); + } + } + + /* + * ACTIONS + */ + + /** + * Gets the {@link ActionManager} responsible for handling {@link Action}s + * added to this Form.<br/> + * Note that Form has another ActionManager inherited from + * {@link AbstractField}. The ownActionManager handles Actions attached to + * this Form specifically, while the ActionManager in AbstractField + * delegates to the containing Window (i.e global Actions). + * + * @return + */ + protected ActionManager getOwnActionManager() { + if (ownActionManager == null) { + ownActionManager = new ActionManager(this); + } + return ownActionManager; + } + + @Override + public void addActionHandler(Handler actionHandler) { + getOwnActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (ownActionManager != null) { + ownActionManager.removeActionHandler(actionHandler); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + if (ownActionManager != null) { + ownActionManager.removeAllActionHandlers(); + } + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getOwnActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (ownActionManager != null) { + ownActionManager.removeAction(action); + } + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + /** + * Modifiable and Serializable Iterator for the components, used by + * {@link Form#getComponentIterator()}. + */ + private class ComponentIterator implements Iterator<Component>, + Serializable { + + int i = 0; + + @Override + public boolean hasNext() { + if (i < getComponentCount()) { + return true; + } + return false; + } + + @Override + public Component next() { + if (!hasNext()) { + return null; + } + i++; + if (i == 1) { + return getLayout() != null ? getLayout() : getFooter(); + } else if (i == 2) { + return getFooter(); + } + return null; + } + + @Override + public void remove() { + if (i == 1) { + if (getLayout() != null) { + setLayout(null); + i = 0; + } else { + setFooter(null); + } + } else if (i == 2) { + setFooter(null); + } + } + } + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + public int getComponentCount() { + int count = 0; + if (getLayout() != null) { + count++; + } + if (getFooter() != null) { + count++; + } + + return count; + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + }; +} diff --git a/server/src/com/vaadin/ui/FormFieldFactory.java b/server/src/com/vaadin/ui/FormFieldFactory.java new file mode 100644 index 0000000000..1efa05c5f5 --- /dev/null +++ b/server/src/com/vaadin/ui/FormFieldFactory.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.data.Item; + +/** + * Factory interface for creating new Field-instances based on {@link Item}, + * property id and uiContext (the component responsible for displaying fields). + * Currently this interface is used by {@link Form}, but might later be used by + * some other components for {@link Field} generation. + * + * <p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.0 + * @see TableFieldFactory + */ +public interface FormFieldFactory extends Serializable { + /** + * Creates a field based on the item, property id and the component (most + * commonly {@link Form}) where the Field will be presented. + * + * @param item + * the item where the property belongs to. + * @param propertyId + * the Id of the property. + * @param uiContext + * the component where the field is presented, most commonly this + * is {@link Form}. uiContext will not necessary be the parent + * component of the field, but the one that is responsible for + * creating it. + * @return Field the field suitable for editing the specified data. + */ + Field<?> createField(Item item, Object propertyId, Component uiContext); +} diff --git a/server/src/com/vaadin/ui/FormLayout.java b/server/src/com/vaadin/ui/FormLayout.java new file mode 100644 index 0000000000..c0be784a7b --- /dev/null +++ b/server/src/com/vaadin/ui/FormLayout.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +/** + * FormLayout is used by {@link Form} to layout fields. It may also be used + * separately without {@link Form}. + * + * FormLayout is a close relative to vertical {@link OrderedLayout}, but in + * FormLayout caption is rendered on left side of component. Required and + * validation indicators are between captions and fields. + * + * FormLayout does not currently support some advanced methods from + * OrderedLayout like setExpandRatio and setComponentAlignment. + * + * FormLayout by default has component spacing on. Also margin top and margin + * bottom are by default on. + * + */ +public class FormLayout extends AbstractOrderedLayout { + + public FormLayout() { + super(); + setSpacing(true); + setMargin(true, false, true, false); + setWidth(100, UNITS_PERCENTAGE); + } + +} diff --git a/server/src/com/vaadin/ui/GridLayout.java b/server/src/com/vaadin/ui/GridLayout.java new file mode 100644 index 0000000000..2391a9cd3a --- /dev/null +++ b/server/src/com/vaadin/ui/GridLayout.java @@ -0,0 +1,1415 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Map.Entry; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc; +import com.vaadin.shared.ui.gridlayout.GridLayoutState; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +/** + * A layout where the components are laid out on a grid using cell coordinates. + * + * <p> + * The GridLayout also maintains a cursor for adding components in + * left-to-right, top-to-bottom order. + * </p> + * + * <p> + * Each component in a <code>GridLayout</code> uses a defined + * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The + * components may not overlap with the existing components - if you try to do so + * you will get an {@link OverlapsException}. Adding a component with cursor + * automatically extends the grid by increasing the grid height. + * </p> + * + * <p> + * The grid coordinates, which are specified by a row and column index, always + * start from 0 for the topmost row and the leftmost column. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class GridLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier, + Vaadin6Component { + + private GridLayoutServerRpc rpc = new GridLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(GridLayout.this, + mouseDetails, clickedConnector)); + + } + }; + /** + * Cursor X position: this is where the next component with unspecified x,y + * is inserted + */ + private int cursorX = 0; + + /** + * Cursor Y position: this is where the next component with unspecified x,y + * is inserted + */ + private int cursorY = 0; + + /** + * Contains all items that are placed on the grid. These are components with + * grid area definition. + */ + private final LinkedList<Area> areas = new LinkedList<Area>(); + + /** + * Mapping from components to their respective areas. + */ + private final LinkedList<Component> components = new LinkedList<Component>(); + + /** + * Mapping from components to alignments (horizontal + vertical). + */ + private Map<Component, Alignment> componentToAlignment = new HashMap<Component, Alignment>(); + + private static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT; + + /** + * Has there been rows inserted or deleted in the middle of the layout since + * the last paint operation. + */ + private boolean structuralChange = false; + + private Map<Integer, Float> columnExpandRatio = new HashMap<Integer, Float>(); + private Map<Integer, Float> rowExpandRatio = new HashMap<Integer, Float>(); + + /** + * Constructor for a grid of given size (number of columns and rows). + * + * The grid may grow or shrink later. Grid grows automatically if you add + * components outside its area. + * + * @param columns + * Number of columns in the grid. + * @param rows + * Number of rows in the grid. + */ + public GridLayout(int columns, int rows) { + setColumns(columns); + setRows(rows); + registerRpc(rpc); + } + + /** + * Constructs an empty (1x1) grid layout that is extended as needed. + */ + public GridLayout() { + this(1, 1); + } + + @Override + public GridLayoutState getState() { + return (GridLayoutState) super.getState(); + } + + /** + * <p> + * Adds a component to the grid in the specified area. The area is defined + * by specifying the upper left corner (column1, row1) and the lower right + * corner (column2, row2) of the area. The coordinates are zero-based. + * </p> + * + * <p> + * If the area overlaps with any of the existing components already present + * in the grid, the operation will fail and an {@link OverlapsException} is + * thrown. + * </p> + * + * @param component + * the component to be added. + * @param column1 + * the column of the upper left corner of the area <code>c</code> + * is supposed to occupy. The leftmost column has index 0. + * @param row1 + * the row of the upper left corner of the area <code>c</code> is + * supposed to occupy. The topmost row has index 0. + * @param column2 + * the column of the lower right corner of the area + * <code>c</code> is supposed to occupy. + * @param row2 + * the row of the lower right corner of the area <code>c</code> + * is supposed to occupy. + * @throws OverlapsException + * if the new component overlaps with any of the components + * already in the grid. + * @throws OutOfBoundsException + * if the cells are outside the grid area. + */ + public void addComponent(Component component, int column1, int row1, + int column2, int row2) throws OverlapsException, + OutOfBoundsException { + + if (component == null) { + throw new NullPointerException("Component must not be null"); + } + + // Checks that the component does not already exist in the container + if (components.contains(component)) { + throw new IllegalArgumentException( + "Component is already in the container"); + } + + // Creates the area + final Area area = new Area(component, column1, row1, column2, row2); + + // Checks the validity of the coordinates + if (column2 < column1 || row2 < row1) { + throw new IllegalArgumentException( + "Illegal coordinates for the component"); + } + if (column1 < 0 || row1 < 0 || column2 >= getColumns() + || row2 >= getRows()) { + throw new OutOfBoundsException(area); + } + + // Checks that newItem does not overlap with existing items + checkExistingOverlaps(area); + + // Inserts the component to right place at the list + // Respect top-down, left-right ordering + // component.setParent(this); + final Iterator<Area> i = areas.iterator(); + int index = 0; + boolean done = false; + while (!done && i.hasNext()) { + final Area existingArea = i.next(); + if ((existingArea.row1 >= row1 && existingArea.column1 > column1) + || existingArea.row1 > row1) { + areas.add(index, area); + components.add(index, component); + done = true; + } + index++; + } + if (!done) { + areas.addLast(area); + components.addLast(component); + } + + // Attempt to add to super + try { + super.addComponent(component); + } catch (IllegalArgumentException e) { + areas.remove(area); + components.remove(component); + throw e; + } + + // update cursor position, if it's within this area; use first position + // outside this area, even if it's occupied + if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1 + && cursorY <= row2) { + // cursor within area + cursorX = column2 + 1; // one right of area + if (cursorX >= getColumns()) { + // overflowed columns + cursorX = 0; // first col + // move one row down, or one row under the area + cursorY = (column1 == 0 ? row2 : row1) + 1; + } else { + cursorY = row1; + } + } + + requestRepaint(); + } + + /** + * Tests if the given area overlaps with any of the items already on the + * grid. + * + * @param area + * the Area to be checked for overlapping. + * @throws OverlapsException + * if <code>area</code> overlaps with any existing area. + */ + private void checkExistingOverlaps(Area area) throws OverlapsException { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area existingArea = i.next(); + if (existingArea.overlaps(area)) { + // Component not added, overlaps with existing component + throw new OverlapsException(existingArea); + } + } + } + + /** + * Adds the component to the grid in cells column1,row1 (NortWest corner of + * the area.) End coordinates (SouthEast corner of the area) are the same as + * column1,row1. The coordinates are zero-based. Component width and height + * is 1. + * + * @param component + * the component to be added. + * @param column + * the column index, starting from 0. + * @param row + * the row index, starting from 0. + * @throws OverlapsException + * if the new component overlaps with any of the components + * already in the grid. + * @throws OutOfBoundsException + * if the cell is outside the grid area. + */ + public void addComponent(Component component, int column, int row) + throws OverlapsException, OutOfBoundsException { + this.addComponent(component, column, row, column, row); + } + + /** + * Forces the next component to be added at the beginning of the next line. + * + * <p> + * Sets the cursor column to 0 and increments the cursor row by one. + * </p> + * + * <p> + * By calling this function you can ensure that no more components are added + * right of the previous component. + * </p> + * + * @see #space() + */ + public void newLine() { + cursorX = 0; + cursorY++; + } + + /** + * Moves the cursor forward by one. If the cursor goes out of the right grid + * border, it is moved to the first column of the next row. + * + * @see #newLine() + */ + public void space() { + cursorX++; + if (cursorX >= getColumns()) { + cursorX = 0; + cursorY++; + } + } + + /** + * Adds the component into this container to the cursor position. If the + * cursor position is already occupied, the cursor is moved forwards to find + * free position. If the cursor goes out from the bottom of the grid, the + * grid is automatically extended. + * + * @param component + * the component to be added. + */ + @Override + public void addComponent(Component component) { + + // Finds first available place from the grid + Area area; + boolean done = false; + while (!done) { + try { + area = new Area(component, cursorX, cursorY, cursorX, cursorY); + checkExistingOverlaps(area); + done = true; + } catch (final OverlapsException e) { + space(); + } + } + + // Extends the grid if needed + if (cursorX >= getColumns()) { + setColumns(cursorX + 1); + } + if (cursorY >= getRows()) { + setRows(cursorY + 1); + } + + addComponent(component, cursorX, cursorY); + } + + /** + * Removes the specified component from the layout. + * + * @param component + * the component to be removed. + */ + @Override + public void removeComponent(Component component) { + + // Check that the component is contained in the container + if (component == null || !components.contains(component)) { + return; + } + + Area area = null; + for (final Iterator<Area> i = areas.iterator(); area == null + && i.hasNext();) { + final Area a = i.next(); + if (a.getComponent() == component) { + area = a; + } + } + + components.remove(component); + if (area != null) { + areas.remove(area); + } + + componentToAlignment.remove(component); + + super.removeComponent(component); + + requestRepaint(); + } + + /** + * Removes the component specified by its cell coordinates. + * + * @param column + * the component's column, starting from 0. + * @param row + * the component's row, starting from 0. + */ + public void removeComponent(int column, int row) { + + // Finds the area + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.getColumn1() == column && area.getRow1() == row) { + removeComponent(area.getComponent()); + return; + } + } + } + + /** + * Gets an Iterator for the components contained in the layout. By using the + * Iterator it is possible to step through the contents of the layout. + * + * @return the Iterator of the components inside the layout. + */ + @Override + public Iterator<Component> getComponentIterator() { + return Collections.unmodifiableCollection(components).iterator(); + } + + /** + * Gets the number of components contained in the layout. Consistent with + * the iterator returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + + /** + * Paints the contents of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + // TODO refactor attribute names in future release. + target.addAttribute("structuralChange", structuralChange); + structuralChange = false; + + // Area iterator + final Iterator<Area> areaiterator = areas.iterator(); + + // Current item to be processed (fetch first item) + Area area = areaiterator.hasNext() ? (Area) areaiterator.next() : null; + + // Collects rowspan related information here + final HashMap<Integer, Integer> cellUsed = new HashMap<Integer, Integer>(); + + // Empty cell collector + int emptyCells = 0; + + final String[] alignmentsArray = new String[components.size()]; + final Integer[] columnExpandRatioArray = new Integer[getColumns()]; + final Integer[] rowExpandRatioArray = new Integer[getRows()]; + + int realColExpandRatioSum = 0; + float colSum = getExpandRatioSum(columnExpandRatio); + if (colSum == 0) { + // no columns has been expanded, all cols have same expand + // rate + float equalSize = 1 / (float) getColumns(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < getColumns(); i++) { + columnExpandRatioArray[i] = myRatio; + } + realColExpandRatioSum = myRatio * getColumns(); + } else { + for (int i = 0; i < getColumns(); i++) { + int myRatio = Math + .round((getColumnExpandRatio(i) / colSum) * 1000); + columnExpandRatioArray[i] = myRatio; + realColExpandRatioSum += myRatio; + } + } + + boolean equallyDividedRows = false; + int realRowExpandRatioSum = 0; + float rowSum = getExpandRatioSum(rowExpandRatio); + if (rowSum == 0) { + // no rows have been expanded + equallyDividedRows = true; + float equalSize = 1 / (float) getRows(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < getRows(); i++) { + rowExpandRatioArray[i] = myRatio; + } + realRowExpandRatioSum = myRatio * getRows(); + } + + int index = 0; + + // Iterates every applicable row + for (int cury = 0; cury < getRows(); cury++) { + target.startTag("gr"); + + if (!equallyDividedRows) { + int myRatio = Math + .round((getRowExpandRatio(cury) / rowSum) * 1000); + rowExpandRatioArray[cury] = myRatio; + realRowExpandRatioSum += myRatio; + + } + // Iterates every applicable column + for (int curx = 0; curx < getColumns(); curx++) { + + // Checks if current item is located at curx,cury + if (area != null && (area.row1 == cury) + && (area.column1 == curx)) { + + // First check if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", curx - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + emptyCells = 0; + } + + // Now proceed rendering current item + final int cols = (area.column2 - area.column1) + 1; + final int rows = (area.row2 - area.row1) + 1; + target.startTag("gc"); + + target.addAttribute("x", curx); + target.addAttribute("y", cury); + + if (cols > 1) { + target.addAttribute("w", cols); + } + if (rows > 1) { + target.addAttribute("h", rows); + } + LegacyPaint.paint(area.getComponent(), target); + + alignmentsArray[index++] = String + .valueOf(getComponentAlignment(area.getComponent()) + .getBitMask()); + + target.endTag("gc"); + + // Fetch next item + if (areaiterator.hasNext()) { + area = areaiterator.next(); + } else { + area = null; + } + + // Updates the cellUsed if rowspan needed + if (rows > 1) { + int spannedx = curx; + for (int j = 1; j <= cols; j++) { + cellUsed.put(new Integer(spannedx), new Integer( + cury + rows - 1)); + spannedx++; + } + } + + // Skips the current item's spanned columns + if (cols > 1) { + curx += cols - 1; + } + + } else { + + // Checks against cellUsed, render space or ignore cell + if (cellUsed.containsKey(new Integer(curx))) { + + // Current column contains already an item, + // check if rowspan affects at current x,y position + final int rowspanDepth = cellUsed + .get(new Integer(curx)).intValue(); + + if (rowspanDepth >= cury) { + + // ignore cell + // Check if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", curx - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + + emptyCells = 0; + } + } else { + + // empty cell is needed + emptyCells++; + + // Removes the cellUsed key as it has become + // obsolete + cellUsed.remove(Integer.valueOf(curx)); + } + } else { + + // empty cell is needed + emptyCells++; + } + } + + } // iterates every column + + // Last column handled of current row + + // Checks if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", getColumns() - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + + emptyCells = 0; + } + + target.endTag("gr"); + } // iterates every row + + // Last row handled + + // correct possible rounding error + if (rowExpandRatioArray.length > 0) { + rowExpandRatioArray[0] -= realRowExpandRatioSum - 1000; + } + if (columnExpandRatioArray.length > 0) { + columnExpandRatioArray[0] -= realColExpandRatioSum - 1000; + } + + target.addAttribute("colExpand", columnExpandRatioArray); + target.addAttribute("rowExpand", rowExpandRatioArray); + + // Add child component alignment info to layout tag + target.addAttribute("alignments", alignmentsArray); + + } + + private float getExpandRatioSum(Map<Integer, Float> ratioMap) { + float sum = 0; + for (Iterator<Entry<Integer, Float>> iterator = ratioMap.entrySet() + .iterator(); iterator.hasNext();) { + sum += iterator.next().getValue(); + } + return sum; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com + * .vaadin.ui.Component) + */ + @Override + public Alignment getComponentAlignment(Component childComponent) { + Alignment alignment = componentToAlignment.get(childComponent); + if (alignment == null) { + return ALIGNMENT_DEFAULT; + } else { + return alignment; + } + } + + /** + * Defines a rectangular area of cells in a GridLayout. + * + * <p> + * Also maintains a reference to the component contained in the area. + * </p> + * + * <p> + * The area is specified by the cell coordinates of its upper left corner + * (column1,row1) and lower right corner (column2,row2). As otherwise with + * GridLayout, the column and row coordinates start from zero. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class Area implements Serializable { + + /** + * The column of the upper left corner cell of the area. + */ + private final int column1; + + /** + * The row of the upper left corner cell of the area. + */ + private int row1; + + /** + * The column of the lower right corner cell of the area. + */ + private final int column2; + + /** + * The row of the lower right corner cell of the area. + */ + private int row2; + + /** + * Component painted in the area. + */ + private Component component; + + /** + * <p> + * Construct a new area on a grid. + * </p> + * + * @param component + * the component connected to the area. + * @param column1 + * The column of the upper left corner cell of the area. The + * leftmost column has index 0. + * @param row1 + * The row of the upper left corner cell of the area. The + * topmost row has index 0. + * @param column2 + * The column of the lower right corner cell of the area. The + * leftmost column has index 0. + * @param row2 + * The row of the lower right corner cell of the area. The + * topmost row has index 0. + */ + public Area(Component component, int column1, int row1, int column2, + int row2) { + this.column1 = column1; + this.row1 = row1; + this.column2 = column2; + this.row2 = row2; + this.component = component; + } + + /** + * Tests if this Area overlaps with another Area. + * + * @param other + * the other Area that is to be tested for overlap with this + * area + * @return <code>true</code> if <code>other</code> area overlaps with + * this on, <code>false</code> if it does not. + */ + public boolean overlaps(Area other) { + return column1 <= other.getColumn2() && row1 <= other.getRow2() + && column2 >= other.getColumn1() && row2 >= other.getRow1(); + + } + + /** + * Gets the component connected to the area. + * + * @return the Component. + */ + public Component getComponent() { + return component; + } + + /** + * Sets the component connected to the area. + * + * <p> + * This function only sets the value in the data structure and does not + * send any events or set parents. + * </p> + * + * @param newComponent + * the new connected overriding the existing one. + */ + protected void setComponent(Component newComponent) { + component = newComponent; + } + + /** + * @deprecated Use {@link #getColumn1()} instead. + */ + @Deprecated + public int getX1() { + return getColumn1(); + } + + /** + * Gets the column of the top-left corner cell. + * + * @return the column of the top-left corner cell. + */ + public int getColumn1() { + return column1; + } + + /** + * @deprecated Use {@link #getColumn2()} instead. + */ + @Deprecated + public int getX2() { + return getColumn2(); + } + + /** + * Gets the column of the bottom-right corner cell. + * + * @return the column of the bottom-right corner cell. + */ + public int getColumn2() { + return column2; + } + + /** + * @deprecated Use {@link #getRow1()} instead. + */ + @Deprecated + public int getY1() { + return getRow1(); + } + + /** + * Gets the row of the top-left corner cell. + * + * @return the row of the top-left corner cell. + */ + public int getRow1() { + return row1; + } + + /** + * @deprecated Use {@link #getRow2()} instead. + */ + @Deprecated + public int getY2() { + return getRow2(); + } + + /** + * Gets the row of the bottom-right corner cell. + * + * @return the row of the bottom-right corner cell. + */ + public int getRow2() { + return row2; + } + + } + + /** + * Gridlayout does not support laying components on top of each other. An + * <code>OverlapsException</code> is thrown when a component already exists + * (even partly) at the same space on a grid with the new component. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class OverlapsException extends java.lang.RuntimeException { + + private final Area existingArea; + + /** + * Constructs an <code>OverlapsException</code>. + * + * @param existingArea + */ + public OverlapsException(Area existingArea) { + this.existingArea = existingArea; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(); + Component component = existingArea.getComponent(); + sb.append(component); + sb.append("( type = "); + sb.append(component.getClass().getName()); + if (component.getCaption() != null) { + sb.append(", caption = \""); + sb.append(component.getCaption()); + sb.append("\""); + } + sb.append(")"); + sb.append(" is already added to "); + sb.append(existingArea.column1); + sb.append(","); + sb.append(existingArea.column1); + sb.append(","); + sb.append(existingArea.row1); + sb.append(","); + sb.append(existingArea.row2); + sb.append("(column1, column2, row1, row2)."); + + return sb.toString(); + } + + /** + * Gets the area . + * + * @return the existing area. + */ + public Area getArea() { + return existingArea; + } + } + + /** + * An <code>Exception</code> object which is thrown when an area exceeds the + * bounds of the grid. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class OutOfBoundsException extends java.lang.RuntimeException { + + private final Area areaOutOfBounds; + + /** + * Constructs an <code>OoutOfBoundsException</code> with the specified + * detail message. + * + * @param areaOutOfBounds + */ + public OutOfBoundsException(Area areaOutOfBounds) { + this.areaOutOfBounds = areaOutOfBounds; + } + + /** + * Gets the area that is out of bounds. + * + * @return the area out of Bound. + */ + public Area getArea() { + return areaOutOfBounds; + } + } + + /** + * Sets the number of columns in the grid. The column count can not be + * reduced if there are any areas that would be outside of the shrunk grid. + * + * @param columns + * the new number of columns in the grid. + */ + public void setColumns(int columns) { + + // The the param + if (columns < 1) { + throw new IllegalArgumentException( + "The number of columns and rows in the grid must be at least 1"); + } + + // In case of no change + if (getColumns() == columns) { + return; + } + + // Checks for overlaps + if (getColumns() > columns) { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.column2 >= columns) { + throw new OutOfBoundsException(area); + } + } + } + + getState().setColumns(columns); + + requestRepaint(); + } + + /** + * Get the number of columns in the grid. + * + * @return the number of columns in the grid. + */ + public int getColumns() { + return getState().getColumns(); + } + + /** + * Sets the number of rows in the grid. The number of rows can not be + * reduced if there are any areas that would be outside of the shrunk grid. + * + * @param rows + * the new number of rows in the grid. + */ + public void setRows(int rows) { + + // The the param + if (rows < 1) { + throw new IllegalArgumentException( + "The number of columns and rows in the grid must be at least 1"); + } + + // In case of no change + if (getRows() == rows) { + return; + } + + // Checks for overlaps + if (getRows() > rows) { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.row2 >= rows) { + throw new OutOfBoundsException(area); + } + } + } + + getState().setRows(rows); + + requestRepaint(); + } + + /** + * Get the number of rows in the grid. + * + * @return the number of rows in the grid. + */ + public int getRows() { + return getState().getRows(); + } + + /** + * Gets the current x-position (column) of the cursor. + * + * <p> + * The cursor position points the position for the next component that is + * added without specifying its coordinates (grid cell). When the cursor + * position is occupied, the next component will be added to first free + * position after the cursor. + * </p> + * + * @return the grid column the cursor is on, starting from 0. + */ + public int getCursorX() { + return cursorX; + } + + /** + * Sets the current cursor x-position. This is usually handled automatically + * by GridLayout. + * + * @param cursorX + */ + public void setCursorX(int cursorX) { + this.cursorX = cursorX; + } + + /** + * Gets the current y-position (row) of the cursor. + * + * <p> + * The cursor position points the position for the next component that is + * added without specifying its coordinates (grid cell). When the cursor + * position is occupied, the next component will be added to the first free + * position after the cursor. + * </p> + * + * @return the grid row the Cursor is on. + */ + public int getCursorY() { + return cursorY; + } + + /** + * Sets the current y-coordinate (row) of the cursor. This is usually + * handled automatically by GridLayout. + * + * @param cursorY + * the row number, starting from 0 for the topmost row. + */ + public void setCursorY(int cursorY) { + this.cursorY = cursorY; + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + Area oldLocation = null; + Area newLocation = null; + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area location = i.next(); + final Component component = location.getComponent(); + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + } + + if (oldLocation == null) { + addComponent(newComponent); + } else if (newLocation == null) { + removeComponent(oldComponent); + addComponent(newComponent, oldLocation.getColumn1(), + oldLocation.getRow1(), oldLocation.getColumn2(), + oldLocation.getRow2()); + } else { + oldLocation.setComponent(newComponent); + newLocation.setComponent(oldComponent); + requestRepaint(); + } + } + + /* + * Removes all components from this container. + * + * @see com.vaadin.ui.ComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + super.removeAllComponents(); + componentToAlignment = new HashMap<Component, Alignment>(); + cursorX = 0; + cursorY = 0; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .vaadin.ui.Component, int, int) + */ + @Override + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + componentToAlignment.put(childComponent, new Alignment( + horizontalAlignment + verticalAlignment)); + requestRepaint(); + } + + @Override + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + componentToAlignment.put(childComponent, alignment); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + @Override + public void setSpacing(boolean spacing) { + getState().setSpacing(spacing); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Override + public boolean isSpacing() { + return getState().isSpacing(); + } + + /** + * Inserts an empty row at the specified position in the grid. + * + * @param row + * Index of the row before which the new row will be inserted. + * The leftmost row has index 0. + */ + public void insertRow(int row) { + if (row > getRows()) { + throw new IllegalArgumentException("Cannot insert row at " + row + + " in a gridlayout with height " + getRows()); + } + + for (Iterator<Area> i = areas.iterator(); i.hasNext();) { + Area existingArea = i.next(); + // Areas ending below the row needs to be moved down or stretched + if (existingArea.row2 >= row) { + existingArea.row2++; + + // Stretch areas that span over the selected row + if (existingArea.row1 >= row) { + existingArea.row1++; + } + + } + } + + if (cursorY >= row) { + cursorY++; + } + + setRows(getRows() + 1); + structuralChange = true; + requestRepaint(); + } + + /** + * Removes a row and all the components in the row. + * + * <p> + * Components which span over several rows are removed if the selected row + * is on the first row of such a component. + * </p> + * + * <p> + * If the last row is removed then all remaining components will be removed + * and the grid will be reduced to one row. The cursor will be moved to the + * upper left cell of the grid. + * </p> + * + * @param row + * Index of the row to remove. The leftmost row has index 0. + */ + public void removeRow(int row) { + if (row >= getRows()) { + throw new IllegalArgumentException("Cannot delete row " + row + + " from a gridlayout with height " + getRows()); + } + + // Remove all components in row + for (int col = 0; col < getColumns(); col++) { + removeComponent(col, row); + } + + // Shrink or remove areas in the selected row + for (Iterator<Area> i = areas.iterator(); i.hasNext();) { + Area existingArea = i.next(); + if (existingArea.row2 >= row) { + existingArea.row2--; + + if (existingArea.row1 > row) { + existingArea.row1--; + } + } + } + + if (getRows() == 1) { + /* + * Removing the last row means that the dimensions of the Grid + * layout will be truncated to 1 empty row and the cursor is moved + * to the first cell + */ + cursorX = 0; + cursorY = 0; + } else { + setRows(getRows() - 1); + if (cursorY > row) { + cursorY--; + } + } + + structuralChange = true; + requestRepaint(); + + } + + /** + * Sets the expand ratio of given column. + * + * <p> + * The expand ratio defines how excess space is distributed among columns. + * Excess space means space that is left over from components that are not + * sized relatively. By default, the excess space is distributed evenly. + * </p> + * + * <p> + * Note that the component width of the GridLayout must be defined (fixed or + * relative, as opposed to undefined) for this method to have any effect. + * </p> + * + * @see #setWidth(float, int) + * + * @param columnIndex + * @param ratio + */ + public void setColumnExpandRatio(int columnIndex, float ratio) { + columnExpandRatio.put(columnIndex, ratio); + requestRepaint(); + } + + /** + * Returns the expand ratio of given column + * + * @see #setColumnExpandRatio(int, float) + * + * @param columnIndex + * @return the expand ratio, 0.0f by default + */ + public float getColumnExpandRatio(int columnIndex) { + Float r = columnExpandRatio.get(columnIndex); + return r == null ? 0 : r.floatValue(); + } + + /** + * Sets the expand ratio of given row. + * + * <p> + * Expand ratio defines how excess space is distributed among rows. Excess + * space means the space left over from components that are not sized + * relatively. By default, the excess space is distributed evenly. + * </p> + * + * <p> + * Note, that height needs to be defined (fixed or relative, as opposed to + * undefined height) for this method to have any effect. + * </p> + * + * @see #setHeight(float, int) + * + * @param rowIndex + * The row index, starting from 0 for the topmost row. + * @param ratio + */ + public void setRowExpandRatio(int rowIndex, float ratio) { + rowExpandRatio.put(rowIndex, ratio); + requestRepaint(); + } + + /** + * Returns the expand ratio of given row. + * + * @see #setRowExpandRatio(int, float) + * + * @param rowIndex + * The row index, starting from 0 for the topmost row. + * @return the expand ratio, 0.0f by default + */ + public float getRowExpandRatio(int rowIndex) { + Float r = rowExpandRatio.get(rowIndex); + return r == null ? 0 : r.floatValue(); + } + + /** + * Gets the Component at given index. + * + * @param x + * The column index, starting from 0 for the leftmost column. + * @param y + * The row index, starting from 0 for the topmost row. + * @return Component in given cell or null if empty + */ + public Component getComponent(int x, int y) { + for (final Iterator<Area> iterator = areas.iterator(); iterator + .hasNext();) { + final Area area = iterator.next(); + if (area.getColumn1() <= x && x <= area.getColumn2() + && area.getRow1() <= y && y <= area.getRow2()) { + return area.getComponent(); + } + } + return null; + } + + /** + * Returns information about the area where given component is laid in the + * GridLayout. + * + * @param component + * the component whose area information is requested. + * @return an Area object that contains information how component is laid in + * the grid + */ + public Area getComponentArea(Component component) { + for (final Iterator<Area> iterator = areas.iterator(); iterator + .hasNext();) { + final Area area = iterator.next(); + if (area.getComponent() == component) { + return area; + } + } + return null; + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + +} diff --git a/server/src/com/vaadin/ui/HasComponents.java b/server/src/com/vaadin/ui/HasComponents.java new file mode 100644 index 0000000000..3ebd63bff2 --- /dev/null +++ b/server/src/com/vaadin/ui/HasComponents.java @@ -0,0 +1,49 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.util.Iterator; + +/** + * Interface that must be implemented by all {@link Component}s that contain + * other {@link Component}s. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public interface HasComponents extends Component, Iterable<Component> { + /** + * Gets an iterator to the collection of contained components. Using this + * iterator it is possible to step through all components contained in this + * container. + * + * @return the component iterator. + * + * @deprecated Use {@link #iterator()} instead. + */ + @Deprecated + public Iterator<Component> getComponentIterator(); + + /** + * Checks if the child component is visible. This method allows hiding a + * child component from updates and communication to and from the client. + * This is useful for components that show only a limited number of its + * children at any given time and want to allow updates only for the + * children that are visible (e.g. TabSheet has one tab open at a time). + * <p> + * Note that this will prevent updates from reaching the child even though + * the child itself is set to visible. Also if a child is set to invisible + * this will not force it to be visible. + * </p> + * + * @param childComponent + * The child component to check + * @return true if the child component is visible to the user, false + * otherwise + */ + public boolean isComponentVisible(Component childComponent); + +} diff --git a/server/src/com/vaadin/ui/HorizontalLayout.java b/server/src/com/vaadin/ui/HorizontalLayout.java new file mode 100644 index 0000000000..b9dc1c13ca --- /dev/null +++ b/server/src/com/vaadin/ui/HorizontalLayout.java @@ -0,0 +1,24 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * Horizontal layout + * + * <code>HorizontalLayout</code> is a component container, which shows the + * subcomponents in the order of their addition (horizontally). + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.3 + */ +@SuppressWarnings("serial") +public class HorizontalLayout extends AbstractOrderedLayout { + + public HorizontalLayout() { + + } + +} diff --git a/server/src/com/vaadin/ui/HorizontalSplitPanel.java b/server/src/com/vaadin/ui/HorizontalSplitPanel.java new file mode 100644 index 0000000000..5bd6c8a075 --- /dev/null +++ b/server/src/com/vaadin/ui/HorizontalSplitPanel.java @@ -0,0 +1,34 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * A horizontal split panel contains two components and lays them horizontally. + * The first component is on the left side. + * + * <pre> + * + * +---------------------++----------------------+ + * | || | + * | The first component || The second component | + * | || | + * +---------------------++----------------------+ + * + * ^ + * | + * the splitter + * + * </pre> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.5 + */ +public class HorizontalSplitPanel extends AbstractSplitPanel { + public HorizontalSplitPanel() { + super(); + setSizeFull(); + } +} diff --git a/server/src/com/vaadin/ui/Html5File.java b/server/src/com/vaadin/ui/Html5File.java new file mode 100644 index 0000000000..aa3fb558fa --- /dev/null +++ b/server/src/com/vaadin/ui/Html5File.java @@ -0,0 +1,65 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.event.dd.DropHandler; +import com.vaadin.terminal.StreamVariable; + +/** + * {@link DragAndDropWrapper} can receive also files from client computer if + * appropriate HTML 5 features are supported on client side. This class wraps + * information about dragged file on server side. + */ +public class Html5File implements Serializable { + + private String name; + private long size; + private StreamVariable streamVariable; + private String type; + + Html5File(String name, long size, String mimeType) { + this.name = name; + this.size = size; + type = mimeType; + } + + public String getFileName() { + return name; + } + + public long getFileSize() { + return size; + } + + public String getType() { + return type; + } + + /** + * Sets the {@link StreamVariable} that into which the file contents will be + * written. Usage of StreamVariable is similar to {@link Upload} component. + * <p> + * If the {@link StreamVariable} is not set in the {@link DropHandler} the + * file contents will not be sent to server. + * <p> + * <em>Note!</em> receiving file contents is experimental feature depending + * on HTML 5 API's. It is supported only by modern web browsers like Firefox + * 3.6 and above and recent webkit based browsers (Safari 5, Chrome 6) at + * this time. + * + * @param streamVariable + * the callback that returns stream where the implementation + * writes the file contents as it arrives. + */ + public void setStreamVariable(StreamVariable streamVariable) { + this.streamVariable = streamVariable; + } + + public StreamVariable getStreamVariable() { + return streamVariable; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/InlineDateField.java b/server/src/com/vaadin/ui/InlineDateField.java new file mode 100644 index 0000000000..cf61703318 --- /dev/null +++ b/server/src/com/vaadin/ui/InlineDateField.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Property; + +/** + * <p> + * A date entry component, which displays the actual date selector inline. + * + * </p> + * + * @see DateField + * @see PopupDateField + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +public class InlineDateField extends DateField { + + public InlineDateField() { + super(); + } + + public InlineDateField(Property dataSource) throws IllegalArgumentException { + super(dataSource); + } + + public InlineDateField(String caption, Date value) { + super(caption, value); + } + + public InlineDateField(String caption, Property dataSource) { + super(caption, dataSource); + } + + public InlineDateField(String caption) { + super(caption); + } + +} diff --git a/server/src/com/vaadin/ui/JavaScript.java b/server/src/com/vaadin/ui/JavaScript.java new file mode 100644 index 0000000000..0b4669728a --- /dev/null +++ b/server/src/com/vaadin/ui/JavaScript.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc; +import com.vaadin.shared.extension.javascriptmanager.JavaScriptManagerState; +import com.vaadin.terminal.AbstractExtension; +import com.vaadin.terminal.Page; + +/** + * Provides access to JavaScript functionality in the web browser. To get an + * instance of JavaScript, either use Page.getJavaScript() or + * JavaScript.getCurrent() as a shorthand for getting the JavaScript object + * corresponding to the current Page. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public class JavaScript extends AbstractExtension { + private Map<String, JavaScriptFunction> functions = new HashMap<String, JavaScriptFunction>(); + + // Can not be defined in client package as this JSONArray is not available + // in GWT + public interface JavaScriptCallbackRpc extends ServerRpc { + public void call(String name, JSONArray arguments); + } + + /** + * Creates a new JavaScript object. You should typically not this, but + * instead use the JavaScript object already associated with your Page + * object. + */ + public JavaScript() { + registerRpc(new JavaScriptCallbackRpc() { + @Override + public void call(String name, JSONArray arguments) { + JavaScriptFunction function = functions.get(name); + // TODO handle situation if name is not registered + try { + function.call(arguments); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + }); + } + + @Override + public JavaScriptManagerState getState() { + return (JavaScriptManagerState) super.getState(); + } + + /** + * Add a new function to the global JavaScript namespace (i.e. the window + * object). The <code>call</code> method in the passed + * {@link JavaScriptFunction} object will be invoked with the same + * parameters whenever the JavaScript function is called in the browser. + * + * A function added with the name <code>"myFunction"</code> can thus be + * invoked with the following JavaScript code: + * <code>window.myFunction(argument1, argument2)</code>. + * + * If the name parameter contains dots, simple objects are created on demand + * to allow calling the function using the same name (e.g. + * <code>window.myObject.myFunction</code>). + * + * @param name + * the name that the function should get in the global JavaScript + * namespace. + * @param function + * the JavaScriptFunction that will be invoked if the JavaScript + * function is called. + */ + public void addFunction(String name, JavaScriptFunction function) { + functions.put(name, function); + if (getState().getNames().add(name)) { + requestRepaint(); + } + } + + /** + * Removes a JavaScripFunction from the browser's global JavaScript + * namespace. + * + * If the name contains dots and intermediate objects were created by + * {@link #addFunction(String, JavaScriptFunction)}, these objects will not + * be removed by this method. + * + * @param name + * the name of the callback to remove + */ + public void removeFunction(String name) { + functions.remove(name); + if (getState().getNames().remove(name)) { + requestRepaint(); + } + } + + /** + * Executes the given JavaScript code in the browser. + * + * @param script + * The JavaScript code to run. + */ + public void execute(String script) { + getRpcProxy(ExecuteJavaScriptRpc.class).executeJavaScript(script); + } + + /** + * Executes the given JavaScript code in the browser. + * + * @param script + * The JavaScript code to run. + */ + public static void eval(String script) { + getCurrent().execute(script); + } + + /** + * Get the JavaScript object for the current Page, or null if there is no + * current page. + * + * @see Page#getCurrent() + * + * @return the JavaScript object corresponding to the current Page, or + * <code>null</code> if there is no current page. + */ + public static JavaScript getCurrent() { + Page page = Page.getCurrent(); + if (page == null) { + return null; + } + return page.getJavaScript(); + } + + /** + * JavaScript is not designed to be removed. + * + * @throws UnsupportedOperationException + * when invoked + */ + @Override + public void removeFromTarget() { + throw new UnsupportedOperationException( + "JavaScript is not designed to be removed."); + } + +} diff --git a/server/src/com/vaadin/ui/JavaScriptFunction.java b/server/src/com/vaadin/ui/JavaScriptFunction.java new file mode 100644 index 0000000000..e39ae9b87b --- /dev/null +++ b/server/src/com/vaadin/ui/JavaScriptFunction.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.terminal.AbstractJavaScriptExtension; + +/** + * Defines a method that is called by a client-side JavaScript function. When + * the corresponding JavaScript function is called, the {@link #call(JSONArray)} + * method is invoked. + * + * @see JavaScript#addFunction(String, JavaScriptCallback) + * @see AbstractJavaScriptComponent#addFunction(String, JavaScriptCallback) + * @see AbstractJavaScriptExtension#addFunction(String, JavaScriptCallback) + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public interface JavaScriptFunction extends Serializable { + /** + * Invoked whenever the corresponding JavaScript function is called in the + * browser. + * <p> + * Because of the asynchronous nature of the communication between client + * and server, no return value can be sent back to the browser. + * + * @param arguments + * an array with JSON representations of the arguments with which + * the JavaScript function was called. + * @throws JSONException + * if the arguments can not be interpreted + */ + public void call(JSONArray arguments) throws JSONException; +} diff --git a/server/src/com/vaadin/ui/Label.java b/server/src/com/vaadin/ui/Label.java new file mode 100644 index 0000000000..7e50a37805 --- /dev/null +++ b/server/src/com/vaadin/ui/Label.java @@ -0,0 +1,483 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; +import java.util.logging.Logger; + +import com.vaadin.data.Property; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.shared.ui.label.LabelState; + +/** + * Label component for showing non-editable short texts. + * + * The label content can be set to the modes specified by {@link ContentMode} + * + * <p> + * The contents of the label may contain simple formatting: + * <ul> + * <li><b><b></b> Bold + * <li><b><i></b> Italic + * <li><b><u></b> Underlined + * <li><b><br/></b> Linebreak + * <li><b><ul><li>item 1</li><li>item 2</li></ul></b> List of + * items + * </ul> + * The <b>b</b>,<b>i</b>,<b>u</b> and <b>li</b> tags can contain all the tags in + * the list recursively. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Label extends AbstractComponent implements Property<String>, + Property.Viewer, Property.ValueChangeListener, + Property.ValueChangeNotifier, Comparable<Label> { + + private static final Logger logger = Logger + .getLogger(Label.class.getName()); + + /** + * @deprecated From 7.0, use {@link ContentMode#TEXT} instead + */ + @Deprecated + public static final ContentMode CONTENT_TEXT = ContentMode.TEXT; + + /** + * @deprecated From 7.0, use {@link ContentMode#PREFORMATTED} instead + */ + @Deprecated + public static final ContentMode CONTENT_PREFORMATTED = ContentMode.PREFORMATTED; + + /** + * @deprecated From 7.0, use {@link ContentMode#XHTML} instead + */ + @Deprecated + public static final ContentMode CONTENT_XHTML = ContentMode.XHTML; + + /** + * @deprecated From 7.0, use {@link ContentMode#XML} instead + */ + @Deprecated + public static final ContentMode CONTENT_XML = ContentMode.XML; + + /** + * @deprecated From 7.0, use {@link ContentMode#RAW} instead + */ + @Deprecated + public static final ContentMode CONTENT_RAW = ContentMode.RAW; + + /** + * @deprecated From 7.0, use {@link ContentMode#TEXT} instead + */ + @Deprecated + public static final ContentMode CONTENT_DEFAULT = ContentMode.TEXT; + + /** + * A converter used to convert from the data model type to the field type + * and vice versa. Label type is always String. + */ + private Converter<String, Object> converter = null; + + private Property<String> dataSource = null; + + /** + * Creates an empty Label. + */ + public Label() { + this(""); + } + + /** + * Creates a new instance of Label with text-contents. + * + * @param content + */ + public Label(String content) { + this(content, ContentMode.TEXT); + } + + /** + * Creates a new instance of Label with text-contents read from given + * datasource. + * + * @param contentSource + */ + public Label(Property contentSource) { + this(contentSource, ContentMode.TEXT); + } + + /** + * Creates a new instance of Label with text-contents. + * + * @param content + * @param contentMode + */ + public Label(String content, ContentMode contentMode) { + setValue(content); + setContentMode(contentMode); + setWidth(100, Unit.PERCENTAGE); + } + + /** + * Creates a new instance of Label with text-contents read from given + * datasource. + * + * @param contentSource + * @param contentMode + */ + public Label(Property contentSource, ContentMode contentMode) { + setPropertyDataSource(contentSource); + setContentMode(contentMode); + setWidth(100, Unit.PERCENTAGE); + } + + @Override + public LabelState getState() { + return (LabelState) super.getState(); + } + + /** + * Gets the value of the label. + * <p> + * The value of the label is the text that is shown to the end user. + * Depending on the {@link ContentMode} it is plain text or markup. + * </p> + * + * @return the value of the label. + */ + @Override + public String getValue() { + if (getPropertyDataSource() == null) { + // Use internal value if we are running without a data source + return getState().getText(); + } + return ConverterUtil.convertFromModel(getPropertyDataSource() + .getValue(), String.class, getConverter(), getLocale()); + } + + /** + * Set the value of the label. Value of the label is the XML contents of the + * label. + * + * @param newStringValue + * the New value of the label. + */ + @Override + public void setValue(Object newStringValue) { + if (newStringValue != null && newStringValue.getClass() != String.class) { + throw new Converter.ConversionException("Value of type " + + newStringValue.getClass() + " cannot be assigned to " + + String.class.getName()); + } + if (getPropertyDataSource() == null) { + getState().setText((String) newStringValue); + requestRepaint(); + } else { + throw new IllegalStateException( + "Label is only a Property.Viewer and cannot update its data source"); + } + } + + /** + * Returns the value displayed by this label. + * + * @see java.lang.Object#toString() + * @deprecated As of 7.0.0, use {@link #getValue()} to get the value of the + * label or {@link #getPropertyDataSource()} .getValue() to get + * the value of the data source. + */ + @Deprecated + @Override + public String toString() { + logger.warning("You are using Label.toString() to get the value for a " + + getClass().getSimpleName() + + ". This is not recommended and will not be supported in future versions."); + return getValue(); + } + + /** + * Gets the type of the Property. + * + * @see com.vaadin.data.Property#getType() + */ + @Override + public Class<String> getType() { + return String.class; + } + + /** + * Gets the viewing data-source property. + * + * @return the data source property. + * @see com.vaadin.data.Property.Viewer#getPropertyDataSource() + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * Sets the property as data-source for viewing. + * + * @param newDataSource + * the new data source Property + * @see com.vaadin.data.Property.Viewer#setPropertyDataSource(com.vaadin.data.Property) + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + // Stops listening the old data source changes + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).removeListener(this); + } + + if (!ConverterUtil.canConverterHandle(getConverter(), String.class, + newDataSource.getType())) { + // Try to find a converter + Converter<String, ?> c = ConverterUtil.getConverter(String.class, + newDataSource.getType(), getApplication()); + setConverter(c); + } + dataSource = newDataSource; + + // Listens the new data source if possible + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + requestRepaint(); + } + + /** + * Gets the content mode of the Label. + * + * @return the Content mode of the label. + * + * @see ContentMode + */ + public ContentMode getContentMode() { + return getState().getContentMode(); + } + + /** + * Sets the content mode of the Label. + * + * @param contentMode + * the New content mode of the label. + * + * @see ContentMode + */ + public void setContentMode(ContentMode contentMode) { + if (contentMode == null) { + throw new IllegalArgumentException("Content mode can not be null"); + } + + getState().setContentMode(contentMode); + requestRepaint(); + } + + /* Value change events */ + + private static final Method VALUE_CHANGE_METHOD; + + static { + try { + VALUE_CHANGE_METHOD = Property.ValueChangeListener.class + .getDeclaredMethod("valueChange", + new Class[] { Property.ValueChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in Label"); + } + } + + /** + * Value change event + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ValueChangeEvent extends Component.Event implements + Property.ValueChangeEvent { + + /** + * New instance of text change event + * + * @param source + * the Source of the event. + */ + public ValueChangeEvent(Label source) { + super(source); + } + + /** + * Gets the Property that has been modified. + * + * @see com.vaadin.data.Property.ValueChangeEvent#getProperty() + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + } + + /** + * Adds the value change listener. + * + * @param listener + * the Listener to be added. + * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void addListener(Property.ValueChangeListener listener) { + addListener(Label.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD); + } + + /** + * Removes the value change listener. + * + * @param listener + * the Listener to be removed. + * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void removeListener(Property.ValueChangeListener listener) { + removeListener(Label.ValueChangeEvent.class, listener, + VALUE_CHANGE_METHOD); + } + + /** + * Emits the options change event. + */ + protected void fireValueChange() { + // Set the error message + fireEvent(new Label.ValueChangeEvent(this)); + } + + /** + * Listens the value change events from data source. + * + * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + @Override + public void valueChange(Property.ValueChangeEvent event) { + // Update the internal value from the data source + getState().setText(getValue()); + requestRepaint(); + + fireValueChange(); + } + + private String getComparableValue() { + String stringValue = getValue(); + if (stringValue == null) { + stringValue = ""; + } + + if (getContentMode() == ContentMode.XHTML + || getContentMode() == ContentMode.XML) { + return stripTags(stringValue); + } else { + return stringValue; + } + + } + + /** + * Compares the Label to other objects. + * + * <p> + * Labels can be compared to other labels for sorting label contents. This + * is especially handy for sorting table columns. + * </p> + * + * <p> + * In RAW, PREFORMATTED and TEXT modes, the label contents are compared as + * is. In XML, UIDL and XHTML modes, only CDATA is compared and tags + * ignored. If the other object is not a Label, its toString() return value + * is used in comparison. + * </p> + * + * @param other + * the Other object to compare to. + * @return a negative integer, zero, or a positive integer as this object is + * less than, equal to, or greater than the specified object. + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(Label other) { + + String thisValue = getComparableValue(); + String otherValue = other.getComparableValue(); + + return thisValue.compareTo(otherValue); + } + + /** + * Strips the tags from the XML. + * + * @param xml + * the String containing a XML snippet. + * @return the original XML without tags. + */ + private String stripTags(String xml) { + + final StringBuffer res = new StringBuffer(); + + int processed = 0; + final int xmlLen = xml.length(); + while (processed < xmlLen) { + int next = xml.indexOf('<', processed); + if (next < 0) { + next = xmlLen; + } + res.append(xml.substring(processed, next)); + if (processed < xmlLen) { + next = xml.indexOf('>', processed); + if (next < 0) { + next = xmlLen; + } + processed = next + 1; + } + } + + return res.toString(); + } + + /** + * Gets the converter used to convert the property data source value to the + * label value. + * + * @return The converter or null if none is set. + */ + public Converter<String, Object> getConverter() { + return converter; + } + + /** + * Sets the converter used to convert the label value to the property data + * source type. The converter must have a presentation type of String. + * + * @param converter + * The new converter to use. + */ + public void setConverter(Converter<String, ?> converter) { + this.converter = (Converter<String, Object>) converter; + requestRepaint(); + } + +} diff --git a/server/src/com/vaadin/ui/Layout.java b/server/src/com/vaadin/ui/Layout.java new file mode 100644 index 0000000000..d083f9afdc --- /dev/null +++ b/server/src/com/vaadin/ui/Layout.java @@ -0,0 +1,229 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.shared.ui.AlignmentInfo.Bits; + +/** + * Extension to the {@link ComponentContainer} interface which adds the + * layouting control to the elements in the container. This is required by the + * various layout components to enable them to place other components in + * specific locations in the UI. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Layout extends ComponentContainer, Serializable { + + /** + * Enable layout margins. Affects all four sides of the layout. This will + * tell the client-side implementation to leave extra space around the + * layout. The client-side implementation decides the actual amount, and it + * can vary between themes. + * + * @param enabled + */ + public void setMargin(boolean enabled); + + /** + * Enable specific layout margins. This will tell the client-side + * implementation to leave extra space around the layout in specified edges, + * clockwise from top (top, right, bottom, left). The client-side + * implementation decides the actual amount, and it can vary between themes. + * + * @param top + * @param right + * @param bottom + * @param left + */ + public void setMargin(boolean top, boolean right, boolean bottom, + boolean left); + + /** + * AlignmentHandler is most commonly an advanced {@link Layout} that can + * align its components. + */ + public interface AlignmentHandler extends Serializable { + + /** + * Contained component should be aligned horizontally to the left. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_LEFT = Bits.ALIGNMENT_LEFT; + + /** + * Contained component should be aligned horizontally to the right. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_RIGHT = Bits.ALIGNMENT_RIGHT; + + /** + * Contained component should be aligned vertically to the top. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_TOP = Bits.ALIGNMENT_TOP; + + /** + * Contained component should be aligned vertically to the bottom. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_BOTTOM = Bits.ALIGNMENT_BOTTOM; + + /** + * Contained component should be horizontally aligned to center. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_HORIZONTAL_CENTER = Bits.ALIGNMENT_HORIZONTAL_CENTER; + + /** + * Contained component should be vertically aligned to center. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_VERTICAL_CENTER = Bits.ALIGNMENT_VERTICAL_CENTER; + + /** + * Set alignment for one contained component in this layout. Alignment + * is calculated as a bit mask of the two passed values. + * + * @deprecated Use {@link #setComponentAlignment(Component, Alignment)} + * instead + * + * @param childComponent + * the component to align within it's layout cell. + * @param horizontalAlignment + * the horizontal alignment for the child component (left, + * center, right). Use ALIGNMENT constants. + * @param verticalAlignment + * the vertical alignment for the child component (top, + * center, bottom). Use ALIGNMENT constants. + */ + @Deprecated + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment); + + /** + * Set alignment for one contained component in this layout. Use + * predefined alignments from Alignment class. + * + * Example: <code> + * layout.setComponentAlignment(myComponent, Alignment.TOP_RIGHT); + * </code> + * + * @param childComponent + * the component to align within it's layout cell. + * @param alignment + * the Alignment value to be set + */ + public void setComponentAlignment(Component childComponent, + Alignment alignment); + + /** + * Returns the current Alignment of given component. + * + * @param childComponent + * @return the {@link Alignment} + */ + public Alignment getComponentAlignment(Component childComponent); + + } + + /** + * This type of layout supports automatic addition of space between its + * components. + * + */ + public interface SpacingHandler extends Serializable { + /** + * Enable spacing between child components within this layout. + * + * <p> + * <strong>NOTE:</strong> This will only affect the space between + * components, not the space around all the components in the layout + * (i.e. do not confuse this with the cellspacing attribute of a HTML + * Table). Use {@link #setMargin(boolean)} to add space around the + * layout. + * </p> + * + * <p> + * See the reference manual for more information about CSS rules for + * defining the amount of spacing to use. + * </p> + * + * @param enabled + * true if spacing should be turned on, false if it should be + * turned off + */ + public void setSpacing(boolean enabled); + + /** + * + * @return true if spacing between child components within this layout + * is enabled, false otherwise + */ + public boolean isSpacing(); + } + + /** + * This type of layout supports automatic addition of margins (space around + * its components). + */ + public interface MarginHandler extends Serializable { + /** + * Enable margins for this layout. + * + * <p> + * <strong>NOTE:</strong> This will only affect the space around the + * components in the layout, not space between the components in the + * layout. Use {@link #setSpacing(boolean)} to add space between the + * components in the layout. + * </p> + * + * <p> + * See the reference manual for more information about CSS rules for + * defining the size of the margin. + * </p> + * + * @param marginInfo + * MarginInfo object containing the new margins. + */ + public void setMargin(MarginInfo marginInfo); + + /** + * + * @return MarginInfo containing the currently enabled margins. + */ + public MarginInfo getMargin(); + } + + @SuppressWarnings("serial") + public static class MarginInfo extends VMarginInfo implements Serializable { + + public MarginInfo(boolean enabled) { + super(enabled, enabled, enabled, enabled); + } + + public MarginInfo(boolean top, boolean right, boolean bottom, + boolean left) { + super(top, right, bottom, left); + } + } +} diff --git a/server/src/com/vaadin/ui/Link.java b/server/src/com/vaadin/ui/Link.java new file mode 100644 index 0000000000..fd105f3255 --- /dev/null +++ b/server/src/com/vaadin/ui/Link.java @@ -0,0 +1,242 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.terminal.Page; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; + +/** + * Link is used to create external or internal URL links. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Link extends AbstractComponent implements Vaadin6Component { + + /* Target window border type constant: No window border */ + public static final int TARGET_BORDER_NONE = Page.BORDER_NONE; + + /* Target window border type constant: Minimal window border */ + public static final int TARGET_BORDER_MINIMAL = Page.BORDER_MINIMAL; + + /* Target window border type constant: Default window border */ + public static final int TARGET_BORDER_DEFAULT = Page.BORDER_DEFAULT; + + private Resource resource = null; + + private String targetName; + + private int targetBorder = TARGET_BORDER_DEFAULT; + + private int targetWidth = -1; + + private int targetHeight = -1; + + /** + * Creates a new link. + */ + public Link() { + + } + + /** + * Creates a new instance of Link. + * + * @param caption + * @param resource + */ + public Link(String caption, Resource resource) { + setCaption(caption); + this.resource = resource; + } + + /** + * Creates a new instance of Link that opens a new window. + * + * + * @param caption + * the Link text. + * @param targetName + * the name of the target window where the link opens to. Empty + * name of null implies that the target is opened to the window + * containing the link. + * @param width + * the Width of the target window. + * @param height + * the Height of the target window. + * @param border + * the Border style of the target window. + * + */ + public Link(String caption, Resource resource, String targetName, + int width, int height, int border) { + setCaption(caption); + this.resource = resource; + setTargetName(targetName); + setTargetWidth(width); + setTargetHeight(height); + setTargetBorder(border); + } + + /** + * Paints the content of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + if (resource != null) { + target.addAttribute("src", resource); + } else { + return; + } + + // Target window name + final String name = getTargetName(); + if (name != null && name.length() > 0) { + target.addAttribute("name", name); + } + + // Target window size + if (getTargetWidth() >= 0) { + target.addAttribute("targetWidth", getTargetWidth()); + } + if (getTargetHeight() >= 0) { + target.addAttribute("targetHeight", getTargetHeight()); + } + + // Target window border + switch (getTargetBorder()) { + case TARGET_BORDER_MINIMAL: + target.addAttribute("border", "minimal"); + break; + case TARGET_BORDER_NONE: + target.addAttribute("border", "none"); + break; + } + } + + /** + * Returns the target window border. + * + * @return the target window border. + */ + public int getTargetBorder() { + return targetBorder; + } + + /** + * Returns the target window height or -1 if not set. + * + * @return the target window height. + */ + public int getTargetHeight() { + return targetHeight < 0 ? -1 : targetHeight; + } + + /** + * Returns the target window name. Empty name of null implies that the + * target is opened to the window containing the link. + * + * @return the target window name. + */ + public String getTargetName() { + return targetName; + } + + /** + * Returns the target window width or -1 if not set. + * + * @return the target window width. + */ + public int getTargetWidth() { + return targetWidth < 0 ? -1 : targetWidth; + } + + /** + * Sets the border of the target window. + * + * @param targetBorder + * the targetBorder to set. + */ + public void setTargetBorder(int targetBorder) { + if (targetBorder == TARGET_BORDER_DEFAULT + || targetBorder == TARGET_BORDER_MINIMAL + || targetBorder == TARGET_BORDER_NONE) { + this.targetBorder = targetBorder; + requestRepaint(); + } + } + + /** + * Sets the target window height. + * + * @param targetHeight + * the targetHeight to set. + */ + public void setTargetHeight(int targetHeight) { + this.targetHeight = targetHeight; + requestRepaint(); + } + + /** + * Sets the target window name. + * + * @param targetName + * the targetName to set. + */ + public void setTargetName(String targetName) { + this.targetName = targetName; + requestRepaint(); + } + + /** + * Sets the target window width. + * + * @param targetWidth + * the targetWidth to set. + */ + public void setTargetWidth(int targetWidth) { + this.targetWidth = targetWidth; + requestRepaint(); + } + + /** + * Returns the resource this link opens. + * + * @return the Resource. + */ + public Resource getResource() { + return resource; + } + + /** + * Sets the resource this link opens. + * + * @param resource + * the resource to set. + */ + public void setResource(Resource resource) { + this.resource = resource; + requestRepaint(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } +} diff --git a/server/src/com/vaadin/ui/ListSelect.java b/server/src/com/vaadin/ui/ListSelect.java new file mode 100644 index 0000000000..35ccb34b3c --- /dev/null +++ b/server/src/com/vaadin/ui/ListSelect.java @@ -0,0 +1,96 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * This is a simple list select without, for instance, support for new items, + * lazyloading, and other advanced features. + */ +@SuppressWarnings("serial") +public class ListSelect extends AbstractSelect { + + private int columns = 0; + private int rows = 0; + + public ListSelect() { + super(); + } + + public ListSelect(String caption, Collection<?> options) { + super(caption, options); + } + + public ListSelect(String caption, Container dataSource) { + super(caption, dataSource); + } + + public ListSelect(String caption) { + super(caption); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + requestRepaint(); + } + } + + public int getColumns() { + return columns; + } + + public int getRows() { + return rows; + } + + /** + * Sets the number of rows in the editor. If the number of rows is set 0, + * the actual number of displayed rows is determined implicitly by the + * adapter. + * + * @param rows + * the number of rows to set. + */ + public void setRows(int rows) { + if (rows < 0) { + rows = 0; + } + if (this.rows != rows) { + this.rows = rows; + requestRepaint(); + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "list"); + // Adds the number of columns + if (columns != 0) { + target.addAttribute("cols", columns); + } + // Adds the number of rows + if (rows != 0) { + target.addAttribute("rows", rows); + } + super.paintContent(target); + } +} diff --git a/server/src/com/vaadin/ui/LoginForm.java b/server/src/com/vaadin/ui/LoginForm.java new file mode 100644 index 0000000000..db7e5f9dd9 --- /dev/null +++ b/server/src/com/vaadin/ui/LoginForm.java @@ -0,0 +1,353 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * LoginForm is a Vaadin component to handle common problem among Ajax + * applications: browsers password managers don't fill dynamically created forms + * like all those UI elements created by Vaadin. + * <p> + * For developer it is easy to use: add component to a desired place in you UI + * and add LoginListener to validate form input. Behind the curtain LoginForm + * creates an iframe with static html that browsers detect. + * <p> + * Login form is by default 100% width and height, so consider using it inside a + * sized {@link Panel} or {@link Window}. + * <p> + * Login page html can be overridden by replacing protected getLoginHTML method. + * As the login page is actually an iframe, styles must be handled manually. By + * default component tries to guess the right place for theme css. + * + * @since 5.3 + */ +public class LoginForm extends CustomComponent { + + private String usernameCaption = "Username"; + private String passwordCaption = "Password"; + private String loginButtonCaption = "Login"; + + private Embedded iframe = new Embedded(); + + private ApplicationResource loginPage = new ApplicationResource() { + + @Override + public Application getApplication() { + return LoginForm.this.getApplication(); + } + + @Override + public int getBufferSize() { + return getLoginHTML().length; + } + + @Override + public long getCacheTime() { + return -1; + } + + @Override + public String getFilename() { + return "login"; + } + + @Override + public DownloadStream getStream() { + return new DownloadStream(new ByteArrayInputStream(getLoginHTML()), + getMIMEType(), getFilename()); + } + + @Override + public String getMIMEType() { + return "text/html; charset=utf-8"; + } + }; + + private final RequestHandler requestHandler = new RequestHandler() { + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + String requestPathInfo = request.getRequestPathInfo(); + if ("/loginHandler".equals(requestPathInfo)) { + response.setCacheTime(-1); + response.setContentType("text/html; charset=utf-8"); + response.getWriter() + .write("<html><body>Login form handled." + + "<script type='text/javascript'>parent.parent.vaadin.forceSync();" + + "</script></body></html>"); + + Map<String, String[]> parameters = request.getParameterMap(); + + HashMap<String, String> params = new HashMap<String, String>(); + // expecting single params + for (Iterator<String> it = parameters.keySet().iterator(); it + .hasNext();) { + String key = it.next(); + String value = (parameters.get(key))[0]; + params.put(key, value); + } + LoginEvent event = new LoginEvent(params); + fireEvent(event); + return true; + } + return false; + } + }; + + public LoginForm() { + iframe.setType(Embedded.TYPE_BROWSER); + iframe.setSizeFull(); + setSizeFull(); + setCompositionRoot(iframe); + addStyleName("v-loginform"); + } + + /** + * Returns byte array containing login page html. If you need to override + * the login html, use the default html as basis. Login page sets its target + * with javascript. + * + * @return byte array containing login page html + */ + protected byte[] getLoginHTML() { + String appUri = getApplication().getURL().toString(); + + try { + return ("<!DOCTYPE html PUBLIC \"-//W3C//DTD " + + "XHTML 1.0 Transitional//EN\" " + + "\"http://www.w3.org/TR/xhtml1/" + + "DTD/xhtml1-transitional.dtd\">\n" + "<html>" + + "<head><script type='text/javascript'>" + + "var setTarget = function() {" + "var uri = '" + + appUri + + "loginHandler" + + "'; var f = document.getElementById('loginf');" + + "document.forms[0].action = uri;document.forms[0].username.focus();};" + + "" + + "var styles = window.parent.document.styleSheets;" + + "for(var j = 0; j < styles.length; j++) {\n" + + "if(styles[j].href) {" + + "var stylesheet = document.createElement('link');\n" + + "stylesheet.setAttribute('rel', 'stylesheet');\n" + + "stylesheet.setAttribute('type', 'text/css');\n" + + "stylesheet.setAttribute('href', styles[j].href);\n" + + "document.getElementsByTagName('head')[0].appendChild(stylesheet);\n" + + "}" + + "}\n" + + "function submitOnEnter(e) { var keycode = e.keyCode || e.which;" + + " if (keycode == 13) {document.forms[0].submit();} } \n" + + "</script>" + + "</head><body onload='setTarget();' style='margin:0;padding:0; background:transparent;' class=\"" + + ApplicationConnection.GENERATED_BODY_CLASSNAME + + "\">" + + "<div class='v-app v-app-loginpage' style=\"background:transparent;\">" + + "<iframe name='logintarget' style='width:0;height:0;" + + "border:0;margin:0;padding:0;'></iframe>" + + "<form id='loginf' target='logintarget' onkeypress=\"submitOnEnter(event)\" method=\"post\">" + + "<div>" + + usernameCaption + + "</div><div >" + + "<input class='v-textfield v-connector' style='display:block;' type='text' name='username'></div>" + + "<div>" + + passwordCaption + + "</div>" + + "<div><input class='v-textfield v-connector' style='display:block;' type='password' name='password'></div>" + + "<div><div onclick=\"document.forms[0].submit();\" tabindex=\"0\" class=\"v-button\" role=\"button\" ><span class=\"v-button-wrap\"><span class=\"v-button-caption\">" + + loginButtonCaption + + "</span></span></div></div></form></div>" + "</body></html>") + .getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not avalable", e); + } + } + + @Override + public void attach() { + super.attach(); + getApplication().addResource(loginPage); + getApplication().addRequestHandler(requestHandler); + iframe.setSource(loginPage); + } + + @Override + public void detach() { + getApplication().removeResource(loginPage); + getApplication().removeRequestHandler(requestHandler); + + super.detach(); + } + + /** + * This event is sent when login form is submitted. + */ + public class LoginEvent extends Event { + + private Map<String, String> params; + + private LoginEvent(Map<String, String> params) { + super(LoginForm.this); + this.params = params; + } + + /** + * Access method to form values by field names. + * + * @param name + * @return value in given field + */ + public String getLoginParameter(String name) { + if (params.containsKey(name)) { + return params.get(name); + } else { + return null; + } + } + } + + /** + * Login listener is a class capable to listen LoginEvents sent from + * LoginBox + */ + public interface LoginListener extends Serializable { + /** + * This method is fired on each login form post. + * + * @param event + */ + public void onLogin(LoginForm.LoginEvent event); + } + + private static final Method ON_LOGIN_METHOD; + + private static final String UNDEFINED_HEIGHT = "140px"; + private static final String UNDEFINED_WIDTH = "200px"; + + static { + try { + ON_LOGIN_METHOD = LoginListener.class.getDeclaredMethod("onLogin", + new Class[] { LoginEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in LoginForm"); + } + } + + /** + * Adds LoginListener to handle login logic + * + * @param listener + */ + public void addListener(LoginListener listener) { + addListener(LoginEvent.class, listener, ON_LOGIN_METHOD); + } + + /** + * Removes LoginListener + * + * @param listener + */ + public void removeListener(LoginListener listener) { + removeListener(LoginEvent.class, listener, ON_LOGIN_METHOD); + } + + @Override + public void setWidth(float width, Unit unit) { + super.setWidth(width, unit); + if (iframe != null) { + if (width < 0) { + iframe.setWidth(UNDEFINED_WIDTH); + } else { + iframe.setWidth("100%"); + } + } + } + + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + if (iframe != null) { + if (height < 0) { + iframe.setHeight(UNDEFINED_HEIGHT); + } else { + iframe.setHeight("100%"); + } + } + } + + /** + * Returns the caption for the user name field. + * + * @return String + */ + public String getUsernameCaption() { + return usernameCaption; + } + + /** + * Sets the caption to show for the user name field. The caption cannot be + * changed after the form has been shown to the user. + * + * @param usernameCaption + */ + public void setUsernameCaption(String usernameCaption) { + this.usernameCaption = usernameCaption; + } + + /** + * Returns the caption for the password field. + * + * @return String + */ + public String getPasswordCaption() { + return passwordCaption; + } + + /** + * Sets the caption to show for the password field. The caption cannot be + * changed after the form has been shown to the user. + * + * @param passwordCaption + */ + public void setPasswordCaption(String passwordCaption) { + this.passwordCaption = passwordCaption; + } + + /** + * Returns the caption for the login button. + * + * @return String + */ + public String getLoginButtonCaption() { + return loginButtonCaption; + } + + /** + * Sets the caption (button text) to show for the login button. The caption + * cannot be changed after the form has been shown to the user. + * + * @param loginButtonCaption + */ + public void setLoginButtonCaption(String loginButtonCaption) { + this.loginButtonCaption = loginButtonCaption; + } + +} diff --git a/server/src/com/vaadin/ui/MenuBar.java b/server/src/com/vaadin/ui/MenuBar.java new file mode 100644 index 0000000000..5b5dc13e20 --- /dev/null +++ b/server/src/com/vaadin/ui/MenuBar.java @@ -0,0 +1,890 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.menubar.VMenuBar; + +/** + * <p> + * A class representing a horizontal menu bar. The menu can contain MenuItem + * objects, which in turn can contain more MenuBars. These sub-level MenuBars + * are represented as vertical menu. + * </p> + */ +@SuppressWarnings("serial") +public class MenuBar extends AbstractComponent implements Vaadin6Component { + + // Items of the top-level menu + private final List<MenuItem> menuItems; + + // Number of items in this menu + private int numberOfItems = 0; + + private MenuItem moreItem; + + private boolean openRootOnHover; + + private boolean htmlContentAllowed; + + /** Paint (serialise) the component for the client. */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute(VMenuBar.OPEN_ROOT_MENU_ON_HOWER, openRootOnHover); + + if (isHtmlContentAllowed()) { + target.addAttribute(VMenuBar.HTML_CONTENT_ALLOWED, true); + } + + target.startTag("options"); + + if (getWidth() > -1) { + target.startTag("moreItem"); + target.addAttribute("text", moreItem.getText()); + if (moreItem.getIcon() != null) { + target.addAttribute("icon", moreItem.getIcon()); + } + target.endTag("moreItem"); + } + + target.endTag("options"); + target.startTag("items"); + + // This generates the tree from the contents of the menu + for (MenuItem item : menuItems) { + paintItem(target, item); + } + + target.endTag("items"); + } + + private void paintItem(PaintTarget target, MenuItem item) + throws PaintException { + if (!item.isVisible()) { + return; + } + + target.startTag("item"); + + target.addAttribute("id", item.getId()); + + if (item.getStyleName() != null) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_STYLE, + item.getStyleName()); + } + + if (item.isSeparator()) { + target.addAttribute("separator", true); + } else { + target.addAttribute("text", item.getText()); + + Command command = item.getCommand(); + if (command != null) { + target.addAttribute("command", true); + } + + Resource icon = item.getIcon(); + if (icon != null) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_ICON, icon); + } + + if (!item.isEnabled()) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_DISABLED, true); + } + + String description = item.getDescription(); + if (description != null && description.length() > 0) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_DESCRIPTION, + description); + } + if (item.isCheckable()) { + // if the "checked" attribute is present (either true or false), + // the item is checkable + target.addAttribute(VMenuBar.ATTRIBUTE_CHECKED, + item.isChecked()); + } + if (item.hasChildren()) { + for (MenuItem child : item.getChildren()) { + paintItem(target, child); + } + } + + } + + target.endTag("item"); + } + + /** Deserialize changes received from client. */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + Stack<MenuItem> items = new Stack<MenuItem>(); + boolean found = false; + + if (variables.containsKey("clickedId")) { + + Integer clickedId = (Integer) variables.get("clickedId"); + Iterator<MenuItem> itr = getItems().iterator(); + while (itr.hasNext()) { + items.push(itr.next()); + } + + MenuItem tmpItem = null; + + // Go through all the items in the menu + while (!found && !items.empty()) { + tmpItem = items.pop(); + found = (clickedId.intValue() == tmpItem.getId()); + + if (tmpItem.hasChildren()) { + itr = tmpItem.getChildren().iterator(); + while (itr.hasNext()) { + items.push(itr.next()); + } + } + + }// while + + // If we got the clicked item, launch the command. + if (found && tmpItem.isEnabled()) { + if (tmpItem.isCheckable()) { + tmpItem.setChecked(!tmpItem.isChecked()); + } + if (null != tmpItem.getCommand()) { + tmpItem.getCommand().menuSelected(tmpItem); + } + } + }// if + }// changeVariables + + /** + * Constructs an empty, horizontal menu + */ + public MenuBar() { + menuItems = new ArrayList<MenuItem>(); + setMoreMenuItem(null); + } + + /** + * Add a new item to the menu bar. Command can be null, but a caption must + * be given. + * + * @param caption + * the text for the menu item + * @param command + * the command for the menu item + * @throws IllegalArgumentException + */ + public MenuBar.MenuItem addItem(String caption, MenuBar.Command command) { + return addItem(caption, null, command); + } + + /** + * Add a new item to the menu bar. Icon and command can be null, but a + * caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @throws IllegalArgumentException + */ + public MenuBar.MenuItem addItem(String caption, Resource icon, + MenuBar.Command command) { + if (caption == null) { + throw new IllegalArgumentException("caption cannot be null"); + } + MenuItem newItem = new MenuItem(caption, icon, command); + menuItems.add(newItem); + requestRepaint(); + + return newItem; + + } + + /** + * Add an item before some item. If the given item does not exist the item + * is added at the end of the menu. Icon and command can be null, but a + * caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @param itemToAddBefore + * the item that will be after the new item + * @throws IllegalArgumentException + */ + public MenuBar.MenuItem addItemBefore(String caption, Resource icon, + MenuBar.Command command, MenuBar.MenuItem itemToAddBefore) { + if (caption == null) { + throw new IllegalArgumentException("caption cannot be null"); + } + + MenuItem newItem = new MenuItem(caption, icon, command); + if (menuItems.contains(itemToAddBefore)) { + int index = menuItems.indexOf(itemToAddBefore); + menuItems.add(index, newItem); + + } else { + menuItems.add(newItem); + } + + requestRepaint(); + + return newItem; + } + + /** + * Returns a list with all the MenuItem objects in the menu bar + * + * @return a list containing the MenuItem objects in the menu bar + */ + public List<MenuItem> getItems() { + return menuItems; + } + + /** + * Remove first occurrence the specified item from the main menu + * + * @param item + * The item to be removed + */ + public void removeItem(MenuBar.MenuItem item) { + if (item != null) { + menuItems.remove(item); + } + requestRepaint(); + } + + /** + * Empty the menu bar + */ + public void removeItems() { + menuItems.clear(); + requestRepaint(); + } + + /** + * Returns the size of the menu. + * + * @return The size of the menu + */ + public int getSize() { + return menuItems.size(); + } + + /** + * Set the item that is used when collapsing the top level menu. All + * "overflowing" items will be added below this. The item command will be + * ignored. If set to null, the default item with a downwards arrow is used. + * + * The item command (if specified) is ignored. + * + * @param item + */ + public void setMoreMenuItem(MenuItem item) { + if (item != null) { + moreItem = item; + } else { + moreItem = new MenuItem("", null, null); + } + requestRepaint(); + } + + /** + * Get the MenuItem used as the collapse menu item. + * + * @return + */ + public MenuItem getMoreMenuItem() { + return moreItem; + } + + /** + * Using this method menubar can be put into a special mode where top level + * menus opens without clicking on the menu, but automatically when mouse + * cursor is moved over the menu. In this mode the menu also closes itself + * if the mouse is moved out of the opened menu. + * <p> + * Note, that on touch devices the menu still opens on a click event. + * + * @param autoOpenTopLevelMenu + * true if menus should be opened without click, the default is + * false + */ + public void setAutoOpen(boolean autoOpenTopLevelMenu) { + if (autoOpenTopLevelMenu != openRootOnHover) { + openRootOnHover = autoOpenTopLevelMenu; + requestRepaint(); + } + } + + /** + * Detects whether the menubar is in a mode where top level menus are + * automatically opened when the mouse cursor is moved over the menu. + * Normally root menu opens only by clicking on the menu. Submenus always + * open automatically. + * + * @return true if the root menus open without click, the default is false + */ + public boolean isAutoOpen() { + return openRootOnHover; + } + + /** + * Sets whether html is allowed in the item captions. If set to true, the + * captions are passed to the browser as html and the developer is + * responsible for ensuring no harmful html is used. If set to false, the + * content is passed to the browser as plain text. + * + * @param htmlContentAllowed + * true if the captions are used as html, false if used as plain + * text + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + requestRepaint(); + } + + /** + * Checks whether item captions are interpreted as html or plain text. + * + * @return true if the captions are used as html, false if used as plain + * text + * @see #setHtmlContentAllowed(boolean) + */ + public boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } + + /** + * This interface contains the layer for menu commands of the + * {@link com.vaadin.ui.MenuBar} class. It's method will fire when the user + * clicks on the containing {@link com.vaadin.ui.MenuBar.MenuItem}. The + * selected item is given as an argument. + */ + public interface Command extends Serializable { + public void menuSelected(MenuBar.MenuItem selectedItem); + } + + /** + * A composite class for menu items and sub-menus. You can set commands to + * be fired on user click by implementing the + * {@link com.vaadin.ui.MenuBar.Command} interface. You can also add + * multiple MenuItems to a MenuItem and create a sub-menu. + * + */ + public class MenuItem implements Serializable { + + /** Private members * */ + private final int itsId; + private Command itsCommand; + private String itsText; + private List<MenuItem> itsChildren; + private Resource itsIcon; + private MenuItem itsParent; + private boolean enabled = true; + private boolean visible = true; + private boolean isSeparator = false; + private String styleName; + private String description; + private boolean checkable = false; + private boolean checked = false; + + /** + * Constructs a new menu item that can optionally have an icon and a + * command associated with it. Icon and command can be null, but a + * caption must be given. + * + * @param text + * The text associated with the command + * @param command + * The command to be fired + * @throws IllegalArgumentException + */ + public MenuItem(String caption, Resource icon, MenuBar.Command command) { + if (caption == null) { + throw new IllegalArgumentException("caption cannot be null"); + } + itsId = ++numberOfItems; + itsText = caption; + itsIcon = icon; + itsCommand = command; + } + + /** + * Checks if the item has children (if it is a sub-menu). + * + * @return True if this item has children + */ + public boolean hasChildren() { + return !isSeparator() && itsChildren != null; + } + + /** + * Adds a separator to this menu. A separator is a way to visually group + * items in a menu, to make it easier for users to find what they are + * looking for in the menu. + * + * @author Jouni Koivuviita / Vaadin Ltd. + * @since 6.2.0 + */ + public MenuBar.MenuItem addSeparator() { + MenuItem item = addItem("", null, null); + item.setSeparator(true); + return item; + } + + public MenuBar.MenuItem addSeparatorBefore(MenuItem itemToAddBefore) { + MenuItem item = addItemBefore("", null, null, itemToAddBefore); + item.setSeparator(true); + return item; + } + + /** + * Add a new item inside this item, thus creating a sub-menu. Command + * can be null, but a caption must be given. + * + * @param caption + * the text for the menu item + * @param command + * the command for the menu item + */ + public MenuBar.MenuItem addItem(String caption, MenuBar.Command command) { + return addItem(caption, null, command); + } + + /** + * Add a new item inside this item, thus creating a sub-menu. Icon and + * command can be null, but a caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @throws IllegalStateException + * If the item is checkable and thus cannot have children. + */ + public MenuBar.MenuItem addItem(String caption, Resource icon, + MenuBar.Command command) throws IllegalStateException { + if (isSeparator()) { + throw new UnsupportedOperationException( + "Cannot add items to a separator"); + } + if (isCheckable()) { + throw new IllegalStateException( + "A checkable item cannot have children"); + } + if (caption == null) { + throw new IllegalArgumentException("Caption cannot be null"); + } + + if (itsChildren == null) { + itsChildren = new ArrayList<MenuItem>(); + } + + MenuItem newItem = new MenuItem(caption, icon, command); + + // The only place where the parent is set + newItem.setParent(this); + itsChildren.add(newItem); + + requestRepaint(); + + return newItem; + } + + /** + * Add an item before some item. If the given item does not exist the + * item is added at the end of the menu. Icon and command can be null, + * but a caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @param itemToAddBefore + * the item that will be after the new item + * @throws IllegalStateException + * If the item is checkable and thus cannot have children. + */ + public MenuBar.MenuItem addItemBefore(String caption, Resource icon, + MenuBar.Command command, MenuBar.MenuItem itemToAddBefore) + throws IllegalStateException { + if (isCheckable()) { + throw new IllegalStateException( + "A checkable item cannot have children"); + } + MenuItem newItem = null; + + if (hasChildren() && itsChildren.contains(itemToAddBefore)) { + int index = itsChildren.indexOf(itemToAddBefore); + newItem = new MenuItem(caption, icon, command); + newItem.setParent(this); + itsChildren.add(index, newItem); + } else { + newItem = addItem(caption, icon, command); + } + + requestRepaint(); + + return newItem; + } + + /** + * For the associated command. + * + * @return The associated command, or null if there is none + */ + public Command getCommand() { + return itsCommand; + } + + /** + * Gets the objects icon. + * + * @return The icon of the item, null if the item doesn't have an icon + */ + public Resource getIcon() { + return itsIcon; + } + + /** + * For the containing item. This will return null if the item is in the + * top-level menu bar. + * + * @return The containing {@link com.vaadin.ui.MenuBar.MenuItem} , or + * null if there is none + */ + public MenuBar.MenuItem getParent() { + return itsParent; + } + + /** + * This will return the children of this item or null if there are none. + * + * @return List of children items, or null if there are none + */ + public List<MenuItem> getChildren() { + return itsChildren; + } + + /** + * Gets the objects text + * + * @return The text + */ + public java.lang.String getText() { + return itsText; + } + + /** + * Returns the number of children. + * + * @return The number of child items + */ + public int getSize() { + if (itsChildren != null) { + return itsChildren.size(); + } + return -1; + } + + /** + * Get the unique identifier for this item. + * + * @return The id of this item + */ + public int getId() { + return itsId; + } + + /** + * Set the command for this item. Set null to remove. + * + * @param command + * The MenuCommand of this item + */ + public void setCommand(MenuBar.Command command) { + itsCommand = command; + } + + /** + * Sets the icon. Set null to remove. + * + * @param icon + * The icon for this item + */ + public void setIcon(Resource icon) { + itsIcon = icon; + requestRepaint(); + } + + /** + * Set the text of this object. + * + * @param text + * Text for this object + */ + public void setText(java.lang.String text) { + if (text != null) { + itsText = text; + } + requestRepaint(); + } + + /** + * Remove the first occurrence of the item. + * + * @param item + * The item to be removed + */ + public void removeChild(MenuBar.MenuItem item) { + if (item != null && itsChildren != null) { + itsChildren.remove(item); + if (itsChildren.isEmpty()) { + itsChildren = null; + } + requestRepaint(); + } + } + + /** + * Empty the list of children items. + */ + public void removeChildren() { + if (itsChildren != null) { + itsChildren.clear(); + itsChildren = null; + requestRepaint(); + } + } + + /** + * Set the parent of this item. This is called by the addItem method. + * + * @param parent + * The parent item + */ + protected void setParent(MenuBar.MenuItem parent) { + itsParent = parent; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + requestRepaint(); + } + + public boolean isEnabled() { + return enabled; + } + + public void setVisible(boolean visible) { + this.visible = visible; + requestRepaint(); + } + + public boolean isVisible() { + return visible; + } + + private void setSeparator(boolean isSeparator) { + this.isSeparator = isSeparator; + requestRepaint(); + } + + public boolean isSeparator() { + return isSeparator; + } + + public void setStyleName(String styleName) { + this.styleName = styleName; + requestRepaint(); + } + + public String getStyleName() { + return styleName; + } + + /** + * Sets the items's description. See {@link #getDescription()} for more + * information on what the description is. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param description + * the new description string for the component. + */ + public void setDescription(String description) { + this.description = description; + requestRepaint(); + } + + /** + * <p> + * Gets the items's description. The description can be used to briefly + * describe the state of the item to the user. The description string + * may contain certain XML tags: + * </p> + * + * <p> + * <table border=1> + * <tr> + * <td width=120><b>Tag</b></td> + * <td width=120><b>Description</b></td> + * <td width=120><b>Example</b></td> + * </tr> + * <tr> + * <td><b></td> + * <td>bold</td> + * <td><b>bold text</b></td> + * </tr> + * <tr> + * <td><i></td> + * <td>italic</td> + * <td><i>italic text</i></td> + * </tr> + * <tr> + * <td><u></td> + * <td>underlined</td> + * <td><u>underlined text</u></td> + * </tr> + * <tr> + * <td><br></td> + * <td>linebreak</td> + * <td>N/A</td> + * </tr> + * <tr> + * <td><ul><br> + * <li>item1<br> + * <li>item1<br> + * </ul></td> + * <td>item list</td> + * <td> + * <ul> + * <li>item1 + * <li>item2 + * </ul> + * </td> + * </tr> + * </table> + * </p> + * + * <p> + * These tags may be nested. + * </p> + * + * @return item's description <code>String</code> + */ + public String getDescription() { + return description; + } + + /** + * Gets the checkable state of the item - whether the item has checked + * and unchecked states. If an item is checkable its checked state (as + * returned by {@link #isChecked()}) is indicated in the UI. + * + * <p> + * An item is not checkable by default. + * </p> + * + * @return true if the item is checkable, false otherwise + * @since 6.6.2 + */ + public boolean isCheckable() { + return checkable; + } + + /** + * Sets the checkable state of the item. If an item is checkable its + * checked state (as returned by {@link #isChecked()}) is indicated in + * the UI. + * + * <p> + * An item is not checkable by default. + * </p> + * + * <p> + * Items with sub items cannot be checkable. + * </p> + * + * @param checkable + * true if the item should be checkable, false otherwise + * @throws IllegalStateException + * If the item has children + * @since 6.6.2 + */ + public void setCheckable(boolean checkable) + throws IllegalStateException { + if (hasChildren()) { + throw new IllegalStateException( + "A menu item with children cannot be checkable"); + } + this.checkable = checkable; + requestRepaint(); + } + + /** + * Gets the checked state of the item (checked or unchecked). Only used + * if the item is checkable (as indicated by {@link #isCheckable()}). + * The checked state is indicated in the UI with the item, if the item + * is checkable. + * + * <p> + * An item is not checked by default. + * </p> + * + * <p> + * The CSS style corresponding to the checked state is "-checked". + * </p> + * + * @return true if the item is checked, false otherwise + * @since 6.6.2 + */ + public boolean isChecked() { + return checked; + } + + /** + * Sets the checked state of the item. Only used if the item is + * checkable (indicated by {@link #isCheckable()}). The checked state is + * indicated in the UI with the item, if the item is checkable. + * + * <p> + * An item is not checked by default. + * </p> + * + * <p> + * The CSS style corresponding to the checked state is "-checked". + * </p> + * + * @return true if the item is checked, false otherwise + * @since 6.6.2 + */ + public void setChecked(boolean checked) { + this.checked = checked; + requestRepaint(); + } + + }// class MenuItem + +}// class MenuBar diff --git a/server/src/com/vaadin/ui/NativeButton.java b/server/src/com/vaadin/ui/NativeButton.java new file mode 100644 index 0000000000..6eb4379261 --- /dev/null +++ b/server/src/com/vaadin/ui/NativeButton.java @@ -0,0 +1,21 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +@SuppressWarnings("serial") +public class NativeButton extends Button { + + public NativeButton() { + super(); + } + + public NativeButton(String caption) { + super(caption); + } + + public NativeButton(String caption, ClickListener listener) { + super(caption, listener); + } + +} diff --git a/server/src/com/vaadin/ui/NativeSelect.java b/server/src/com/vaadin/ui/NativeSelect.java new file mode 100644 index 0000000000..1f85f57c97 --- /dev/null +++ b/server/src/com/vaadin/ui/NativeSelect.java @@ -0,0 +1,91 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * This is a simple drop-down select without, for instance, support for + * multiselect, new items, lazyloading, and other advanced features. Sometimes + * "native" select without all the bells-and-whistles of the ComboBox is a + * better choice. + */ +@SuppressWarnings("serial") +public class NativeSelect extends AbstractSelect { + + // width in characters, mimics TextField + private int columns = 0; + + public NativeSelect() { + super(); + } + + public NativeSelect(String caption, Collection<?> options) { + super(caption, options); + } + + public NativeSelect(String caption, Container dataSource) { + super(caption, dataSource); + } + + public NativeSelect(String caption) { + super(caption); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + requestRepaint(); + } + } + + public int getColumns() { + return columns; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "native"); + // Adds the number of columns + if (columns != 0) { + target.addAttribute("cols", columns); + } + + super.paintContent(target); + } + + @Override + public void setMultiSelect(boolean multiSelect) + throws UnsupportedOperationException { + if (multiSelect == true) { + throw new UnsupportedOperationException("Multiselect not supported"); + } + } + + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions == true) { + throw new UnsupportedOperationException( + "newItemsAllowed not supported"); + } + } + +} diff --git a/server/src/com/vaadin/ui/Notification.java b/server/src/com/vaadin/ui/Notification.java new file mode 100644 index 0000000000..502e5ff788 --- /dev/null +++ b/server/src/com/vaadin/ui/Notification.java @@ -0,0 +1,367 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.terminal.Page; +import com.vaadin.terminal.Resource; + +/** + * A notification message, used to display temporary messages to the user - for + * example "Document saved", or "Save failed". + * <p> + * The notification message can consist of several parts: caption, description + * and icon. It is usually used with only caption - one should be wary of + * filling the notification with too much information. + * </p> + * <p> + * The notification message tries to be as unobtrusive as possible, while still + * drawing needed attention. There are several basic types of messages that can + * be used in different situations: + * <ul> + * <li>TYPE_HUMANIZED_MESSAGE fades away quickly as soon as the user uses the + * mouse or types something. It can be used to show fairly unimportant messages, + * such as feedback that an operation succeeded ("Document Saved") - the kind of + * messages the user ignores once the application is familiar.</li> + * <li>TYPE_WARNING_MESSAGE is shown for a short while after the user uses the + * mouse or types something. It's default style is also more noticeable than the + * humanized message. It can be used for messages that do not contain a lot of + * important information, but should be noticed by the user. Despite the name, + * it does not have to be a warning, but can be used instead of the humanized + * message whenever you want to make the message a little more noticeable.</li> + * <li>TYPE_ERROR_MESSAGE requires to user to click it before disappearing, and + * can be used for critical messages.</li> + * <li>TYPE_TRAY_NOTIFICATION is shown for a while in the lower left corner of + * the window, and can be used for "convenience notifications" that do not have + * to be noticed immediately, and should not interfere with the current task - + * for instance to show "You have a new message in your inbox" while the user is + * working in some other area of the application.</li> + * </ul> + * </p> + * <p> + * In addition to the basic pre-configured types, a Notification can also be + * configured to show up in a custom position, for a specified time (or until + * clicked), and with a custom stylename. An icon can also be added. + * </p> + * + */ +public class Notification implements Serializable { + public static final int TYPE_HUMANIZED_MESSAGE = 1; + public static final int TYPE_WARNING_MESSAGE = 2; + public static final int TYPE_ERROR_MESSAGE = 3; + public static final int TYPE_TRAY_NOTIFICATION = 4; + + public static final int POSITION_CENTERED = 1; + public static final int POSITION_CENTERED_TOP = 2; + public static final int POSITION_CENTERED_BOTTOM = 3; + public static final int POSITION_TOP_LEFT = 4; + public static final int POSITION_TOP_RIGHT = 5; + public static final int POSITION_BOTTOM_LEFT = 6; + public static final int POSITION_BOTTOM_RIGHT = 7; + + public static final int DELAY_FOREVER = -1; + public static final int DELAY_NONE = 0; + + private String caption; + private String description; + private Resource icon; + private int position = POSITION_CENTERED; + private int delayMsec = 0; + private String styleName; + private boolean htmlContentAllowed; + + /** + * Creates a "humanized" notification message. + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @param caption + * The message to show + */ + public Notification(String caption) { + this(caption, null, TYPE_HUMANIZED_MESSAGE); + } + + /** + * Creates a notification message of the specified type. + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @param caption + * The message to show + * @param type + * The type of message + */ + public Notification(String caption, int type) { + this(caption, null, type); + } + + /** + * Creates a "humanized" notification message with a bigger caption and + * smaller description. + * + * The caption and description are rendered as plain text with HTML + * automatically escaped. + * + * @param caption + * The message caption + * @param description + * The message description + */ + public Notification(String caption, String description) { + this(caption, description, TYPE_HUMANIZED_MESSAGE); + } + + /** + * Creates a notification message of the specified type, with a bigger + * caption and smaller description. + * + * The caption and description are rendered as plain text with HTML + * automatically escaped. + * + * @param caption + * The message caption + * @param description + * The message description + * @param type + * The type of message + */ + public Notification(String caption, String description, int type) { + this(caption, description, type, false); + } + + /** + * Creates a notification message of the specified type, with a bigger + * caption and smaller description. + * + * Care should be taken to to avoid XSS vulnerabilities if html is allowed. + * + * @param caption + * The message caption + * @param description + * The message description + * @param type + * The type of message + * @param htmlContentAllowed + * Whether html in the caption and description should be + * displayed as html or as plain text + */ + public Notification(String caption, String description, int type, + boolean htmlContentAllowed) { + this.caption = caption; + this.description = description; + this.htmlContentAllowed = htmlContentAllowed; + setType(type); + } + + private void setType(int type) { + switch (type) { + case TYPE_WARNING_MESSAGE: + delayMsec = 1500; + styleName = "warning"; + break; + case TYPE_ERROR_MESSAGE: + delayMsec = -1; + styleName = "error"; + break; + case TYPE_TRAY_NOTIFICATION: + delayMsec = 3000; + position = POSITION_BOTTOM_RIGHT; + styleName = "tray"; + + case TYPE_HUMANIZED_MESSAGE: + default: + break; + } + + } + + /** + * Gets the caption part of the notification message. + * + * @return The message caption + */ + public String getCaption() { + return caption; + } + + /** + * Sets the caption part of the notification message + * + * @param caption + * The message caption + */ + public void setCaption(String caption) { + this.caption = caption; + } + + /** + * Gets the description part of the notification message. + * + * @return The message description. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description part of the notification message. + * + * @param description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the position of the notification message. + * + * @return The position + */ + public int getPosition() { + return position; + } + + /** + * Sets the position of the notification message. + * + * @param position + * The desired notification position + */ + public void setPosition(int position) { + this.position = position; + } + + /** + * Gets the icon part of the notification message. + * + * @return The message icon + */ + public Resource getIcon() { + return icon; + } + + /** + * Sets the icon part of the notification message. + * + * @param icon + * The desired message icon + */ + public void setIcon(Resource icon) { + this.icon = icon; + } + + /** + * Gets the delay before the notification disappears. + * + * @return the delay in msec, -1 indicates the message has to be clicked. + */ + public int getDelayMsec() { + return delayMsec; + } + + /** + * Sets the delay before the notification disappears. + * + * @param delayMsec + * the desired delay in msec, -1 to require the user to click the + * message + */ + public void setDelayMsec(int delayMsec) { + this.delayMsec = delayMsec; + } + + /** + * Sets the style name for the notification message. + * + * @param styleName + * The desired style name. + */ + public void setStyleName(String styleName) { + this.styleName = styleName; + } + + /** + * Gets the style name for the notification message. + * + * @return + */ + public String getStyleName() { + return styleName; + } + + /** + * Sets whether html is allowed in the caption and description. If set to + * true, the texts are passed to the browser as html and the developer is + * responsible for ensuring no harmful html is used. If set to false, the + * texts are passed to the browser as plain text. + * + * @param htmlContentAllowed + * true if the texts are used as html, false if used as plain + * text + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + } + + /** + * Checks whether caption and description are interpreted as html or plain + * text. + * + * @return true if the texts are used as html, false if used as plain text + * @see #setHtmlContentAllowed(boolean) + */ + public boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } + + /** + * Shows this notification on a Page. + * + * @param page + * The page on which the notification should be shown + */ + public void show(Page page) { + // TODO Can avoid deprecated API when Notification extends Extension + page.showNotification(this); + } + + /** + * Shows a notification message on the middle of the current page. The + * message automatically disappears ("humanized message"). + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @see #Notification(String) + * @see #show(Page) + * + * @param caption + * The message + */ + public static void show(String caption) { + new Notification(caption).show(Page.getCurrent()); + } + + /** + * Shows a notification message the current page. The position and behavior + * of the message depends on the type, which is one of the basic types + * defined in {@link Notification}, for instance + * Notification.TYPE_WARNING_MESSAGE. + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @see #Notification(String, int) + * @see #show(Page) + * + * @param caption + * The message + * @param type + * The message type + */ + public static void show(String caption, int type) { + new Notification(caption, type).show(Page.getCurrent()); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/OptionGroup.java b/server/src/com/vaadin/ui/OptionGroup.java new file mode 100644 index 0000000000..e3bcdd61b7 --- /dev/null +++ b/server/src/com/vaadin/ui/OptionGroup.java @@ -0,0 +1,203 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.event.FieldEvents; +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroup; + +/** + * Configures select to be used as an option group. + */ +@SuppressWarnings("serial") +public class OptionGroup extends AbstractSelect implements + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier { + + private Set<Object> disabledItemIds = new HashSet<Object>(); + private boolean htmlContentAllowed = false; + + public OptionGroup() { + super(); + } + + public OptionGroup(String caption, Collection<?> options) { + super(caption, options); + } + + public OptionGroup(String caption, Container dataSource) { + super(caption, dataSource); + } + + public OptionGroup(String caption) { + super(caption); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "optiongroup"); + if (isHtmlContentAllowed()) { + target.addAttribute(VOptionGroup.HTML_CONTENT_ALLOWED, true); + } + super.paintContent(target); + } + + @Override + protected void paintItem(PaintTarget target, Object itemId) + throws PaintException { + super.paintItem(target, itemId); + if (!isItemEnabled(itemId)) { + target.addAttribute(VOptionGroup.ATTRIBUTE_OPTION_DISABLED, true); + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + super.changeVariables(source, variables); + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + @Override + protected void setValue(Object newValue, boolean repaintIsNotNeeded) { + if (repaintIsNotNeeded) { + /* + * Check that value from changeVariables() doesn't contain unallowed + * selections: In the multi select mode, the user has selected or + * deselected a disabled item. In the single select mode, the user + * has selected a disabled item. + */ + if (isMultiSelect()) { + Set<?> currentValueSet = (Set<?>) getValue(); + Set<?> newValueSet = (Set<?>) newValue; + for (Object itemId : currentValueSet) { + if (!isItemEnabled(itemId) && !newValueSet.contains(itemId)) { + requestRepaint(); + return; + } + } + for (Object itemId : newValueSet) { + if (!isItemEnabled(itemId) + && !currentValueSet.contains(itemId)) { + requestRepaint(); + return; + } + } + } else { + if (newValue == null) { + newValue = getNullSelectionItemId(); + } + if (!isItemEnabled(newValue)) { + requestRepaint(); + return; + } + } + } + super.setValue(newValue, repaintIsNotNeeded); + } + + /** + * Sets an item disabled or enabled. In the multiselect mode, a disabled + * item cannot be selected or deselected by the user. In the single + * selection mode, a disable item cannot be selected. + * + * However, programmatical selection or deselection of an disable item is + * possible. By default, items are enabled. + * + * @param itemId + * the id of the item to be disabled or enabled + * @param enabled + * if true the item is enabled, otherwise the item is disabled + */ + public void setItemEnabled(Object itemId, boolean enabled) { + if (itemId != null) { + if (enabled) { + disabledItemIds.remove(itemId); + } else { + disabledItemIds.add(itemId); + } + requestRepaint(); + } + } + + /** + * Returns true if the item is enabled. + * + * @param itemId + * the id of the item to be checked + * @return true if the item is enabled, false otherwise + * @see #setItemEnabled(Object, boolean) + */ + public boolean isItemEnabled(Object itemId) { + if (itemId != null) { + return !disabledItemIds.contains(itemId); + } + return true; + } + + /** + * Sets whether html is allowed in the item captions. If set to true, the + * captions are passed to the browser as html and the developer is + * responsible for ensuring no harmful html is used. If set to false, the + * content is passed to the browser as plain text. + * + * @param htmlContentAllowed + * true if the captions are used as html, false if used as plain + * text + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + requestRepaint(); + } + + /** + * Checks whether captions are interpreted as html or plain text. + * + * @return true if the captions are used as html, false if used as plain + * text + * @see #setHtmlContentAllowed(boolean) + */ + public boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } +} diff --git a/server/src/com/vaadin/ui/Panel.java b/server/src/com/vaadin/ui/Panel.java new file mode 100644 index 0000000000..3c26b73f09 --- /dev/null +++ b/server/src/com/vaadin/ui/Panel.java @@ -0,0 +1,486 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ActionManager; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.panel.PanelServerRpc; +import com.vaadin.shared.ui.panel.PanelState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Scrollable; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.ui.Component.Focusable; + +/** + * Panel - a simple single component container. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Panel extends AbstractComponentContainer implements Scrollable, + ComponentContainer.ComponentAttachListener, + ComponentContainer.ComponentDetachListener, Action.Notifier, Focusable, + Vaadin6Component { + + /** + * Content of the panel. + */ + private ComponentContainer content; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. + */ + protected ActionManager actionManager; + + private PanelServerRpc rpc = new PanelServerRpc() { + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Panel.this, mouseDetails)); + } + }; + + /** + * Creates a new empty panel. A VerticalLayout is used as content. + */ + public Panel() { + this((ComponentContainer) null); + } + + /** + * Creates a new empty panel which contains the given content. The content + * cannot be null. + * + * @param content + * the content for the panel. + */ + public Panel(ComponentContainer content) { + registerRpc(rpc); + setContent(content); + setWidth(100, Unit.PERCENTAGE); + getState().setTabIndex(-1); + } + + /** + * Creates a new empty panel with caption. Default layout is used. + * + * @param caption + * the caption used in the panel (HTML/XHTML). + */ + public Panel(String caption) { + this(caption, null); + } + + /** + * Creates a new empty panel with the given caption and content. + * + * @param caption + * the caption of the panel (HTML/XHTML). + * @param content + * the content used in the panel. + */ + public Panel(String caption, ComponentContainer content) { + this(content); + setCaption(caption); + } + + /** + * Sets the caption of the panel. + * + * Note that the caption is interpreted as HTML/XHTML and therefore care + * should be taken not to enable HTML injection and XSS attacks using panel + * captions. This behavior may change in future versions. + * + * @see AbstractComponent#setCaption(String) + */ + @Override + public void setCaption(String caption) { + super.setCaption(caption); + } + + /** + * Returns the content of the Panel. + * + * @return + */ + public ComponentContainer getContent() { + return content; + } + + /** + * + * Set the content of the Panel. If null is given as the new content then a + * layout is automatically created and set as the content. + * + * @param content + * The new content + */ + public void setContent(ComponentContainer newContent) { + + // If the content is null we create the default content + if (newContent == null) { + newContent = createDefaultContent(); + } + + // if (newContent == null) { + // throw new IllegalArgumentException("Content cannot be null"); + // } + + if (newContent == content) { + // don't set the same content twice + return; + } + + // detach old content if present + if (content != null) { + content.setParent(null); + content.removeListener((ComponentContainer.ComponentAttachListener) this); + content.removeListener((ComponentContainer.ComponentDetachListener) this); + } + + // Sets the panel to be parent for the content + newContent.setParent(this); + + // Sets the new content + content = newContent; + + // Adds the event listeners for new content + newContent + .addListener((ComponentContainer.ComponentAttachListener) this); + newContent + .addListener((ComponentContainer.ComponentDetachListener) this); + + content = newContent; + requestRepaint(); + } + + /** + * Create a ComponentContainer which is added by default to the Panel if + * user does not specify any content. + * + * @return + */ + private ComponentContainer createDefaultContent() { + VerticalLayout layout = new VerticalLayout(); + // Force margins by default + layout.setMargin(true); + return layout; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.Vaadin6Component#paintContent(com.vaadin.terminal + * .PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (actionManager != null) { + actionManager.paintActions(null, target); + } + } + + /** + * Adds the component into this container. + * + * @param c + * the component to be added. + * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component) + */ + @Override + public void addComponent(Component c) { + content.addComponent(c); + // No repaint request is made as we except the underlying container to + // request repaints + } + + /** + * Removes the component from this container. + * + * @param c + * The component to be removed. + * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component) + */ + @Override + public void removeComponent(Component c) { + content.removeComponent(c); + // No repaint request is made as we except the underlying container to + // request repaints + } + + /** + * Gets the component container iterator for going through all the + * components in the container. + * + * @return the Iterator of the components inside the container. + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + @Override + public Iterator<Component> getComponentIterator() { + return Collections.singleton((Component) content).iterator(); + } + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @see com.vaadin.terminal.VariableOwner#changeVariables(Object, Map) + */ + @Override + @SuppressWarnings("unchecked") + public void changeVariables(Object source, Map<String, Object> variables) { + // Get new size + final Integer newWidth = (Integer) variables.get("width"); + final Integer newHeight = (Integer) variables.get("height"); + if (newWidth != null && newWidth.intValue() != getWidth()) { + setWidth(newWidth.intValue(), UNITS_PIXELS); + } + if (newHeight != null && newHeight.intValue() != getHeight()) { + setHeight(newHeight.intValue(), UNITS_PIXELS); + } + + // Scrolling + final Integer newScrollX = (Integer) variables.get("scrollLeft"); + final Integer newScrollY = (Integer) variables.get("scrollTop"); + if (newScrollX != null && newScrollX.intValue() != getScrollLeft()) { + // set internally, not to fire request repaint + getState().setScrollLeft(newScrollX.intValue()); + } + if (newScrollY != null && newScrollY.intValue() != getScrollTop()) { + // set internally, not to fire request repaint + getState().setScrollTop(newScrollY.intValue()); + } + + // Actions + if (actionManager != null) { + actionManager.handleActions(variables, this); + } + + } + + /* Scrolling functionality */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollable(boolean) + */ + @Override + public int getScrollLeft() { + return getState().getScrollLeft(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollable(boolean) + */ + @Override + public int getScrollTop() { + return getState().getScrollTop(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollLeft(int) + */ + @Override + public void setScrollLeft(int scrollLeft) { + if (scrollLeft < 0) { + throw new IllegalArgumentException( + "Scroll offset must be at least 0"); + } + getState().setScrollLeft(scrollLeft); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollTop(int) + */ + @Override + public void setScrollTop(int scrollTop) { + if (scrollTop < 0) { + throw new IllegalArgumentException( + "Scroll offset must be at least 0"); + } + getState().setScrollTop(scrollTop); + requestRepaint(); + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + content.replaceComponent(oldComponent, newComponent); + } + + /** + * A new component is attached to container. + * + * @see com.vaadin.ui.ComponentContainer.ComponentAttachListener#componentAttachedToContainer(com.vaadin.ui.ComponentContainer.ComponentAttachEvent) + */ + @Override + public void componentAttachedToContainer(ComponentAttachEvent event) { + if (event.getContainer() == content) { + fireComponentAttachEvent(event.getAttachedComponent()); + } + } + + /** + * A component has been detached from container. + * + * @see com.vaadin.ui.ComponentContainer.ComponentDetachListener#componentDetachedFromContainer(com.vaadin.ui.ComponentContainer.ComponentDetachEvent) + */ + @Override + public void componentDetachedFromContainer(ComponentDetachEvent event) { + if (event.getContainer() == content) { + fireComponentDetachEvent(event.getDetachedComponent()); + } + } + + /** + * Removes all components from this container. + * + * @see com.vaadin.ui.ComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + content.removeAllComponents(); + } + + /* + * ACTIONS + */ + @Override + protected ActionManager getActionManager() { + if (actionManager == null) { + actionManager = new ActionManager(this); + } + return actionManager; + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (actionManager != null) { + actionManager.removeAction(action); + } + } + + @Override + public void addActionHandler(Handler actionHandler) { + getActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (actionManager != null) { + actionManager.removeActionHandler(actionHandler); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + if (actionManager != null) { + actionManager.removeAllActionHandlers(); + } + } + + /** + * Add a click listener to the Panel. The listener is called whenever the + * user clicks inside the Panel. Also when the click targets a component + * inside the Panel, provided the targeted component does not prevent the + * click event from propagating. + * + * Use {@link #removeListener(ClickListener)} to remove the listener. + * + * @param listener + * The listener to add + */ + public void addListener(ClickListener listener) { + addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, ClickEvent.class, + listener, ClickListener.clickMethod); + } + + /** + * Remove a click listener from the Panel. The listener should earlier have + * been added using {@link #addListener(ClickListener)}. + * + * @param listener + * The listener to remove + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + ClickEvent.class, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public int getTabIndex() { + return getState().getTabIndex(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTabIndex(int tabIndex) { + getState().setTabIndex(tabIndex); + requestRepaint(); + } + + /** + * Moves keyboard focus to the component. {@see Focusable#focus()} + * + */ + @Override + public void focus() { + super.focus(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentCount() + */ + @Override + public int getComponentCount() { + // This is so wrong... (#2924) + return content.getComponentCount(); + } + + @Override + public PanelState getState() { + return (PanelState) super.getState(); + } + +} diff --git a/server/src/com/vaadin/ui/PasswordField.java b/server/src/com/vaadin/ui/PasswordField.java new file mode 100644 index 0000000000..c1fccebbfe --- /dev/null +++ b/server/src/com/vaadin/ui/PasswordField.java @@ -0,0 +1,67 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import com.vaadin.data.Property; + +/** + * A field that is used to enter secret text information like passwords. The + * entered text is not displayed on the screen. + */ +public class PasswordField extends AbstractTextField { + + /** + * Constructs an empty PasswordField. + */ + public PasswordField() { + setValue(""); + } + + /** + * Constructs a PasswordField with given property data source. + * + * @param dataSource + * the property data source for the field + */ + public PasswordField(Property dataSource) { + setPropertyDataSource(dataSource); + } + + /** + * Constructs a PasswordField with given caption and property data source. + * + * @param caption + * the caption for the field + * @param dataSource + * the property data source for the field + */ + public PasswordField(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a PasswordField with given value and caption. + * + * @param caption + * the caption for the field + * @param value + * the value for the field + */ + public PasswordField(String caption, String value) { + setValue(value); + setCaption(caption); + } + + /** + * Constructs a PasswordField with given caption. + * + * @param caption + * the caption for the field + */ + public PasswordField(String caption) { + this(); + setCaption(caption); + } +} diff --git a/server/src/com/vaadin/ui/PopupDateField.java b/server/src/com/vaadin/ui/PopupDateField.java new file mode 100644 index 0000000000..3688d4035f --- /dev/null +++ b/server/src/com/vaadin/ui/PopupDateField.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Property; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <p> + * A date entry component, which displays the actual date selector as a popup. + * + * </p> + * + * @see DateField + * @see InlineDateField + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +public class PopupDateField extends DateField { + + private String inputPrompt = null; + + public PopupDateField() { + super(); + } + + public PopupDateField(Property dataSource) throws IllegalArgumentException { + super(dataSource); + } + + public PopupDateField(String caption, Date value) { + super(caption, value); + } + + public PopupDateField(String caption, Property dataSource) { + super(caption, dataSource); + } + + public PopupDateField(String caption) { + super(caption); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + if (inputPrompt != null) { + target.addAttribute("prompt", inputPrompt); + } + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return inputPrompt; + } + + /** + * Sets the input prompt - a textual prompt that is displayed when the field + * would otherwise be empty, to prompt the user for input. + * + * @param inputPrompt + */ + public void setInputPrompt(String inputPrompt) { + this.inputPrompt = inputPrompt; + requestRepaint(); + } + +} diff --git a/server/src/com/vaadin/ui/PopupView.java b/server/src/com/vaadin/ui/PopupView.java new file mode 100644 index 0000000000..766181b50f --- /dev/null +++ b/server/src/com/vaadin/ui/PopupView.java @@ -0,0 +1,453 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * + * A component for displaying a two different views to data. The minimized view + * is normally used to render the component, and when it is clicked the full + * view is displayed on a popup. The inner class {@link PopupView.Content} is + * used to deliver contents to this component. + * + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class PopupView extends AbstractComponentContainer implements + Vaadin6Component { + + private Content content; + private boolean hideOnMouseOut; + private Component visibleComponent; + + private static final Method POPUP_VISIBILITY_METHOD; + static { + try { + POPUP_VISIBILITY_METHOD = PopupVisibilityListener.class + .getDeclaredMethod("popupVisibilityChange", + new Class[] { PopupVisibilityEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in PopupView"); + } + } + + /** + * Iterator for the visible components (zero or one components), used by + * {@link PopupView#getComponentIterator()}. + */ + private static class SingleComponentIterator implements + Iterator<Component>, Serializable { + + private final Component component; + private boolean first; + + public SingleComponentIterator(Component component) { + this.component = component; + first = (component == null); + } + + @Override + public boolean hasNext() { + return !first; + } + + @Override + public Component next() { + if (!first) { + first = true; + return component; + } else { + return null; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /* Constructors */ + + /** + * A simple way to create a PopupPanel. Note that the minimal representation + * may not be dynamically updated, in order to achieve this create your own + * Content object and use {@link PopupView#PopupView(Content)}. + * + * @param small + * the minimal textual representation as HTML + * @param large + * the full, Component-type representation + */ + public PopupView(final java.lang.String small, final Component large) { + this(new PopupView.Content() { + @Override + public java.lang.String getMinimizedValueAsHTML() { + return small; + } + + @Override + public Component getPopupComponent() { + return large; + } + }); + + } + + /** + * Creates a PopupView through the PopupView.Content interface. This allows + * the creator to dynamically change the contents of the PopupView. + * + * @param content + * the PopupView.Content that contains the information for this + */ + public PopupView(PopupView.Content content) { + super(); + hideOnMouseOut = true; + setContent(content); + } + + /** + * This method will replace the current content of the panel with a new one. + * + * @param newContent + * PopupView.Content object containing new information for the + * PopupView + * @throws IllegalArgumentException + * if the method is passed a null value, or if one of the + * content methods returns null + */ + public void setContent(PopupView.Content newContent) + throws IllegalArgumentException { + if (newContent == null) { + throw new IllegalArgumentException("Content must not be null"); + } + content = newContent; + requestRepaint(); + } + + /** + * Returns the content-package for this PopupView. + * + * @return the PopupView.Content for this object or null + */ + public PopupView.Content getContent() { + return content; + } + + /** + * @deprecated Use {@link #setPopupVisible()} instead. + */ + @Deprecated + public void setPopupVisibility(boolean visible) { + setPopupVisible(visible); + } + + /** + * @deprecated Use {@link #isPopupVisible()} instead. + */ + @Deprecated + public boolean getPopupVisibility() { + return isPopupVisible(); + } + + /** + * Set the visibility of the popup. Does not hide the minimal + * representation. + * + * @param visible + */ + public void setPopupVisible(boolean visible) { + if (isPopupVisible() != visible) { + if (visible) { + visibleComponent = content.getPopupComponent(); + if (visibleComponent == null) { + throw new java.lang.IllegalStateException( + "PopupView.Content did not return Component to set visible"); + } + super.addComponent(visibleComponent); + } else { + super.removeComponent(visibleComponent); + visibleComponent = null; + } + fireEvent(new PopupVisibilityEvent(this)); + requestRepaint(); + } + } + + /** + * Return whether the popup is visible. + * + * @return true if the popup is showing + */ + public boolean isPopupVisible() { + return visibleComponent != null; + } + + /** + * Check if this popup will be hidden when the user takes the mouse cursor + * out of the popup area. + * + * @return true if the popup is hidden on mouse out, false otherwise + */ + public boolean isHideOnMouseOut() { + return hideOnMouseOut; + } + + /** + * Should the popup automatically hide when the user takes the mouse cursor + * out of the popup area? If this is false, the user must click outside the + * popup to close it. The default is true. + * + * @param hideOnMouseOut + * + */ + public void setHideOnMouseOut(boolean hideOnMouseOut) { + this.hideOnMouseOut = hideOnMouseOut; + } + + /* + * Methods inherited from AbstractComponentContainer. These are unnecessary + * (but mandatory). Most of them are not supported in this implementation. + */ + + /** + * This class only contains other components when the popup is showing. + * + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + @Override + public Iterator<Component> getComponentIterator() { + return new SingleComponentIterator(visibleComponent); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components (zero or one) + */ + @Override + public int getComponentCount() { + return (visibleComponent != null ? 1 : 0); + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.AbstractComponentContainer#removeAllComponents() + * @throws UnsupportedOperationException + */ + @Override + public void removeAllComponents() { + throw new UnsupportedOperationException(); + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.AbstractComponentContainer#moveComponentsFrom(com.vaadin.ui.ComponentContainer) + * @throws UnsupportedOperationException + */ + @Override + public void moveComponentsFrom(ComponentContainer source) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException(); + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component) + * @throws UnsupportedOperationException + */ + @Override + public void addComponent(Component c) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component, + * com.vaadin.ui.Component) + * @throws UnsupportedOperationException + */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException(); + } + + /** + * Not supported in this implementation + * + * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component) + */ + @Override + public void removeComponent(Component c) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + + } + + /* + * Methods for server-client communications. + */ + + /** + * Paint (serialize) the component for the client. + * + * @see com.vaadin.ui.AbstractComponent#paintContent(com.vaadin.terminal.PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + String html = content.getMinimizedValueAsHTML(); + if (html == null) { + html = ""; + } + target.addAttribute("html", html); + target.addAttribute("hideOnMouseOut", hideOnMouseOut); + + // Only paint component to client if we know that the popup is showing + if (isPopupVisible()) { + target.startTag("popupComponent"); + LegacyPaint.paint(visibleComponent, target); + target.endTag("popupComponent"); + } + + target.addVariable(this, "popupVisibility", isPopupVisible()); + } + + /** + * Deserialize changes received from client. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("popupVisibility")) { + setPopupVisible(((Boolean) variables.get("popupVisibility")) + .booleanValue()); + } + } + + /** + * Used to deliver customized content-packages to the PopupView. These are + * dynamically loaded when they are redrawn. The user must take care that + * neither of these methods ever return null. + */ + public interface Content extends Serializable { + + /** + * This should return a small view of the full data. + * + * @return value in HTML format + */ + public String getMinimizedValueAsHTML(); + + /** + * This should return the full Component representing the data + * + * @return a Component for the value + */ + public Component getPopupComponent(); + } + + /** + * Add a listener that is called whenever the visibility of the popup is + * changed. + * + * @param listener + * the listener to add + * @see PopupVisibilityListener + * @see PopupVisibilityEvent + * @see #removeListener(PopupVisibilityListener) + * + */ + public void addListener(PopupVisibilityListener listener) { + addListener(PopupVisibilityEvent.class, listener, + POPUP_VISIBILITY_METHOD); + } + + /** + * Removes a previously added listener, so that it no longer receives events + * when the visibility of the popup changes. + * + * @param listener + * the listener to remove + * @see PopupVisibilityListener + * @see #addListener(PopupVisibilityListener) + */ + public void removeListener(PopupVisibilityListener listener) { + removeListener(PopupVisibilityEvent.class, listener, + POPUP_VISIBILITY_METHOD); + } + + /** + * This event is received by the PopupVisibilityListeners when the + * visibility of the popup changes. You can get the new visibility directly + * with {@link #isPopupVisible()}, or get the PopupView that produced the + * event with {@link #getPopupView()}. + * + */ + public class PopupVisibilityEvent extends Event { + + public PopupVisibilityEvent(PopupView source) { + super(source); + } + + /** + * Get the PopupView instance that is the source of this event. + * + * @return the source PopupView + */ + public PopupView getPopupView() { + return (PopupView) getSource(); + } + + /** + * Returns the current visibility of the popup. + * + * @return true if the popup is visible + */ + public boolean isPopupVisible() { + return getPopupView().isPopupVisible(); + } + } + + /** + * Defines a listener that can receive a PopupVisibilityEvent when the + * visibility of the popup changes. + * + */ + public interface PopupVisibilityListener extends Serializable { + /** + * Pass to {@link PopupView#PopupVisibilityEvent} to start listening for + * popup visibility changes. + * + * @param event + * the event + * + * @see {@link PopupVisibilityEvent} + * @see {@link PopupView#addListener(PopupVisibilityListener)} + */ + public void popupVisibilityChange(PopupVisibilityEvent event); + } +} diff --git a/server/src/com/vaadin/ui/ProgressIndicator.java b/server/src/com/vaadin/ui/ProgressIndicator.java new file mode 100644 index 0000000000..fef54a267c --- /dev/null +++ b/server/src/com/vaadin/ui/ProgressIndicator.java @@ -0,0 +1,257 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.data.Property; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * <code>ProgressIndicator</code> is component that shows user state of a + * process (like long computing or file upload) + * + * <code>ProgressIndicator</code> has two mainmodes. One for indeterminate + * processes and other (default) for processes which progress can be measured + * + * May view an other property that indicates progress 0...1 + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 4 + */ +@SuppressWarnings("serial") +public class ProgressIndicator extends AbstractField<Number> implements + Property.Viewer, Property.ValueChangeListener, Vaadin6Component { + + /** + * Content mode, where the label contains only plain text. The getValue() + * result is coded to XML when painting. + */ + public static final int CONTENT_TEXT = 0; + + /** + * Content mode, where the label contains preformatted text. + */ + public static final int CONTENT_PREFORMATTED = 1; + + private boolean indeterminate = false; + + private Property dataSource; + + private int pollingInterval = 1000; + + /** + * Creates an a new ProgressIndicator. + */ + public ProgressIndicator() { + setPropertyDataSource(new ObjectProperty<Float>(new Float(0), + Float.class)); + } + + /** + * Creates a new instance of ProgressIndicator with given state. + * + * @param value + */ + public ProgressIndicator(Float value) { + setPropertyDataSource(new ObjectProperty<Float>(value, Float.class)); + } + + /** + * Creates a new instance of ProgressIndicator with stae read from given + * datasource. + * + * @param contentSource + */ + public ProgressIndicator(Property contentSource) { + setPropertyDataSource(contentSource); + } + + /** + * Sets the component to read-only. Readonly is not used in + * ProgressIndicator. + * + * @param readOnly + * True to enable read-only mode, False to disable it. + */ + @Override + public void setReadOnly(boolean readOnly) { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be se"); + } + dataSource.setReadOnly(readOnly); + } + + /** + * Is the component read-only ? Readonly is not used in ProgressIndicator - + * this returns allways false. + * + * @return True if the component is in read only mode. + */ + @Override + public boolean isReadOnly() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be se"); + } + return dataSource.isReadOnly(); + } + + /** + * Paints the content of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the Paint Operation fails. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("indeterminate", indeterminate); + target.addAttribute("pollinginterval", pollingInterval); + target.addAttribute("state", getValue().toString()); + } + + /** + * Gets the value of the ProgressIndicator. Value of the ProgressIndicator + * is Float between 0 and 1. + * + * @return the Value of the ProgressIndicator. + * @see com.vaadin.ui.AbstractField#getValue() + */ + @Override + public Number getValue() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + // TODO conversions to eliminate cast + return (Number) dataSource.getValue(); + } + + /** + * Sets the value of the ProgressIndicator. Value of the ProgressIndicator + * is the Float between 0 and 1. + * + * @param newValue + * the New value of the ProgressIndicator. + * @see com.vaadin.ui.AbstractField#setValue() + */ + @Override + public void setValue(Object newValue) { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + dataSource.setValue(newValue); + } + + /** + * @see com.vaadin.ui.AbstractField#getType() + */ + @Override + public Class<? extends Number> getType() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + return dataSource.getType(); + } + + /** + * Gets the viewing data-source property. + * + * @return the datasource. + * @see com.vaadin.ui.AbstractField#getPropertyDataSource() + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * Sets the property as data-source for viewing. + * + * @param newDataSource + * the new data source. + * @see com.vaadin.ui.AbstractField#setPropertyDataSource(com.vaadin.data.Property) + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + // Stops listening the old data source changes + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).removeListener(this); + } + + // Sets the new data source + dataSource = newDataSource; + + // Listens the new data source if possible + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + } + + /** + * Gets the mode of ProgressIndicator. + * + * @return true if in indeterminate mode. + */ + public boolean getContentMode() { + return indeterminate; + } + + /** + * Sets wheter or not the ProgressIndicator is indeterminate. + * + * @param newValue + * true to set to indeterminate mode. + */ + public void setIndeterminate(boolean newValue) { + indeterminate = newValue; + requestRepaint(); + } + + /** + * Gets whether or not the ProgressIndicator is indeterminate. + * + * @return true to set to indeterminate mode. + */ + public boolean isIndeterminate() { + return indeterminate; + } + + /** + * Sets the interval that component checks for progress. + * + * @param newValue + * the interval in milliseconds. + */ + public void setPollingInterval(int newValue) { + pollingInterval = newValue; + requestRepaint(); + } + + /** + * Gets the interval that component checks for progress. + * + * @return the interval in milliseconds. + */ + public int getPollingInterval() { + return pollingInterval; + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + + } + +} diff --git a/server/src/com/vaadin/ui/RichTextArea.java b/server/src/com/vaadin/ui/RichTextArea.java new file mode 100644 index 0000000000..cec952926b --- /dev/null +++ b/server/src/com/vaadin/ui/RichTextArea.java @@ -0,0 +1,344 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.text.Format; +import java.util.Map; + +import com.vaadin.data.Property; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * A simple RichTextArea to edit HTML format text. + * + * Note, that using {@link TextField#setMaxLength(int)} method in + * {@link RichTextArea} may produce unexpected results as formatting is counted + * into length of field. + */ +public class RichTextArea extends AbstractField<String> implements + Vaadin6Component { + + /** + * Value formatter used to format the string contents. + */ + @Deprecated + private Format format; + + /** + * Null representation. + */ + private String nullRepresentation = "null"; + + /** + * Is setting to null from non-null value allowed by setting with null + * representation . + */ + private boolean nullSettingAllowed = false; + + /** + * Temporary flag that indicates all content will be selected after the next + * paint. Reset to false after painted. + */ + private boolean selectAll = false; + + /** + * Constructs an empty <code>RichTextArea</code> with no caption. + */ + public RichTextArea() { + setValue(""); + } + + /** + * + * Constructs an empty <code>RichTextArea</code> with the given caption. + * + * @param caption + * the caption for the editor. + */ + public RichTextArea(String caption) { + this(); + setCaption(caption); + } + + /** + * Constructs a new <code>RichTextArea</code> that's bound to the specified + * <code>Property</code> and has no caption. + * + * @param dataSource + * the data source for the editor value + */ + public RichTextArea(Property dataSource) { + setPropertyDataSource(dataSource); + } + + /** + * Constructs a new <code>RichTextArea</code> that's bound to the specified + * <code>Property</code> and has the given caption. + * + * @param caption + * the caption for the editor. + * @param dataSource + * the data source for the editor value + */ + public RichTextArea(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a new <code>RichTextArea</code> with the given caption and + * initial text contents. + * + * @param caption + * the caption for the editor. + * @param value + * the initial text content of the editor. + */ + public RichTextArea(String caption, String value) { + setValue(value); + setCaption(caption); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (selectAll) { + target.addAttribute("selectAll", true); + selectAll = false; + } + + // Adds the content as variable + String value = getFormattedValue(); + if (value == null) { + value = getNullRepresentation(); + } + if (value == null) { + throw new IllegalStateException( + "Null values are not allowed if the null-representation is null"); + } + target.addVariable(this, "text", value); + + } + + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + // IE6 cannot support multi-classname selectors properly + // TODO Can be optimized now that support for I6 is dropped + if (readOnly) { + addStyleName("v-richtextarea-readonly"); + } else { + removeStyleName("v-richtextarea-readonly"); + } + } + + /** + * Selects all text in the rich text area. As a side effect, focuses the + * rich text area. + * + * @since 6.5 + */ + public void selectAll() { + /* + * Set selection range functionality is currently being + * planned/developed for GWT RTA. Only selecting all is currently + * supported. Consider moving selectAll and other selection related + * functions to AbstractTextField at that point to share the + * implementation. Some third party components extending + * AbstractTextField might however not want to support them. + */ + selectAll = true; + focus(); + requestRepaint(); + } + + /** + * Gets the formatted string value. Sets the field value by using the + * assigned Format. + * + * @return the Formatted value. + * @see #setFormat(Format) + * @see Format + * @deprecated + */ + @Deprecated + protected String getFormattedValue() { + Object v = getValue(); + if (v == null) { + return null; + } + return v.toString(); + } + + @Override + public String getValue() { + String v = super.getValue(); + if (format == null || v == null) { + return v; + } + try { + return format.format(v); + } catch (final IllegalArgumentException e) { + return v; + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Sets the text + if (variables.containsKey("text") && !isReadOnly()) { + + // Only do the setting if the string representation of the value + // has been updated + String newValue = (String) variables.get("text"); + + final String oldValue = getFormattedValue(); + if (newValue != null + && (oldValue == null || isNullSettingAllowed()) + && newValue.equals(getNullRepresentation())) { + newValue = null; + } + if (newValue != oldValue + && (newValue == null || !newValue.equals(oldValue))) { + boolean wasModified = isModified(); + setValue(newValue, true); + + // If the modified status changes, or if we have a formatter, + // repaint is needed after all. + if (format != null || wasModified != isModified()) { + requestRepaint(); + } + } + } + + } + + @Override + public Class<String> getType() { + return String.class; + } + + /** + * Gets the null-string representation. + * + * <p> + * The null-valued strings are represented on the user interface by + * replacing the null value with this string. If the null representation is + * set null (not 'null' string), painting null value throws exception. + * </p> + * + * <p> + * The default value is string 'null'. + * </p> + * + * @return the String Textual representation for null strings. + * @see TextField#isNullSettingAllowed() + */ + public String getNullRepresentation() { + return nullRepresentation; + } + + /** + * Is setting nulls with null-string representation allowed. + * + * <p> + * If this property is true, writing null-representation string to text + * field always sets the field value to real null. If this property is + * false, null setting is not made, but the null values are maintained. + * Maintenance of null-values is made by only converting the textfield + * contents to real null, if the text field matches the null-string + * representation and the current value of the field is null. + * </p> + * + * <p> + * By default this setting is false + * </p> + * + * @return boolean Should the null-string represenation be always converted + * to null-values. + * @see TextField#getNullRepresentation() + */ + public boolean isNullSettingAllowed() { + return nullSettingAllowed; + } + + /** + * Sets the null-string representation. + * + * <p> + * The null-valued strings are represented on the user interface by + * replacing the null value with this string. If the null representation is + * set null (not 'null' string), painting null value throws exception. + * </p> + * + * <p> + * The default value is string 'null' + * </p> + * + * @param nullRepresentation + * Textual representation for null strings. + * @see TextField#setNullSettingAllowed(boolean) + */ + public void setNullRepresentation(String nullRepresentation) { + this.nullRepresentation = nullRepresentation; + } + + /** + * Sets the null conversion mode. + * + * <p> + * If this property is true, writing null-representation string to text + * field always sets the field value to real null. If this property is + * false, null setting is not made, but the null values are maintained. + * Maintenance of null-values is made by only converting the textfield + * contents to real null, if the text field matches the null-string + * representation and the current value of the field is null. + * </p> + * + * <p> + * By default this setting is false. + * </p> + * + * @param nullSettingAllowed + * Should the null-string represenation be always converted to + * null-values. + * @see TextField#getNullRepresentation() + */ + public void setNullSettingAllowed(boolean nullSettingAllowed) { + this.nullSettingAllowed = nullSettingAllowed; + } + + /** + * Gets the value formatter of TextField. + * + * @return the Format used to format the value. + * @deprecated replaced by {@link com.vaadin.data.util.PropertyFormatter} + */ + @Deprecated + public Format getFormat() { + return format; + } + + /** + * Gets the value formatter of TextField. + * + * @param format + * the Format used to format the value. Null disables the + * formatting. + * @deprecated replaced by {@link com.vaadin.data.util.PropertyFormatter} + */ + @Deprecated + public void setFormat(Format format) { + this.format = format; + requestRepaint(); + } + + @Override + protected boolean isEmpty() { + return super.isEmpty() || getValue().length() == 0; + } + +} diff --git a/server/src/com/vaadin/ui/Root.java b/server/src/com/vaadin/ui/Root.java new file mode 100644 index 0000000000..bd4842632b --- /dev/null +++ b/server/src/com/vaadin/ui/Root.java @@ -0,0 +1,1227 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.annotations.EagerInit; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ActionManager; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.root.RootServerRpc; +import com.vaadin.shared.ui.root.RootState; +import com.vaadin.terminal.Page; +import com.vaadin.terminal.Page.BrowserWindowResizeEvent; +import com.vaadin.terminal.Page.BrowserWindowResizeListener; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedRequest.BrowserDetails; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; +import com.vaadin.ui.Window.CloseListener; + +/** + * The topmost component in any component hierarchy. There is one root for every + * Vaadin instance in a browser window. A root may either represent an entire + * browser window (or tab) or some part of a html page where a Vaadin + * application is embedded. + * <p> + * The root is the server side entry point for various client side features that + * are not represented as components added to a layout, e.g notifications, sub + * windows, and executing javascript in the browser. + * </p> + * <p> + * When a new application instance is needed, typically because the user opens + * the application in a browser window, + * {@link Application#gerRoot(WrappedRequest)} is invoked to get a root. That + * method does by default create a root according to the + * {@value Application#ROOT_PARAMETER} parameter from web.xml. + * </p> + * <p> + * After a root has been created by the application, it is initialized using + * {@link #init(WrappedRequest)}. This method is intended to be overridden by + * the developer to add components to the user interface and initialize + * non-component functionality. The component hierarchy is initialized by + * passing a {@link ComponentContainer} with the main layout of the view to + * {@link #setContent(ComponentContainer)}. + * </p> + * <p> + * If a {@link EagerInit} annotation is present on a class extending + * <code>Root</code>, the framework will use a faster initialization method + * which will not ensure that {@link BrowserDetails} are present in the + * {@link WrappedRequest} passed to the init method. + * </p> + * + * @see #init(WrappedRequest) + * @see Application#getRoot(WrappedRequest) + * + * @since 7.0 + */ +public abstract class Root extends AbstractComponentContainer implements + Action.Container, Action.Notifier, Vaadin6Component { + + /** + * Helper class to emulate the main window from Vaadin 6 using roots. This + * class should be used in the same way as Window used as a browser level + * window in Vaadin 6 with {@link com.vaadin.Application.LegacyApplication} + */ + @Deprecated + @EagerInit + public static class LegacyWindow extends Root { + private String name; + + /** + * Create a new legacy window + */ + public LegacyWindow() { + super(); + } + + /** + * Creates a new legacy window with the given caption + * + * @param caption + * the caption of the window + */ + public LegacyWindow(String caption) { + super(caption); + } + + /** + * Creates a legacy window with the given caption and content layout + * + * @param caption + * @param content + */ + public LegacyWindow(String caption, ComponentContainer content) { + super(caption, content); + } + + @Override + protected void init(WrappedRequest request) { + // Just empty + } + + /** + * Gets the unique name of the window. The name of the window is used to + * uniquely identify it. + * <p> + * The name also determines the URL that can be used for direct access + * to a window. All windows can be accessed through + * {@code http://host:port/app/win} where {@code http://host:port/app} + * is the application URL (as returned by {@link Application#getURL()} + * and {@code win} is the window name. + * </p> + * <p> + * Note! Portlets do not support direct window access through URLs. + * </p> + * + * @return the Name of the Window. + */ + public String getName() { + return name; + } + + /** + * Sets the unique name of the window. The name of the window is used to + * uniquely identify it inside the application. + * <p> + * The name also determines the URL that can be used for direct access + * to a window. All windows can be accessed through + * {@code http://host:port/app/win} where {@code http://host:port/app} + * is the application URL (as returned by {@link Application#getURL()} + * and {@code win} is the window name. + * </p> + * <p> + * This method can only be called before the window is added to an + * application. + * <p> + * Note! Portlets do not support direct window access through URLs. + * </p> + * + * @param name + * the new name for the window or null if the application + * should automatically assign a name to it + * @throws IllegalStateException + * if the window is attached to an application + */ + public void setName(String name) { + this.name = name; + // The name can not be changed in application + if (getApplication() != null) { + throw new IllegalStateException( + "Window name can not be changed while " + + "the window is in application"); + } + + } + + /** + * Gets the full URL of the window. The returned URL is window specific + * and can be used to directly refer to the window. + * <p> + * Note! This method can not be used for portlets. + * </p> + * + * @return the URL of the window or null if the window is not attached + * to an application + */ + public URL getURL() { + Application application = getApplication(); + if (application == null) { + return null; + } + + try { + return new URL(application.getURL(), getName() + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException( + "Internal problem getting window URL, please report"); + } + } + + /** + * Opens the given resource in this root. The contents of this Root is + * replaced by the {@code Resource}. + * + * @param resource + * the resource to show in this root + * + * @deprecated As of 7.0, use getPage().open instead + */ + @Deprecated + public void open(Resource resource) { + getPage().open(resource); + } + + /* ********************************************************************* */ + + /** + * Opens the given resource in a window with the given name. + * <p> + * The supplied {@code windowName} is used as the target name in a + * window.open call in the client. This means that special values such + * as "_blank", "_self", "_top", "_parent" have special meaning. An + * empty or <code>null</code> window name is also a special case. + * </p> + * <p> + * "", null and "_self" as {@code windowName} all causes the resource to + * be opened in the current window, replacing any old contents. For + * downloadable content you should avoid "_self" as "_self" causes the + * client to skip rendering of any other changes as it considers them + * irrelevant (the page will be replaced by the resource). This can + * speed up the opening of a resource, but it might also put the client + * side into an inconsistent state if the window content is not + * completely replaced e.g., if the resource is downloaded instead of + * displayed in the browser. + * </p> + * <p> + * "_blank" as {@code windowName} causes the resource to always be + * opened in a new window or tab (depends on the browser and browser + * settings). + * </p> + * <p> + * "_top" and "_parent" as {@code windowName} works as specified by the + * HTML standard. + * </p> + * <p> + * Any other {@code windowName} will open the resource in a window with + * that name, either by opening a new window/tab in the browser or by + * replacing the contents of an existing window with that name. + * </p> + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + * @deprecated As of 7.0, use getPage().open instead + */ + @Deprecated + public void open(Resource resource, String windowName) { + getPage().open(resource, windowName); + } + + /** + * Opens the given resource in a window with the given size, border and + * name. For more information on the meaning of {@code windowName}, see + * {@link #open(Resource, String)}. + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + * @param width + * the width of the window in pixels + * @param height + * the height of the window in pixels + * @param border + * the border style of the window. See {@link #BORDER_NONE + * Window.BORDER_* constants} + * @deprecated As of 7.0, use getPage().open instead + */ + @Deprecated + public void open(Resource resource, String windowName, int width, + int height, int border) { + getPage().open(resource, windowName, width, height, border); + } + + /** + * Adds a new {@link BrowserWindowResizeListener} to this root. The + * listener will be notified whenever the browser window within which + * this root resides is resized. + * + * @param resizeListener + * the listener to add + * + * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent) + * @see #setResizeLazy(boolean) + * + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public void addListener(BrowserWindowResizeListener resizeListener) { + getPage().addListener(resizeListener); + } + + /** + * Removes a {@link BrowserWindowResizeListener} from this root. The + * listener will no longer be notified when the browser window is + * resized. + * + * @param resizeListener + * the listener to remove + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public void removeListener(BrowserWindowResizeListener resizeListener) { + getPage().removeListener(resizeListener); + } + + /** + * Gets the last known height of the browser window in which this root + * resides. + * + * @return the browser window height in pixels + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public int getBrowserWindowHeight() { + return getPage().getBrowserWindowHeight(); + } + + /** + * Gets the last known width of the browser window in which this root + * resides. + * + * @return the browser window width in pixels + * + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public int getBrowserWindowWidth() { + return getPage().getBrowserWindowWidth(); + } + + /** + * Executes JavaScript in this window. + * + * <p> + * This method allows one to inject javascript from the server to + * client. A client implementation is not required to implement this + * functionality, but currently all web-based clients do implement this. + * </p> + * + * <p> + * Executing javascript this way often leads to cross-browser + * compatibility issues and regressions that are hard to resolve. Use of + * this method should be avoided and instead it is recommended to create + * new widgets with GWT. For more info on creating own, reusable + * client-side widgets in Java, read the corresponding chapter in Book + * of Vaadin. + * </p> + * + * @param script + * JavaScript snippet that will be executed. + * + * @deprecated as of 7.0, use JavaScript.getCurrent().execute(String) + * instead + */ + @Deprecated + public void executeJavaScript(String script) { + getPage().getJavaScript().execute(script); + } + + @Override + public void setCaption(String caption) { + // Override to provide backwards compatibility + getState().setCaption(caption); + getPage().setTitle(caption); + } + + } + + /** + * The application to which this root belongs + */ + private Application application; + + /** + * List of windows in this root. + */ + private final LinkedHashSet<Window> windows = new LinkedHashSet<Window>(); + + /** + * The component that should be scrolled into view after the next repaint. + * Null if nothing should be scrolled into view. + */ + private Component scrollIntoView; + + /** + * The id of this root, used to find the server side instance of the root + * form which a request originates. A negative value indicates that the root + * id has not yet been assigned by the Application. + * + * @see Application#nextRootId + */ + private int rootId = -1; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. + */ + protected ActionManager actionManager; + + /** + * Thread local for keeping track of the current root. + */ + private static final ThreadLocal<Root> currentRoot = new ThreadLocal<Root>(); + + /** Identifies the click event */ + private static final String CLICK_EVENT_ID = VRoot.CLICK_EVENT_ID; + + private ConnectorTracker connectorTracker = new ConnectorTracker(this); + + private Page page = new Page(this); + + private RootServerRpc rpc = new RootServerRpc() { + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Root.this, mouseDetails)); + } + }; + + /** + * Creates a new empty root without a caption. This root will have a + * {@link VerticalLayout} with margins enabled as its content. + */ + public Root() { + this((ComponentContainer) null); + } + + /** + * Creates a new root with the given component container as its content. + * + * @param content + * the content container to use as this roots content. + * + * @see #setContent(ComponentContainer) + */ + public Root(ComponentContainer content) { + registerRpc(rpc); + setSizeFull(); + setContent(content); + } + + /** + * Creates a new empty root with the given caption. This root will have a + * {@link VerticalLayout} with margins enabled as its content. + * + * @param caption + * the caption of the root, used as the page title if there's + * nothing but the application on the web page + * + * @see #setCaption(String) + */ + public Root(String caption) { + this((ComponentContainer) null); + setCaption(caption); + } + + /** + * Creates a new root with the given caption and content. + * + * @param caption + * the caption of the root, used as the page title if there's + * nothing but the application on the web page + * @param content + * the content container to use as this roots content. + * + * @see #setContent(ComponentContainer) + * @see #setCaption(String) + */ + public Root(String caption, ComponentContainer content) { + this(content); + setCaption(caption); + } + + @Override + public RootState getState() { + return (RootState) super.getState(); + } + + @Override + public Class<? extends RootState> getStateType() { + // This is a workaround for a problem with creating the correct state + // object during build + return RootState.class; + } + + /** + * Overridden to return a value instead of referring to the parent. + * + * @return this root + * + * @see com.vaadin.ui.AbstractComponent#getRoot() + */ + @Override + public Root getRoot() { + return this; + } + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + throw new UnsupportedOperationException(); + } + + @Override + public Application getApplication() { + return application; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + page.paintContent(target); + + if (scrollIntoView != null) { + target.addAttribute("scrollTo", scrollIntoView); + scrollIntoView = null; + } + + if (pendingFocus != null) { + // ensure focused component is still attached to this main window + if (pendingFocus.getRoot() == this + || (pendingFocus.getRoot() != null && pendingFocus + .getRoot().getParent() == this)) { + target.addAttribute("focused", pendingFocus); + } + pendingFocus = null; + } + + if (actionManager != null) { + actionManager.paintActions(null, target); + } + + if (isResizeLazy()) { + target.addAttribute(VRoot.RESIZE_LAZY, true); + } + } + + /** + * Fire a click event to all click listeners. + * + * @param object + * The raw "value" of the variable change from the client side. + */ + private void fireClick(Map<String, Object> parameters) { + MouseEventDetails mouseDetails = MouseEventDetails + .deSerialize((String) parameters.get("mouseDetails")); + fireEvent(new ClickEvent(this, mouseDetails)); + } + + @Override + @SuppressWarnings("unchecked") + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey(CLICK_EVENT_ID)) { + fireClick((Map<String, Object>) variables.get(CLICK_EVENT_ID)); + } + + // Actions + if (actionManager != null) { + actionManager.handleActions(variables, this); + } + + if (variables.containsKey(VRoot.FRAGMENT_VARIABLE)) { + String fragment = (String) variables.get(VRoot.FRAGMENT_VARIABLE); + getPage().setFragment(fragment, true); + } + + if (variables.containsKey("height") || variables.containsKey("width")) { + getPage().setBrowserWindowSize((Integer) variables.get("width"), + (Integer) variables.get("height")); + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + @Override + public Iterator<Component> getComponentIterator() { + // TODO could directly create some kind of combined iterator instead of + // creating a new ArrayList + ArrayList<Component> components = new ArrayList<Component>(); + + if (getContent() != null) { + components.add(getContent()); + } + + components.addAll(windows); + + return components.iterator(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentCount() + */ + @Override + public int getComponentCount() { + return windows.size() + (getContent() == null ? 0 : 1); + } + + /** + * Sets the application to which this root is assigned. It is not legal to + * change the application once it has been set nor to set a + * <code>null</code> application. + * <p> + * This method is mainly intended for internal use by the framework. + * </p> + * + * @param application + * the application to set + * + * @throws IllegalStateException + * if the application has already been set + * + * @see #getApplication() + */ + public void setApplication(Application application) { + if ((application == null) == (this.application == null)) { + throw new IllegalStateException("Application has already been set"); + } else { + this.application = application; + } + + if (application != null) { + attach(); + } else { + detach(); + } + } + + /** + * Sets the id of this root within its application. The root id is used to + * route requests to the right root. + * <p> + * This method is mainly intended for internal use by the framework. + * </p> + * + * @param rootId + * the id of this root + * + * @throws IllegalStateException + * if the root id has already been set + * + * @see #getRootId() + */ + public void setRootId(int rootId) { + if (this.rootId != -1) { + throw new IllegalStateException("Root id has already been defined"); + } + this.rootId = rootId; + } + + /** + * Gets the id of the root, used to identify this root within its + * application when processing requests. The root id should be present in + * every request to the server that originates from this root. + * {@link Application#getRootForRequest(WrappedRequest)} uses this id to + * find the route to which the request belongs. + * + * @return + */ + public int getRootId() { + return rootId; + } + + /** + * Adds a window as a subwindow inside this root. To open a new browser + * window or tab, you should instead use {@link open(Resource)} with an url + * pointing to this application and ensure + * {@link Application#getRoot(WrappedRequest)} returns an appropriate root + * for the request. + * + * @param window + * @throws IllegalArgumentException + * if the window is already added to an application + * @throws NullPointerException + * if the given <code>Window</code> is <code>null</code>. + */ + public void addWindow(Window window) throws IllegalArgumentException, + NullPointerException { + + if (window == null) { + throw new NullPointerException("Argument must not be null"); + } + + if (window.getApplication() != null) { + throw new IllegalArgumentException( + "Window is already attached to an application."); + } + + attachWindow(window); + } + + /** + * Helper method to attach a window. + * + * @param w + * the window to add + */ + private void attachWindow(Window w) { + windows.add(w); + w.setParent(this); + requestRepaint(); + } + + /** + * Remove the given subwindow from this root. + * + * Since Vaadin 6.5, {@link CloseListener}s are called also when explicitly + * removing a window by calling this method. + * + * Since Vaadin 6.5, returns a boolean indicating if the window was removed + * or not. + * + * @param window + * Window to be removed. + * @return true if the subwindow was removed, false otherwise + */ + public boolean removeWindow(Window window) { + if (!windows.remove(window)) { + // Window window is not a subwindow of this root. + return false; + } + window.setParent(null); + window.fireClose(); + requestRepaint(); + + return true; + } + + /** + * Gets all the windows added to this root. + * + * @return an unmodifiable collection of windows + */ + public Collection<Window> getWindows() { + return Collections.unmodifiableCollection(windows); + } + + @Override + public void focus() { + super.focus(); + } + + /** + * Component that should be focused after the next repaint. Null if no focus + * change should take place. + */ + private Focusable pendingFocus; + + private boolean resizeLazy = false; + + /** + * This method is used by Component.Focusable objects to request focus to + * themselves. Focus renders must be handled at window level (instead of + * Component.Focusable) due we want the last focused component to be focused + * in client too. Not the one that is rendered last (the case we'd get if + * implemented in Focusable only). + * + * To focus component from Vaadin application, use Focusable.focus(). See + * {@link Focusable}. + * + * @param focusable + * to be focused on next paint + */ + public void setFocusedComponent(Focusable focusable) { + pendingFocus = focusable; + requestRepaint(); + } + + /** + * Scrolls any component between the component and root to a suitable + * position so the component is visible to the user. The given component + * must belong to this root. + * + * @param component + * the component to be scrolled into view + * @throws IllegalArgumentException + * if {@code component} does not belong to this root + */ + public void scrollIntoView(Component component) + throws IllegalArgumentException { + if (component.getRoot() != this) { + throw new IllegalArgumentException( + "The component where to scroll must belong to this root."); + } + scrollIntoView = component; + requestRepaint(); + } + + /** + * Gets the content of this root. The content is a component container that + * serves as the outermost item of the visual contents of this root. + * + * @return a component container to use as content + * + * @see #setContent(ComponentContainer) + * @see #createDefaultLayout() + */ + public ComponentContainer getContent() { + return (ComponentContainer) getState().getContent(); + } + + /** + * Helper method to create the default content layout that is used if no + * content has not been explicitly defined. + * + * @return a newly created layout + */ + private static VerticalLayout createDefaultLayout() { + VerticalLayout layout = new VerticalLayout(); + layout.setMargin(true); + return layout; + } + + /** + * Sets the content of this root. The content is a component container that + * serves as the outermost item of the visual contents of this root. If no + * content has been set, a {@link VerticalLayout} with margins enabled will + * be used by default - see {@link #createDefaultLayout()}. The content can + * also be set in a constructor. + * + * @return a component container to use as content + * + * @see #Root(ComponentContainer) + * @see #createDefaultLayout() + */ + public void setContent(ComponentContainer content) { + if (content == null) { + content = createDefaultLayout(); + } + + if (getState().getContent() != null) { + super.removeComponent((Component) getState().getContent()); + } + getState().setContent(content); + if (content != null) { + super.addComponent(content); + } + + requestRepaint(); + } + + /** + * Adds a component to this root. The component is not added directly to the + * root, but instead to the content container ({@link #getContent()}). + * + * @param component + * the component to add to this root + * + * @see #getContent() + */ + @Override + public void addComponent(Component component) { + getContent().addComponent(component); + } + + /** + * This implementation removes the component from the content container ( + * {@link #getContent()}) instead of from the actual root. + */ + @Override + public void removeComponent(Component component) { + getContent().removeComponent(component); + } + + /** + * This implementation removes the components from the content container ( + * {@link #getContent()}) instead of from the actual root. + */ + @Override + public void removeAllComponents() { + getContent().removeAllComponents(); + } + + /** + * Internal initialization method, should not be overridden. This method is + * not declared as final because that would break compatibility with e.g. + * CDI. + * + * @param request + * the initialization request + */ + public void doInit(WrappedRequest request) { + getPage().init(request); + + // Call the init overridden by the application developer + init(request); + } + + /** + * Initializes this root. This method is intended to be overridden by + * subclasses to build the view and configure non-component functionality. + * Performing the initialization in a constructor is not suggested as the + * state of the root is not properly set up when the constructor is invoked. + * <p> + * The {@link WrappedRequest} can be used to get information about the + * request that caused this root to be created. By default, the + * {@link BrowserDetails} will be available in the request. If the browser + * details are not required, loading the application in the browser can take + * some shortcuts giving a faster initial rendering. This can be indicated + * by adding the {@link EagerInit} annotation to the Root class. + * </p> + * + * @param request + * the wrapped request that caused this root to be created + */ + protected abstract void init(WrappedRequest request); + + /** + * Sets the thread local for the current root. This method is used by the + * framework to set the current application whenever a new request is + * processed and it is cleared when the request has been processed. + * <p> + * The application developer can also use this method to define the current + * root outside the normal request handling, e.g. when initiating custom + * background threads. + * </p> + * + * @param root + * the root to register as the current root + * + * @see #getCurrent() + * @see ThreadLocal + */ + public static void setCurrent(Root root) { + currentRoot.set(root); + } + + /** + * Gets the currently used root. The current root is automatically defined + * when processing requests to the server. In other cases, (e.g. from + * background threads), the current root is not automatically defined. + * + * @return the current root instance if available, otherwise + * <code>null</code> + * + * @see #setCurrent(Root) + */ + public static Root getCurrent() { + return currentRoot.get(); + } + + public void setScrollTop(int scrollTop) { + throw new RuntimeException("Not yet implemented"); + } + + @Override + protected ActionManager getActionManager() { + if (actionManager == null) { + actionManager = new ActionManager(this); + } + return actionManager; + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (actionManager != null) { + actionManager.removeAction(action); + } + } + + @Override + public void addActionHandler(Handler actionHandler) { + getActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (actionManager != null) { + actionManager.removeActionHandler(actionHandler); + } + } + + /** + * Should resize operations be lazy, i.e. should there be a delay before + * layout sizes are recalculated. Speeds up resize operations in slow UIs + * with the penalty of slightly decreased usability. + * <p> + * Default value: <code>false</code> + * + * @param resizeLazy + * true to use a delay before recalculating sizes, false to + * calculate immediately. + */ + public void setResizeLazy(boolean resizeLazy) { + this.resizeLazy = resizeLazy; + requestRepaint(); + } + + /** + * Checks whether lazy resize is enabled. + * + * @return <code>true</code> if lazy resize is enabled, <code>false</code> + * if lazy resize is not enabled + */ + public boolean isResizeLazy() { + return resizeLazy; + } + + /** + * Add a click listener to the Root. The listener is called whenever the + * user clicks inside the Root. Also when the click targets a component + * inside the Root, provided the targeted component does not prevent the + * click event from propagating. + * + * Use {@link #removeListener(ClickListener)} to remove the listener. + * + * @param listener + * The listener to add + */ + public void addListener(ClickListener listener) { + addListener(CLICK_EVENT_ID, ClickEvent.class, listener, + ClickListener.clickMethod); + } + + /** + * Remove a click listener from the Root. The listener should earlier have + * been added using {@link #addListener(ClickListener)}. + * + * @param listener + * The listener to remove + */ + public void removeListener(ClickListener listener) { + removeListener(CLICK_EVENT_ID, ClickEvent.class, listener); + } + + @Override + public boolean isConnectorEnabled() { + // TODO How can a Root be invisible? What does it mean? + return isVisible() && isEnabled(); + } + + public ConnectorTracker getConnectorTracker() { + return connectorTracker; + } + + public Page getPage() { + return page; + } + + /** + * Setting the caption of a Root is not supported. To set the title of the + * HTML page, use Page.setTitle + * + * @deprecated as of 7.0.0, use {@link Page#setTitle(String)} + */ + @Override + @Deprecated + public void setCaption(String caption) { + throw new IllegalStateException( + "You can not set the title of a Root. To set the title of the HTML page, use Page.setTitle"); + } + + /** + * Shows a notification message on the middle of the root. The message + * automatically disappears ("humanized message"). + * + * Care should be taken to to avoid XSS vulnerabilities as the caption is + * rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The message + * + * @deprecated As of 7.0, use Notification.show instead but be aware that + * Notification.show does not allow HTML. + */ + @Deprecated + public void showNotification(String caption) { + Notification notification = new Notification(caption); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification message the root. The position and behavior of the + * message depends on the type, which is one of the basic types defined in + * {@link Notification}, for instance Notification.TYPE_WARNING_MESSAGE. + * + * Care should be taken to to avoid XSS vulnerabilities as the caption is + * rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The message + * @param type + * The message type + * + * @deprecated As of 7.0, use Notification.show instead but be aware that + * Notification.show does not allow HTML. + */ + @Deprecated + public void showNotification(String caption, int type) { + Notification notification = new Notification(caption, type); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description on the middle of the root. The message automatically + * disappears ("humanized message"). + * + * Care should be taken to to avoid XSS vulnerabilities as the caption and + * description are rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The caption of the message + * @param description + * The message description + * + * @deprecated As of 7.0, use new Notification(...).show(Page) instead but + * be aware that HTML by default not allowed. + */ + @Deprecated + public void showNotification(String caption, String description) { + Notification notification = new Notification(caption, description); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description. The position and behavior of the message depends on the + * type, which is one of the basic types defined in {@link Notification} , + * for instance Notification.TYPE_WARNING_MESSAGE. + * + * Care should be taken to to avoid XSS vulnerabilities as the caption and + * description are rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The caption of the message + * @param description + * The message description + * @param type + * The message type + * + * @deprecated As of 7.0, use new Notification(...).show(Page) instead but + * be aware that HTML by default not allowed. + */ + @Deprecated + public void showNotification(String caption, String description, int type) { + Notification notification = new Notification(caption, description, type); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description. The position and behavior of the message depends on the + * type, which is one of the basic types defined in {@link Notification} , + * for instance Notification.TYPE_WARNING_MESSAGE. + * + * Care should be taken to avoid XSS vulnerabilities if html content is + * allowed. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The message caption + * @param description + * The message description + * @param type + * The type of message + * @param htmlContentAllowed + * Whether html in the caption and description should be + * displayed as html or as plain text + * + * @deprecated As of 7.0, use new Notification(...).show(Page). + */ + @Deprecated + public void showNotification(String caption, String description, int type, + boolean htmlContentAllowed) { + getPage() + .showNotification( + new Notification(caption, description, type, + htmlContentAllowed)); + } + + /** + * Shows a notification message. + * + * @see Notification + * @see #showNotification(String) + * @see #showNotification(String, int) + * @see #showNotification(String, String) + * @see #showNotification(String, String, int) + * + * @param notification + * The notification message to show + * + * @deprecated As of 7.0, use Notification.show instead + */ + @Deprecated + public void showNotification(Notification notification) { + getPage().showNotification(notification); + } + +} diff --git a/server/src/com/vaadin/ui/Select.java b/server/src/com/vaadin/ui/Select.java new file mode 100644 index 0000000000..f60935c64b --- /dev/null +++ b/server/src/com/vaadin/ui/Select.java @@ -0,0 +1,803 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.event.FieldEvents; +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; + +/** + * <p> + * A class representing a selection of items the user has selected in a UI. The + * set of choices is presented as a set of {@link com.vaadin.data.Item}s in a + * {@link com.vaadin.data.Container}. + * </p> + * + * <p> + * A <code>Select</code> component may be in single- or multiselect mode. + * Multiselect mode means that more than one item can be selected + * simultaneously. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Select extends AbstractSelect implements AbstractSelect.Filtering, + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier { + + /** + * Holds value of property pageLength. 0 disables paging. + */ + protected int pageLength = 10; + + private int columns = 0; + + // Current page when the user is 'paging' trough options + private int currentPage = -1; + + private int filteringMode = FILTERINGMODE_STARTSWITH; + + private String filterstring; + private String prevfilterstring; + + /** + * Number of options that pass the filter, excluding the null item if any. + */ + private int filteredSize; + + /** + * Cache of filtered options, used only by the in-memory filtering system. + */ + private List<Object> filteredOptions; + + /** + * Flag to indicate that request repaint is called by filter request only + */ + private boolean optionRequest; + + /** + * True if the container is being filtered temporarily and item set change + * notifications should be suppressed. + */ + private boolean filteringContainer; + + /** + * Flag to indicate whether to scroll the selected item visible (select the + * page on which it is) when opening the popup or not. Only applies to + * single select mode. + * + * This requires finding the index of the item, which can be expensive in + * many large lazy loading containers. + */ + private boolean scrollToSelectedItem = true; + + /* Constructors */ + + /* Component methods */ + + public Select() { + super(); + } + + public Select(String caption, Collection<?> options) { + super(caption, options); + } + + public Select(String caption, Container dataSource) { + super(caption, dataSource); + } + + public Select(String caption) { + super(caption); + } + + /** + * Paints the content of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (isMultiSelect()) { + // background compatibility hack. This object shouldn't be used for + // multiselect lists anymore (ListSelect instead). This fallbacks to + // a simpler paint method in super class. + super.paintContent(target); + // Fix for #4553 + target.addAttribute("type", "legacy-multi"); + return; + } + + // clear caption change listeners + getCaptionChangeListener().clear(); + + // The tab ordering number + if (getTabIndex() != 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // If the field is modified, but not committed, set modified attribute + if (isModified()) { + target.addAttribute("modified", true); + } + + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + + boolean needNullSelectOption = false; + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + needNullSelectOption = (getNullSelectionItemId() == null); + if (!needNullSelectOption) { + target.addAttribute("nullselectitem", true); + } + } + + // Constructs selected keys array + String[] selectedKeys; + if (isMultiSelect()) { + selectedKeys = new String[((Set<?>) getValue()).size()]; + } else { + selectedKeys = new String[(getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + } + + target.addAttribute("pagelength", pageLength); + + target.addAttribute("filteringmode", getFilteringMode()); + + // Paints the options and create array of selected id keys + int keyIndex = 0; + + target.startTag("options"); + + if (currentPage < 0) { + optionRequest = false; + currentPage = 0; + filterstring = ""; + } + + boolean nullFilteredOut = filterstring != null + && !"".equals(filterstring) + && filteringMode != FILTERINGMODE_OFF; + // null option is needed and not filtered out, even if not on current + // page + boolean nullOptionVisible = needNullSelectOption && !nullFilteredOut; + + // first try if using container filters is possible + List<?> options = getOptionsWithFilter(nullOptionVisible); + if (null == options) { + // not able to use container filters, perform explicit in-memory + // filtering + options = getFilteredOptions(); + filteredSize = options.size(); + options = sanitetizeList(options, nullOptionVisible); + } + + final boolean paintNullSelection = needNullSelectOption + && currentPage == 0 && !nullFilteredOut; + + if (paintNullSelection) { + target.startTag("so"); + target.addAttribute("caption", ""); + target.addAttribute("key", ""); + target.endTag("so"); + } + + final Iterator<?> i = options.iterator(); + // Paints the available selection options from data source + + while (i.hasNext()) { + + final Object id = i.next(); + + if (!isNullSelectionAllowed() && id != null + && id.equals(getNullSelectionItemId()) && !isSelected(id)) { + continue; + } + + // Gets the option attribute values + final String key = itemIdMapper.key(id); + final String caption = getItemCaption(id); + final Resource icon = getItemIcon(id); + getCaptionChangeListener().addNotifierForItem(id); + + // Paints the option + target.startTag("so"); + if (icon != null) { + target.addAttribute("icon", icon); + } + target.addAttribute("caption", caption); + if (id != null && id.equals(getNullSelectionItemId())) { + target.addAttribute("nullselection", true); + } + target.addAttribute("key", key); + if (isSelected(id) && keyIndex < selectedKeys.length) { + target.addAttribute("selected", true); + selectedKeys[keyIndex++] = key; + } + target.endTag("so"); + } + target.endTag("options"); + + target.addAttribute("totalitems", size() + + (needNullSelectOption ? 1 : 0)); + if (filteredSize > 0 || nullOptionVisible) { + target.addAttribute("totalMatches", filteredSize + + (nullOptionVisible ? 1 : 0)); + } + + // Paint variables + target.addVariable(this, "selected", selectedKeys); + if (isNewItemsAllowed()) { + target.addVariable(this, "newitem", ""); + } + + target.addVariable(this, "filter", filterstring); + target.addVariable(this, "page", currentPage); + + currentPage = -1; // current page is always set by client + + optionRequest = true; + } + + /** + * Returns the filtered options for the current page using a container + * filter. + * + * As a size effect, {@link #filteredSize} is set to the total number of + * items passing the filter. + * + * The current container must be {@link Filterable} and {@link Indexed}, and + * the filtering mode must be suitable for container filtering (tested with + * {@link #canUseContainerFilter()}). + * + * Use {@link #getFilteredOptions()} and + * {@link #sanitetizeList(List, boolean)} if this is not the case. + * + * @param needNullSelectOption + * @return filtered list of options (may be empty) or null if cannot use + * container filters + */ + protected List<?> getOptionsWithFilter(boolean needNullSelectOption) { + Container container = getContainerDataSource(); + + if (pageLength == 0) { + // no paging: return all items + filteredSize = container.size(); + return new ArrayList<Object>(container.getItemIds()); + } + + if (!(container instanceof Filterable) + || !(container instanceof Indexed) + || getItemCaptionMode() != ITEM_CAPTION_MODE_PROPERTY) { + return null; + } + + Filterable filterable = (Filterable) container; + + Filter filter = buildFilter(filterstring, filteringMode); + + // adding and removing filters leads to extraneous item set + // change events from the underlying container, but the ComboBox does + // not process or propagate them based on the flag filteringContainer + if (filter != null) { + filteringContainer = true; + filterable.addContainerFilter(filter); + } + + Indexed indexed = (Indexed) container; + + int indexToEnsureInView = -1; + + // if not an option request (item list when user changes page), go + // to page with the selected item after filtering if accepted by + // filter + Object selection = getValue(); + if (isScrollToSelectedItem() && !optionRequest && !isMultiSelect() + && selection != null) { + // ensure proper page + indexToEnsureInView = indexed.indexOfId(selection); + } + + filteredSize = container.size(); + currentPage = adjustCurrentPage(currentPage, needNullSelectOption, + indexToEnsureInView, filteredSize); + int first = getFirstItemIndexOnCurrentPage(needNullSelectOption, + filteredSize); + int last = getLastItemIndexOnCurrentPage(needNullSelectOption, + filteredSize, first); + + List<Object> options = new ArrayList<Object>(); + for (int i = first; i <= last && i < filteredSize; ++i) { + options.add(indexed.getIdByIndex(i)); + } + + // to the outside, filtering should not be visible + if (filter != null) { + filterable.removeContainerFilter(filter); + filteringContainer = false; + } + + return options; + } + + /** + * Constructs a filter instance to use when using a Filterable container in + * the <code>ITEM_CAPTION_MODE_PROPERTY</code> mode. + * + * Note that the client side implementation expects the filter string to + * apply to the item caption string it sees, so changing the behavior of + * this method can cause problems. + * + * @param filterString + * @param filteringMode + * @return + */ + protected Filter buildFilter(String filterString, int filteringMode) { + Filter filter = null; + + if (null != filterString && !"".equals(filterString)) { + switch (filteringMode) { + case FILTERINGMODE_OFF: + break; + case FILTERINGMODE_STARTSWITH: + filter = new SimpleStringFilter(getItemCaptionPropertyId(), + filterString, true, true); + break; + case FILTERINGMODE_CONTAINS: + filter = new SimpleStringFilter(getItemCaptionPropertyId(), + filterString, true, false); + break; + } + } + return filter; + } + + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + if (!filteringContainer) { + super.containerItemSetChange(event); + } + } + + /** + * Makes correct sublist of given list of options. + * + * If paint is not an option request (affected by page or filter change), + * page will be the one where possible selection exists. + * + * Detects proper first and last item in list to return right page of + * options. Also, if the current page is beyond the end of the list, it will + * be adjusted. + * + * @param options + * @param needNullSelectOption + * flag to indicate if nullselect option needs to be taken into + * consideration + */ + private List<?> sanitetizeList(List<?> options, boolean needNullSelectOption) { + + if (pageLength != 0 && options.size() > pageLength) { + + int indexToEnsureInView = -1; + + // if not an option request (item list when user changes page), go + // to page with the selected item after filtering if accepted by + // filter + Object selection = getValue(); + if (isScrollToSelectedItem() && !optionRequest && !isMultiSelect() + && selection != null) { + // ensure proper page + indexToEnsureInView = options.indexOf(selection); + } + + int size = options.size(); + currentPage = adjustCurrentPage(currentPage, needNullSelectOption, + indexToEnsureInView, size); + int first = getFirstItemIndexOnCurrentPage(needNullSelectOption, + size); + int last = getLastItemIndexOnCurrentPage(needNullSelectOption, + size, first); + return options.subList(first, last + 1); + } else { + return options; + } + } + + /** + * Returns the index of the first item on the current page. The index is to + * the underlying (possibly filtered) contents. The null item, if any, does + * not have an index but takes up a slot on the first page. + * + * @param needNullSelectOption + * true if a null option should be shown before any other options + * (takes up the first slot on the first page, not counted in + * index) + * @param size + * number of items after filtering (not including the null item, + * if any) + * @return first item to show on the UI (index to the filtered list of + * options, not taking the null item into consideration if any) + */ + private int getFirstItemIndexOnCurrentPage(boolean needNullSelectOption, + int size) { + // Not all options are visible, find out which ones are on the + // current "page". + int first = currentPage * pageLength; + if (needNullSelectOption && currentPage > 0) { + first--; + } + return first; + } + + /** + * Returns the index of the last item on the current page. The index is to + * the underlying (possibly filtered) contents. If needNullSelectOption is + * true, the null item takes up the first slot on the first page, + * effectively reducing the first page size by one. + * + * @param needNullSelectOption + * true if a null option should be shown before any other options + * (takes up the first slot on the first page, not counted in + * index) + * @param size + * number of items after filtering (not including the null item, + * if any) + * @param first + * index in the filtered view of the first item of the page + * @return index in the filtered view of the last item on the page + */ + private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption, + int size, int first) { + // page length usable for non-null items + int effectivePageLength = pageLength + - (needNullSelectOption && (currentPage == 0) ? 1 : 0); + return Math.min(size - 1, first + effectivePageLength - 1); + } + + /** + * Adjusts the index of the current page if necessary: make sure the current + * page is not after the end of the contents, and optionally go to the page + * containg a specific item. There are no side effects but the adjusted page + * index is returned. + * + * @param page + * page number to use as the starting point + * @param needNullSelectOption + * true if a null option should be shown before any other options + * (takes up the first slot on the first page, not counted in + * index) + * @param indexToEnsureInView + * index of an item that should be included on the page (in the + * data set, not counting the null item if any), -1 for none + * @param size + * number of items after filtering (not including the null item, + * if any) + */ + private int adjustCurrentPage(int page, boolean needNullSelectOption, + int indexToEnsureInView, int size) { + if (indexToEnsureInView != -1) { + int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0)) + / pageLength; + page = newPage; + } + // adjust the current page if beyond the end of the list + if (page * pageLength > size) { + page = (size + (needNullSelectOption ? 1 : 0)) / pageLength; + } + return page; + } + + /** + * Filters the options in memory and returns the full filtered list. + * + * This can be less efficient than using container filters, so use + * {@link #getOptionsWithFilter(boolean)} if possible (filterable container + * and suitable item caption mode etc.). + * + * @return + */ + protected List<?> getFilteredOptions() { + if (null == filterstring || "".equals(filterstring) + || FILTERINGMODE_OFF == filteringMode) { + prevfilterstring = null; + filteredOptions = new LinkedList<Object>(getItemIds()); + return filteredOptions; + } + + if (filterstring.equals(prevfilterstring)) { + return filteredOptions; + } + + Collection<?> items; + if (prevfilterstring != null + && filterstring.startsWith(prevfilterstring)) { + items = filteredOptions; + } else { + items = getItemIds(); + } + prevfilterstring = filterstring; + + filteredOptions = new LinkedList<Object>(); + for (final Iterator<?> it = items.iterator(); it.hasNext();) { + final Object itemId = it.next(); + String caption = getItemCaption(itemId); + if (caption == null || caption.equals("")) { + continue; + } else { + caption = caption.toLowerCase(); + } + switch (filteringMode) { + case FILTERINGMODE_CONTAINS: + if (caption.indexOf(filterstring) > -1) { + filteredOptions.add(itemId); + } + break; + case FILTERINGMODE_STARTSWITH: + default: + if (caption.startsWith(filterstring)) { + filteredOptions.add(itemId); + } + break; + } + } + + return filteredOptions; + } + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Not calling super.changeVariables due the history of select + // component hierarchy + + // Selection change + if (variables.containsKey("selected")) { + final String[] ka = (String[]) variables.get("selected"); + + if (isMultiSelect()) { + // Multiselect mode + + // TODO Optimize by adding repaintNotNeeded whan applicaple + + // Converts the key-array to id-set + final LinkedList<Object> s = new LinkedList<Object>(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (id != null && containsId(id)) { + s.add(id); + } + } + + // Limits the deselection to the set of visible items + // (non-visible items can not be deselected) + final Collection<?> visible = getVisibleItemIds(); + if (visible != null) { + @SuppressWarnings("unchecked") + Set<Object> newsel = (Set<Object>) getValue(); + if (newsel == null) { + newsel = new HashSet<Object>(); + } else { + newsel = new HashSet<Object>(newsel); + } + newsel.removeAll(visible); + newsel.addAll(s); + setValue(newsel, true); + } + } else { + // Single select mode + if (ka.length == 0) { + + // Allows deselection only if the deselected item is visible + final Object current = getValue(); + final Collection<?> visible = getVisibleItemIds(); + if (visible != null && visible.contains(current)) { + setValue(null, true); + } + } else { + final Object id = itemIdMapper.get(ka[0]); + if (id != null && id.equals(getNullSelectionItemId())) { + setValue(null, true); + } else { + setValue(id, true); + } + } + } + } + + String newFilter; + if ((newFilter = (String) variables.get("filter")) != null) { + // this is a filter request + currentPage = ((Integer) variables.get("page")).intValue(); + filterstring = newFilter; + if (filterstring != null) { + filterstring = filterstring.toLowerCase(); + } + optionRepaint(); + } else if (isNewItemsAllowed()) { + // New option entered (and it is allowed) + final String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + getNewItemHandler().addNewItem(newitem); + // rebuild list + filterstring = null; + prevfilterstring = null; + } + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + + } + + @Override + public void requestRepaint() { + super.requestRepaint(); + optionRequest = false; + prevfilterstring = filterstring; + filterstring = null; + } + + private void optionRepaint() { + super.requestRepaint(); + } + + @Override + public void setFilteringMode(int filteringMode) { + this.filteringMode = filteringMode; + } + + @Override + public int getFilteringMode() { + return filteringMode; + } + + /** + * Note, one should use more generic setWidth(String) method instead of + * this. This now days actually converts columns to width with em css unit. + * + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @deprecated + * + * @param columns + * the number of columns to set. + */ + @Deprecated + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + setWidth(columns, Select.UNITS_EM); + requestRepaint(); + } + } + + /** + * @deprecated see setter function + * @return + */ + @Deprecated + public int getColumns() { + return columns; + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + /** + * @deprecated use {@link ListSelect}, {@link OptionGroup} or + * {@link TwinColSelect} instead + * @see com.vaadin.ui.AbstractSelect#setMultiSelect(boolean) + * @throws UnsupportedOperationException + * if trying to activate multiselect mode + */ + @Deprecated + @Override + public void setMultiSelect(boolean multiSelect) { + if (multiSelect) { + throw new UnsupportedOperationException("Multiselect not supported"); + } + } + + /** + * @deprecated use {@link ListSelect}, {@link OptionGroup} or + * {@link TwinColSelect} instead + * + * @see com.vaadin.ui.AbstractSelect#isMultiSelect() + */ + @Deprecated + @Override + public boolean isMultiSelect() { + return super.isMultiSelect(); + } + + /** + * Sets whether to scroll the selected item visible (directly open the page + * on which it is) when opening the combo box popup or not. Only applies to + * single select mode. + * + * This requires finding the index of the item, which can be expensive in + * many large lazy loading containers. + * + * @param scrollToSelectedItem + * true to find the page with the selected item when opening the + * selection popup + */ + public void setScrollToSelectedItem(boolean scrollToSelectedItem) { + this.scrollToSelectedItem = scrollToSelectedItem; + } + + /** + * Returns true if the select should find the page with the selected item + * when opening the popup (single select combo box only). + * + * @see #setScrollToSelectedItem(boolean) + * + * @return true if the page with the selected item will be shown when + * opening the popup + */ + public boolean isScrollToSelectedItem() { + return scrollToSelectedItem; + } + +} diff --git a/server/src/com/vaadin/ui/Slider.java b/server/src/com/vaadin/ui/Slider.java new file mode 100644 index 0000000000..94afe4e2bd --- /dev/null +++ b/server/src/com/vaadin/ui/Slider.java @@ -0,0 +1,372 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * A component for selecting a numerical value within a range. + * + * Example code: <code> + * class MyPlayer extends CustomComponent implements ValueChangeListener { + * + * Label volumeIndicator = new Label(); + * Slider slider; + * + * public MyPlayer() { + * VerticalLayout vl = new VerticalLayout(); + * setCompositionRoot(vl); + * slider = new Slider("Volume", 0, 100); + * slider.setImmediate(true); + * slider.setValue(new Double(50)); + * vl.addComponent(slider); + * vl.addComponent(volumeIndicator); + * volumeIndicator.setValue("Current volume:" + 50.0); + * slider.addListener(this); + * + * } + * + * public void setVolume(double d) { + * volumeIndicator.setValue("Current volume: " + d); + * } + * + * public void valueChange(ValueChangeEvent event) { + * Double d = (Double) event.getProperty().getValue(); + * setVolume(d.doubleValue()); + * } + * } + * + * </code> + * + * @author Vaadin Ltd. + */ +public class Slider extends AbstractField<Double> implements Vaadin6Component { + + public static final int ORIENTATION_HORIZONTAL = 0; + + public static final int ORIENTATION_VERTICAL = 1; + + /** Minimum value of slider */ + private double min = 0; + + /** Maximum value of slider */ + private double max = 100; + + /** + * Resolution, how many digits are considered relevant after the decimal + * point. Must be a non-negative value + */ + private int resolution = 0; + + /** + * Slider orientation (horizontal/vertical), defaults . + */ + private int orientation = ORIENTATION_HORIZONTAL; + + /** + * Default slider constructor. Sets all values to defaults and the slide + * handle at minimum value. + * + */ + public Slider() { + super(); + super.setValue(new Double(min)); + } + + /** + * Create a new slider with the caption given as parameter. + * + * The range of the slider is set to 0-100 and only integer values are + * allowed. + * + * @param caption + * The caption for this slider (e.g. "Volume"). + */ + public Slider(String caption) { + this(); + setCaption(caption); + } + + /** + * Create a new slider with the given range and resolution. + * + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + * @param resolution + * The number of digits after the decimal point. + */ + public Slider(double min, double max, int resolution) { + this(); + setMin(min); + setMax(max); + setResolution(resolution); + } + + /** + * Create a new slider with the given range that only allows integer values. + * + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + */ + public Slider(int min, int max) { + this(); + setMin(min); + setMax(max); + setResolution(0); + } + + /** + * Create a new slider with the given caption and range that only allows + * integer values. + * + * @param caption + * The caption for the slider + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + */ + public Slider(String caption, int min, int max) { + this(min, max); + setCaption(caption); + } + + /** + * Gets the maximum slider value + * + * @return the largest value the slider can have + */ + public double getMax() { + return max; + } + + /** + * Set the maximum slider value. If the current value of the slider is + * larger than this, the value is set to the new maximum. + * + * @param max + * The new maximum slider value + */ + public void setMax(double max) { + this.max = max; + if (getValue() > max) { + setValue(max); + } + requestRepaint(); + } + + /** + * Gets the minimum slider value + * + * @return the smallest value the slider can have + */ + public double getMin() { + return min; + } + + /** + * Set the minimum slider value. If the current value of the slider is + * smaller than this, the value is set to the new minimum. + * + * @param max + * The new minimum slider value + */ + public void setMin(double min) { + this.min = min; + if (getValue() < min) { + setValue(min); + } + requestRepaint(); + } + + /** + * Get the current orientation of the slider (horizontal or vertical). + * + * @return {@link #ORIENTATION_HORIZONTAL} or + * {@link #ORIENTATION_HORIZONTAL} + */ + public int getOrientation() { + return orientation; + } + + /** + * Set the orientation of the slider. + * + * @param The + * new orientation, either {@link #ORIENTATION_HORIZONTAL} or + * {@link #ORIENTATION_VERTICAL} + */ + public void setOrientation(int orientation) { + this.orientation = orientation; + requestRepaint(); + } + + /** + * Get the current resolution of the slider. The resolution is the number of + * digits after the decimal point. + * + * @return resolution + */ + public int getResolution() { + return resolution; + } + + /** + * Set a new resolution for the slider. The resolution is the number of + * digits after the decimal point. + * + * @param resolution + */ + public void setResolution(int resolution) { + if (resolution < 0) { + return; + } + this.resolution = resolution; + requestRepaint(); + } + + /** + * Sets the value of the slider. + * + * @param value + * The new value of the slider. + * @param repaintIsNotNeeded + * If true, client-side is not requested to repaint itself. + * @throws ValueOutOfBoundsException + * If the given value is not inside the range of the slider. + * @see #setMin(double) {@link #setMax(double)} + */ + @Override + protected void setValue(Double value, boolean repaintIsNotNeeded) { + final double v = value.doubleValue(); + double newValue; + if (resolution > 0) { + // Round up to resolution + newValue = (int) (v * Math.pow(10, resolution)); + newValue = newValue / Math.pow(10, resolution); + if (min > newValue || max < newValue) { + throw new ValueOutOfBoundsException(value); + } + } else { + newValue = (int) v; + if (min > newValue || max < newValue) { + throw new ValueOutOfBoundsException(value); + } + } + super.setValue(newValue, repaintIsNotNeeded); + } + + @Override + public void setValue(Object newFieldValue) + throws com.vaadin.data.Property.ReadOnlyException { + if (newFieldValue != null && newFieldValue instanceof Number + && !(newFieldValue instanceof Double)) { + // Support setting all types of Numbers + newFieldValue = ((Number) newFieldValue).doubleValue(); + } + + super.setValue(newFieldValue); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + + target.addAttribute("min", min); + if (max > min) { + target.addAttribute("max", max); + } else { + target.addAttribute("max", min); + } + target.addAttribute("resolution", resolution); + + if (resolution > 0) { + target.addVariable(this, "value", getValue().doubleValue()); + } else { + target.addVariable(this, "value", getValue().intValue()); + } + + if (orientation == ORIENTATION_VERTICAL) { + target.addAttribute("vertical", true); + } + + } + + /** + * Invoked when the value of a variable has changed. Slider listeners are + * notified if the slider value has changed. + * + * @param source + * @param variables + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("value")) { + final Object value = variables.get("value"); + final Double newValue = new Double(value.toString()); + if (newValue != null && newValue != getValue() + && !newValue.equals(getValue())) { + try { + setValue(newValue, true); + } catch (final ValueOutOfBoundsException e) { + // Convert to nearest bound + double out = e.getValue().doubleValue(); + if (out < min) { + out = min; + } + if (out > max) { + out = max; + } + super.setValue(new Double(out), false); + } + } + } + } + + /** + * Thrown when the value of the slider is about to be set to a value that is + * outside the valid range of the slider. + * + * @author Vaadin Ltd. + * + */ + public class ValueOutOfBoundsException extends RuntimeException { + + private final Double value; + + /** + * Constructs an <code>ValueOutOfBoundsException</code> with the + * specified detail message. + * + * @param valueOutOfBounds + */ + public ValueOutOfBoundsException(Double valueOutOfBounds) { + value = valueOutOfBounds; + } + + /** + * Gets the value that is outside the valid range of the slider. + * + * @return the value that is out of bounds + */ + public Double getValue() { + return value; + } + + } + + @Override + public Class<Double> getType() { + return Double.class; + } + +} diff --git a/server/src/com/vaadin/ui/TabSheet.java b/server/src/com/vaadin/ui/TabSheet.java new file mode 100644 index 0000000000..c52e9394c0 --- /dev/null +++ b/server/src/com/vaadin/ui/TabSheet.java @@ -0,0 +1,1328 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector; +import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheet; +import com.vaadin.ui.Component.Focusable; +import com.vaadin.ui.themes.Reindeer; +import com.vaadin.ui.themes.Runo; + +/** + * TabSheet component. + * + * Tabs are typically identified by the component contained on the tab (see + * {@link ComponentContainer}), and tab metadata (including caption, icon, + * visibility, enabledness, closability etc.) is kept in separate {@link Tab} + * instances. + * + * Tabs added with {@link #addComponent(Component)} get the caption and the icon + * of the component at the time when the component is created, and these are not + * automatically updated after tab creation. + * + * A tab sheet can have multiple tab selection listeners and one tab close + * handler ({@link CloseHandler}), which by default removes the tab from the + * TabSheet. + * + * The {@link TabSheet} can be styled with the .v-tabsheet, .v-tabsheet-tabs and + * .v-tabsheet-content styles. Themes may also have pre-defined variations of + * the tab sheet presentation, such as {@link Reindeer#TABSHEET_BORDERLESS}, + * {@link Runo#TABSHEET_SMALL} and several other styles in {@link Reindeer}. + * + * The current implementation does not load the tabs to the UI before the first + * time they are shown, but this may change in future releases. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public class TabSheet extends AbstractComponentContainer implements Focusable, + FocusNotifier, BlurNotifier, Vaadin6Component { + + /** + * List of component tabs (tab contents). In addition to being on this list, + * there is a {@link Tab} object in tabs for each tab with meta-data about + * the tab. + */ + private final ArrayList<Component> components = new ArrayList<Component>(); + + /** + * Map containing information related to the tabs (caption, icon etc). + */ + private final HashMap<Component, Tab> tabs = new HashMap<Component, Tab>(); + + /** + * Selected tab content component. + */ + private Component selected = null; + + /** + * Mapper between server-side component instances (tab contents) and keys + * given to the client that identify tabs. + */ + private final KeyMapper<Component> keyMapper = new KeyMapper<Component>(); + + /** + * When true, the tab selection area is not displayed to the user. + */ + private boolean tabsHidden; + + /** + * Handler to be called when a tab is closed. + */ + private CloseHandler closeHandler; + + private int tabIndex; + + /** + * Constructs a new Tabsheet. Tabsheet is immediate by default, and the + * default close handler removes the tab being closed. + */ + public TabSheet() { + super(); + // expand horizontally by default + setWidth(100, UNITS_PERCENTAGE); + setImmediate(true); + setCloseHandler(new CloseHandler() { + + @Override + public void onTabClose(TabSheet tabsheet, Component c) { + tabsheet.removeComponent(c); + } + }); + } + + /** + * Gets the component container iterator for going through all the + * components (tab contents). + * + * @return the unmodifiable Iterator of the tab content components + */ + + @Override + public Iterator<Component> getComponentIterator() { + return Collections.unmodifiableList(components).iterator(); + } + + /** + * Gets the number of contained components (tabs). Consistent with the + * iterator returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + + @Override + public int getComponentCount() { + return components.size(); + } + + /** + * Removes a component and its corresponding tab. + * + * If the tab was selected, the first eligible (visible and enabled) + * remaining tab is selected. + * + * @param c + * the component to be removed. + */ + + @Override + public void removeComponent(Component c) { + if (c != null && components.contains(c)) { + super.removeComponent(c); + keyMapper.remove(c); + components.remove(c); + tabs.remove(c); + if (c.equals(selected)) { + if (components.isEmpty()) { + setSelected(null); + } else { + // select the first enabled and visible tab, if any + updateSelection(); + fireSelectedTabChange(); + } + } + requestRepaint(); + } + } + + /** + * Removes a {@link Tab} and the component associated with it, as previously + * added with {@link #addTab(Component)}, + * {@link #addTab(Component, String, Resource)} or + * {@link #addComponent(Component)}. + * <p> + * If the tab was selected, the first eligible (visible and enabled) + * remaining tab is selected. + * </p> + * + * @see #addTab(Component) + * @see #addTab(Component, String, Resource) + * @see #addComponent(Component) + * @see #removeComponent(Component) + * @param tab + * the Tab to remove + */ + public void removeTab(Tab tab) { + removeComponent(tab.getComponent()); + } + + /** + * Adds a new tab into TabSheet. Component caption and icon are copied to + * the tab metadata at creation time. + * + * @see #addTab(Component) + * + * @param c + * the component to be added. + */ + + @Override + public void addComponent(Component c) { + addTab(c); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and returns the corresponding (old) tab, preserving other tab metadata. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption) { + return addTab(c, caption, null); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and icon and returns the corresponding (old) tab, preserving other tab + * metadata. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @param icon + * the icon to be set for the component and used rendered in tab + * bar + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption, Resource icon) { + return addTab(c, caption, icon, components.size()); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and icon and returns the corresponding (old) tab, preserving other tab + * metadata like the position. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @param icon + * the icon to be set for the component and used rendered in tab + * bar + * @param position + * the position at where the the tab should be added. + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption, Resource icon, int position) { + if (c == null) { + return null; + } else if (tabs.containsKey(c)) { + Tab tab = tabs.get(c); + tab.setCaption(caption); + tab.setIcon(icon); + return tab; + } else { + components.add(position, c); + + Tab tab = new TabSheetTabImpl(caption, icon); + + tabs.put(c, tab); + if (selected == null) { + setSelected(c); + fireSelectedTabChange(); + } + super.addComponent(c); + requestRepaint(); + return tab; + } + } + + /** + * Adds a new tab into TabSheet. Component caption and icon are copied to + * the tab metadata at creation time. + * + * If the tab sheet already contains the component, its tab is returned. + * + * @param c + * the component to be added onto tab - should not be null. + * @return the created {@link Tab} + */ + public Tab addTab(Component c) { + return addTab(c, components.size()); + } + + /** + * Adds a new tab into TabSheet. Component caption and icon are copied to + * the tab metadata at creation time. + * + * If the tab sheet already contains the component, its tab is returned. + * + * @param c + * the component to be added onto tab - should not be null. + * @param position + * The position where the tab should be added + * @return the created {@link Tab} + */ + public Tab addTab(Component c, int position) { + if (c == null) { + return null; + } else if (tabs.containsKey(c)) { + return tabs.get(c); + } else { + return addTab(c, c.getCaption(), c.getIcon(), position); + } + } + + /** + * Moves all components from another container to this container. The + * components are removed from the other container. + * + * If the source container is a {@link TabSheet}, component captions and + * icons are copied from it. + * + * @param source + * the container components are removed from. + */ + + @Override + public void moveComponentsFrom(ComponentContainer source) { + for (final Iterator<Component> i = source.getComponentIterator(); i + .hasNext();) { + final Component c = i.next(); + String caption = null; + Resource icon = null; + if (TabSheet.class.isAssignableFrom(source.getClass())) { + caption = ((TabSheet) source).getTabCaption(c); + icon = ((TabSheet) source).getTabIcon(c); + } + source.removeComponent(c); + addTab(c, caption, icon); + + } + } + + /** + * Paints the content of this component. + * + * @param target + * the paint target + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void paintContent(PaintTarget target) throws PaintException { + + if (areTabsHidden()) { + target.addAttribute("hidetabs", true); + } + + if (tabIndex != 0) { + target.addAttribute("tabindex", tabIndex); + } + + target.startTag("tabs"); + + for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { + final Component component = i.next(); + + Tab tab = tabs.get(component); + + target.startTag("tab"); + if (!tab.isEnabled() && tab.isVisible()) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED, true); + } + + if (!tab.isVisible()) { + target.addAttribute("hidden", true); + } + + if (tab.isClosable()) { + target.addAttribute("closable", true); + } + + // tab icon, caption and description, but used via + // VCaption.updateCaption(uidl) + final Resource icon = tab.getIcon(); + if (icon != null) { + target.addAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON, + icon); + } + final String caption = tab.getCaption(); + if (caption != null && caption.length() > 0) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION, caption); + } + ErrorMessage tabError = tab.getComponentError(); + if (tabError != null) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE, + tabError.getFormattedHtmlMessage()); + } + final String description = tab.getDescription(); + if (description != null) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION, + description); + } + + final String styleName = tab.getStyleName(); + if (styleName != null && styleName.length() != 0) { + target.addAttribute(VTabsheet.TAB_STYLE_NAME, styleName); + } + + target.addAttribute("key", keyMapper.key(component)); + if (component.equals(selected)) { + target.addAttribute("selected", true); + LegacyPaint.paint(component, target); + } + target.endTag("tab"); + } + + target.endTag("tabs"); + + if (selected != null) { + target.addVariable(this, "selected", keyMapper.key(selected)); + } + + } + + /** + * Are the tab selection parts ("tabs") hidden. + * + * @return true if the tabs are hidden in the UI + */ + public boolean areTabsHidden() { + return tabsHidden; + } + + /** + * Hides or shows the tab selection parts ("tabs"). + * + * @param tabsHidden + * true if the tabs should be hidden + */ + public void hideTabs(boolean tabsHidden) { + this.tabsHidden = tabsHidden; + requestRepaint(); + } + + /** + * Gets tab caption. The tab is identified by the tab content component. + * + * @param c + * the component in the tab + * @deprecated Use {@link #getTab(Component)} and {@link Tab#getCaption()} + * instead. + */ + @Deprecated + public String getTabCaption(Component c) { + Tab info = tabs.get(c); + if (info == null) { + return ""; + } else { + return info.getCaption(); + } + } + + /** + * Sets tab caption. The tab is identified by the tab content component. + * + * @param c + * the component in the tab + * @param caption + * the caption to set. + * @deprecated Use {@link #getTab(Component)} and + * {@link Tab#setCaption(String)} instead. + */ + @Deprecated + public void setTabCaption(Component c, String caption) { + Tab info = tabs.get(c); + if (info != null) { + info.setCaption(caption); + requestRepaint(); + } + } + + /** + * Gets the icon for a tab. The tab is identified by the tab content + * component. + * + * @param c + * the component in the tab + * @deprecated Use {@link #getTab(Component)} and {@link Tab#getIcon()} + * instead. + */ + @Deprecated + public Resource getTabIcon(Component c) { + Tab info = tabs.get(c); + if (info == null) { + return null; + } else { + return info.getIcon(); + } + } + + /** + * Sets icon for the given component. The tab is identified by the tab + * content component. + * + * @param c + * the component in the tab + * @param icon + * the icon to set + * @deprecated Use {@link #getTab(Component)} and + * {@link Tab#setIcon(Resource)} instead. + */ + @Deprecated + public void setTabIcon(Component c, Resource icon) { + Tab info = tabs.get(c); + if (info != null) { + info.setIcon(icon); + requestRepaint(); + } + } + + /** + * Returns the {@link Tab} (metadata) for a component. The {@link Tab} + * object can be used for setting caption,icon, etc for the tab. + * + * @param c + * the component + * @return The tab instance associated with the given component, or null if + * the tabsheet does not contain the component. + */ + public Tab getTab(Component c) { + return tabs.get(c); + } + + /** + * Returns the {@link Tab} (metadata) for a component. The {@link Tab} + * object can be used for setting caption,icon, etc for the tab. + * + * @param position + * the position of the tab + * @return The tab in the given position, or null if the position is out of + * bounds. + */ + public Tab getTab(int position) { + if (position >= 0 && position < getComponentCount()) { + return getTab(components.get(position)); + } else { + return null; + } + } + + /** + * Sets the selected tab. The tab is identified by the tab content + * component. Does nothing if the tabsheet doesn't contain the component. + * + * @param c + */ + public void setSelectedTab(Component c) { + if (c != null && components.contains(c) && !c.equals(selected)) { + setSelected(c); + updateSelection(); + fireSelectedTabChange(); + requestRepaint(); + } + } + + /** + * Sets the selected tab in the TabSheet. Ensures that the selected tab is + * repainted if needed. + * + * @param c + * The new selection or null for no selection + */ + private void setSelected(Component c) { + selected = c; + // Repaint of the selected component is needed as only the selected + // component is communicated to the client. Otherwise this will be a + // "cached" update even though the client knows nothing about the + // connector + if (selected instanceof ComponentContainer) { + ((ComponentContainer) selected).requestRepaintAll(); + } else if (selected instanceof Table) { + // Workaround until there's a generic way of telling a component + // that there is no client side state to rely on. See #8642 + ((Table) selected).refreshRowCache(); + } else if (selected != null) { + selected.requestRepaint(); + } + + } + + /** + * Sets the selected tab. The tab is identified by the corresponding + * {@link Tab Tab} instance. Does nothing if the tabsheet doesn't contain + * the given tab. + * + * @param tab + */ + public void setSelectedTab(Tab tab) { + if (tab != null) { + setSelectedTab(tab.getComponent()); + } + } + + /** + * Sets the selected tab, identified by its position. Does nothing if the + * position is out of bounds. + * + * @param position + */ + public void setSelectedTab(int position) { + setSelectedTab(getTab(position)); + } + + /** + * Checks if the current selection is valid, and updates the selection if + * the previously selected component is not visible and enabled. The first + * visible and enabled tab is selected if the current selection is empty or + * invalid. + * + * This method does not fire tab change events, but the caller should do so + * if appropriate. + * + * @return true if selection was changed, false otherwise + */ + private boolean updateSelection() { + Component originalSelection = selected; + for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { + final Component component = i.next(); + + Tab tab = tabs.get(component); + + /* + * If we have no selection, if the current selection is invisible or + * if the current selection is disabled (but the whole component is + * not) we select this tab instead + */ + Tab selectedTabInfo = null; + if (selected != null) { + selectedTabInfo = tabs.get(selected); + } + if (selected == null || selectedTabInfo == null + || !selectedTabInfo.isVisible() + || !selectedTabInfo.isEnabled()) { + + // The current selection is not valid so we need to change + // it + if (tab.isEnabled() && tab.isVisible()) { + setSelected(component); + break; + } else { + /* + * The current selection is not valid but this tab cannot be + * selected either. + */ + setSelected(null); + } + } + } + return originalSelection != selected; + } + + /** + * Gets the selected tab content component. + * + * @return the selected tab contents + */ + public Component getSelectedTab() { + return selected; + } + + // inherits javadoc + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("selected")) { + setSelectedTab(keyMapper.get((String) variables.get("selected"))); + } + if (variables.containsKey("close")) { + final Component tab = keyMapper + .get((String) variables.get("close")); + if (tab != null) { + closeHandler.onTabClose(this, tab); + } + } + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } + + /** + * Replaces a component (tab content) with another. This can be used to + * change tab contents or to rearrange tabs. The tab position and some + * metadata are preserved when moving components within the same + * {@link TabSheet}. + * + * If the oldComponent is not present in the tab sheet, the new one is added + * at the end. + * + * If the oldComponent is already in the tab sheet but the newComponent + * isn't, the old tab is replaced with a new one, and the caption and icon + * of the old one are copied to the new tab. + * + * If both old and new components are present, their positions are swapped. + * + * {@inheritDoc} + */ + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + if (selected == oldComponent) { + // keep selection w/o selectedTabChange event + setSelected(newComponent); + } + + Tab newTab = tabs.get(newComponent); + Tab oldTab = tabs.get(oldComponent); + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component component = i.next(); + + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + + location++; + } + + if (oldLocation == -1) { + addComponent(newComponent); + } else if (newLocation == -1) { + removeComponent(oldComponent); + newTab = addTab(newComponent, oldLocation); + // Copy all relevant metadata to the new tab (#8793) + // TODO Should reuse the old tab instance instead? + copyTabMetadata(oldTab, newTab); + } else { + components.set(oldLocation, newComponent); + components.set(newLocation, oldComponent); + + // Tab associations are not changed, but metadata is swapped between + // the instances + // TODO Should reassociate the instances instead? + Tab tmp = new TabSheetTabImpl(null, null); + copyTabMetadata(newTab, tmp); + copyTabMetadata(oldTab, newTab); + copyTabMetadata(tmp, oldTab); + + requestRepaint(); + } + + } + + /* Click event */ + + private static final Method SELECTED_TAB_CHANGE_METHOD; + static { + try { + SELECTED_TAB_CHANGE_METHOD = SelectedTabChangeListener.class + .getDeclaredMethod("selectedTabChange", + new Class[] { SelectedTabChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in TabSheet"); + } + } + + /** + * Selected tab change event. This event is sent when the selected (shown) + * tab in the tab sheet is changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class SelectedTabChangeEvent extends Component.Event { + + /** + * New instance of selected tab change event + * + * @param source + * the Source of the event. + */ + public SelectedTabChangeEvent(Component source) { + super(source); + } + + /** + * TabSheet where the event occurred. + * + * @return the Source of the event. + */ + public TabSheet getTabSheet() { + return (TabSheet) getSource(); + } + } + + /** + * Selected tab change event listener. The listener is called whenever + * another tab is selected, including when adding the first tab to a + * tabsheet. + * + * @author Vaadin Ltd. + * + * @version + * @VERSION@ + * @since 3.0 + */ + public interface SelectedTabChangeListener extends Serializable { + + /** + * Selected (shown) tab in tab sheet has has been changed. + * + * @param event + * the selected tab change event. + */ + public void selectedTabChange(SelectedTabChangeEvent event); + } + + /** + * Adds a tab selection listener + * + * @param listener + * the Listener to be added. + */ + public void addListener(SelectedTabChangeListener listener) { + addListener(SelectedTabChangeEvent.class, listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** + * Removes a tab selection listener + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(SelectedTabChangeListener listener) { + removeListener(SelectedTabChangeEvent.class, listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** + * Sends an event that the currently selected tab has changed. + */ + protected void fireSelectedTabChange() { + fireEvent(new SelectedTabChangeEvent(this)); + } + + /** + * Tab meta-data for a component in a {@link TabSheet}. + * + * The meta-data includes the tab caption, icon, visibility and enabledness, + * closability, description (tooltip) and an optional component error shown + * in the tab. + * + * Tabs are identified by the component contained on them in most cases, and + * the meta-data can be obtained with {@link TabSheet#getTab(Component)}. + */ + public interface Tab extends Serializable { + /** + * Returns the visible status for the tab. An invisible tab is not shown + * in the tab bar and cannot be selected. + * + * @return true for visible, false for hidden + */ + public boolean isVisible(); + + /** + * Sets the visible status for the tab. An invisible tab is not shown in + * the tab bar and cannot be selected, selection is changed + * automatically when there is an attempt to select an invisible tab. + * + * @param visible + * true for visible, false for hidden + */ + public void setVisible(boolean visible); + + /** + * Returns the closability status for the tab. + * + * @return true if the tab is allowed to be closed by the end user, + * false for not allowing closing + */ + public boolean isClosable(); + + /** + * Sets the closability status for the tab. A closable tab can be closed + * by the user through the user interface. This also controls if a close + * button is shown to the user or not. + * <p> + * Note! Currently only supported by TabSheet, not Accordion. + * </p> + * + * @param visible + * true if the end user is allowed to close the tab, false + * for not allowing to close. Should default to false. + */ + public void setClosable(boolean closable); + + /** + * Returns the enabled status for the tab. A disabled tab is shown as + * such in the tab bar and cannot be selected. + * + * @return true for enabled, false for disabled + */ + public boolean isEnabled(); + + /** + * Sets the enabled status for the tab. A disabled tab is shown as such + * in the tab bar and cannot be selected. + * + * @param enabled + * true for enabled, false for disabled + */ + public void setEnabled(boolean enabled); + + /** + * Sets the caption for the tab. + * + * @param caption + * the caption to set + */ + public void setCaption(String caption); + + /** + * Gets the caption for the tab. + */ + public String getCaption(); + + /** + * Gets the icon for the tab. + */ + public Resource getIcon(); + + /** + * Sets the icon for the tab. + * + * @param icon + * the icon to set + */ + public void setIcon(Resource icon); + + /** + * Gets the description for the tab. The description can be used to + * briefly describe the state of the tab to the user, and is typically + * shown as a tooltip when hovering over the tab. + * + * @return the description for the tab + */ + public String getDescription(); + + /** + * Sets the description for the tab. The description can be used to + * briefly describe the state of the tab to the user, and is typically + * shown as a tooltip when hovering over the tab. + * + * @param description + * the new description string for the tab. + */ + public void setDescription(String description); + + /** + * Sets an error indicator to be shown in the tab. This can be used e.g. + * to communicate to the user that there is a problem in the contents of + * the tab. + * + * @see AbstractComponent#setComponentError(ErrorMessage) + * + * @param componentError + * error message or null for none + */ + public void setComponentError(ErrorMessage componentError); + + /** + * Gets the current error message shown for the tab. + * + * TODO currently not sent to the client + * + * @see AbstractComponent#setComponentError(ErrorMessage) + */ + public ErrorMessage getComponentError(); + + /** + * Get the component related to the Tab + */ + public Component getComponent(); + + /** + * Sets a style name for the tab. The style name will be rendered as a + * HTML class name, which can be used in a CSS definition. + * + * <pre> + * Tab tab = tabsheet.addTab(tabContent, "Tab text"); + * tab.setStyleName("mystyle"); + * </pre> + * <p> + * The used style name will be prefixed with " + * {@code v-tabsheet-tabitemcell-}". For example, if you give a tab the + * style "{@code mystyle}", the tab will get a " + * {@code v-tabsheet-tabitemcell-mystyle}" style. You could then style + * the component with: + * </p> + * + * <pre> + * .v-tabsheet-tabitemcell-mystyle {font-style: italic;} + * </pre> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent} on the + * TabSheet to which the Tab belongs. + * </p> + * + * @param styleName + * the new style to be set for tab + * @see #getStyleName() + */ + public void setStyleName(String styleName); + + /** + * Gets the user-defined CSS style name of the tab. Built-in style names + * defined in Vaadin or GWT are not returned. + * + * @return the style name or of the tab + * @see #setStyleName(String) + */ + public String getStyleName(); + } + + /** + * TabSheet's implementation of {@link Tab} - tab metadata. + */ + public class TabSheetTabImpl implements Tab { + + private String caption = ""; + private Resource icon = null; + private boolean enabled = true; + private boolean visible = true; + private boolean closable = false; + private String description = null; + private ErrorMessage componentError = null; + private String styleName; + + public TabSheetTabImpl(String caption, Resource icon) { + if (caption == null) { + caption = ""; + } + this.caption = caption; + this.icon = icon; + } + + /** + * Returns the tab caption. Can never be null. + */ + + @Override + public String getCaption() { + return caption; + } + + @Override + public void setCaption(String caption) { + this.caption = caption; + requestRepaint(); + } + + @Override + public Resource getIcon() { + return icon; + } + + @Override + public void setIcon(Resource icon) { + this.icon = icon; + requestRepaint(); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (updateSelection()) { + fireSelectedTabChange(); + } + requestRepaint(); + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; + if (updateSelection()) { + fireSelectedTabChange(); + } + requestRepaint(); + } + + @Override + public boolean isClosable() { + return closable; + } + + @Override + public void setClosable(boolean closable) { + this.closable = closable; + requestRepaint(); + } + + public void close() { + + } + + @Override + public String getDescription() { + return description; + } + + @Override + public void setDescription(String description) { + this.description = description; + requestRepaint(); + } + + @Override + public ErrorMessage getComponentError() { + return componentError; + } + + @Override + public void setComponentError(ErrorMessage componentError) { + this.componentError = componentError; + requestRepaint(); + } + + @Override + public Component getComponent() { + for (Map.Entry<Component, Tab> entry : tabs.entrySet()) { + if (entry.getValue() == this) { + return entry.getKey(); + } + } + return null; + } + + @Override + public void setStyleName(String styleName) { + this.styleName = styleName; + requestRepaint(); + } + + @Override + public String getStyleName() { + return styleName; + } + } + + /** + * CloseHandler is used to process tab closing events. Default behavior is + * to remove the tab from the TabSheet. + * + * @author Jouni Koivuviita / Vaadin Ltd. + * @since 6.2.0 + * + */ + public interface CloseHandler extends Serializable { + + /** + * Called when a user has pressed the close icon of a tab in the client + * side widget. + * + * @param tabsheet + * the TabSheet to which the tab belongs to + * @param tabContent + * the component that corresponds to the tab whose close + * button was clicked + */ + void onTabClose(final TabSheet tabsheet, final Component tabContent); + } + + /** + * Provide a custom {@link CloseHandler} for this TabSheet if you wish to + * perform some additional tasks when a user clicks on a tabs close button, + * e.g. show a confirmation dialogue before removing the tab. + * + * To remove the tab, if you provide your own close handler, you must call + * {@link #removeComponent(Component)} yourself. + * + * The default CloseHandler for TabSheet will only remove the tab. + * + * @param handler + */ + public void setCloseHandler(CloseHandler handler) { + closeHandler = handler; + } + + /** + * Sets the position of the tab. + * + * @param tab + * The tab + * @param position + * The new position of the tab + */ + public void setTabPosition(Tab tab, int position) { + int oldPosition = getTabPosition(tab); + components.remove(oldPosition); + components.add(position, tab.getComponent()); + requestRepaint(); + } + + /** + * Gets the position of the tab + * + * @param tab + * The tab + * @return + */ + public int getTabPosition(Tab tab) { + return components.indexOf(tab.getComponent()); + } + + @Override + public void focus() { + super.focus(); + } + + @Override + public int getTabIndex() { + return tabIndex; + } + + @Override + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + requestRepaint(); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return childComponent == getSelectedTab(); + } + + /** + * Copies properties from one Tab to another. + * + * @param from + * The tab whose data to copy. + * @param to + * The tab to which copy the data. + */ + private static void copyTabMetadata(Tab from, Tab to) { + to.setCaption(from.getCaption()); + to.setIcon(from.getIcon()); + to.setDescription(from.getDescription()); + to.setVisible(from.isVisible()); + to.setEnabled(from.isEnabled()); + to.setClosable(from.isClosable()); + to.setStyleName(from.getStyleName()); + to.setComponentError(from.getComponentError()); + } +} diff --git a/server/src/com/vaadin/ui/Table.java b/server/src/com/vaadin/ui/Table.java new file mode 100644 index 0000000000..39b7fb7473 --- /dev/null +++ b/server/src/com/vaadin/ui/Table.java @@ -0,0 +1,5449 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.ContainerOrderedWrapper; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickNotifier; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable; + +/** + * <p> + * <code>Table</code> is used for representing data or components in a pageable + * and selectable table. + * </p> + * + * <p> + * Scalability of the Table is largely dictated by the container. A table does + * not have a limit for the number of items and is just as fast with hundreds of + * thousands of items as with just a few. The current GWT implementation with + * scrolling however limits the number of rows to around 500000, depending on + * the browser and the pixel height of rows. + * </p> + * + * <p> + * Components in a Table will not have their caption nor icon rendered. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings({ "deprecation" }) +public class Table extends AbstractSelect implements Action.Container, + Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource, + DropTarget, HasComponents { + + private transient Logger logger = null; + + /** + * Modes that Table support as drag sourse. + */ + public enum TableDragMode { + /** + * Table does not start drag and drop events. HTM5 style events started + * by browser may still happen. + */ + NONE, + /** + * Table starts drag with a one row only. + */ + ROW, + /** + * Table drags selected rows, if drag starts on a selected rows. Else it + * starts like in ROW mode. Note, that in Transferable there will still + * be only the row on which the drag started, other dragged rows need to + * be checked from the source Table. + */ + MULTIROW + } + + protected static final int CELL_KEY = 0; + + protected static final int CELL_HEADER = 1; + + protected static final int CELL_ICON = 2; + + protected static final int CELL_ITEMID = 3; + + protected static final int CELL_GENERATED_ROW = 4; + + protected static final int CELL_FIRSTCOL = 5; + + public enum Align { + /** + * Left column alignment. <b>This is the default behaviour. </b> + */ + LEFT("b"), + + /** + * Center column alignment. + */ + CENTER("c"), + + /** + * Right column alignment. + */ + RIGHT("e"); + + private String alignment; + + private Align(String alignment) { + this.alignment = alignment; + } + + @Override + public String toString() { + return alignment; + } + + public Align convertStringToAlign(String string) { + if (string == null) { + return null; + } + if (string.equals("b")) { + return Align.LEFT; + } else if (string.equals("c")) { + return Align.CENTER; + } else if (string.equals("e")) { + return Align.RIGHT; + } else { + return null; + } + } + } + + /** + * @deprecated from 7.0, use {@link Align#LEFT} instead + */ + @Deprecated + public static final Align ALIGN_LEFT = Align.LEFT; + + /** + * @deprecated from 7.0, use {@link Align#CENTER} instead + */ + @Deprecated + public static final Align ALIGN_CENTER = Align.CENTER; + + /** + * @deprecated from 7.0, use {@link Align#RIGHT} instead + */ + @Deprecated + public static final Align ALIGN_RIGHT = Align.RIGHT; + + public enum ColumnHeaderMode { + /** + * Column headers are hidden. + */ + HIDDEN, + /** + * Property ID:s are used as column headers. + */ + ID, + /** + * Column headers are explicitly specified with + * {@link #setColumnHeaders(String[])}. + */ + EXPLICIT, + /** + * Column headers are explicitly specified with + * {@link #setColumnHeaders(String[])}. If a header is not specified for + * a given property, its property id is used instead. + * <p> + * <b>This is the default behavior. </b> + */ + EXPLICIT_DEFAULTS_ID + } + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#ID} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#EXPLICIT_DEFAULTS_ID} + * instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID; + + public enum RowHeaderMode { + /** + * Row caption mode: The row headers are hidden. <b>This is the default + * mode. </b> + */ + HIDDEN(null), + /** + * Row caption mode: Items Id-objects toString is used as row caption. + */ + ID(ItemCaptionMode.ID), + /** + * Row caption mode: Item-objects toString is used as row caption. + */ + ITEM(ItemCaptionMode.ITEM), + /** + * Row caption mode: Index of the item is used as item caption. The + * index mode can only be used with the containers implementing the + * {@link com.vaadin.data.Container.Indexed} interface. + */ + INDEX(ItemCaptionMode.INDEX), + /** + * Row caption mode: Item captions are explicitly specified, but if the + * caption is missing, the item id objects <code>toString()</code> is + * used instead. + */ + EXPLICIT_DEFAULTS_ID(ItemCaptionMode.EXPLICIT_DEFAULTS_ID), + /** + * Row caption mode: Item captions are explicitly specified. + */ + EXPLICIT(ItemCaptionMode.EXPLICIT), + /** + * Row caption mode: Only icons are shown, the captions are hidden. + */ + ICON_ONLY(ItemCaptionMode.ICON_ONLY), + /** + * Row caption mode: Item captions are read from property specified with + * {@link #setItemCaptionPropertyId(Object)}. + */ + PROPERTY(ItemCaptionMode.PROPERTY); + + ItemCaptionMode mode; + + private RowHeaderMode(ItemCaptionMode mode) { + this.mode = mode; + } + + public ItemCaptionMode getItemCaptionMode() { + return mode; + } + } + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ID} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ITEM} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#INDEX} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#EXPLICIT_DEFAULTS_ID} + * instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = RowHeaderMode.EXPLICIT_DEFAULTS_ID; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ICON_ONLY} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#PROPERTY} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_PROPERTY = RowHeaderMode.PROPERTY; + + /** + * The default rate that table caches rows for smooth scrolling. + */ + private static final double CACHE_RATE_DEFAULT = 2; + + private static final String ROW_HEADER_COLUMN_KEY = "0"; + private static final Object ROW_HEADER_FAKE_PROPERTY_ID = new UniqueSerializable() { + }; + + /* Private table extensions to Select */ + + /** + * True if column collapsing is allowed. + */ + private boolean columnCollapsingAllowed = false; + + /** + * True if reordering of columns is allowed on the client side. + */ + private boolean columnReorderingAllowed = false; + + /** + * Keymapper for column ids. + */ + private final KeyMapper<Object> columnIdMap = new KeyMapper<Object>(); + + /** + * Holds visible column propertyIds - in order. + */ + private LinkedList<Object> visibleColumns = new LinkedList<Object>(); + + /** + * Holds noncollapsible columns. + */ + private HashSet<Object> noncollapsibleColumns = new HashSet<Object>(); + + /** + * Holds propertyIds of currently collapsed columns. + */ + private final HashSet<Object> collapsedColumns = new HashSet<Object>(); + + /** + * Holds headers for visible columns (by propertyId). + */ + private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>(); + + /** + * Holds footers for visible columns (by propertyId). + */ + private final HashMap<Object, String> columnFooters = new HashMap<Object, String>(); + + /** + * Holds icons for visible columns (by propertyId). + */ + private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>(); + + /** + * Holds alignments for visible columns (by propertyId). + */ + private HashMap<Object, Align> columnAlignments = new HashMap<Object, Align>(); + + /** + * Holds column widths in pixels (Integer) or expand ratios (Float) for + * visible columns (by propertyId). + */ + private final HashMap<Object, Object> columnWidths = new HashMap<Object, Object>(); + + /** + * Holds column generators + */ + private final HashMap<Object, ColumnGenerator> columnGenerators = new LinkedHashMap<Object, ColumnGenerator>(); + + /** + * Holds value of property pageLength. 0 disables paging. + */ + private int pageLength = 15; + + /** + * Id the first item on the current page. + */ + private Object currentPageFirstItemId = null; + + /** + * Index of the first item on the current page. + */ + private int currentPageFirstItemIndex = 0; + + /** + * Holds value of property selectable. + */ + private boolean selectable = false; + + /** + * Holds value of property columnHeaderMode. + */ + private ColumnHeaderMode columnHeaderMode = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID; + + /** + * Holds value of property rowHeaderMode. + */ + private RowHeaderMode rowHeaderMode = RowHeaderMode.EXPLICIT_DEFAULTS_ID; + + /** + * Should the Table footer be visible? + */ + private boolean columnFootersVisible = false; + + /** + * Page contents buffer used in buffered mode. + */ + private Object[][] pageBuffer = null; + + /** + * Set of properties listened - the list is kept to release the listeners + * later. + */ + private HashSet<Property<?>> listenedProperties = null; + + /** + * Set of visible components - the is used for needsRepaint calculation. + */ + private HashSet<Component> visibleComponents = null; + + /** + * List of action handlers. + */ + private LinkedList<Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * Table cell editor factory. + */ + private TableFieldFactory fieldFactory = DefaultFieldFactory.get(); + + /** + * Is table editable. + */ + private boolean editable = false; + + /** + * Current sorting direction. + */ + private boolean sortAscending = true; + + /** + * Currently table is sorted on this propertyId. + */ + private Object sortContainerPropertyId = null; + + /** + * Is table sorting by the user enabled. + */ + private boolean sortEnabled = true; + + /** + * Number of rows explicitly requested by the client to be painted on next + * paint. This is -1 if no request by the client is made. Painting the + * component will automatically reset this to -1. + */ + private int reqRowsToPaint = -1; + + /** + * Index of the first rows explicitly requested by the client to be painted. + * This is -1 if no request by the client is made. Painting the component + * will automatically reset this to -1. + */ + private int reqFirstRowToPaint = -1; + + private int firstToBeRenderedInClient = -1; + + private int lastToBeRenderedInClient = -1; + + private boolean isContentRefreshesEnabled = true; + + private int pageBufferFirstIndex; + + private boolean containerChangeToBeRendered = false; + + /** + * Table cell specific style generator + */ + private CellStyleGenerator cellStyleGenerator = null; + + /** + * Table cell specific tooltip generator + */ + private ItemDescriptionGenerator itemDescriptionGenerator; + + /* + * EXPERIMENTAL feature: will tell the client to re-calculate column widths + * if set to true. Currently no setter: extend to enable. + */ + protected boolean alwaysRecalculateColumnWidths = false; + + private double cacheRate = CACHE_RATE_DEFAULT; + + private TableDragMode dragMode = TableDragMode.NONE; + + private DropHandler dropHandler; + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + private boolean rowCacheInvalidated; + + private RowGenerator rowGenerator = null; + + private final Map<Field<?>, Property<?>> associatedProperties = new HashMap<Field<?>, Property<?>>(); + + private boolean painted = false; + + private HashMap<Object, Converter<String, Object>> propertyValueConverters = new HashMap<Object, Converter<String, Object>>(); + + /** + * Set to true if the client-side should be informed that the key mapper has + * been reset so it can avoid sending back references to keys that are no + * longer present. + */ + private boolean keyMapperReset; + + /* Table constructors */ + + /** + * Creates a new empty table. + */ + public Table() { + setRowHeaderMode(ROW_HEADER_MODE_HIDDEN); + } + + /** + * Creates a new empty table with caption. + * + * @param caption + */ + public Table(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new table with caption and connect it to a Container. + * + * @param caption + * @param dataSource + */ + public Table(String caption, Container dataSource) { + this(); + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* Table functionality */ + + /** + * Gets the array of visible column id:s, including generated columns. + * + * <p> + * The columns are show in the order of their appearance in this array. + * </p> + * + * @return an array of currently visible propertyIds and generated column + * ids. + */ + public Object[] getVisibleColumns() { + if (visibleColumns == null) { + return null; + } + return visibleColumns.toArray(); + } + + /** + * Sets the array of visible column property id:s. + * + * <p> + * The columns are show in the order of their appearance in this array. + * </p> + * + * @param visibleColumns + * the Array of shown property id:s. + */ + public void setVisibleColumns(Object[] visibleColumns) { + + // Visible columns must exist + if (visibleColumns == null) { + throw new NullPointerException( + "Can not set visible columns to null value"); + } + + // TODO add error check that no duplicate identifiers exist + + // Checks that the new visible columns contains no nulls and properties + // exist + final Collection<?> properties = getContainerPropertyIds(); + for (int i = 0; i < visibleColumns.length; i++) { + if (visibleColumns[i] == null) { + throw new NullPointerException("Ids must be non-nulls"); + } else if (!properties.contains(visibleColumns[i]) + && !columnGenerators.containsKey(visibleColumns[i])) { + throw new IllegalArgumentException( + "Ids must exist in the Container or as a generated column , missing id: " + + visibleColumns[i]); + } + } + + // If this is called before the constructor is finished, it might be + // uninitialized + final LinkedList<Object> newVC = new LinkedList<Object>(); + for (int i = 0; i < visibleColumns.length; i++) { + newVC.add(visibleColumns[i]); + } + + // Removes alignments, icons and headers from hidden columns + if (this.visibleColumns != null) { + boolean disabledHere = disableContentRefreshing(); + try { + for (final Iterator<Object> i = this.visibleColumns.iterator(); i + .hasNext();) { + final Object col = i.next(); + if (!newVC.contains(col)) { + setColumnHeader(col, null); + setColumnAlignment(col, (Align) null); + setColumnIcon(col, null); + } + } + } finally { + if (disabledHere) { + enableContentRefreshing(false); + } + } + } + + this.visibleColumns = newVC; + + // Assures visual refresh + refreshRowCache(); + } + + /** + * Gets the headers of the columns. + * + * <p> + * The headers match the property id:s given my the set visible column + * headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the + * headers. In the defaults mode any nulls in the headers array are replaced + * with id.toString(). + * </p> + * + * @return the Array of column headers. + */ + public String[] getColumnHeaders() { + if (columnHeaders == null) { + return null; + } + final String[] headers = new String[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + headers[i] = getColumnHeader(it.next()); + } + return headers; + } + + /** + * Sets the headers of the columns. + * + * <p> + * The headers match the property id:s given my the set visible column + * headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the + * headers. In the defaults mode any nulls in the headers array are replaced + * with id.toString() outputs when rendering. + * </p> + * + * @param columnHeaders + * the Array of column headers that match the + * {@link #getVisibleColumns()} method. + */ + public void setColumnHeaders(String[] columnHeaders) { + + if (columnHeaders.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the headers array must match the number of visible columns"); + } + + this.columnHeaders.clear(); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() && i < columnHeaders.length; i++) { + this.columnHeaders.put(it.next(), columnHeaders[i]); + } + + requestRepaint(); + } + + /** + * Gets the icons of the columns. + * + * <p> + * The icons in headers match the property id:s given my the set visible + * column headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers + * with icons. + * </p> + * + * @return the Array of icons that match the {@link #getVisibleColumns()}. + */ + public Resource[] getColumnIcons() { + if (columnIcons == null) { + return null; + } + final Resource[] icons = new Resource[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + icons[i] = columnIcons.get(it.next()); + } + + return icons; + } + + /** + * Sets the icons of the columns. + * + * <p> + * The icons in headers match the property id:s given my the set visible + * column headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers + * with icons. + * </p> + * + * @param columnIcons + * the Array of icons that match the {@link #getVisibleColumns()} + * . + */ + public void setColumnIcons(Resource[] columnIcons) { + + if (columnIcons.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the icons array must match the number of visible columns"); + } + + this.columnIcons.clear(); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() && i < columnIcons.length; i++) { + this.columnIcons.put(it.next(), columnIcons[i]); + } + + requestRepaint(); + } + + /** + * Gets the array of column alignments. + * + * <p> + * The items in the array must match the properties identified by + * {@link #getVisibleColumns()}. The possible values for the alignments + * include: + * <ul> + * <li>{@link Align#LEFT}: Left alignment</li> + * <li>{@link Align#CENTER}: Centered</li> + * <li>{@link Align#RIGHT}: Right alignment</li> + * </ul> + * The alignments default to {@link Align#LEFT}: any null values are + * rendered as align lefts. + * </p> + * + * @return the Column alignments array. + */ + public Align[] getColumnAlignments() { + if (columnAlignments == null) { + return null; + } + final Align[] alignments = new Align[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + alignments[i] = getColumnAlignment(it.next()); + } + + return alignments; + } + + /** + * Sets the column alignments. + * + * <p> + * The amount of items in the array must match the amount of properties + * identified by {@link #getVisibleColumns()}. The possible values for the + * alignments include: + * <ul> + * <li>{@link Align#LEFT}: Left alignment</li> + * <li>{@link Align#CENTER}: Centered</li> + * <li>{@link Align#RIGHT}: Right alignment</li> + * </ul> + * The alignments default to {@link Align#LEFT} + * </p> + * + * @param columnAlignments + * the Column alignments array. + */ + public void setColumnAlignments(Align... columnAlignments) { + + if (columnAlignments.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the alignments array must match the number of visible columns"); + } + + // Resets the alignments + final HashMap<Object, Align> newCA = new HashMap<Object, Align>(); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() && i < columnAlignments.length; i++) { + newCA.put(it.next(), columnAlignments[i]); + } + this.columnAlignments = newCA; + + // Assures the visual refresh. No need to reset the page buffer before + // as the content has not changed, only the alignments. + refreshRenderedCells(); + } + + /** + * Sets columns width (in pixels). Theme may not necessary respect very + * small or very big values. Setting width to -1 (default) means that theme + * will make decision of width. + * + * <p> + * Column can either have a fixed width or expand ratio. The latter one set + * is used. See @link {@link #setColumnExpandRatio(Object, float)}. + * + * @param propertyId + * colunmns property id + * @param width + * width to be reserved for colunmns content + * @since 4.0.3 + */ + public void setColumnWidth(Object propertyId, int width) { + if (propertyId == null) { + // Since propertyId is null, this is the row header. Use the magic + // id to store the width of the row header. + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + if (width < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, Integer.valueOf(width)); + } + requestRepaint(); + } + + /** + * Sets the column expand ratio for given column. + * <p> + * Expand ratios can be defined to customize the way how excess space is + * divided among columns. Table can have excess space if it has its width + * defined and there is horizontally more space than columns consume + * naturally. Excess space is the space that is not used by columns with + * explicit width (see {@link #setColumnWidth(Object, int)}) or with natural + * width (no width nor expand ratio). + * + * <p> + * By default (without expand ratios) the excess space is divided + * proportionally to columns natural widths. + * + * <p> + * Only expand ratios of visible columns are used in final calculations. + * + * <p> + * Column can either have a fixed width or expand ratio. The latter one set + * is used. + * + * <p> + * A column with expand ratio is considered to be minimum width by default + * (if no excess space exists). The minimum width is defined by terminal + * implementation. + * + * <p> + * If terminal implementation supports re-sizable columns the column becomes + * fixed width column if users resizes the column. + * + * @param propertyId + * columns property id + * @param expandRatio + * the expandRatio used to divide excess space for this column + */ + public void setColumnExpandRatio(Object propertyId, float expandRatio) { + if (expandRatio < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, new Float(expandRatio)); + } + } + + public float getColumnExpandRatio(Object propertyId) { + final Object width = columnWidths.get(propertyId); + if (width == null || !(width instanceof Float)) { + return -1; + } + final Float value = (Float) width; + return value.floatValue(); + + } + + /** + * Gets the pixel width of column + * + * @param propertyId + * @return width of column or -1 when value not set + */ + public int getColumnWidth(Object propertyId) { + if (propertyId == null) { + // Since propertyId is null, this is the row header. Use the magic + // id to retrieve the width of the row header. + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + final Object width = columnWidths.get(propertyId); + if (width == null || !(width instanceof Integer)) { + return -1; + } + final Integer value = (Integer) width; + return value.intValue(); + } + + /** + * Gets the page length. + * + * <p> + * Setting page length 0 disables paging. + * </p> + * + * @return the Length of one page. + */ + public int getPageLength() { + return pageLength; + } + + /** + * Sets the page length. + * + * <p> + * Setting page length 0 disables paging. The page length defaults to 15. + * </p> + * + * <p> + * If Table has width set ({@link #setWidth(float, int)} ) the client side + * may update the page length automatically the correct value. + * </p> + * + * @param pageLength + * the length of one page. + */ + public void setPageLength(int pageLength) { + if (pageLength >= 0 && this.pageLength != pageLength) { + this.pageLength = pageLength; + // Assures the visual refresh + refreshRowCache(); + } + } + + /** + * This method adjusts a possible caching mechanism of table implementation. + * + * <p> + * Table component may fetch and render some rows outside visible area. With + * complex tables (for example containing layouts and components), the + * client side may become unresponsive. Setting the value lower, UI will + * become more responsive. With higher values scrolling in client will hit + * server less frequently. + * + * <p> + * The amount of cached rows will be cacheRate multiplied with pageLength ( + * {@link #setPageLength(int)} both below and above visible area.. + * + * @param cacheRate + * a value over 0 (fastest rendering time). Higher value will + * cache more rows on server (smoother scrolling). Default value + * is 2. + */ + public void setCacheRate(double cacheRate) { + if (cacheRate < 0) { + throw new IllegalArgumentException( + "cacheRate cannot be less than zero"); + } + if (this.cacheRate != cacheRate) { + this.cacheRate = cacheRate; + requestRepaint(); + } + } + + /** + * @see #setCacheRate(double) + * + * @return the current cache rate value + */ + public double getCacheRate() { + return cacheRate; + } + + /** + * Getter for property currentPageFirstItem. + * + * @return the Value of property currentPageFirstItem. + */ + public Object getCurrentPageFirstItemId() { + + // Priorise index over id if indexes are supported + if (items instanceof Container.Indexed) { + final int index = getCurrentPageFirstItemIndex(); + Object id = null; + if (index >= 0 && index < size()) { + id = getIdByIndex(index); + } + if (id != null && !id.equals(currentPageFirstItemId)) { + currentPageFirstItemId = id; + } + } + + // If there is no item id at all, use the first one + if (currentPageFirstItemId == null) { + currentPageFirstItemId = firstItemId(); + } + + return currentPageFirstItemId; + } + + protected Object getIdByIndex(int index) { + return ((Container.Indexed) items).getIdByIndex(index); + } + + /** + * Setter for property currentPageFirstItemId. + * + * @param currentPageFirstItemId + * the New value of property currentPageFirstItemId. + */ + public void setCurrentPageFirstItemId(Object currentPageFirstItemId) { + + // Gets the corresponding index + int index = -1; + if (items instanceof Container.Indexed) { + index = indexOfId(currentPageFirstItemId); + } else { + // If the table item container does not have index, we have to + // calculates the index by hand + Object id = firstItemId(); + while (id != null && !id.equals(currentPageFirstItemId)) { + index++; + id = nextItemId(id); + } + if (id == null) { + index = -1; + } + } + + // If the search for item index was successful + if (index >= 0) { + /* + * The table is not capable of displaying an item in the container + * as the first if there are not enough items following the selected + * item so the whole table (pagelength) is filled. + */ + int maxIndex = size() - pageLength; + if (maxIndex < 0) { + maxIndex = 0; + } + + if (index > maxIndex) { + // Note that we pass index, not maxIndex, letting + // setCurrentPageFirstItemIndex handle the situation. + setCurrentPageFirstItemIndex(index); + return; + } + + this.currentPageFirstItemId = currentPageFirstItemId; + currentPageFirstItemIndex = index; + } + + // Assures the visual refresh + refreshRowCache(); + + } + + protected int indexOfId(Object itemId) { + return ((Container.Indexed) items).indexOfId(itemId); + } + + /** + * Gets the icon Resource for the specified column. + * + * @param propertyId + * the propertyId indentifying the column. + * @return the icon for the specified column; null if the column has no icon + * set, or if the column is not visible. + */ + public Resource getColumnIcon(Object propertyId) { + return columnIcons.get(propertyId); + } + + /** + * Sets the icon Resource for the specified column. + * <p> + * Throws IllegalArgumentException if the specified column is not visible. + * </p> + * + * @param propertyId + * the propertyId identifying the column. + * @param icon + * the icon Resource to set. + */ + public void setColumnIcon(Object propertyId, Resource icon) { + + if (icon == null) { + columnIcons.remove(propertyId); + } else { + columnIcons.put(propertyId, icon); + } + + requestRepaint(); + } + + /** + * Gets the header for the specified column. + * + * @param propertyId + * the propertyId identifying the column. + * @return the header for the specified column if it has one. + */ + public String getColumnHeader(Object propertyId) { + if (getColumnHeaderMode() == ColumnHeaderMode.HIDDEN) { + return null; + } + + String header = columnHeaders.get(propertyId); + if ((header == null && getColumnHeaderMode() == ColumnHeaderMode.EXPLICIT_DEFAULTS_ID) + || getColumnHeaderMode() == ColumnHeaderMode.ID) { + header = propertyId.toString(); + } + + return header; + } + + /** + * Sets the column header for the specified column; + * + * @param propertyId + * the propertyId identifying the column. + * @param header + * the header to set. + */ + public void setColumnHeader(Object propertyId, String header) { + + if (header == null) { + columnHeaders.remove(propertyId); + } else { + columnHeaders.put(propertyId, header); + } + + requestRepaint(); + } + + /** + * Gets the specified column's alignment. + * + * @param propertyId + * the propertyID identifying the column. + * @return the specified column's alignment if it as one; null otherwise. + */ + public Align getColumnAlignment(Object propertyId) { + final Align a = columnAlignments.get(propertyId); + return a == null ? Align.LEFT : a; + } + + /** + * Sets the specified column's alignment. + * + * <p> + * Throws IllegalArgumentException if the alignment is not one of the + * following: {@link Align#LEFT}, {@link Align#CENTER} or + * {@link Align#RIGHT} + * </p> + * + * @param propertyId + * the propertyID identifying the column. + * @param alignment + * the desired alignment. + */ + public void setColumnAlignment(Object propertyId, Align alignment) { + if (alignment == null || alignment == Align.LEFT) { + columnAlignments.remove(propertyId); + } else { + columnAlignments.put(propertyId, alignment); + } + + // Assures the visual refresh. No need to reset the page buffer before + // as the content has not changed, only the alignments. + refreshRenderedCells(); + } + + /** + * Checks if the specified column is collapsed. + * + * @param propertyId + * the propertyID identifying the column. + * @return true if the column is collapsed; false otherwise; + */ + public boolean isColumnCollapsed(Object propertyId) { + return collapsedColumns != null + && collapsedColumns.contains(propertyId); + } + + /** + * Sets whether the specified column is collapsed or not. + * + * + * @param propertyId + * the propertyID identifying the column. + * @param collapsed + * the desired collapsedness. + * @throws IllegalStateException + * if column collapsing is not allowed + */ + public void setColumnCollapsed(Object propertyId, boolean collapsed) + throws IllegalStateException { + if (!isColumnCollapsingAllowed()) { + throw new IllegalStateException("Column collapsing not allowed!"); + } + if (collapsed && noncollapsibleColumns.contains(propertyId)) { + throw new IllegalStateException("The column is noncollapsible!"); + } + + if (collapsed) { + collapsedColumns.add(propertyId); + } else { + collapsedColumns.remove(propertyId); + } + + // Assures the visual refresh + refreshRowCache(); + } + + /** + * Checks if column collapsing is allowed. + * + * @return true if columns can be collapsed; false otherwise. + */ + public boolean isColumnCollapsingAllowed() { + return columnCollapsingAllowed; + } + + /** + * Sets whether column collapsing is allowed or not. + * + * @param collapsingAllowed + * specifies whether column collapsing is allowed. + */ + public void setColumnCollapsingAllowed(boolean collapsingAllowed) { + columnCollapsingAllowed = collapsingAllowed; + + if (!collapsingAllowed) { + collapsedColumns.clear(); + } + + // Assures the visual refresh. No need to reset the page buffer before + // as the content has not changed, only the alignments. + refreshRenderedCells(); + } + + /** + * Sets whether the given column is collapsible. Note that collapsible + * columns can only be actually collapsed (via UI or with + * {@link #setColumnCollapsed(Object, boolean) setColumnCollapsed()}) if + * {@link #isColumnCollapsingAllowed()} is true. By default all columns are + * collapsible. + * + * @param propertyId + * the propertyID identifying the column. + * @param collapsible + * true if the column should be collapsible, false otherwise. + */ + public void setColumnCollapsible(Object propertyId, boolean collapsible) { + if (collapsible) { + noncollapsibleColumns.remove(propertyId); + } else { + noncollapsibleColumns.add(propertyId); + collapsedColumns.remove(propertyId); + } + refreshRowCache(); + } + + /** + * Checks if the given column is collapsible. Note that even if this method + * returns <code>true</code>, the column can only be actually collapsed (via + * UI or with {@link #setColumnCollapsed(Object, boolean) + * setColumnCollapsed()}) if {@link #isColumnCollapsingAllowed()} is also + * true. + * + * @return true if the column can be collapsed; false otherwise. + */ + public boolean isColumnCollapsible(Object propertyId) { + return !noncollapsibleColumns.contains(propertyId); + } + + /** + * Checks if column reordering is allowed. + * + * @return true if columns can be reordered; false otherwise. + */ + public boolean isColumnReorderingAllowed() { + return columnReorderingAllowed; + } + + /** + * Sets whether column reordering is allowed or not. + * + * @param columnReorderingAllowed + * specifies whether column reordering is allowed. + */ + public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { + if (columnReorderingAllowed != this.columnReorderingAllowed) { + this.columnReorderingAllowed = columnReorderingAllowed; + requestRepaint(); + } + } + + /* + * Arranges visible columns according to given columnOrder. Silently ignores + * colimnId:s that are not visible columns, and keeps the internal order of + * visible columns left out of the ordering (trailing). Silently does + * nothing if columnReordering is not allowed. + */ + private void setColumnOrder(Object[] columnOrder) { + if (columnOrder == null || !isColumnReorderingAllowed()) { + return; + } + final LinkedList<Object> newOrder = new LinkedList<Object>(); + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i] != null + && visibleColumns.contains(columnOrder[i])) { + visibleColumns.remove(columnOrder[i]); + newOrder.add(columnOrder[i]); + } + } + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext();) { + final Object columnId = it.next(); + if (!newOrder.contains(columnId)) { + newOrder.add(columnId); + } + } + visibleColumns = newOrder; + + // Assure visual refresh + refreshRowCache(); + } + + /** + * Getter for property currentPageFirstItem. + * + * @return the Value of property currentPageFirstItem. + */ + public int getCurrentPageFirstItemIndex() { + return currentPageFirstItemIndex; + } + + void setCurrentPageFirstItemIndex(int newIndex, boolean needsPageBufferReset) { + + if (newIndex < 0) { + newIndex = 0; + } + + /* + * minimize Container.size() calls which may be expensive. For example + * it may cause sql query. + */ + final int size = size(); + + /* + * The table is not capable of displaying an item in the container as + * the first if there are not enough items following the selected item + * so the whole table (pagelength) is filled. + */ + int maxIndex = size - pageLength; + if (maxIndex < 0) { + maxIndex = 0; + } + + /* + * FIXME #7607 Take somehow into account the case where we want to + * scroll to the bottom so that the last row is completely visible even + * if (table height) / (row height) is not an integer. Reverted the + * original fix because of #8662 regression. + */ + if (newIndex > maxIndex) { + newIndex = maxIndex; + } + + // Refresh first item id + if (items instanceof Container.Indexed) { + try { + currentPageFirstItemId = getIdByIndex(newIndex); + } catch (final IndexOutOfBoundsException e) { + currentPageFirstItemId = null; + } + currentPageFirstItemIndex = newIndex; + } else { + + // For containers not supporting indexes, we must iterate the + // container forwards / backwards + // next available item forward or backward + + currentPageFirstItemId = firstItemId(); + + // Go forwards in the middle of the list (respect borders) + while (currentPageFirstItemIndex < newIndex + && !isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex++; + currentPageFirstItemId = nextItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex = size - 1; + } + + // Go backwards in the middle of the list (respect borders) + while (currentPageFirstItemIndex > newIndex + && !isFirstId(currentPageFirstItemId)) { + currentPageFirstItemIndex--; + currentPageFirstItemId = prevItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (isFirstId(currentPageFirstItemId)) { + currentPageFirstItemIndex = 0; + } + + // Go forwards once more + while (currentPageFirstItemIndex < newIndex + && !isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex++; + currentPageFirstItemId = nextItemId(currentPageFirstItemId); + } + + // If for some reason we do hit border again, override + // the user index request + if (isLastId(currentPageFirstItemId)) { + newIndex = currentPageFirstItemIndex = size - 1; + } + } + if (needsPageBufferReset) { + // Assures the visual refresh + refreshRowCache(); + } + } + + /** + * Setter for property currentPageFirstItem. + * + * @param newIndex + * the New value of property currentPageFirstItem. + */ + public void setCurrentPageFirstItemIndex(int newIndex) { + setCurrentPageFirstItemIndex(newIndex, true); + } + + /** + * Getter for property pageBuffering. + * + * @deprecated functionality is not needed in ajax rendering model + * + * @return the Value of property pageBuffering. + */ + @Deprecated + public boolean isPageBufferingEnabled() { + return true; + } + + /** + * Setter for property pageBuffering. + * + * @deprecated functionality is not needed in ajax rendering model + * + * @param pageBuffering + * the New value of property pageBuffering. + */ + @Deprecated + public void setPageBufferingEnabled(boolean pageBuffering) { + + } + + /** + * Getter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @return the Value of property selectable. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Setter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @param selectable + * the New value of property selectable. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /** + * Getter for property columnHeaderMode. + * + * @return the Value of property columnHeaderMode. + */ + public ColumnHeaderMode getColumnHeaderMode() { + return columnHeaderMode; + } + + /** + * Setter for property columnHeaderMode. + * + * @param columnHeaderMode + * the New value of property columnHeaderMode. + */ + public void setColumnHeaderMode(ColumnHeaderMode columnHeaderMode) { + if (columnHeaderMode == null) { + throw new IllegalArgumentException( + "Column header mode can not be null"); + } + if (columnHeaderMode != this.columnHeaderMode) { + this.columnHeaderMode = columnHeaderMode; + requestRepaint(); + } + + } + + /** + * Refreshes the rows in the internal cache. Only if + * {@link #resetPageBuffer()} is called before this then all values are + * guaranteed to be recreated. + */ + protected void refreshRenderedCells() { + if (getParent() == null) { + return; + } + + if (!isContentRefreshesEnabled) { + return; + } + + // Collects the basic facts about the table page + final int pagelen = getPageLength(); + int rows, totalRows; + rows = totalRows = size(); + int firstIndex = Math + .min(getCurrentPageFirstItemIndex(), totalRows - 1); + if (rows > 0 && firstIndex >= 0) { + rows -= firstIndex; + } + if (pagelen > 0 && pagelen < rows) { + rows = pagelen; + } + + // If "to be painted next" variables are set, use them + if (lastToBeRenderedInClient - firstToBeRenderedInClient > 0) { + rows = lastToBeRenderedInClient - firstToBeRenderedInClient + 1; + } + if (firstToBeRenderedInClient >= 0) { + if (firstToBeRenderedInClient < totalRows) { + firstIndex = firstToBeRenderedInClient; + } else { + firstIndex = totalRows - 1; + } + } else { + // initial load + + // #8805 send one extra row in the beginning in case a partial + // row is shown on the UI + if (firstIndex > 0) { + firstIndex = firstIndex - 1; + rows = rows + 1; + } + firstToBeRenderedInClient = firstIndex; + } + if (totalRows > 0) { + if (rows + firstIndex > totalRows) { + rows = totalRows - firstIndex; + } + } else { + rows = 0; + } + + // Saves the results to internal buffer + pageBuffer = getVisibleCellsNoCache(firstIndex, rows, true); + + if (rows > 0) { + pageBufferFirstIndex = firstIndex; + } + + setRowCacheInvalidated(true); + requestRepaint(); + } + + /** + * Requests that the Table should be repainted as soon as possible. + * + * Note that a {@code Table} does not necessarily repaint its contents when + * this method has been called. See {@link #refreshRowCache()} for forcing + * an update of the contents. + */ + + @Override + public void requestRepaint() { + // Overridden only for javadoc + super.requestRepaint(); + } + + @Override + public void requestRepaintAll() { + super.requestRepaintAll(); + + // Avoid sending a partial repaint (#8714) + refreshRowCache(); + } + + private void removeRowsFromCacheAndFillBottom(int firstIndex, int rows) { + int totalCachedRows = pageBuffer[CELL_ITEMID].length; + int totalRows = size(); + int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex; + + /* + * firstIndexInPageBuffer is the first row to be removed. "rows" rows + * after that should be removed. If the page buffer does not contain + * that many rows, we only remove the rows that actually are in the page + * buffer. + */ + if (firstIndexInPageBuffer + rows > totalCachedRows) { + rows = totalCachedRows - firstIndexInPageBuffer; + } + + /* + * Unregister components that will no longer be in the page buffer to + * make sure that no components leak. + */ + unregisterComponentsAndPropertiesInRows(firstIndex, rows); + + /* + * The number of rows that should be in the cache after this operation + * is done (pageBuffer currently contains the expanded items). + */ + int newCachedRowCount = totalCachedRows; + if (newCachedRowCount + pageBufferFirstIndex > totalRows) { + newCachedRowCount = totalRows - pageBufferFirstIndex; + } + + /* + * The index at which we should render the first row that does not come + * from the previous page buffer. + */ + int firstAppendedRowInPageBuffer = totalCachedRows - rows; + int firstAppendedRow = firstAppendedRowInPageBuffer + + pageBufferFirstIndex; + + /* + * Calculate the maximum number of new rows that we can add to the page + * buffer. Less than the rows we removed if the container does not + * contain that many items afterwards. + */ + int maxRowsToRender = (totalRows - firstAppendedRow); + int rowsToAdd = rows; + if (rowsToAdd > maxRowsToRender) { + rowsToAdd = maxRowsToRender; + } + + Object[][] cells = null; + if (rowsToAdd > 0) { + cells = getVisibleCellsNoCache(firstAppendedRow, rowsToAdd, false); + } + /* + * Create the new cache buffer by copying the first rows from the old + * buffer, moving the following rows upwards and appending more rows if + * applicable. + */ + Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount]; + + for (int i = 0; i < pageBuffer.length; i++) { + for (int row = 0; row < firstIndexInPageBuffer; row++) { + // Copy the first rows + newPageBuffer[i][row] = pageBuffer[i][row]; + } + for (int row = firstIndexInPageBuffer; row < firstAppendedRowInPageBuffer; row++) { + // Move the rows that were after the expanded rows + newPageBuffer[i][row] = pageBuffer[i][row + rows]; + } + for (int row = firstAppendedRowInPageBuffer; row < newCachedRowCount; row++) { + // Add the newly rendered rows. Only used if rowsToAdd > 0 + // (cells != null) + newPageBuffer[i][row] = cells[i][row + - firstAppendedRowInPageBuffer]; + } + } + pageBuffer = newPageBuffer; + } + + private Object[][] getVisibleCellsUpdateCacheRows(int firstIndex, int rows) { + Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false); + int cacheIx = firstIndex - pageBufferFirstIndex; + // update the new rows in the cache. + int totalCachedRows = pageBuffer[CELL_ITEMID].length; + int end = Math.min(cacheIx + rows, totalCachedRows); + for (int ix = cacheIx; ix < end; ix++) { + for (int i = 0; i < pageBuffer.length; i++) { + pageBuffer[i][ix] = cells[i][ix - cacheIx]; + } + } + return cells; + } + + /** + * @param firstIndex + * The position where new rows should be inserted + * @param rows + * The maximum number of rows that should be inserted at position + * firstIndex. Less rows will be inserted if the page buffer is + * too small. + * @return + */ + private Object[][] getVisibleCellsInsertIntoCache(int firstIndex, int rows) { + getLogger().finest( + "Insert " + rows + " rows at index " + firstIndex + + " to existing page buffer requested"); + + // Page buffer must not become larger than pageLength*cacheRate before + // or after the current page + int minPageBufferIndex = getCurrentPageFirstItemIndex() + - (int) (getPageLength() * getCacheRate()); + if (minPageBufferIndex < 0) { + minPageBufferIndex = 0; + } + + int maxPageBufferIndex = getCurrentPageFirstItemIndex() + + (int) (getPageLength() * (1 + getCacheRate())); + int maxBufferSize = maxPageBufferIndex - minPageBufferIndex; + + if (getPageLength() == 0) { + // If pageLength == 0 then all rows should be rendered + maxBufferSize = pageBuffer[0].length + rows; + } + /* + * Number of rows that were previously cached. This is not necessarily + * the same as pageLength if we do not have enough rows in the + * container. + */ + int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length; + + /* + * firstIndexInPageBuffer is the offset in pageBuffer where the new rows + * will be inserted (firstIndex is the index in the whole table). + * + * E.g. scrolled down to row 1000: firstIndex==1010, + * pageBufferFirstIndex==1000 -> cacheIx==10 + */ + int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex; + + /* If rows > size available in page buffer */ + if (firstIndexInPageBuffer + rows > maxBufferSize) { + rows = maxBufferSize - firstIndexInPageBuffer; + } + + /* + * "rows" rows will be inserted at firstIndex. Find out how many old + * rows fall outside the new buffer so we can unregister components in + * the cache. + */ + + /* All rows until the insertion point remain, always. */ + int firstCacheRowToRemoveInPageBuffer = firstIndexInPageBuffer; + + /* + * IF there is space remaining in the buffer after the rows have been + * inserted, we can keep more rows. + */ + int numberOfOldRowsAfterInsertedRows = maxBufferSize + - firstIndexInPageBuffer - rows; + if (numberOfOldRowsAfterInsertedRows > 0) { + firstCacheRowToRemoveInPageBuffer += numberOfOldRowsAfterInsertedRows; + } + + if (firstCacheRowToRemoveInPageBuffer <= currentlyCachedRowCount) { + /* + * Unregister all components that fall beyond the cache limits after + * inserting the new rows. + */ + unregisterComponentsAndPropertiesInRows( + firstCacheRowToRemoveInPageBuffer + pageBufferFirstIndex, + currentlyCachedRowCount - firstCacheRowToRemoveInPageBuffer + + pageBufferFirstIndex); + } + + // Calculate the new cache size + int newCachedRowCount = currentlyCachedRowCount; + if (maxBufferSize == 0 || currentlyCachedRowCount < maxBufferSize) { + newCachedRowCount = currentlyCachedRowCount + rows; + if (maxBufferSize > 0 && newCachedRowCount > maxBufferSize) { + newCachedRowCount = maxBufferSize; + } + } + + /* Paint the new rows into a separate buffer */ + Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false); + + /* + * Create the new cache buffer and fill it with the data from the old + * buffer as well as the inserted rows. + */ + Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount]; + + for (int i = 0; i < pageBuffer.length; i++) { + for (int row = 0; row < firstIndexInPageBuffer; row++) { + // Copy the first rows + newPageBuffer[i][row] = pageBuffer[i][row]; + } + for (int row = firstIndexInPageBuffer; row < firstIndexInPageBuffer + + rows; row++) { + // Copy the newly created rows + newPageBuffer[i][row] = cells[i][row - firstIndexInPageBuffer]; + } + for (int row = firstIndexInPageBuffer + rows; row < newCachedRowCount; row++) { + // Move the old rows down below the newly inserted rows + newPageBuffer[i][row] = pageBuffer[i][row - rows]; + } + } + pageBuffer = newPageBuffer; + getLogger().finest( + "Page Buffer now contains " + + pageBuffer[CELL_ITEMID].length + + " rows (" + + pageBufferFirstIndex + + "-" + + (pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1) + ")"); + return cells; + } + + /** + * Render rows with index "firstIndex" to "firstIndex+rows-1" to a new + * buffer. + * + * Reuses values from the current page buffer if the rows are found there. + * + * @param firstIndex + * @param rows + * @param replaceListeners + * @return + */ + private Object[][] getVisibleCellsNoCache(int firstIndex, int rows, + boolean replaceListeners) { + getLogger().finest( + "Render visible cells for rows " + firstIndex + "-" + + (firstIndex + rows - 1)); + final Object[] colids = getVisibleColumns(); + final int cols = colids.length; + + HashSet<Property<?>> oldListenedProperties = listenedProperties; + HashSet<Component> oldVisibleComponents = visibleComponents; + + if (replaceListeners) { + // initialize the listener collections, this should only be done if + // the entire cache is refreshed (through refreshRenderedCells) + listenedProperties = new HashSet<Property<?>>(); + visibleComponents = new HashSet<Component>(); + } + + Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows]; + if (rows == 0) { + unregisterPropertiesAndComponents(oldListenedProperties, + oldVisibleComponents); + return cells; + } + + // Gets the first item id + Object id; + if (items instanceof Container.Indexed) { + id = getIdByIndex(firstIndex); + } else { + id = firstItemId(); + for (int i = 0; i < firstIndex; i++) { + id = nextItemId(id); + } + } + + final RowHeaderMode headmode = getRowHeaderMode(); + final boolean[] iscomponent = new boolean[cols]; + for (int i = 0; i < cols; i++) { + iscomponent[i] = columnGenerators.containsKey(colids[i]) + || Component.class.isAssignableFrom(getType(colids[i])); + } + int firstIndexNotInCache; + if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) { + firstIndexNotInCache = pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length; + } else { + firstIndexNotInCache = -1; + } + + // Creates the page contents + int filledRows = 0; + for (int i = 0; i < rows && id != null; i++) { + cells[CELL_ITEMID][i] = id; + cells[CELL_KEY][i] = itemIdMapper.key(id); + if (headmode != ROW_HEADER_MODE_HIDDEN) { + switch (headmode) { + case INDEX: + cells[CELL_HEADER][i] = String.valueOf(i + firstIndex + 1); + break; + default: + cells[CELL_HEADER][i] = getItemCaption(id); + } + cells[CELL_ICON][i] = getItemIcon(id); + } + + GeneratedRow generatedRow = rowGenerator != null ? rowGenerator + .generateRow(this, id) : null; + cells[CELL_GENERATED_ROW][i] = generatedRow; + + for (int j = 0; j < cols; j++) { + if (isColumnCollapsed(colids[j])) { + continue; + } + Property<?> p = null; + Object value = ""; + boolean isGeneratedRow = generatedRow != null; + boolean isGeneratedColumn = columnGenerators + .containsKey(colids[j]); + boolean isGenerated = isGeneratedRow || isGeneratedColumn; + + if (!isGenerated) { + p = getContainerProperty(id, colids[j]); + } + + if (isGeneratedRow) { + if (generatedRow.isSpanColumns() && j > 0) { + value = null; + } else if (generatedRow.isSpanColumns() && j == 0 + && generatedRow.getValue() instanceof Component) { + value = generatedRow.getValue(); + } else if (generatedRow.getText().length > j) { + value = generatedRow.getText()[j]; + } + } else { + // check in current pageBuffer already has row + int index = firstIndex + i; + if (p != null || isGenerated) { + int indexInOldBuffer = index - pageBufferFirstIndex; + if (index < firstIndexNotInCache + && index >= pageBufferFirstIndex + && pageBuffer[CELL_GENERATED_ROW][indexInOldBuffer] == null + && id.equals(pageBuffer[CELL_ITEMID][indexInOldBuffer])) { + // we already have data in our cache, + // recycle it instead of fetching it via + // getValue/getPropertyValue + value = pageBuffer[CELL_FIRSTCOL + j][indexInOldBuffer]; + if (!isGeneratedColumn && iscomponent[j] + || !(value instanceof Component)) { + listenProperty(p, oldListenedProperties); + } + } else { + if (isGeneratedColumn) { + ColumnGenerator cg = columnGenerators + .get(colids[j]); + value = cg.generateCell(this, id, colids[j]); + if (value != null + && !(value instanceof Component) + && !(value instanceof String)) { + // Avoid errors if a generator returns + // something + // other than a Component or a String + value = value.toString(); + } + } else if (iscomponent[j]) { + value = p.getValue(); + listenProperty(p, oldListenedProperties); + } else if (p != null) { + value = getPropertyValue(id, colids[j], p); + /* + * If returned value is Component (via + * fieldfactory or overridden getPropertyValue) + * we excpect it to listen property value + * changes. Otherwise if property emits value + * change events, table will start to listen + * them and refresh content when needed. + */ + if (!(value instanceof Component)) { + listenProperty(p, oldListenedProperties); + } + } else { + value = getPropertyValue(id, colids[j], null); + } + } + } + } + + if (value instanceof Component) { + registerComponent((Component) value); + } + cells[CELL_FIRSTCOL + j][i] = value; + } + + // Gets the next item id + if (items instanceof Container.Indexed) { + int index = firstIndex + i + 1; + if (index < size()) { + id = getIdByIndex(index); + } else { + id = null; + } + } else { + id = nextItemId(id); + } + + filledRows++; + } + + // Assures that all the rows of the cell-buffer are valid + if (filledRows != cells[0].length) { + final Object[][] temp = new Object[cells.length][filledRows]; + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < filledRows; j++) { + temp[i][j] = cells[i][j]; + } + } + cells = temp; + } + + unregisterPropertiesAndComponents(oldListenedProperties, + oldVisibleComponents); + + return cells; + } + + protected void registerComponent(Component component) { + getLogger().finest( + "Registered " + component.getClass().getSimpleName() + ": " + + component.getCaption()); + if (component.getParent() != this) { + component.setParent(this); + } + visibleComponents.add(component); + } + + private void listenProperty(Property<?> p, + HashSet<Property<?>> oldListenedProperties) { + if (p instanceof Property.ValueChangeNotifier) { + if (oldListenedProperties == null + || !oldListenedProperties.contains(p)) { + ((Property.ValueChangeNotifier) p).addListener(this); + } + /* + * register listened properties, so we can do proper cleanup to free + * memory. Essential if table has loads of data and it is used for a + * long time. + */ + listenedProperties.add(p); + + } + } + + /** + * @param firstIx + * Index of the first row to process. Global index, not relative + * to page buffer. + * @param count + */ + private void unregisterComponentsAndPropertiesInRows(int firstIx, int count) { + getLogger().finest( + "Unregistering components in rows " + firstIx + "-" + + (firstIx + count - 1)); + Object[] colids = getVisibleColumns(); + if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) { + int bufSize = pageBuffer[CELL_ITEMID].length; + int ix = firstIx - pageBufferFirstIndex; + ix = ix < 0 ? 0 : ix; + if (ix < bufSize) { + count = count > bufSize - ix ? bufSize - ix : count; + for (int i = 0; i < count; i++) { + for (int c = 0; c < colids.length; c++) { + Object cellVal = pageBuffer[CELL_FIRSTCOL + c][i + ix]; + if (cellVal instanceof Component + && visibleComponents.contains(cellVal)) { + visibleComponents.remove(cellVal); + unregisterComponent((Component) cellVal); + } else { + Property<?> p = getContainerProperty( + pageBuffer[CELL_ITEMID][i + ix], colids[c]); + if (p instanceof ValueChangeNotifier + && listenedProperties.contains(p)) { + listenedProperties.remove(p); + ((ValueChangeNotifier) p).removeListener(this); + } + } + } + } + } + } + } + + /** + * Helper method to remove listeners and maintain correct component + * hierarchy. Detaches properties and components if those are no more + * rendered in client. + * + * @param oldListenedProperties + * set of properties that where listened in last render + * @param oldVisibleComponents + * set of components that where attached in last render + */ + private void unregisterPropertiesAndComponents( + HashSet<Property<?>> oldListenedProperties, + HashSet<Component> oldVisibleComponents) { + if (oldVisibleComponents != null) { + for (final Iterator<Component> i = oldVisibleComponents.iterator(); i + .hasNext();) { + Component c = i.next(); + if (!visibleComponents.contains(c)) { + unregisterComponent(c); + } + } + } + + if (oldListenedProperties != null) { + for (final Iterator<Property<?>> i = oldListenedProperties + .iterator(); i.hasNext();) { + Property.ValueChangeNotifier o = (ValueChangeNotifier) i.next(); + if (!listenedProperties.contains(o)) { + o.removeListener(this); + } + } + } + } + + /** + * This method cleans up a Component that has been generated when Table is + * in editable mode. The component needs to be detached from its parent and + * if it is a field, it needs to be detached from its property data source + * in order to allow garbage collection to take care of removing the unused + * component from memory. + * + * Override this method and getPropertyValue(Object, Object, Property) with + * custom logic if you need to deal with buffered fields. + * + * @see #getPropertyValue(Object, Object, Property) + * + * @param oldVisibleComponents + * a set of components that should be unregistered. + */ + protected void unregisterComponent(Component component) { + getLogger().finest( + "Unregistered " + component.getClass().getSimpleName() + ": " + + component.getCaption()); + component.setParent(null); + /* + * Also remove property data sources to unregister listeners keeping the + * fields in memory. + */ + if (component instanceof Field) { + Field<?> field = (Field<?>) component; + Property<?> associatedProperty = associatedProperties + .remove(component); + if (associatedProperty != null + && field.getPropertyDataSource() == associatedProperty) { + // Remove the property data source only if it's the one we + // added in getPropertyValue + field.setPropertyDataSource(null); + } + } + } + + /** + * Refreshes the current page contents. + * + * @deprecated should not need to be used + */ + @Deprecated + public void refreshCurrentPage() { + + } + + /** + * Sets the row header mode. + * <p> + * The mode can be one of the following ones: + * <ul> + * <li>{@link #ROW_HEADER_MODE_HIDDEN}: The row captions are hidden.</li> + * <li>{@link #ROW_HEADER_MODE_ID}: Items Id-objects <code>toString()</code> + * is used as row caption. + * <li>{@link #ROW_HEADER_MODE_ITEM}: Item-objects <code>toString()</code> + * is used as row caption. + * <li>{@link #ROW_HEADER_MODE_PROPERTY}: Property set with + * {@link #setItemCaptionPropertyId(Object)} is used as row header. + * <li>{@link #ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID}: Items Id-objects + * <code>toString()</code> is used as row header. If caption is explicitly + * specified, it overrides the id-caption. + * <li>{@link #ROW_HEADER_MODE_EXPLICIT}: The row headers must be explicitly + * specified.</li> + * <li>{@link #ROW_HEADER_MODE_INDEX}: The index of the item is used as row + * caption. The index mode can only be used with the containers implementing + * <code>Container.Indexed</code> interface.</li> + * </ul> + * The default value is {@link #ROW_HEADER_MODE_HIDDEN} + * </p> + * + * @param mode + * the One of the modes listed above. + */ + public void setRowHeaderMode(RowHeaderMode mode) { + if (mode != null) { + rowHeaderMode = mode; + if (mode != RowHeaderMode.HIDDEN) { + setItemCaptionMode(mode.getItemCaptionMode()); + } + // Assures the visual refresh. No need to reset the page buffer + // before + // as the content has not changed, only the alignments. + refreshRenderedCells(); + } + } + + /** + * Gets the row header mode. + * + * @return the Row header mode. + * @see #setRowHeaderMode(int) + */ + public RowHeaderMode getRowHeaderMode() { + return rowHeaderMode; + } + + /** + * Adds the new row to table and fill the visible cells (except generated + * columns) with given values. + * + * @param cells + * the Object array that is used for filling the visible cells + * new row. The types must be settable to visible column property + * types. + * @param itemId + * the Id the new row. If null, a new id is automatically + * assigned. If given, the table cant already have a item with + * given id. + * @return Returns item id for the new row. Returns null if operation fails. + */ + public Object addItem(Object[] cells, Object itemId) + throws UnsupportedOperationException { + + // remove generated columns from the list of columns being assigned + final LinkedList<Object> availableCols = new LinkedList<Object>(); + for (Iterator<Object> it = visibleColumns.iterator(); it.hasNext();) { + Object id = it.next(); + if (!columnGenerators.containsKey(id)) { + availableCols.add(id); + } + } + // Checks that a correct number of cells are given + if (cells.length != availableCols.size()) { + return null; + } + + // Creates new item + Item item; + if (itemId == null) { + itemId = items.addItem(); + if (itemId == null) { + return null; + } + item = items.getItem(itemId); + } else { + item = items.addItem(itemId); + } + if (item == null) { + return null; + } + + // Fills the item properties + for (int i = 0; i < availableCols.size(); i++) { + item.getItemProperty(availableCols.get(i)).setValue(cells[i]); + } + + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + + return itemId; + } + + /** + * Discards and recreates the internal row cache. Call this if you make + * changes that affect the rows but the information about the changes are + * not automatically propagated to the Table. + * <p> + * Do not call this e.g. if you have updated the data model through a + * Property. These types of changes are automatically propagated to the + * Table. + * <p> + * A typical case when this is needed is if you update a generator (e.g. + * CellStyleGenerator) and want to ensure that the rows are redrawn with new + * styles. + * <p> + * <i>Note that calling this method is not cheap so avoid calling it + * unnecessarily.</i> + * + * @since 6.7.2 + */ + public void refreshRowCache() { + resetPageBuffer(); + refreshRenderedCells(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + + disableContentRefreshing(); + + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + + // Assures that the data source is ordered by making unordered + // containers ordered by wrapping them + if (newDataSource instanceof Container.Ordered) { + super.setContainerDataSource(newDataSource); + } else { + super.setContainerDataSource(new ContainerOrderedWrapper( + newDataSource)); + } + + // Resets page position + currentPageFirstItemId = null; + currentPageFirstItemIndex = 0; + + // Resets column properties + if (collapsedColumns != null) { + collapsedColumns.clear(); + } + + // columnGenerators 'override' properties, don't add the same id twice + Collection<Object> col = new LinkedList<Object>(); + for (Iterator<?> it = getContainerPropertyIds().iterator(); it + .hasNext();) { + Object id = it.next(); + if (columnGenerators == null || !columnGenerators.containsKey(id)) { + col.add(id); + } + } + // generators added last + if (columnGenerators != null && columnGenerators.size() > 0) { + col.addAll(columnGenerators.keySet()); + } + + setVisibleColumns(col.toArray()); + + // Assure visual refresh + resetPageBuffer(); + + enableContentRefreshing(true); + } + + /** + * Gets items ids from a range of key values + * + * @param startRowKey + * The start key + * @param endRowKey + * The end key + * @return + */ + private LinkedHashSet<Object> getItemIdsInRange(Object itemId, + final int length) { + LinkedHashSet<Object> ids = new LinkedHashSet<Object>(); + for (int i = 0; i < length; i++) { + assert itemId != null; // should not be null unless client-server + // are out of sync + ids.add(itemId); + itemId = nextItemId(itemId); + } + return ids; + } + + /** + * Handles selection if selection is a multiselection + * + * @param variables + * The variables + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + final String[] ranges = (String[]) variables.get("selectedRanges"); + + Set<Object> renderedButNotSelectedItemIds = getCurrentlyRenderedItemIds(); + + @SuppressWarnings("unchecked") + HashSet<Object> newValue = new LinkedHashSet<Object>( + (Collection<Object>) getValue()); + + if (variables.containsKey("clearSelections")) { + // the client side has instructed to swipe all previous selections + newValue.clear(); + } + + /* + * Then add (possibly some of them back) rows that are currently + * selected on the client side (the ones that the client side is aware + * of). + */ + for (int i = 0; i < ka.length; i++) { + // key to id + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + newValue.add(id); + renderedButNotSelectedItemIds.remove(id); + } + } + + /* Add range items aka shift clicked multiselection areas */ + if (ranges != null) { + for (String range : ranges) { + String[] split = range.split("-"); + Object startItemId = itemIdMapper.get(split[0]); + int length = Integer.valueOf(split[1]); + LinkedHashSet<Object> itemIdsInRange = getItemIdsInRange( + startItemId, length); + newValue.addAll(itemIdsInRange); + renderedButNotSelectedItemIds.removeAll(itemIdsInRange); + } + } + /* + * finally clear all currently rendered rows (the ones that the client + * side counterpart is aware of) that the client didn't send as selected + */ + newValue.removeAll(renderedButNotSelectedItemIds); + + if (!isNullSelectionAllowed() && newValue.isEmpty()) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + setValue(newValue, true); + + } + + private Set<Object> getCurrentlyRenderedItemIds() { + HashSet<Object> ids = new HashSet<Object>(); + if (pageBuffer != null) { + for (int i = 0; i < pageBuffer[CELL_ITEMID].length; i++) { + ids.add(pageBuffer[CELL_ITEMID][i]); + } + } + return ids; + } + + /* Component basics */ + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.Select#changeVariables(java.lang.Object, + * java.util.Map) + */ + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + boolean clientNeedsContentRefresh = false; + + handleClickEvent(variables); + + handleColumnResizeEvent(variables); + + handleColumnWidthUpdates(variables); + + disableContentRefreshing(); + + if (!isSelectable() && variables.containsKey("selected")) { + // Not-selectable is a special case, AbstractSelect does not support + // TODO could be optimized. + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + /* + * The AbstractSelect cannot handle the multiselection properly, instead + * we handle it ourself + */ + else if (isSelectable() && isMultiSelect() + && variables.containsKey("selected") + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + super.changeVariables(source, variables); + + // Client might update the pagelength if Table height is fixed + if (variables.containsKey("pagelength")) { + // Sets pageLength directly to avoid repaint that setter causes + pageLength = (Integer) variables.get("pagelength"); + } + + // Page start index + if (variables.containsKey("firstvisible")) { + final Integer value = (Integer) variables.get("firstvisible"); + if (value != null) { + setCurrentPageFirstItemIndex(value.intValue(), false); + } + } + + // Sets requested firstrow and rows for the next paint + if (variables.containsKey("reqfirstrow") + || variables.containsKey("reqrows")) { + + try { + firstToBeRenderedInClient = ((Integer) variables + .get("firstToBeRendered")).intValue(); + lastToBeRenderedInClient = ((Integer) variables + .get("lastToBeRendered")).intValue(); + } catch (Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Could not parse the first and/or last rows.", e); + } + + // respect suggested rows only if table is not otherwise updated + // (row caches emptied by other event) + if (!containerChangeToBeRendered) { + Integer value = (Integer) variables.get("reqfirstrow"); + if (value != null) { + reqFirstRowToPaint = value.intValue(); + } + value = (Integer) variables.get("reqrows"); + if (value != null) { + reqRowsToPaint = value.intValue(); + // sanity check + if (reqFirstRowToPaint + reqRowsToPaint > size()) { + reqRowsToPaint = size() - reqFirstRowToPaint; + } + } + } + getLogger().finest( + "Client wants rows " + reqFirstRowToPaint + "-" + + (reqFirstRowToPaint + reqRowsToPaint - 1)); + clientNeedsContentRefresh = true; + } + + if (isSortEnabled()) { + // Sorting + boolean doSort = false; + if (variables.containsKey("sortcolumn")) { + final String colId = (String) variables.get("sortcolumn"); + if (colId != null && !"".equals(colId) && !"null".equals(colId)) { + final Object id = columnIdMap.get(colId); + setSortContainerPropertyId(id, false); + doSort = true; + } + } + if (variables.containsKey("sortascending")) { + final boolean state = ((Boolean) variables.get("sortascending")) + .booleanValue(); + if (state != sortAscending) { + setSortAscending(state, false); + doSort = true; + } + } + if (doSort) { + this.sort(); + resetPageBuffer(); + } + } + + // Dynamic column hide/show and order + // Update visible columns + if (isColumnCollapsingAllowed()) { + if (variables.containsKey("collapsedcolumns")) { + try { + final Object[] ids = (Object[]) variables + .get("collapsedcolumns"); + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext();) { + setColumnCollapsed(it.next(), false); + } + for (int i = 0; i < ids.length; i++) { + setColumnCollapsed(columnIdMap.get(ids[i].toString()), + true); + } + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Could not determine column collapsing state", e); + } + clientNeedsContentRefresh = true; + } + } + if (isColumnReorderingAllowed()) { + if (variables.containsKey("columnorder")) { + try { + final Object[] ids = (Object[]) variables + .get("columnorder"); + // need a real Object[], ids can be a String[] + final Object[] idsTemp = new Object[ids.length]; + for (int i = 0; i < ids.length; i++) { + idsTemp[i] = columnIdMap.get(ids[i].toString()); + } + setColumnOrder(idsTemp); + if (hasListeners(ColumnReorderEvent.class)) { + fireEvent(new ColumnReorderEvent(this)); + } + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Could not determine column reordering state", e); + } + clientNeedsContentRefresh = true; + } + } + + enableContentRefreshing(clientNeedsContentRefresh); + + // Actions + if (variables.containsKey("action")) { + final StringTokenizer st = new StringTokenizer( + (String) variables.get("action"), ","); + if (st.countTokens() == 2) { + final Object itemId = itemIdMapper.get(st.nextToken()); + final Action action = actionMapper.get(st.nextToken()); + + if (action != null && (itemId == null || containsId(itemId)) + && actionHandlers != null) { + for (Handler ah : actionHandlers) { + ah.handleAction(action, this, itemId); + } + } + } + } + + } + + /** + * Handles click event + * + * @param variables + */ + private void handleClickEvent(Map<String, Object> variables) { + + // Item click event + if (variables.containsKey("clickEvent")) { + String key = (String) variables.get("clickedKey"); + Object itemId = itemIdMapper.get(key); + Object propertyId = null; + String colkey = (String) variables.get("clickedColKey"); + // click is not necessary on a property + if (colkey != null) { + propertyId = columnIdMap.get(colkey); + } + MouseEventDetails evt = MouseEventDetails + .deSerialize((String) variables.get("clickEvent")); + Item item = getItem(itemId); + if (item != null) { + fireEvent(new ItemClickEvent(this, item, itemId, propertyId, + evt)); + } + } + + // Header click event + else if (variables.containsKey("headerClickEvent")) { + + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("headerClickEvent")); + + Object cid = variables.get("headerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new HeaderClickEvent(this, propertyId, details)); + } + + // Footer click event + else if (variables.containsKey("footerClickEvent")) { + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("footerClickEvent")); + + Object cid = variables.get("footerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new FooterClickEvent(this, propertyId, details)); + } + } + + /** + * Handles the column resize event sent by the client. + * + * @param variables + */ + private void handleColumnResizeEvent(Map<String, Object> variables) { + if (variables.containsKey("columnResizeEventColumn")) { + Object cid = variables.get("columnResizeEventColumn"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + + Object prev = variables.get("columnResizeEventPrev"); + int previousWidth = -1; + if (prev != null) { + previousWidth = Integer.valueOf(prev.toString()); + } + + Object curr = variables.get("columnResizeEventCurr"); + int currentWidth = -1; + if (curr != null) { + currentWidth = Integer.valueOf(curr.toString()); + } + + fireColumnResizeEvent(propertyId, previousWidth, currentWidth); + } + } + } + + private void fireColumnResizeEvent(Object propertyId, int previousWidth, + int currentWidth) { + /* + * Update the sizes on the server side. If a column previously had a + * expand ratio and the user resized the column then the expand ratio + * will be turned into a static pixel size. + */ + setColumnWidth(propertyId, currentWidth); + + fireEvent(new ColumnResizeEvent(this, propertyId, previousWidth, + currentWidth)); + } + + private void handleColumnWidthUpdates(Map<String, Object> variables) { + if (variables.containsKey("columnWidthUpdates")) { + String[] events = (String[]) variables.get("columnWidthUpdates"); + for (String str : events) { + String[] eventDetails = str.split(":"); + Object propertyId = columnIdMap.get(eventDetails[0]); + if (propertyId == null) { + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + int width = Integer.valueOf(eventDetails[1]); + setColumnWidth(propertyId, width); + } + } + } + + /** + * Go to mode where content updates are not done. This is due we want to + * bypass expensive content for some reason (like when we know we may have + * other content changes on their way). + * + * @return true if content refresh flag was enabled prior this call + */ + protected boolean disableContentRefreshing() { + boolean wasDisabled = isContentRefreshesEnabled; + isContentRefreshesEnabled = false; + return wasDisabled; + } + + /** + * Go to mode where content content refreshing has effect. + * + * @param refreshContent + * true if content refresh needs to be done + */ + protected void enableContentRefreshing(boolean refreshContent) { + isContentRefreshesEnabled = true; + if (refreshContent) { + refreshRenderedCells(); + // Ensure that client gets a response + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin. + * terminal.PaintTarget) + */ + + @Override + public void paintContent(PaintTarget target) throws PaintException { + /* + * Body actions - Actions which has the target null and can be invoked + * by right clicking on the table body. + */ + final Set<Action> actionSet = findAndPaintBodyActions(target); + + final Object[][] cells = getVisibleCells(); + int rows = findNumRowsToPaint(target, cells); + + int total = size(); + if (shouldHideNullSelectionItem()) { + total--; + rows--; + } + + // Table attributes + paintTableAttributes(target, rows, total); + + paintVisibleColumnOrder(target); + + // Rows + if (isPartialRowUpdate() && painted && !target.isFullRepaint()) { + paintPartialRowUpdate(target, actionSet); + /* + * Send the page buffer indexes to ensure that the client side stays + * in sync. Otherwise we _might_ have the situation where the client + * side discards too few or too many rows, causing out of sync + * issues. + * + * This could probably be done for full repaints also to simplify + * the client side. + */ + int pageBufferLastIndex = pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1; + target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST, + pageBufferFirstIndex); + target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_LAST, + pageBufferLastIndex); + } else if (target.isFullRepaint() || isRowCacheInvalidated()) { + paintRows(target, cells, actionSet); + setRowCacheInvalidated(false); + } + + paintSorting(target); + + resetVariablesAndPageBuffer(target); + + // Actions + paintActions(target, actionSet); + + paintColumnOrder(target); + + // Available columns + paintAvailableColumns(target); + + paintVisibleColumns(target); + + if (keyMapperReset) { + keyMapperReset = false; + target.addAttribute(VScrollTable.ATTRIBUTE_KEY_MAPPER_RESET, true); + } + + if (dropHandler != null) { + dropHandler.getAcceptCriterion().paint(target); + } + + painted = true; + } + + private void setRowCacheInvalidated(boolean invalidated) { + rowCacheInvalidated = invalidated; + } + + protected boolean isRowCacheInvalidated() { + return rowCacheInvalidated; + } + + private void paintPartialRowUpdate(PaintTarget target, Set<Action> actionSet) + throws PaintException { + paintPartialRowUpdates(target, actionSet); + paintPartialRowAdditions(target, actionSet); + } + + private void paintPartialRowUpdates(PaintTarget target, + Set<Action> actionSet) throws PaintException { + final boolean[] iscomponent = findCellsWithComponents(); + + int firstIx = getFirstUpdatedItemIndex(); + int count = getUpdatedRowCount(); + + target.startTag("urows"); + target.addAttribute("firsturowix", firstIx); + target.addAttribute("numurows", count); + + // Partial row updates bypass the normal caching mechanism. + Object[][] cells = getVisibleCellsUpdateCacheRows(firstIx, count); + for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; + + if (shouldHideNullSelectionItem()) { + // Remove null selection item if null selection is not allowed + continue; + } + + paintRow(target, cells, isEditable(), actionSet, iscomponent, + indexInRowbuffer, itemId); + } + target.endTag("urows"); + } + + private void paintPartialRowAdditions(PaintTarget target, + Set<Action> actionSet) throws PaintException { + final boolean[] iscomponent = findCellsWithComponents(); + + int firstIx = getFirstAddedItemIndex(); + int count = getAddedRowCount(); + + target.startTag("prows"); + + if (!shouldHideAddedRows()) { + getLogger().finest( + "Paint rows for add. Index: " + firstIx + ", count: " + + count + "."); + + // Partial row additions bypass the normal caching mechanism. + Object[][] cells = getVisibleCellsInsertIntoCache(firstIx, count); + if (cells[0].length < count) { + // delete the rows below, since they will fall beyond the cache + // page. + target.addAttribute("delbelow", true); + count = cells[0].length; + } + + for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; + if (shouldHideNullSelectionItem()) { + // Remove null selection item if null selection is not + // allowed + continue; + } + + paintRow(target, cells, isEditable(), actionSet, iscomponent, + indexInRowbuffer, itemId); + } + } else { + getLogger().finest( + "Paint rows for remove. Index: " + firstIx + ", count: " + + count + "."); + removeRowsFromCacheAndFillBottom(firstIx, count); + target.addAttribute("hide", true); + } + + target.addAttribute("firstprowix", firstIx); + target.addAttribute("numprows", count); + target.endTag("prows"); + } + + /** + * Subclass and override this to enable partial row updates and additions, + * which bypass the normal caching mechanism. This is useful for e.g. + * TreeTable. + * + * @return true if this update is a partial row update, false if not. For + * plain Table it is always false. + */ + protected boolean isPartialRowUpdate() { + return false; + } + + /** + * Subclass and override this to enable partial row additions, bypassing the + * normal caching mechanism. This is useful for e.g. TreeTable, where + * expanding a node should only fetch and add the items inside of that node. + * + * @return The index of the first added item. For plain Table it is always + * 0. + */ + protected int getFirstAddedItemIndex() { + return 0; + } + + /** + * Subclass and override this to enable partial row additions, bypassing the + * normal caching mechanism. This is useful for e.g. TreeTable, where + * expanding a node should only fetch and add the items inside of that node. + * + * @return the number of rows to be added, starting at the index returned by + * {@link #getFirstAddedItemIndex()}. For plain Table it is always + * 0. + */ + protected int getAddedRowCount() { + return 0; + } + + /** + * Subclass and override this to enable removing of rows, bypassing the + * normal caching and lazy loading mechanism. This is useful for e.g. + * TreeTable, when you need to hide certain rows as a node is collapsed. + * + * This should return true if the rows pointed to by + * {@link #getFirstAddedItemIndex()} and {@link #getAddedRowCount()} should + * be hidden instead of added. + * + * @return whether the rows to add (see {@link #getFirstAddedItemIndex()} + * and {@link #getAddedRowCount()}) should be added or hidden. For + * plain Table it is always false. + */ + protected boolean shouldHideAddedRows() { + return false; + } + + /** + * Subclass and override this to enable partial row updates, bypassing the + * normal caching and lazy loading mechanism. This is useful for updating + * the state of certain rows, e.g. in the TreeTable the collapsed state of a + * single node is updated using this mechanism. + * + * @return the index of the first item to be updated. For plain Table it is + * always 0. + */ + protected int getFirstUpdatedItemIndex() { + return 0; + } + + /** + * Subclass and override this to enable partial row updates, bypassing the + * normal caching and lazy loading mechanism. This is useful for updating + * the state of certain rows, e.g. in the TreeTable the collapsed state of a + * single node is updated using this mechanism. + * + * @return the number of rows to update, starting at the index returned by + * {@link #getFirstUpdatedItemIndex()}. For plain table it is always + * 0. + */ + protected int getUpdatedRowCount() { + return 0; + } + + private void paintTableAttributes(PaintTarget target, int rows, int total) + throws PaintException { + paintTabIndex(target); + paintDragMode(target); + paintSelectMode(target); + + if (cacheRate != CACHE_RATE_DEFAULT) { + target.addAttribute("cr", cacheRate); + } + + target.addAttribute("cols", getVisibleColumns().length); + target.addAttribute("rows", rows); + + target.addAttribute("firstrow", + (reqFirstRowToPaint >= 0 ? reqFirstRowToPaint + : firstToBeRenderedInClient)); + target.addAttribute("totalrows", total); + if (getPageLength() != 0) { + target.addAttribute("pagelength", getPageLength()); + } + if (areColumnHeadersEnabled()) { + target.addAttribute("colheaders", true); + } + if (rowHeadersAreEnabled()) { + target.addAttribute("rowheaders", true); + } + + target.addAttribute("colfooters", columnFootersVisible); + + // The cursors are only shown on pageable table + if (getCurrentPageFirstItemIndex() != 0 || getPageLength() > 0) { + target.addVariable(this, "firstvisible", + getCurrentPageFirstItemIndex()); + } + } + + /** + * Resets and paints "to be painted next" variables. Also reset pageBuffer + */ + private void resetVariablesAndPageBuffer(PaintTarget target) + throws PaintException { + reqFirstRowToPaint = -1; + reqRowsToPaint = -1; + containerChangeToBeRendered = false; + target.addVariable(this, "reqrows", reqRowsToPaint); + target.addVariable(this, "reqfirstrow", reqFirstRowToPaint); + } + + private boolean areColumnHeadersEnabled() { + return getColumnHeaderMode() != ColumnHeaderMode.HIDDEN; + } + + private void paintVisibleColumns(PaintTarget target) throws PaintException { + target.startTag("visiblecolumns"); + if (rowHeadersAreEnabled()) { + target.startTag("column"); + target.addAttribute("cid", ROW_HEADER_COLUMN_KEY); + paintColumnWidth(target, ROW_HEADER_FAKE_PROPERTY_ID); + target.endTag("column"); + } + final Collection<?> sortables = getSortableContainerPropertyIds(); + for (Object colId : visibleColumns) { + if (colId != null) { + target.startTag("column"); + target.addAttribute("cid", columnIdMap.key(colId)); + final String head = getColumnHeader(colId); + target.addAttribute("caption", (head != null ? head : "")); + final String foot = getColumnFooter(colId); + target.addAttribute("fcaption", (foot != null ? foot : "")); + if (isColumnCollapsed(colId)) { + target.addAttribute("collapsed", true); + } + if (areColumnHeadersEnabled()) { + if (getColumnIcon(colId) != null) { + target.addAttribute("icon", getColumnIcon(colId)); + } + if (sortables.contains(colId)) { + target.addAttribute("sortable", true); + } + } + if (!Align.LEFT.equals(getColumnAlignment(colId))) { + target.addAttribute("align", getColumnAlignment(colId) + .toString()); + } + paintColumnWidth(target, colId); + target.endTag("column"); + } + } + target.endTag("visiblecolumns"); + } + + private void paintAvailableColumns(PaintTarget target) + throws PaintException { + if (columnCollapsingAllowed) { + final HashSet<Object> collapsedCols = new HashSet<Object>(); + for (Object colId : visibleColumns) { + if (isColumnCollapsed(colId)) { + collapsedCols.add(colId); + } + } + final String[] collapsedKeys = new String[collapsedCols.size()]; + int nextColumn = 0; + for (Object colId : visibleColumns) { + if (isColumnCollapsed(colId)) { + collapsedKeys[nextColumn++] = columnIdMap.key(colId); + } + } + target.addVariable(this, "collapsedcolumns", collapsedKeys); + + final String[] noncollapsibleKeys = new String[noncollapsibleColumns + .size()]; + nextColumn = 0; + for (Object colId : noncollapsibleColumns) { + noncollapsibleKeys[nextColumn++] = columnIdMap.key(colId); + } + target.addVariable(this, "noncollapsiblecolumns", + noncollapsibleKeys); + } + + } + + private void paintActions(PaintTarget target, final Set<Action> actionSet) + throws PaintException { + if (!actionSet.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + for (Action a : actionSet) { + target.startTag("action"); + if (a.getCaption() != null) { + target.addAttribute("caption", a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute("icon", a.getIcon()); + } + target.addAttribute("key", actionMapper.key(a)); + target.endTag("action"); + } + target.endTag("actions"); + } + } + + private void paintColumnOrder(PaintTarget target) throws PaintException { + if (columnReorderingAllowed) { + final String[] colorder = new String[visibleColumns.size()]; + int i = 0; + for (Object colId : visibleColumns) { + colorder[i++] = columnIdMap.key(colId); + } + target.addVariable(this, "columnorder", colorder); + } + } + + private void paintSorting(PaintTarget target) throws PaintException { + // Sorting + if (getContainerDataSource() instanceof Container.Sortable) { + target.addVariable(this, "sortcolumn", + columnIdMap.key(sortContainerPropertyId)); + target.addVariable(this, "sortascending", sortAscending); + } + } + + private void paintRows(PaintTarget target, final Object[][] cells, + final Set<Action> actionSet) throws PaintException { + final boolean[] iscomponent = findCellsWithComponents(); + + target.startTag("rows"); + // cells array contains all that are supposed to be visible on client, + // but we'll start from the one requested by client + int start = 0; + if (reqFirstRowToPaint != -1 && firstToBeRenderedInClient != -1) { + start = reqFirstRowToPaint - firstToBeRenderedInClient; + } + int end = cells[0].length; + if (reqRowsToPaint != -1) { + end = start + reqRowsToPaint; + } + // sanity check + if (lastToBeRenderedInClient != -1 && lastToBeRenderedInClient < end) { + end = lastToBeRenderedInClient + 1; + } + if (start > cells[CELL_ITEMID].length || start < 0) { + start = 0; + } + if (end > cells[CELL_ITEMID].length) { + end = cells[CELL_ITEMID].length; + } + + for (int indexInRowbuffer = start; indexInRowbuffer < end; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; + + if (shouldHideNullSelectionItem()) { + // Remove null selection item if null selection is not allowed + continue; + } + + paintRow(target, cells, isEditable(), actionSet, iscomponent, + indexInRowbuffer, itemId); + } + target.endTag("rows"); + } + + private boolean[] findCellsWithComponents() { + final boolean[] isComponent = new boolean[visibleColumns.size()]; + int ix = 0; + for (Object columnId : visibleColumns) { + if (columnGenerators.containsKey(columnId)) { + isComponent[ix++] = true; + } else { + final Class<?> colType = getType(columnId); + isComponent[ix++] = colType != null + && Component.class.isAssignableFrom(colType); + } + } + return isComponent; + } + + private void paintVisibleColumnOrder(PaintTarget target) { + // Visible column order + final ArrayList<String> visibleColOrder = new ArrayList<String>(); + for (Object columnId : visibleColumns) { + if (!isColumnCollapsed(columnId)) { + visibleColOrder.add(columnIdMap.key(columnId)); + } + } + target.addAttribute("vcolorder", visibleColOrder.toArray()); + } + + private Set<Action> findAndPaintBodyActions(PaintTarget target) { + Set<Action> actionSet = new LinkedHashSet<Action>(); + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + // Getting actions for the null item, which in this case means + // the body item + final Action[] actions = ah.getActions(null, this); + if (actions != null) { + for (Action action : actions) { + actionSet.add(action); + keys.add(actionMapper.key(action)); + } + } + } + target.addAttribute("alb", keys.toArray()); + } + return actionSet; + } + + private boolean shouldHideNullSelectionItem() { + return !isNullSelectionAllowed() && getNullSelectionItemId() != null + && containsId(getNullSelectionItemId()); + } + + private int findNumRowsToPaint(PaintTarget target, final Object[][] cells) + throws PaintException { + int rows; + if (reqRowsToPaint >= 0) { + rows = reqRowsToPaint; + } else { + rows = cells[0].length; + if (alwaysRecalculateColumnWidths) { + // TODO experimental feature for now: tell the client to + // recalculate column widths. + // We'll only do this for paints that do not originate from + // table scroll/cache requests (i.e when reqRowsToPaint<0) + target.addAttribute("recalcWidths", true); + } + } + return rows; + } + + private void paintSelectMode(PaintTarget target) throws PaintException { + if (multiSelectMode != MultiSelectMode.DEFAULT) { + target.addAttribute("multiselectmode", multiSelectMode.ordinal()); + } + if (isSelectable()) { + target.addAttribute("selectmode", (isMultiSelect() ? "multi" + : "single")); + } else { + target.addAttribute("selectmode", "none"); + } + if (!isNullSelectionAllowed()) { + target.addAttribute("nsa", false); + } + + // selection support + // The select variable is only enabled if selectable + if (isSelectable()) { + target.addVariable(this, "selected", findSelectedKeys()); + } + } + + private String[] findSelectedKeys() { + LinkedList<String> selectedKeys = new LinkedList<String>(); + if (isMultiSelect()) { + HashSet<?> sel = new HashSet<Object>((Set<?>) getValue()); + Collection<?> vids = getVisibleItemIds(); + for (Iterator<?> it = vids.iterator(); it.hasNext();) { + Object id = it.next(); + if (sel.contains(id)) { + selectedKeys.add(itemIdMapper.key(id)); + } + } + } else { + Object value = getValue(); + if (value == null) { + value = getNullSelectionItemId(); + } + if (value != null) { + selectedKeys.add(itemIdMapper.key(value)); + } + } + return selectedKeys.toArray(new String[selectedKeys.size()]); + } + + private void paintDragMode(PaintTarget target) throws PaintException { + if (dragMode != TableDragMode.NONE) { + target.addAttribute("dragmode", dragMode.ordinal()); + } + } + + private void paintTabIndex(PaintTarget target) throws PaintException { + // The tab ordering number + if (getTabIndex() > 0) { + target.addAttribute("tabindex", getTabIndex()); + } + } + + private void paintColumnWidth(PaintTarget target, final Object columnId) + throws PaintException { + if (columnWidths.containsKey(columnId)) { + if (getColumnWidth(columnId) > -1) { + target.addAttribute("width", + String.valueOf(getColumnWidth(columnId))); + } else { + target.addAttribute("er", getColumnExpandRatio(columnId)); + } + } + } + + private boolean rowHeadersAreEnabled() { + return getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN; + } + + private void paintRow(PaintTarget target, final Object[][] cells, + final boolean iseditable, final Set<Action> actionSet, + final boolean[] iscomponent, int indexInRowbuffer, + final Object itemId) throws PaintException { + target.startTag("tr"); + + paintRowAttributes(target, cells, actionSet, indexInRowbuffer, itemId); + + // cells + int currentColumn = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); currentColumn++) { + final Object columnId = it.next(); + if (columnId == null || isColumnCollapsed(columnId)) { + continue; + } + /* + * For each cell, if a cellStyleGenerator is specified, get the + * specific style for the cell. If there is any, add it to the + * target. + */ + if (cellStyleGenerator != null) { + String cellStyle = cellStyleGenerator + .getStyle(itemId, columnId); + if (cellStyle != null && !cellStyle.equals("")) { + target.addAttribute("style-" + columnIdMap.key(columnId), + cellStyle); + } + } + + if ((iscomponent[currentColumn] || iseditable || cells[CELL_GENERATED_ROW][indexInRowbuffer] != null) + && Component.class.isInstance(cells[CELL_FIRSTCOL + + currentColumn][indexInRowbuffer])) { + final Component c = (Component) cells[CELL_FIRSTCOL + + currentColumn][indexInRowbuffer]; + if (c == null) { + target.addText(""); + paintCellTooltips(target, itemId, columnId); + } else { + LegacyPaint.paint(c, target); + } + } else { + target.addText((String) cells[CELL_FIRSTCOL + currentColumn][indexInRowbuffer]); + paintCellTooltips(target, itemId, columnId); + } + } + + target.endTag("tr"); + } + + private void paintCellTooltips(PaintTarget target, Object itemId, + Object columnId) throws PaintException { + if (itemDescriptionGenerator != null) { + String itemDescription = itemDescriptionGenerator + .generateDescription(this, itemId, columnId); + if (itemDescription != null && !itemDescription.equals("")) { + target.addAttribute("descr-" + columnIdMap.key(columnId), + itemDescription); + } + } + } + + private void paintRowTooltips(PaintTarget target, Object itemId) + throws PaintException { + if (itemDescriptionGenerator != null) { + String rowDescription = itemDescriptionGenerator + .generateDescription(this, itemId, null); + if (rowDescription != null && !rowDescription.equals("")) { + target.addAttribute("rowdescr", rowDescription); + } + } + } + + private void paintRowAttributes(PaintTarget target, final Object[][] cells, + final Set<Action> actionSet, int indexInRowbuffer, + final Object itemId) throws PaintException { + // tr attributes + + paintRowIcon(target, cells, indexInRowbuffer); + paintRowHeader(target, cells, indexInRowbuffer); + paintGeneratedRowInfo(target, cells, indexInRowbuffer); + target.addAttribute("key", + Integer.parseInt(cells[CELL_KEY][indexInRowbuffer].toString())); + + if (isSelected(itemId)) { + target.addAttribute("selected", true); + } + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + final Action[] aa = ah.getActions(itemId, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String key = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(key); + } + } + } + target.addAttribute("al", keys.toArray()); + } + + /* + * For each row, if a cellStyleGenerator is specified, get the specific + * style for the cell, using null as propertyId. If there is any, add it + * to the target. + */ + if (cellStyleGenerator != null) { + String rowStyle = cellStyleGenerator.getStyle(itemId, null); + if (rowStyle != null && !rowStyle.equals("")) { + target.addAttribute("rowstyle", rowStyle); + } + } + + paintRowTooltips(target, itemId); + + paintRowAttributes(target, itemId); + } + + private void paintGeneratedRowInfo(PaintTarget target, Object[][] cells, + int indexInRowBuffer) throws PaintException { + GeneratedRow generatedRow = (GeneratedRow) cells[CELL_GENERATED_ROW][indexInRowBuffer]; + if (generatedRow != null) { + target.addAttribute("gen_html", generatedRow.isHtmlContentAllowed()); + target.addAttribute("gen_span", generatedRow.isSpanColumns()); + target.addAttribute("gen_widget", + generatedRow.getValue() instanceof Component); + } + } + + protected void paintRowHeader(PaintTarget target, Object[][] cells, + int indexInRowbuffer) throws PaintException { + if (rowHeadersAreEnabled()) { + if (cells[CELL_HEADER][indexInRowbuffer] != null) { + target.addAttribute("caption", + (String) cells[CELL_HEADER][indexInRowbuffer]); + } + } + + } + + protected void paintRowIcon(PaintTarget target, final Object[][] cells, + int indexInRowbuffer) throws PaintException { + if (rowHeadersAreEnabled() + && cells[CELL_ICON][indexInRowbuffer] != null) { + target.addAttribute("icon", + (Resource) cells[CELL_ICON][indexInRowbuffer]); + } + } + + /** + * A method where extended Table implementations may add their custom + * attributes for rows. + * + * @param target + * @param itemId + */ + protected void paintRowAttributes(PaintTarget target, Object itemId) + throws PaintException { + + } + + /** + * Gets the cached visible table contents. + * + * @return the cached visible table contents. + */ + private Object[][] getVisibleCells() { + if (pageBuffer == null) { + refreshRenderedCells(); + } + return pageBuffer; + } + + /** + * Gets the value of property. + * + * By default if the table is editable the fieldFactory is used to create + * editors for table cells. Otherwise formatPropertyValue is used to format + * the value representation. + * + * @param rowId + * the Id of the row (same as item Id). + * @param colId + * the Id of the column. + * @param property + * the Property to be presented. + * @return Object Either formatted value or Component for field. + * @see #setTableFieldFactory(TableFieldFactory) + */ + protected Object getPropertyValue(Object rowId, Object colId, + Property property) { + if (isEditable() && fieldFactory != null) { + final Field<?> f = fieldFactory.createField( + getContainerDataSource(), rowId, colId, this); + if (f != null) { + // Remember that we have made this association so we can remove + // it when the component is removed + associatedProperties.put(f, property); + bindPropertyToField(rowId, colId, property, f); + return f; + } + } + + return formatPropertyValue(rowId, colId, property); + } + + /** + * Binds an item property to a field generated by TableFieldFactory. The + * default behavior is to bind property straight to Field. If + * Property.Viewer type property (e.g. PropertyFormatter) is already set for + * field, the property is bound to that Property.Viewer. + * + * @param rowId + * @param colId + * @param property + * @param field + * @since 6.7.3 + */ + protected void bindPropertyToField(Object rowId, Object colId, + Property property, Field field) { + // check if field has a property that is Viewer set. In that case we + // expect developer has e.g. PropertyFormatter that he wishes to use and + // assign the property to the Viewer instead. + boolean hasFilterProperty = field.getPropertyDataSource() != null + && (field.getPropertyDataSource() instanceof Property.Viewer); + if (hasFilterProperty) { + ((Property.Viewer) field.getPropertyDataSource()) + .setPropertyDataSource(property); + } else { + field.setPropertyDataSource(property); + } + } + + /** + * Formats table cell property values. By default the property.toString() + * and return a empty string for null properties. + * + * @param rowId + * the Id of the row (same as item Id). + * @param colId + * the Id of the column. + * @param property + * the Property to be formatted. + * @return the String representation of property and its value. + * @since 3.1 + */ + protected String formatPropertyValue(Object rowId, Object colId, + Property<?> property) { + if (property == null) { + return ""; + } + Converter<String, Object> converter = null; + + if (hasConverter(colId)) { + converter = getConverter(colId); + } else { + ConverterUtil.getConverter(String.class, property.getType(), + getApplication()); + } + Object value = property.getValue(); + if (converter != null) { + return converter.convertToPresentation(value, getLocale()); + } + return (null != value) ? value.toString() : ""; + } + + /* Action container */ + + /** + * Registers a new action handler for this container + * + * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler) + */ + + @Override + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Handler>(); + actionMapper = new KeyMapper<Action>(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the action + // handlers. + refreshRenderedCells(); + } + + } + } + + /** + * Removes a previously registered action handler for the contents of this + * container. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + + @Override + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the action + // handlers. + refreshRenderedCells(); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + actionHandlers = null; + actionMapper = null; + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the action + // handlers. + refreshRenderedCells(); + } + + /* Property value change listening support */ + + /** + * Notifies this listener that the Property's value has changed. + * + * Also listens changes in rendered items to refresh content area. + * + * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + + @Override + public void valueChange(Property.ValueChangeEvent event) { + if (event.getProperty() == this + || event.getProperty() == getPropertyDataSource()) { + super.valueChange(event); + } else { + refreshRowCache(); + containerChangeToBeRendered = true; + } + requestRepaint(); + } + + /** + * Clears the current page buffer. Call this before + * {@link #refreshRenderedCells()} to ensure that all content is updated + * from the properties. + */ + protected void resetPageBuffer() { + firstToBeRenderedInClient = -1; + lastToBeRenderedInClient = -1; + reqFirstRowToPaint = -1; + reqRowsToPaint = -1; + pageBuffer = null; + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.Component#attach() + */ + + @Override + public void attach() { + super.attach(); + + refreshRenderedCells(); + } + + /** + * Notifies the component that it is detached from the application + * + * @see com.vaadin.ui.Component#detach() + */ + + @Override + public void detach() { + super.detach(); + } + + /** + * Removes all Items from the Container. + * + * @see com.vaadin.data.Container#removeAllItems() + */ + + @Override + public boolean removeAllItems() { + currentPageFirstItemId = null; + currentPageFirstItemIndex = 0; + return super.removeAllItems(); + } + + /** + * Removes the Item identified by <code>ItemId</code> from the Container. + * + * @see com.vaadin.data.Container#removeItem(Object) + */ + + @Override + public boolean removeItem(Object itemId) { + final Object nextItemId = nextItemId(itemId); + final boolean ret = super.removeItem(itemId); + if (ret && (itemId != null) && (itemId.equals(currentPageFirstItemId))) { + currentPageFirstItemId = nextItemId; + } + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + return ret; + } + + /** + * Removes a Property specified by the given Property ID from the Container. + * + * @see com.vaadin.data.Container#removeContainerProperty(Object) + */ + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + + // If a visible property is removed, remove the corresponding column + visibleColumns.remove(propertyId); + columnAlignments.remove(propertyId); + columnIcons.remove(propertyId); + columnHeaders.remove(propertyId); + columnFooters.remove(propertyId); + + return super.removeContainerProperty(propertyId); + } + + /** + * Adds a new property to the table and show it as a visible column. + * + * @param propertyId + * the Id of the proprty. + * @param type + * the class of the property. + * @param defaultValue + * the default value given for all existing items. + * @see com.vaadin.data.Container#addContainerProperty(Object, Class, + * Object) + */ + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + + boolean visibleColAdded = false; + if (!visibleColumns.contains(propertyId)) { + visibleColumns.add(propertyId); + visibleColAdded = true; + } + + if (!super.addContainerProperty(propertyId, type, defaultValue)) { + if (visibleColAdded) { + visibleColumns.remove(propertyId); + } + return false; + } + if (!(items instanceof Container.PropertySetChangeNotifier)) { + refreshRowCache(); + } + return true; + } + + /** + * Adds a new property to the table and show it as a visible column. + * + * @param propertyId + * the Id of the proprty + * @param type + * the class of the property + * @param defaultValue + * the default value given for all existing items + * @param columnHeader + * the Explicit header of the column. If explicit header is not + * needed, this should be set null. + * @param columnIcon + * the Icon of the column. If icon is not needed, this should be + * set null. + * @param columnAlignment + * the Alignment of the column. Null implies align left. + * @throws UnsupportedOperationException + * if the operation is not supported. + * @see com.vaadin.data.Container#addContainerProperty(Object, Class, + * Object) + */ + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue, String columnHeader, Resource columnIcon, + Align columnAlignment) throws UnsupportedOperationException { + if (!this.addContainerProperty(propertyId, type, defaultValue)) { + return false; + } + setColumnAlignment(propertyId, columnAlignment); + setColumnHeader(propertyId, columnHeader); + setColumnIcon(propertyId, columnIcon); + return true; + } + + /** + * Adds a generated column to the Table. + * <p> + * A generated column is a column that exists only in the Table, not as a + * property in the underlying Container. It shows up just as a regular + * column. + * </p> + * <p> + * A generated column will override a property with the same id, so that the + * generated column is shown instead of the column representing the + * property. Note that getContainerProperty() will still get the real + * property. + * </p> + * <p> + * Table will not listen to value change events from properties overridden + * by generated columns. If the content of your generated column depends on + * properties that are not directly visible in the table, attach value + * change listener to update the content on all depended properties. + * Otherwise your UI might not get updated as expected. + * </p> + * <p> + * Also note that getVisibleColumns() will return the generated columns, + * while getContainerPropertyIds() will not. + * </p> + * + * @param id + * the id of the column to be added + * @param generatedColumn + * the {@link ColumnGenerator} to use for this column + */ + public void addGeneratedColumn(Object id, ColumnGenerator generatedColumn) { + if (generatedColumn == null) { + throw new IllegalArgumentException( + "Can not add null as a GeneratedColumn"); + } + if (columnGenerators.containsKey(id)) { + throw new IllegalArgumentException( + "Can not add the same GeneratedColumn twice, id:" + id); + } else { + columnGenerators.put(id, generatedColumn); + /* + * add to visible column list unless already there (overriding + * column from DS) + */ + if (!visibleColumns.contains(id)) { + visibleColumns.add(id); + } + refreshRowCache(); + } + } + + /** + * Returns the ColumnGenerator used to generate the given column. + * + * @param columnId + * The id of the generated column + * @return The ColumnGenerator used for the given columnId or null. + */ + public ColumnGenerator getColumnGenerator(Object columnId) + throws IllegalArgumentException { + return columnGenerators.get(columnId); + } + + /** + * Removes a generated column previously added with addGeneratedColumn. + * + * @param columnId + * id of the generated column to remove + * @return true if the column could be removed (existed in the Table) + */ + public boolean removeGeneratedColumn(Object columnId) { + if (columnGenerators.containsKey(columnId)) { + columnGenerators.remove(columnId); + // remove column from visibleColumns list unless it exists in + // container (generator previously overrode this column) + if (!items.getContainerPropertyIds().contains(columnId)) { + visibleColumns.remove(columnId); + } + refreshRowCache(); + return true; + } else { + return false; + } + } + + /** + * Returns item identifiers of the items which are currently rendered on the + * client. + * <p> + * Note, that some due to historical reasons the name of the method is bit + * misleading. Some items may be partly or totally out of the viewport of + * the table's scrollable area. Actually detecting rows which can be + * actually seen by the end user may be problematic due to the client server + * architecture. Using {@link #getCurrentPageFirstItemId()} combined with + * {@link #getPageLength()} may produce good enough estimates in some + * situations. + * + * @see com.vaadin.ui.Select#getVisibleItemIds() + */ + + @Override + public Collection<?> getVisibleItemIds() { + + final LinkedList<Object> visible = new LinkedList<Object>(); + + final Object[][] cells = getVisibleCells(); + // may be null if the table has not been rendered yet (e.g. not attached + // to a layout) + if (null != cells) { + for (int i = 0; i < cells[CELL_ITEMID].length; i++) { + visible.add(cells[CELL_ITEMID][i]); + } + } + + return visible; + } + + /** + * Container datasource item set change. Table must flush its buffers on + * change. + * + * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent) + */ + + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + super.containerItemSetChange(event); + + // super method clears the key map, must inform client about this to + // avoid getting invalid keys back (#8584) + keyMapperReset = true; + + // ensure that page still has first item in page, ignore buffer refresh + // (forced in this method) + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false); + refreshRowCache(); + } + + /** + * Container datasource property set change. Table must flush its buffers on + * change. + * + * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent) + */ + + @Override + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + disableContentRefreshing(); + super.containerPropertySetChange(event); + + // sanitetize visibleColumns. note that we are not adding previously + // non-existing properties as columns + Collection<?> containerPropertyIds = getContainerDataSource() + .getContainerPropertyIds(); + + LinkedList<Object> newVisibleColumns = new LinkedList<Object>( + visibleColumns); + for (Iterator<Object> iterator = newVisibleColumns.iterator(); iterator + .hasNext();) { + Object id = iterator.next(); + if (!(containerPropertyIds.contains(id) || columnGenerators + .containsKey(id))) { + iterator.remove(); + } + } + setVisibleColumns(newVisibleColumns.toArray()); + // same for collapsed columns + for (Iterator<Object> iterator = collapsedColumns.iterator(); iterator + .hasNext();) { + Object id = iterator.next(); + if (!(containerPropertyIds.contains(id) || columnGenerators + .containsKey(id))) { + iterator.remove(); + } + } + + resetPageBuffer(); + enableContentRefreshing(true); + } + + /** + * Adding new items is not supported. + * + * @throws UnsupportedOperationException + * if set to true. + * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean) + */ + + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) { + throw new UnsupportedOperationException(); + } + } + + /** + * Gets the ID of the Item following the Item that corresponds to itemId. + * + * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) + */ + + @Override + public Object nextItemId(Object itemId) { + return ((Container.Ordered) items).nextItemId(itemId); + } + + /** + * Gets the ID of the Item preceding the Item that corresponds to the + * itemId. + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ + + @Override + public Object prevItemId(Object itemId) { + return ((Container.Ordered) items).prevItemId(itemId); + } + + /** + * Gets the ID of the first Item in the Container. + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ + + @Override + public Object firstItemId() { + return ((Container.Ordered) items).firstItemId(); + } + + /** + * Gets the ID of the last Item in the Container. + * + * @see com.vaadin.data.Container.Ordered#lastItemId() + */ + + @Override + public Object lastItemId() { + return ((Container.Ordered) items).lastItemId(); + } + + /** + * Tests if the Item corresponding to the given Item ID is the first Item in + * the Container. + * + * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object) + */ + + @Override + public boolean isFirstId(Object itemId) { + return ((Container.Ordered) items).isFirstId(itemId); + } + + /** + * Tests if the Item corresponding to the given Item ID is the last Item in + * the Container. + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ + + @Override + public boolean isLastId(Object itemId) { + return ((Container.Ordered) items).isLastId(itemId); + } + + /** + * Adds new item after the given item. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + Object itemId = ((Container.Ordered) items) + .addItemAfter(previousItemId); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + return itemId; + } + + /** + * Adds new item after the given item. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + Item item = ((Container.Ordered) items).addItemAfter(previousItemId, + newItemId); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + return item; + } + + /** + * Sets the TableFieldFactory that is used to create editor for table cells. + * + * The TableFieldFactory is only used if the Table is editable. By default + * the DefaultFieldFactory is used. + * + * @param fieldFactory + * the field factory to set. + * @see #isEditable + * @see DefaultFieldFactory + */ + public void setTableFieldFactory(TableFieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + + // Assure visual refresh + refreshRowCache(); + } + + /** + * Gets the TableFieldFactory that is used to create editor for table cells. + * + * The FieldFactory is only used if the Table is editable. + * + * @return TableFieldFactory used to create the Field instances. + * @see #isEditable + */ + public TableFieldFactory getTableFieldFactory() { + return fieldFactory; + } + + /** + * Is table editable. + * + * If table is editable a editor of type Field is created for each table + * cell. The assigned FieldFactory is used to create the instances. + * + * To provide custom editors for table cells create a class implementins the + * FieldFactory interface, and assign it to table, and set the editable + * property to true. + * + * @return true if table is editable, false oterwise. + * @see Field + * @see FieldFactory + * + */ + public boolean isEditable() { + return editable; + } + + /** + * Sets the editable property. + * + * If table is editable a editor of type Field is created for each table + * cell. The assigned FieldFactory is used to create the instances. + * + * To provide custom editors for table cells create a class implementins the + * FieldFactory interface, and assign it to table, and set the editable + * property to true. + * + * @param editable + * true if table should be editable by user. + * @see Field + * @see FieldFactory + * + */ + public void setEditable(boolean editable) { + this.editable = editable; + + // Assure visual refresh + refreshRowCache(); + } + + /** + * Sorts the table. + * + * @throws UnsupportedOperationException + * if the container data source does not implement + * Container.Sortable + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + * + */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) + throws UnsupportedOperationException { + final Container c = getContainerDataSource(); + if (c instanceof Container.Sortable) { + final int pageIndex = getCurrentPageFirstItemIndex(); + ((Container.Sortable) c).sort(propertyId, ascending); + setCurrentPageFirstItemIndex(pageIndex); + refreshRowCache(); + + } else if (c != null) { + throw new UnsupportedOperationException( + "Underlying Data does not allow sorting"); + } + } + + /** + * Sorts the table by currently selected sorting column. + * + * @throws UnsupportedOperationException + * if the container data source does not implement + * Container.Sortable + */ + public void sort() { + if (getSortContainerPropertyId() == null) { + return; + } + sort(new Object[] { sortContainerPropertyId }, + new boolean[] { sortAscending }); + } + + /** + * Gets the container property IDs, which can be used to sort the item. + * <p> + * Note that the {@link #isSortEnabled()} state affects what this method + * returns. Disabling sorting causes this method to always return an empty + * collection. + * </p> + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + + @Override + public Collection<?> getSortableContainerPropertyIds() { + final Container c = getContainerDataSource(); + if (c instanceof Container.Sortable && isSortEnabled()) { + return ((Container.Sortable) c).getSortableContainerPropertyIds(); + } else { + return Collections.EMPTY_LIST; + } + } + + /** + * Gets the currently sorted column property ID. + * + * @return the Container property id of the currently sorted column. + */ + public Object getSortContainerPropertyId() { + return sortContainerPropertyId; + } + + /** + * Sets the currently sorted column property id. + * + * @param propertyId + * the Container property id of the currently sorted column. + */ + public void setSortContainerPropertyId(Object propertyId) { + setSortContainerPropertyId(propertyId, true); + } + + /** + * Internal method to set currently sorted column property id. With doSort + * flag actual sorting may be bypassed. + * + * @param propertyId + * @param doSort + */ + private void setSortContainerPropertyId(Object propertyId, boolean doSort) { + if ((sortContainerPropertyId != null && !sortContainerPropertyId + .equals(propertyId)) + || (sortContainerPropertyId == null && propertyId != null)) { + sortContainerPropertyId = propertyId; + + if (doSort) { + sort(); + // Assures the visual refresh. This should not be necessary as + // sort() calls refreshRowCache + refreshRenderedCells(); + } + } + } + + /** + * Is the table currently sorted in ascending order. + * + * @return <code>true</code> if ascending, <code>false</code> if descending. + */ + public boolean isSortAscending() { + return sortAscending; + } + + /** + * Sets the table in ascending order. + * + * @param ascending + * <code>true</code> if ascending, <code>false</code> if + * descending. + */ + public void setSortAscending(boolean ascending) { + setSortAscending(ascending, true); + } + + /** + * Internal method to set sort ascending. With doSort flag actual sort can + * be bypassed. + * + * @param ascending + * @param doSort + */ + private void setSortAscending(boolean ascending, boolean doSort) { + if (sortAscending != ascending) { + sortAscending = ascending; + if (doSort) { + sort(); + // Assures the visual refresh. This should not be necessary as + // sort() calls refreshRowCache + refreshRenderedCells(); + } + } + } + + /** + * Is sorting disabled altogether. + * + * True iff no sortable columns are given even in the case where data source + * would support this. + * + * @return True iff sorting is disabled. + * @deprecated Use {@link #isSortEnabled()} instead + */ + @Deprecated + public boolean isSortDisabled() { + return !isSortEnabled(); + } + + /** + * Checks if sorting is enabled. + * + * @return true if sorting by the user is allowed, false otherwise + */ + public boolean isSortEnabled() { + return sortEnabled; + } + + /** + * Disables the sorting by the user altogether. + * + * @param sortDisabled + * True iff sorting is disabled. + * @deprecated Use {@link #setSortEnabled(boolean)} instead + */ + @Deprecated + public void setSortDisabled(boolean sortDisabled) { + setSortEnabled(!sortDisabled); + } + + /** + * Enables or disables sorting. + * <p> + * Setting this to false disallows sorting by the user. It is still possible + * to call {@link #sort()}. + * </p> + * + * @param sortEnabled + * true to allow the user to sort the table, false to disallow it + */ + public void setSortEnabled(boolean sortEnabled) { + if (this.sortEnabled != sortEnabled) { + this.sortEnabled = sortEnabled; + requestRepaint(); + } + } + + /** + * Used to create "generated columns"; columns that exist only in the Table, + * not in the underlying Container. Implement this interface and pass it to + * Table.addGeneratedColumn along with an id for the column to be generated. + * + */ + public interface ColumnGenerator extends Serializable { + + /** + * Called by Table when a cell in a generated column needs to be + * generated. + * + * @param source + * the source Table + * @param itemId + * the itemId (aka rowId) for the of the cell to be generated + * @param columnId + * the id for the generated column (as specified in + * addGeneratedColumn) + * @return A {@link Component} that should be rendered in the cell or a + * {@link String} that should be displayed in the cell. Other + * return values are not supported. + */ + public abstract Object generateCell(Table source, Object itemId, + Object columnId); + } + + /** + * Set cell style generator for Table. + * + * @param cellStyleGenerator + * New cell style generator or null to remove generator. + */ + public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) { + this.cellStyleGenerator = cellStyleGenerator; + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the style generators + refreshRenderedCells(); + + } + + /** + * Get the current cell style generator. + * + */ + public CellStyleGenerator getCellStyleGenerator() { + return cellStyleGenerator; + } + + /** + * Allow to define specific style on cells (and rows) contents. Implements + * this interface and pass it to Table.setCellStyleGenerator. Row styles are + * generated when porpertyId is null. The CSS class name that will be added + * to the cell content is <tt>v-table-cell-content-[style name]</tt>, and + * the row style will be <tt>v-table-row-[style name]</tt>. + */ + public interface CellStyleGenerator extends Serializable { + + /** + * Called by Table when a cell (and row) is painted. + * + * @param itemId + * The itemId of the painted cell + * @param propertyId + * The propertyId of the cell, null when getting row style + * @return The style name to add to this cell or row. (the CSS class + * name will be v-table-cell-content-[style name], or + * v-table-row-[style name] for rows) + */ + public abstract String getStyle(Object itemId, Object propertyId); + } + + @Override + public void addListener(ItemClickListener listener) { + addListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener, ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + public void removeListener(ItemClickListener listener) { + removeListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + // Identical to AbstractCompoenentContainer.setEnabled(); + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (getParent() != null && !getParent().isEnabled()) { + // some ancestor still disabled, don't update children + return; + } else { + requestRepaintAll(); + } + } + + /** + * Sets the drag start mode of the Table. Drag start mode controls how Table + * behaves as a drag source. + * + * @param newDragMode + */ + public void setDragMode(TableDragMode newDragMode) { + dragMode = newDragMode; + requestRepaint(); + } + + /** + * @return the current start mode of the Table. Drag start mode controls how + * Table behaves as a drag source. + */ + public TableDragMode getDragMode() { + return dragMode; + } + + /** + * Concrete implementation of {@link DataBoundTransferable} for data + * transferred from a table. + * + * @see {@link DataBoundTransferable}. + * + * @since 6.3 + */ + public class TableTransferable extends DataBoundTransferable { + + protected TableTransferable(Map<String, Object> rawVariables) { + super(Table.this, rawVariables); + Object object = rawVariables.get("itemId"); + if (object != null) { + setData("itemId", itemIdMapper.get((String) object)); + } + object = rawVariables.get("propertyId"); + if (object != null) { + setData("propertyId", columnIdMap.get((String) object)); + } + } + + @Override + public Object getItemId() { + return getData("itemId"); + } + + @Override + public Object getPropertyId() { + return getData("propertyId"); + } + + @Override + public Table getSourceComponent() { + return (Table) super.getSourceComponent(); + } + + } + + @Override + public TableTransferable getTransferable(Map<String, Object> rawVariables) { + TableTransferable transferable = new TableTransferable(rawVariables); + return transferable; + } + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + @Override + public AbstractSelectTargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new AbstractSelectTargetDetails(clientVariables); + } + + /** + * Sets the behavior of how the multi-select mode should behave when the + * table is both selectable and in multi-select mode. + * <p> + * Note, that on some clients the mode may not be respected. E.g. on touch + * based devices CTRL/SHIFT base selection method is invalid, so touch based + * browsers always use the {@link MultiSelectMode#SIMPLE}. + * + * @param mode + * The select mode of the table + */ + public void setMultiSelectMode(MultiSelectMode mode) { + multiSelectMode = mode; + requestRepaint(); + } + + /** + * Returns the select mode in which multi-select is used. + * + * @return The multi select mode + */ + public MultiSelectMode getMultiSelectMode() { + return multiSelectMode; + } + + /** + * Lazy loading accept criterion for Table. Accepted target rows are loaded + * from server once per drag and drop operation. Developer must override one + * method that decides on which rows the currently dragged data can be + * dropped. + * + * <p> + * Initially pretty much no data is sent to client. On first required + * criterion check (per drag request) the client side data structure is + * initialized from server and no subsequent requests requests are needed + * during that drag and drop operation. + */ + public static abstract class TableDropCriterion extends ServerSideCriterion { + + private Table table; + + private Set<Object> allowedItemIds; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.ServerSideCriterion#getIdentifier + * () + */ + + @Override + protected String getIdentifier() { + return TableDropCriterion.class.getCanonicalName(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#accepts(com.vaadin + * .event.dd.DragAndDropEvent) + */ + @Override + @SuppressWarnings("unchecked") + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + table = (Table) dragEvent.getTargetDetails().getTarget(); + Collection<?> visibleItemIds = table.getVisibleItemIds(); + allowedItemIds = getAllowedItemIds(dragEvent, table, + (Collection<Object>) visibleItemIds); + + return allowedItemIds.contains(dropTargetData.getItemIdOver()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#paintResponse( + * com.vaadin.terminal.PaintTarget) + */ + + @Override + public void paintResponse(PaintTarget target) throws PaintException { + /* + * send allowed nodes to client so subsequent requests can be + * avoided + */ + Object[] array = allowedItemIds.toArray(); + for (int i = 0; i < array.length; i++) { + String key = table.itemIdMapper.key(array[i]); + array[i] = key; + } + target.addAttribute("allowedIds", array); + } + + /** + * @param dragEvent + * @param table + * the table for which the allowed item identifiers are + * defined + * @param visibleItemIds + * the list of currently rendered item identifiers, accepted + * item id's need to be detected only for these visible items + * @return the set of identifiers for items on which the dragEvent will + * be accepted + */ + protected abstract Set<Object> getAllowedItemIds( + DragAndDropEvent dragEvent, Table table, + Collection<Object> visibleItemIds); + + } + + /** + * Click event fired when clicking on the Table headers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class HeaderClickEvent extends ClickEvent { + public static final Method HEADER_CLICK_METHOD; + + static { + try { + // Set the header click method + HEADER_CLICK_METHOD = HeaderClickListener.class + .getDeclaredMethod("headerClick", + new Class[] { HeaderClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + // The property id of the column which header was pressed + private final Object columnPropertyId; + + public HeaderClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source, details); + columnPropertyId = propertyId; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + } + + /** + * Click event fired when clicking on the Table footers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class FooterClickEvent extends ClickEvent { + public static final Method FOOTER_CLICK_METHOD; + + static { + try { + // Set the header click method + FOOTER_CLICK_METHOD = FooterClickListener.class + .getDeclaredMethod("footerClick", + new Class[] { FooterClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + // The property id of the column which header was pressed + private final Object columnPropertyId; + + /** + * Constructor + * + * @param source + * The source of the component + * @param propertyId + * The propertyId of the column + * @param details + * The mouse details of the click + */ + public FooterClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source, details); + columnPropertyId = propertyId; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + } + + /** + * Interface for the listener for column header mouse click events. The + * headerClick method is called when the user presses a header column cell. + */ + public interface HeaderClickListener extends Serializable { + + /** + * Called when a user clicks a header column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void headerClick(HeaderClickEvent event); + } + + /** + * Interface for the listener for column footer mouse click events. The + * footerClick method is called when the user presses a footer column cell. + */ + public interface FooterClickListener extends Serializable { + + /** + * Called when a user clicks a footer column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void footerClick(FooterClickEvent event); + } + + /** + * Adds a header click listener which handles the click events when the user + * clicks on a column header cell in the Table. + * <p> + * The listener will receive events which contain information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param listener + * The handler which should handle the header click events. + */ + public void addListener(HeaderClickListener listener) { + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, HeaderClickEvent.class, + listener, HeaderClickEvent.HEADER_CLICK_METHOD); + } + + /** + * Removes a header click listener + * + * @param listener + * The listener to remove. + */ + public void removeListener(HeaderClickListener listener) { + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, listener); + } + + /** + * Adds a footer click listener which handles the click events when the user + * clicks on a column footer cell in the Table. + * <p> + * The listener will receive events which contain information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param listener + * The handler which should handle the footer click events. + */ + public void addListener(FooterClickListener listener) { + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, FooterClickEvent.class, + listener, FooterClickEvent.FOOTER_CLICK_METHOD); + } + + /** + * Removes a footer click listener + * + * @param listener + * The listener to remove. + */ + public void removeListener(FooterClickListener listener) { + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, listener); + } + + /** + * Gets the footer caption beneath the rows + * + * @param propertyId + * The propertyId of the column * + * @return The caption of the footer or NULL if not set + */ + public String getColumnFooter(Object propertyId) { + return columnFooters.get(propertyId); + } + + /** + * Sets the column footer caption. The column footer caption is the text + * displayed beneath the column if footers have been set visible. + * + * @param propertyId + * The properyId of the column + * + * @param footer + * The caption of the footer + */ + public void setColumnFooter(Object propertyId, String footer) { + if (footer == null) { + columnFooters.remove(propertyId); + } else { + columnFooters.put(propertyId, footer); + } + + requestRepaint(); + } + + /** + * Sets the footer visible in the bottom of the table. + * <p> + * The footer can be used to add column related data like sums to the bottom + * of the Table using setColumnFooter(Object propertyId, String footer). + * </p> + * + * @param visible + * Should the footer be visible + */ + public void setFooterVisible(boolean visible) { + if (visible != columnFootersVisible) { + columnFootersVisible = visible; + requestRepaint(); + } + } + + /** + * Is the footer currently visible? + * + * @return Returns true if visible else false + */ + public boolean isFooterVisible() { + return columnFootersVisible; + } + + /** + * This event is fired when a column is resized. The event contains the + * columns property id which was fired, the previous width of the column and + * the width of the column after the resize. + */ + public static class ColumnResizeEvent extends Component.Event { + public static final Method COLUMN_RESIZE_METHOD; + + static { + try { + COLUMN_RESIZE_METHOD = ColumnResizeListener.class + .getDeclaredMethod("columnResize", + new Class[] { ColumnResizeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + private final int previousWidth; + private final int currentWidth; + private final Object columnPropertyId; + + /** + * Constructor + * + * @param source + * The source of the event + * @param propertyId + * The columns property id + * @param previous + * The width in pixels of the column before the resize event + * @param current + * The width in pixels of the column after the resize event + */ + public ColumnResizeEvent(Component source, Object propertyId, + int previous, int current) { + super(source); + previousWidth = previous; + currentWidth = current; + columnPropertyId = propertyId; + } + + /** + * Get the column property id of the column that was resized. + * + * @return The column property id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Get the width in pixels of the column before the resize event + * + * @return Width in pixels + */ + public int getPreviousWidth() { + return previousWidth; + } + + /** + * Get the width in pixels of the column after the resize event + * + * @return Width in pixels + */ + public int getCurrentWidth() { + return currentWidth; + } + } + + /** + * Interface for listening to column resize events. + */ + public interface ColumnResizeListener extends Serializable { + + /** + * This method is triggered when the column has been resized + * + * @param event + * The event which contains the column property id, the + * previous width of the column and the current width of the + * column + */ + public void columnResize(ColumnResizeEvent event); + } + + /** + * Adds a column resize listener to the Table. A column resize listener is + * called when a user resizes a columns width. + * + * @param listener + * The listener to attach to the Table + */ + public void addListener(ColumnResizeListener listener) { + addListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener, + ColumnResizeEvent.COLUMN_RESIZE_METHOD); + } + + /** + * Removes a column resize listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnResizeListener listener) { + removeListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener); + } + + /** + * This event is fired when a columns are reordered by the end user user. + */ + public static class ColumnReorderEvent extends Component.Event { + public static final Method METHOD; + + static { + try { + METHOD = ColumnReorderListener.class.getDeclaredMethod( + "columnReorder", + new Class[] { ColumnReorderEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + /** + * Constructor + * + * @param source + * The source of the event + */ + public ColumnReorderEvent(Component source) { + super(source); + } + + } + + /** + * Interface for listening to column reorder events. + */ + public interface ColumnReorderListener extends Serializable { + + /** + * This method is triggered when the column has been reordered + * + * @param event + */ + public void columnReorder(ColumnReorderEvent event); + } + + /** + * Adds a column reorder listener to the Table. A column reorder listener is + * called when a user reorders columns. + * + * @param listener + * The listener to attach to the Table + */ + public void addListener(ColumnReorderListener listener) { + addListener(VScrollTable.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD); + } + + /** + * Removes a column reorder listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnReorderListener listener) { + removeListener(VScrollTable.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener); + } + + /** + * Set the item description generator which generates tooltips for cells and + * rows in the Table + * + * @param generator + * The generator to use or null to disable + */ + public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) { + if (generator != itemDescriptionGenerator) { + itemDescriptionGenerator = generator; + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the descriptions + refreshRenderedCells(); + } + } + + /** + * Get the item description generator which generates tooltips for cells and + * rows in the Table. + */ + public ItemDescriptionGenerator getItemDescriptionGenerator() { + return itemDescriptionGenerator; + } + + /** + * Row generators can be used to replace certain items in a table with a + * generated string. The generator is called each time the table is + * rendered, which means that new strings can be generated each time. + * + * Row generators can be used for e.g. summary rows or grouping of items. + */ + public interface RowGenerator extends Serializable { + /** + * Called for every row that is painted in the Table. Returning a + * GeneratedRow object will cause the row to be painted based on the + * contents of the GeneratedRow. A generated row is by default styled + * similarly to a header or footer row. + * <p> + * The GeneratedRow data object contains the text that should be + * rendered in the row. The itemId in the container thus works only as a + * placeholder. + * <p> + * If GeneratedRow.setSpanColumns(true) is used, there will be one + * String spanning all columns (use setText("Spanning text")). Otherwise + * you can define one String per visible column. + * <p> + * If GeneratedRow.setRenderAsHtml(true) is used, the strings can + * contain HTML markup, otherwise all strings will be rendered as text + * (the default). + * <p> + * A "v-table-generated-row" CSS class is added to all generated rows. + * For custom styling of a generated row you can combine a RowGenerator + * with a CellStyleGenerator. + * <p> + * + * @param table + * The Table that is being painted + * @param itemId + * The itemId for the row + * @return A GeneratedRow describing how the row should be painted or + * null to paint the row with the contents from the container + */ + public GeneratedRow generateRow(Table table, Object itemId); + } + + public static class GeneratedRow implements Serializable { + private boolean htmlContentAllowed = false; + private boolean spanColumns = false; + private String[] text = null; + + /** + * Creates a new generated row. If only one string is passed in, columns + * are automatically spanned. + * + * @param text + */ + public GeneratedRow(String... text) { + setHtmlContentAllowed(false); + setSpanColumns(text == null || text.length == 1); + setText(text); + } + + /** + * Pass one String if spanColumns is used, one String for each visible + * column otherwise + */ + public void setText(String... text) { + if (text == null || (text.length == 1 && text[0] == null)) { + text = new String[] { "" }; + } + this.text = text; + } + + protected String[] getText() { + return text; + } + + protected Object getValue() { + return getText(); + } + + protected boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } + + /** + * If set to true, all strings passed to {@link #setText(String...)} + * will be rendered as HTML. + * + * @param htmlContentAllowed + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + } + + protected boolean isSpanColumns() { + return spanColumns; + } + + /** + * If set to true, only one string will be rendered, spanning the entire + * row. + * + * @param spanColumns + */ + public void setSpanColumns(boolean spanColumns) { + this.spanColumns = spanColumns; + } + } + + /** + * Assigns a row generator to the table. The row generator will be able to + * replace rows in the table when it is rendered. + * + * @param generator + * the new row generator + */ + public void setRowGenerator(RowGenerator generator) { + rowGenerator = generator; + refreshRowCache(); + } + + /** + * @return the current row generator + */ + public RowGenerator getRowGenerator() { + return rowGenerator; + } + + /** + * Sets a converter for a property id. + * <p> + * The converter is used to format the the data for the given property id + * before displaying it in the table. + * </p> + * + * @param propertyId + * The propertyId to format using the converter + * @param converter + * The converter to use for the property id + */ + public void setConverter(Object propertyId, Converter<String, ?> converter) { + if (!getContainerPropertyIds().contains(propertyId)) { + throw new IllegalArgumentException("PropertyId " + propertyId + + " must be in the container"); + } + // FIXME: This check should be here but primitive types like Boolean + // formatter for boolean property must be handled + + // if (!converter.getSourceType().isAssignableFrom(getType(propertyId))) + // { + // throw new IllegalArgumentException("Property type (" + // + getType(propertyId) + // + ") must match converter source type (" + // + converter.getSourceType() + ")"); + // } + propertyValueConverters.put(propertyId, + (Converter<String, Object>) converter); + refreshRowCache(); + } + + /** + * Checks if there is a converter set explicitly for the given property id. + * + * @param propertyId + * The propertyId to check + * @return true if a converter has been set for the property id, false + * otherwise + */ + protected boolean hasConverter(Object propertyId) { + return propertyValueConverters.containsKey(propertyId); + } + + /** + * Returns the converter used to format the given propertyId. + * + * @param propertyId + * The propertyId to check + * @return The converter used to format the propertyId or null if no + * converter has been set + */ + public Converter<String, Object> getConverter(Object propertyId) { + return propertyValueConverters.get(propertyId); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + // We need to ensure that the rows are sent to the client when the + // Table is made visible if it has been rendered as invisible. + setRowCacheInvalidated(true); + } + super.setVisible(visible); + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + @Override + public Iterator<Component> getComponentIterator() { + if (visibleComponents == null) { + Collection<Component> empty = Collections.emptyList(); + return empty.iterator(); + } + + return visibleComponents.iterator(); + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } + + private final Logger getLogger() { + if (logger == null) { + logger = Logger.getLogger(Table.class.getName()); + } + return logger; + } +} diff --git a/server/src/com/vaadin/ui/TableFieldFactory.java b/server/src/com/vaadin/ui/TableFieldFactory.java new file mode 100644 index 0000000000..6c9a641aa8 --- /dev/null +++ b/server/src/com/vaadin/ui/TableFieldFactory.java @@ -0,0 +1,45 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.data.Container; + +/** + * Factory interface for creating new Field-instances based on Container + * (datasource), item id, property id and uiContext (the component responsible + * for displaying fields). Currently this interface is used by {@link Table}, + * but might later be used by some other components for {@link Field} + * generation. + * + * <p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.0 + * @see FormFieldFactory + */ +public interface TableFieldFactory extends Serializable { + /** + * Creates a field based on the Container, item id, property id and the + * component responsible for displaying the field (most commonly + * {@link Table}). + * + * @param container + * the Container where the property belongs to. + * @param itemId + * the item Id. + * @param propertyId + * the Id of the property. + * @param uiContext + * the component where the field is presented. + * @return A field suitable for editing the specified data or null if the + * property should not be editable. + */ + Field<?> createField(Container container, Object itemId, Object propertyId, + Component uiContext); + +} diff --git a/server/src/com/vaadin/ui/TextArea.java b/server/src/com/vaadin/ui/TextArea.java new file mode 100644 index 0000000000..d7837dd33f --- /dev/null +++ b/server/src/com/vaadin/ui/TextArea.java @@ -0,0 +1,121 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; +import com.vaadin.shared.ui.textarea.TextAreaState; + +/** + * A text field that supports multi line editing. + */ +public class TextArea extends AbstractTextField { + + /** + * Constructs an empty TextArea. + */ + public TextArea() { + setValue(""); + } + + /** + * Constructs an empty TextArea with given caption. + * + * @param caption + * the caption for the field. + */ + public TextArea(String caption) { + this(); + setCaption(caption); + } + + /** + * Constructs a TextArea with given property data source. + * + * @param dataSource + * the data source for the field + */ + public TextArea(Property dataSource) { + this(); + setPropertyDataSource(dataSource); + } + + /** + * Constructs a TextArea with given caption and property data source. + * + * @param caption + * the caption for the field + * @param dataSource + * the data source for the field + */ + public TextArea(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a TextArea with given caption and value. + * + * @param caption + * the caption for the field + * @param value + * the value for the field + */ + public TextArea(String caption, String value) { + this(caption); + setValue(value); + + } + + @Override + public TextAreaState getState() { + return (TextAreaState) super.getState(); + } + + /** + * Sets the number of rows in the text area. + * + * @param rows + * the number of rows for this text area. + */ + public void setRows(int rows) { + if (rows < 0) { + rows = 0; + } + getState().setRows(rows); + requestRepaint(); + } + + /** + * Gets the number of rows in the text area. + * + * @return number of explicitly set rows. + */ + public int getRows() { + return getState().getRows(); + } + + /** + * Sets the text area's word-wrap mode on or off. + * + * @param wordwrap + * the boolean value specifying if the text area should be in + * word-wrap mode. + */ + public void setWordwrap(boolean wordwrap) { + getState().setWordwrap(wordwrap); + requestRepaint(); + } + + /** + * Tests if the text area is in word-wrap mode. + * + * @return <code>true</code> if the component is in word-wrap mode, + * <code>false</code> if not. + */ + public boolean isWordwrap() { + return getState().isWordwrap(); + } + +} diff --git a/server/src/com/vaadin/ui/TextField.java b/server/src/com/vaadin/ui/TextField.java new file mode 100644 index 0000000000..567e9c1c10 --- /dev/null +++ b/server/src/com/vaadin/ui/TextField.java @@ -0,0 +1,92 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; + +/** + * <p> + * A text editor component that can be bound to any bindable Property. The text + * editor supports both multiline and single line modes, default is one-line + * mode. + * </p> + * + * <p> + * Since <code>TextField</code> extends <code>AbstractField</code> it implements + * the {@link com.vaadin.data.Buffered} interface. A <code>TextField</code> is + * in write-through mode by default, so + * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)} must be called + * to enable buffering. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class TextField extends AbstractTextField { + + /** + * Constructs an empty <code>TextField</code> with no caption. + */ + public TextField() { + setValue(""); + } + + /** + * Constructs an empty <code>TextField</code> with given caption. + * + * @param caption + * the caption <code>String</code> for the editor. + */ + public TextField(String caption) { + this(); + setCaption(caption); + } + + /** + * Constructs a new <code>TextField</code> that's bound to the specified + * <code>Property</code> and has no caption. + * + * @param dataSource + * the Property to be edited with this editor. + */ + public TextField(Property dataSource) { + setPropertyDataSource(dataSource); + } + + /** + * Constructs a new <code>TextField</code> that's bound to the specified + * <code>Property</code> and has the given caption <code>String</code>. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param dataSource + * the Property to be edited with this editor. + */ + public TextField(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a new <code>TextField</code> with the given caption and + * initial text contents. The editor constructed this way will not be bound + * to a Property unless + * {@link com.vaadin.data.Property.Viewer#setPropertyDataSource(Property)} + * is called to bind it. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param value + * the initial text content of the editor. + */ + public TextField(String caption, String value) { + setValue(value); + setCaption(caption); + } + +} diff --git a/server/src/com/vaadin/ui/Tree.java b/server/src/com/vaadin/ui/Tree.java new file mode 100644 index 0000000000..c15975d879 --- /dev/null +++ b/server/src/com/vaadin/ui/Tree.java @@ -0,0 +1,1615 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.StringTokenizer; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickNotifier; +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.event.dd.acceptcriteria.TargetDetailIs; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.tree.TreeConnector; +import com.vaadin.terminal.gwt.client.ui.tree.VTree; +import com.vaadin.tools.ReflectTools; + +/** + * Tree component. A Tree can be used to select an item (or multiple items) from + * a hierarchical set of items. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings({ "serial", "deprecation" }) +public class Tree extends AbstractSelect implements Container.Hierarchical, + Action.Container, ItemClickNotifier, DragSource, DropTarget { + + /* Private members */ + + /** + * Set of expanded nodes. + */ + private final HashSet<Object> expanded = new HashSet<Object>(); + + /** + * List of action handlers. + */ + private LinkedList<Action.Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * Is the tree selectable on the client side. + */ + private boolean selectable = true; + + /** + * Flag to indicate sub-tree loading + */ + private boolean partialUpdate = false; + + /** + * Holds a itemId which was recently expanded + */ + private Object expandedItemId; + + /** + * a flag which indicates initial paint. After this flag set true partial + * updates are allowed. + */ + private boolean initialPaint = true; + + /** + * Item tooltip generator + */ + private ItemDescriptionGenerator itemDescriptionGenerator; + + /** + * Supported drag modes for Tree. + */ + public enum TreeDragMode { + /** + * When drag mode is NONE, dragging from Tree is not supported. Browsers + * may still support selecting text/icons from Tree which can initiate + * HTML 5 style drag and drop operation. + */ + NONE, + /** + * When drag mode is NODE, users can initiate drag from Tree nodes that + * represent {@link Item}s in from the backed {@link Container}. + */ + NODE + // , SUBTREE + } + + private TreeDragMode dragMode = TreeDragMode.NONE; + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + /* Tree constructors */ + + /** + * Creates a new empty tree. + */ + public Tree() { + } + + /** + * Creates a new empty tree with caption. + * + * @param caption + */ + public Tree(String caption) { + setCaption(caption); + } + + /** + * Creates a new tree with caption and connect it to a Container. + * + * @param caption + * @param dataSource + */ + public Tree(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* Expanding and collapsing */ + + /** + * Check is an item is expanded + * + * @param itemId + * the item id. + * @return true iff the item is expanded. + */ + public boolean isExpanded(Object itemId) { + return expanded.contains(itemId); + } + + /** + * Expands an item. + * + * @param itemId + * the item id. + * @return True iff the expand operation succeeded + */ + public boolean expandItem(Object itemId) { + boolean success = expandItem(itemId, true); + requestRepaint(); + return success; + } + + /** + * Expands an item. + * + * @param itemId + * the item id. + * @param sendChildTree + * flag to indicate if client needs subtree or not (may be + * cached) + * @return True iff the expand operation succeeded + */ + private boolean expandItem(Object itemId, boolean sendChildTree) { + + // Succeeds if the node is already expanded + if (isExpanded(itemId)) { + return true; + } + + // Nodes that can not have children are not expandable + if (!areChildrenAllowed(itemId)) { + return false; + } + + // Expands + expanded.add(itemId); + + expandedItemId = itemId; + if (initialPaint) { + requestRepaint(); + } else if (sendChildTree) { + requestPartialRepaint(); + } + fireExpandEvent(itemId); + + return true; + } + + @Override + public void requestRepaint() { + super.requestRepaint(); + partialUpdate = false; + } + + private void requestPartialRepaint() { + super.requestRepaint(); + partialUpdate = true; + } + + /** + * Expands the items recursively + * + * Expands all the children recursively starting from an item. Operation + * succeeds only if all expandable items are expanded. + * + * @param startItemId + * @return True iff the expand operation succeeded + */ + public boolean expandItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + final Stack<Object> todo = new Stack<Object>(); + todo.add(startItemId); + + // Expands recursively + while (!todo.isEmpty()) { + final Object id = todo.pop(); + if (areChildrenAllowed(id) && !expandItem(id, false)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + requestRepaint(); + return result; + } + + /** + * Collapses an item. + * + * @param itemId + * the item id. + * @return True iff the collapse operation succeeded + */ + public boolean collapseItem(Object itemId) { + + // Succeeds if the node is already collapsed + if (!isExpanded(itemId)) { + return true; + } + + // Collapse + expanded.remove(itemId); + requestRepaint(); + fireCollapseEvent(itemId); + + return true; + } + + /** + * Collapses the items recursively. + * + * Collapse all the children recursively starting from an item. Operation + * succeeds only if all expandable items are collapsed. + * + * @param startItemId + * @return True iff the collapse operation succeeded + */ + public boolean collapseItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + final Stack<Object> todo = new Stack<Object>(); + todo.add(startItemId); + + // Collapse recursively + while (!todo.isEmpty()) { + final Object id = todo.pop(); + if (areChildrenAllowed(id) && !collapseItem(id)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + + return result; + } + + /** + * Returns the current selectable state. Selectable determines if the a node + * can be selected on the client side. Selectable does not affect + * {@link #setValue(Object)} or {@link #select(Object)}. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @return the current selectable state. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Sets the selectable state. Selectable determines if the a node can be + * selected on the client side. Selectable does not affect + * {@link #setValue(Object)} or {@link #select(Object)}. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @param selectable + * The new selectable state. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /** + * Sets the behavior of the multiselect mode + * + * @param mode + * The mode to set + */ + public void setMultiselectMode(MultiSelectMode mode) { + if (multiSelectMode != mode && mode != null) { + multiSelectMode = mode; + requestRepaint(); + } + } + + /** + * Returns the mode the multiselect is in. The mode controls how + * multiselection can be done. + * + * @return The mode + */ + public MultiSelectMode getMultiselectMode() { + return multiSelectMode; + } + + /* Component API */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + if (variables.containsKey("clickedKey")) { + String key = (String) variables.get("clickedKey"); + + Object id = itemIdMapper.get(key); + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("clickEvent")); + Item item = getItem(id); + if (item != null) { + fireEvent(new ItemClickEvent(this, item, id, null, details)); + } + } + + if (!isSelectable() && variables.containsKey("selected")) { + // Not-selectable is a special case, AbstractSelect does not support + // TODO could be optimized. + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + // Collapses the nodes + if (variables.containsKey("collapse")) { + final String[] keys = (String[]) variables.get("collapse"); + for (int i = 0; i < keys.length; i++) { + final Object id = itemIdMapper.get(keys[i]); + if (id != null && isExpanded(id)) { + expanded.remove(id); + fireCollapseEvent(id); + } + } + } + + // Expands the nodes + if (variables.containsKey("expand")) { + boolean sendChildTree = false; + if (variables.containsKey("requestChildTree")) { + sendChildTree = true; + } + final String[] keys = (String[]) variables.get("expand"); + for (int i = 0; i < keys.length; i++) { + final Object id = itemIdMapper.get(keys[i]); + if (id != null) { + expandItem(id, sendChildTree); + } + } + } + + // AbstractSelect cannot handle multiselection so we handle + // it ourself + if (variables.containsKey("selected") && isMultiSelect() + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + // Selections are handled by the select component + super.changeVariables(source, variables); + + // Actions + if (variables.containsKey("action")) { + final StringTokenizer st = new StringTokenizer( + (String) variables.get("action"), ","); + if (st.countTokens() == 2) { + final Object itemId = itemIdMapper.get(st.nextToken()); + final Action action = actionMapper.get(st.nextToken()); + if (action != null && (itemId == null || containsId(itemId)) + && actionHandlers != null) { + for (Handler ah : actionHandlers) { + ah.handleAction(action, this, itemId); + } + } + } + } + } + + /** + * Handles the selection + * + * @param variables + * The variables sent to the server from the client + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + + // Converts the key-array to id-set + final LinkedList<Object> s = new LinkedList<Object>(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + setValue(s, true); + } + + /** + * Paints any needed component-specific things to the given UIDL stream. + * + * @see com.vaadin.ui.AbstractComponent#paintContent(PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + initialPaint = false; + + if (partialUpdate) { + target.addAttribute("partialUpdate", true); + target.addAttribute("rootKey", itemIdMapper.key(expandedItemId)); + } else { + getCaptionChangeListener().clear(); + + // The tab ordering number + if (getTabIndex() > 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // Paint tree attributes + if (isSelectable()) { + target.addAttribute("selectmode", (isMultiSelect() ? "multi" + : "single")); + if (isMultiSelect()) { + target.addAttribute("multiselectmode", + multiSelectMode.ordinal()); + } + } else { + target.addAttribute("selectmode", "none"); + } + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + } + + if (dragMode != TreeDragMode.NONE) { + target.addAttribute("dragMode", dragMode.ordinal()); + } + + } + + // Initialize variables + final Set<Action> actionSet = new LinkedHashSet<Action>(); + + // rendered selectedKeys + LinkedList<String> selectedKeys = new LinkedList<String>(); + + final LinkedList<String> expandedKeys = new LinkedList<String>(); + + // Iterates through hierarchical tree using a stack of iterators + final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>(); + Collection<?> ids; + if (partialUpdate) { + ids = getChildren(expandedItemId); + } else { + ids = rootItemIds(); + } + + if (ids != null) { + iteratorStack.push(ids.iterator()); + } + + /* + * Body actions - Actions which has the target null and can be invoked + * by right clicking on the Tree body + */ + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + + // Getting action for the null item, which in this case + // means the body item + final Action[] aa = ah.getActions(null, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(akey); + } + } + } + target.addAttribute("alb", keys.toArray()); + } + + while (!iteratorStack.isEmpty()) { + + // Gets the iterator for current tree level + final Iterator<?> i = iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Removes used iterator from the stack + iteratorStack.pop(); + + // Closes node + if (!iteratorStack.isEmpty()) { + target.endTag("node"); + } + } + + // Adds the item on current level + else { + final Object itemId = i.next(); + + // Starts the item / node + final boolean isNode = areChildrenAllowed(itemId); + if (isNode) { + target.startTag("node"); + } else { + target.startTag("leaf"); + } + + if (itemStyleGenerator != null) { + String stylename = itemStyleGenerator.getStyle(itemId); + if (stylename != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_STYLE, + stylename); + } + } + + if (itemDescriptionGenerator != null) { + String description = itemDescriptionGenerator + .generateDescription(this, itemId, null); + if (description != null && !description.equals("")) { + target.addAttribute("descr", description); + } + } + + // Adds the attributes + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_CAPTION, + getItemCaption(itemId)); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_ICON, + getItemIcon(itemId)); + } + final String key = itemIdMapper.key(itemId); + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + selectedKeys.add(key); + } + if (areChildrenAllowed(itemId) && isExpanded(itemId)) { + target.addAttribute("expanded", true); + expandedKeys.add(key); + } + + // Add caption change listener + getCaptionChangeListener().addNotifierForItem(itemId); + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + final Iterator<Action.Handler> ahi = actionHandlers + .iterator(); + while (ahi.hasNext()) { + final Action[] aa = ahi.next().getActions(itemId, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(akey); + } + } + } + target.addAttribute("al", keys.toArray()); + } + + // Adds the children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId) + && areChildrenAllowed(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } else { + if (isNode) { + target.endTag("node"); + } else { + target.endTag("leaf"); + } + } + } + } + + // Actions + if (!actionSet.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + final Iterator<Action> i = actionSet.iterator(); + while (i.hasNext()) { + final Action a = i.next(); + target.startTag("action"); + if (a.getCaption() != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_ACTION_CAPTION, + a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_ACTION_ICON, + a.getIcon()); + } + target.addAttribute("key", actionMapper.key(a)); + target.endTag("action"); + } + target.endTag("actions"); + } + + if (partialUpdate) { + partialUpdate = false; + } else { + // Selected + target.addVariable(this, "selected", + selectedKeys.toArray(new String[selectedKeys.size()])); + + // Expand and collapse + target.addVariable(this, "expand", new String[] {}); + target.addVariable(this, "collapse", new String[] {}); + + // New items + target.addVariable(this, "newitem", new String[] {}); + + if (dropHandler != null) { + dropHandler.getAcceptCriterion().paint(target); + } + + } + } + + /* Container.Hierarchical API */ + + /** + * Tests if the Item with given ID can have any children. + * + * @see com.vaadin.data.Container.Hierarchical#areChildrenAllowed(Object) + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + return ((Container.Hierarchical) items).areChildrenAllowed(itemId); + } + + /** + * Gets the IDs of all Items that are children of the specified Item. + * + * @see com.vaadin.data.Container.Hierarchical#getChildren(Object) + */ + @Override + public Collection<?> getChildren(Object itemId) { + return ((Container.Hierarchical) items).getChildren(itemId); + } + + /** + * Gets the ID of the parent Item of the specified Item. + * + * @see com.vaadin.data.Container.Hierarchical#getParent(Object) + */ + @Override + public Object getParent(Object itemId) { + return ((Container.Hierarchical) items).getParent(itemId); + } + + /** + * Tests if the Item specified with <code>itemId</code> has child Items. + * + * @see com.vaadin.data.Container.Hierarchical#hasChildren(Object) + */ + @Override + public boolean hasChildren(Object itemId) { + return ((Container.Hierarchical) items).hasChildren(itemId); + } + + /** + * Tests if the Item specified with <code>itemId</code> is a root Item. + * + * @see com.vaadin.data.Container.Hierarchical#isRoot(Object) + */ + @Override + public boolean isRoot(Object itemId) { + return ((Container.Hierarchical) items).isRoot(itemId); + } + + /** + * Gets the IDs of all Items in the container that don't have a parent. + * + * @see com.vaadin.data.Container.Hierarchical#rootItemIds() + */ + @Override + public Collection<?> rootItemIds() { + return ((Container.Hierarchical) items).rootItemIds(); + } + + /** + * Sets the given Item's capability to have children. + * + * @see com.vaadin.data.Container.Hierarchical#setChildrenAllowed(Object, + * boolean) + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) { + final boolean success = ((Container.Hierarchical) items) + .setChildrenAllowed(itemId, areChildrenAllowed); + if (success) { + requestRepaint(); + } + return success; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Hierarchical#setParent(java.lang.Object , + * java.lang.Object) + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + final boolean success = ((Container.Hierarchical) items).setParent( + itemId, newParentId); + if (success) { + requestRepaint(); + } + return success; + } + + /* Overriding select behavior */ + + /** + * Sets the Container that serves as the data source of the viewer. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + // Note: using wrapped IndexedContainer to match constructor (super + // creates an IndexedContainer, which is then wrapped). + newDataSource = new ContainerHierarchicalWrapper( + new IndexedContainer()); + } + + // Assure that the data source is ordered by making unordered + // containers ordered by wrapping them + if (Container.Hierarchical.class.isAssignableFrom(newDataSource + .getClass())) { + super.setContainerDataSource(newDataSource); + } else { + super.setContainerDataSource(new ContainerHierarchicalWrapper( + newDataSource)); + } + } + + /* Expand event and listener */ + + /** + * Event to fired when a node is expanded. ExapandEvent is fired when a node + * is to be expanded. it can me used to dynamically fill the sub-nodes of + * the node. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ExpandEvent extends Component.Event { + + private final Object expandedItemId; + + /** + * New instance of options change event + * + * @param source + * the Source of the event. + * @param expandedItemId + */ + public ExpandEvent(Component source, Object expandedItemId) { + super(source); + this.expandedItemId = expandedItemId; + } + + /** + * Node where the event occurred. + * + * @return the Source of the event. + */ + public Object getItemId() { + return expandedItemId; + } + } + + /** + * Expand event listener. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ExpandListener extends Serializable { + + public static final Method EXPAND_METHOD = ReflectTools.findMethod( + ExpandListener.class, "nodeExpand", ExpandEvent.class); + + /** + * A node has been expanded. + * + * @param event + * the Expand event. + */ + public void nodeExpand(ExpandEvent event); + } + + /** + * Adds the expand listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * Removes the expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * Emits the expand event. + * + * @param itemId + * the item id. + */ + protected void fireExpandEvent(Object itemId) { + fireEvent(new ExpandEvent(this, itemId)); + } + + /* Collapse event */ + + /** + * Collapse event + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class CollapseEvent extends Component.Event { + + private final Object collapsedItemId; + + /** + * New instance of options change event. + * + * @param source + * the Source of the event. + * @param collapsedItemId + */ + public CollapseEvent(Component source, Object collapsedItemId) { + super(source); + this.collapsedItemId = collapsedItemId; + } + + /** + * Gets tge Collapsed Item id. + * + * @return the collapsed item id. + */ + public Object getItemId() { + return collapsedItemId; + } + } + + /** + * Collapse event listener. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface CollapseListener extends Serializable { + + public static final Method COLLAPSE_METHOD = ReflectTools.findMethod( + CollapseListener.class, "nodeCollapse", CollapseEvent.class); + + /** + * A node has been collapsed. + * + * @param event + * the Collapse event. + */ + public void nodeCollapse(CollapseEvent event); + } + + /** + * Adds the collapse listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Removes the collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Emits collapse event. + * + * @param itemId + * the item id. + */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /* Action container */ + + /** + * Adds an action handler. + * + * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler) + */ + @Override + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Action.Handler>(); + actionMapper = new KeyMapper<Action>(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + requestRepaint(); + } + } + } + + /** + * Removes an action handler. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + @Override + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + requestRepaint(); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + actionHandlers = null; + actionMapper = null; + requestRepaint(); + } + + /** + * Gets the visible item ids. + * + * @see com.vaadin.ui.Select#getVisibleItemIds() + */ + @Override + public Collection<?> getVisibleItemIds() { + + final LinkedList<Object> visible = new LinkedList<Object>(); + + // Iterates trough hierarchical tree using a stack of iterators + final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>(); + final Collection<?> ids = rootItemIds(); + if (ids != null) { + iteratorStack.push(ids.iterator()); + } + while (!iteratorStack.isEmpty()) { + + // Gets the iterator for current tree level + final Iterator<?> i = iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Removes used iterator from the stack + iteratorStack.pop(); + } + + // Adds the item on current level + else { + final Object itemId = i.next(); + + visible.add(itemId); + + // Adds children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } + } + } + + return visible; + } + + /** + * Tree does not support <code>setNullSelectionItemId</code>. + * + * @see com.vaadin.ui.AbstractSelect#setNullSelectionItemId(java.lang.Object) + */ + @Override + public void setNullSelectionItemId(Object nullSelectionItemId) + throws UnsupportedOperationException { + if (nullSelectionItemId != null) { + throw new UnsupportedOperationException(); + } + + } + + /** + * Adding new items is not supported. + * + * @throws UnsupportedOperationException + * if set to true. + * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean) + */ + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) { + throw new UnsupportedOperationException(); + } + } + + /** + * Tree does not support lazy options loading mode. Setting this true will + * throw UnsupportedOperationException. + * + * @see com.vaadin.ui.Select#setLazyLoading(boolean) + */ + public void setLazyLoading(boolean useLazyLoading) { + if (useLazyLoading) { + throw new UnsupportedOperationException( + "Lazy options loading is not supported by Tree."); + } + } + + private ItemStyleGenerator itemStyleGenerator; + + private DropHandler dropHandler; + + @Override + public void addListener(ItemClickListener listener) { + addListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + public void removeListener(ItemClickListener listener) { + removeListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + /** + * Sets the {@link ItemStyleGenerator} to be used with this tree. + * + * @param itemStyleGenerator + * item style generator or null to remove generator + */ + public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) { + if (this.itemStyleGenerator != itemStyleGenerator) { + this.itemStyleGenerator = itemStyleGenerator; + requestRepaint(); + } + } + + /** + * @return the current {@link ItemStyleGenerator} for this tree. Null if + * {@link ItemStyleGenerator} is not set. + */ + public ItemStyleGenerator getItemStyleGenerator() { + return itemStyleGenerator; + } + + /** + * ItemStyleGenerator can be used to add custom styles to tree items. The + * CSS class name that will be added to the cell content is + * <tt>v-tree-node-[style name]</tt>. + */ + public interface ItemStyleGenerator extends Serializable { + + /** + * Called by Tree when an item is painted. + * + * @param itemId + * The itemId of the item to be painted + * @return The style name to add to this item. (the CSS class name will + * be v-tree-node-[style name] + */ + public abstract String getStyle(Object itemId); + } + + // Overriden so javadoc comes from Container.Hierarchical + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + return super.removeItem(itemId); + } + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + /** + * A {@link TargetDetails} implementation with Tree specific api. + * + * @since 6.3 + */ + public class TreeTargetDetails extends AbstractSelectTargetDetails { + + TreeTargetDetails(Map<String, Object> rawVariables) { + super(rawVariables); + } + + @Override + public Tree getTarget() { + return (Tree) super.getTarget(); + } + + /** + * If the event is on a node that can not have children (see + * {@link Tree#areChildrenAllowed(Object)}), this method returns the + * parent item id of the target item (see {@link #getItemIdOver()} ). + * The identifier of the parent node is also returned if the cursor is + * on the top part of node. Else this method returns the same as + * {@link #getItemIdOver()}. + * <p> + * In other words this method returns the identifier of the "folder" + * into the drag operation is targeted. + * <p> + * If the method returns null, the current target is on a root node or + * on other undefined area over the tree component. + * <p> + * The default Tree implementation marks the targetted tree node with + * CSS classnames v-tree-node-dragfolder and + * v-tree-node-caption-dragfolder (for the caption element). + */ + public Object getItemIdInto() { + + Object itemIdOver = getItemIdOver(); + if (areChildrenAllowed(itemIdOver) + && getDropLocation() == VerticalDropLocation.MIDDLE) { + return itemIdOver; + } + return getParent(itemIdOver); + } + + /** + * If drop is targeted into "folder node" (see {@link #getItemIdInto()} + * ), this method returns the item id of the node after the drag was + * targeted. This method is useful when implementing drop into specific + * location (between specific nodes) in tree. + * + * @return the id of the item after the user targets the drop or null if + * "target" is a first item in node list (or the first in root + * node list) + */ + public Object getItemIdAfter() { + Object itemIdOver = getItemIdOver(); + Object itemIdInto2 = getItemIdInto(); + if (itemIdOver.equals(itemIdInto2)) { + return null; + } + VerticalDropLocation dropLocation = getDropLocation(); + if (VerticalDropLocation.TOP == dropLocation) { + // if on top of the caption area, add before + Collection<?> children; + Object itemIdInto = getItemIdInto(); + if (itemIdInto != null) { + // seek the previous from child list + children = getChildren(itemIdInto); + } else { + children = rootItemIds(); + } + Object ref = null; + for (Object object : children) { + if (object.equals(itemIdOver)) { + return ref; + } + ref = object; + } + } + return itemIdOver; + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map) + */ + @Override + public TreeTargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new TreeTargetDetails(clientVariables); + } + + /** + * Helper API for {@link TreeDropCriterion} + * + * @param itemId + * @return + */ + private String key(Object itemId) { + return itemIdMapper.key(itemId); + } + + /** + * Sets the drag mode that controls how Tree behaves as a {@link DragSource} + * . + * + * @param dragMode + */ + public void setDragMode(TreeDragMode dragMode) { + this.dragMode = dragMode; + requestRepaint(); + } + + /** + * @return the drag mode that controls how Tree behaves as a + * {@link DragSource}. + * + * @see TreeDragMode + */ + public TreeDragMode getDragMode() { + return dragMode; + } + + /** + * Concrete implementation of {@link DataBoundTransferable} for data + * transferred from a tree. + * + * @see {@link DataBoundTransferable}. + * + * @since 6.3 + */ + protected class TreeTransferable extends DataBoundTransferable { + + public TreeTransferable(Component sourceComponent, + Map<String, Object> rawVariables) { + super(sourceComponent, rawVariables); + } + + @Override + public Object getItemId() { + return getData("itemId"); + } + + @Override + public Object getPropertyId() { + return getItemCaptionPropertyId(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.event.dd.DragSource#getTransferable(java.util.Map) + */ + @Override + public Transferable getTransferable(Map<String, Object> payload) { + TreeTransferable transferable = new TreeTransferable(this, payload); + // updating drag source variables + Object object = payload.get("itemId"); + if (object != null) { + transferable.setData("itemId", itemIdMapper.get((String) object)); + } + + return transferable; + } + + /** + * Lazy loading accept criterion for Tree. Accepted target nodes are loaded + * from server once per drag and drop operation. Developer must override one + * method that decides accepted tree nodes for the whole Tree. + * + * <p> + * Initially pretty much no data is sent to client. On first required + * criterion check (per drag request) the client side data structure is + * initialized from server and no subsequent requests requests are needed + * during that drag and drop operation. + */ + public static abstract class TreeDropCriterion extends ServerSideCriterion { + + private Tree tree; + + private Set<Object> allowedItemIds; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.ServerSideCriterion#getIdentifier + * () + */ + @Override + protected String getIdentifier() { + return TreeDropCriterion.class.getCanonicalName(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#accepts(com.vaadin + * .event.dd.DragAndDropEvent) + */ + @Override + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + tree = (Tree) dragEvent.getTargetDetails().getTarget(); + allowedItemIds = getAllowedItemIds(dragEvent, tree); + + return allowedItemIds.contains(dropTargetData.getItemIdOver()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#paintResponse( + * com.vaadin.terminal.PaintTarget) + */ + @Override + public void paintResponse(PaintTarget target) throws PaintException { + /* + * send allowed nodes to client so subsequent requests can be + * avoided + */ + Object[] array = allowedItemIds.toArray(); + for (int i = 0; i < array.length; i++) { + String key = tree.key(array[i]); + array[i] = key; + } + target.addAttribute("allowedIds", array); + } + + protected abstract Set<Object> getAllowedItemIds( + DragAndDropEvent dragEvent, Tree tree); + + } + + /** + * A criterion that accepts {@link Transferable} only directly on a tree + * node that can have children. + * <p> + * Class is singleton, use {@link TargetItemAllowsChildren#get()} to get the + * instance. + * + * @see Tree#setChildrenAllowed(Object, boolean) + * + * @since 6.3 + */ + public static class TargetItemAllowsChildren extends TargetDetailIs { + + private static TargetItemAllowsChildren instance = new TargetItemAllowsChildren(); + + public static TargetItemAllowsChildren get() { + return instance; + } + + private TargetItemAllowsChildren() { + super("itemIdOverIsNode", Boolean.TRUE); + } + + /* + * Uses enhanced server side check + */ + @Override + public boolean accept(DragAndDropEvent dragEvent) { + try { + // must be over tree node and in the middle of it (not top or + // bottom + // part) + TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent + .getTargetDetails(); + + Object itemIdOver = eventDetails.getItemIdOver(); + if (!eventDetails.getTarget().areChildrenAllowed(itemIdOver)) { + return false; + } + // return true if directly over + return eventDetails.getDropLocation() == VerticalDropLocation.MIDDLE; + } catch (Exception e) { + return false; + } + } + + } + + /** + * An accept criterion that checks the parent node (or parent hierarchy) for + * the item identifier given in constructor. If the parent is found, content + * is accepted. Criterion can be used to accepts drags on a specific sub + * tree only. + * <p> + * The root items is also consider to be valid target. + */ + public class TargetInSubtree extends ClientSideCriterion { + + private Object rootId; + private int depthToCheck = -1; + + /** + * Constructs a criteria that accepts the drag if the targeted Item is a + * descendant of Item identified by given id + * + * @param parentItemId + * the item identifier of the parent node + */ + public TargetInSubtree(Object parentItemId) { + rootId = parentItemId; + } + + /** + * Constructs a criteria that accepts drops within given level below the + * subtree root identified by given id. + * + * @param rootId + * the item identifier to be sought for + * @param depthToCheck + * the depth that tree is traversed upwards to seek for the + * parent, -1 means that the whole structure should be + * checked + */ + public TargetInSubtree(Object rootId, int depthToCheck) { + this.rootId = rootId; + this.depthToCheck = depthToCheck; + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + try { + TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent + .getTargetDetails(); + + if (eventDetails.getItemIdOver() != null) { + Object itemId = eventDetails.getItemIdOver(); + int i = 0; + while (itemId != null + && (depthToCheck == -1 || i <= depthToCheck)) { + if (itemId.equals(rootId)) { + return true; + } + itemId = getParent(itemId); + i++; + } + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + target.addAttribute("depth", depthToCheck); + target.addAttribute("key", key(rootId)); + } + + } + + /** + * Set the item description generator which generates tooltips for the tree + * items + * + * @param generator + * The generator to use or null to disable + */ + public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) { + if (generator != itemDescriptionGenerator) { + itemDescriptionGenerator = generator; + requestRepaint(); + } + } + + /** + * Get the item description generator which generates tooltips for tree + * items + */ + public ItemDescriptionGenerator getItemDescriptionGenerator() { + return itemDescriptionGenerator; + } + +} diff --git a/server/src/com/vaadin/ui/TreeTable.java b/server/src/com/vaadin/ui/TreeTable.java new file mode 100644 index 0000000000..6132b652f7 --- /dev/null +++ b/server/src/com/vaadin/ui/TreeTable.java @@ -0,0 +1,824 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.vaadin.data.Collapsible; +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.data.util.HierarchicalContainerOrderedWrapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.treetable.TreeTableConnector; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.ui.Tree.ExpandListener; + +/** + * TreeTable extends the {@link Table} component so that it can also visualize a + * hierarchy of its Items in a similar manner that {@link Tree} does. The tree + * hierarchy is always displayed in the first actual column of the TreeTable. + * <p> + * The TreeTable supports the usual {@link Table} features like lazy loading, so + * it should be no problem to display lots of items at once. Only required rows + * and some cache rows are sent to the client. + * <p> + * TreeTable supports standard {@link Hierarchical} container interfaces, but + * also a more fine tuned version - {@link Collapsible}. A container + * implementing the {@link Collapsible} interface stores the collapsed/expanded + * state internally and can this way scale better on the server side than with + * standard Hierarchical implementations. Developer must however note that + * {@link Collapsible} containers can not be shared among several users as they + * share UI state in the container. + */ +@SuppressWarnings({ "serial" }) +public class TreeTable extends Table implements Hierarchical { + + private interface ContainerStrategy extends Serializable { + public int size(); + + public boolean isNodeOpen(Object itemId); + + public int getDepth(Object itemId); + + public void toggleChildVisibility(Object itemId); + + public Object getIdByIndex(int index); + + public int indexOfId(Object id); + + public Object nextItemId(Object itemId); + + public Object lastItemId(); + + public Object prevItemId(Object itemId); + + public boolean isLastId(Object itemId); + + public Collection<?> getItemIds(); + + public void containerItemSetChange(ItemSetChangeEvent event); + } + + private abstract class AbstractStrategy implements ContainerStrategy { + + /** + * Consider adding getDepth to {@link Collapsible}, might help + * scalability with some container implementations. + */ + + @Override + public int getDepth(Object itemId) { + int depth = 0; + Hierarchical hierarchicalContainer = getContainerDataSource(); + while (!hierarchicalContainer.isRoot(itemId)) { + depth++; + itemId = hierarchicalContainer.getParent(itemId); + } + return depth; + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + } + + } + + /** + * This strategy is used if current container implements {@link Collapsible} + * . + * + * open-collapsed logic diverted to container, otherwise use default + * implementations. + */ + private class CollapsibleStrategy extends AbstractStrategy { + + private Collapsible c() { + return (Collapsible) getContainerDataSource(); + } + + @Override + public void toggleChildVisibility(Object itemId) { + c().setCollapsed(itemId, !c().isCollapsed(itemId)); + } + + @Override + public boolean isNodeOpen(Object itemId) { + return !c().isCollapsed(itemId); + } + + @Override + public int size() { + return TreeTable.super.size(); + } + + @Override + public Object getIdByIndex(int index) { + return TreeTable.super.getIdByIndex(index); + } + + @Override + public int indexOfId(Object id) { + return TreeTable.super.indexOfId(id); + } + + @Override + public boolean isLastId(Object itemId) { + // using the default impl + return TreeTable.super.isLastId(itemId); + } + + @Override + public Object lastItemId() { + // using the default impl + return TreeTable.super.lastItemId(); + } + + @Override + public Object nextItemId(Object itemId) { + return TreeTable.super.nextItemId(itemId); + } + + @Override + public Object prevItemId(Object itemId) { + return TreeTable.super.prevItemId(itemId); + } + + @Override + public Collection<?> getItemIds() { + return TreeTable.super.getItemIds(); + } + + } + + /** + * Strategy for Hierarchical but not Collapsible container like + * {@link HierarchicalContainer}. + * + * Store collapsed/open states internally, fool Table to use preorder when + * accessing items from container via Ordered/Indexed methods. + */ + private class HierarchicalStrategy extends AbstractStrategy { + + private final HashSet<Object> openItems = new HashSet<Object>(); + + @Override + public boolean isNodeOpen(Object itemId) { + return openItems.contains(itemId); + } + + @Override + public int size() { + return getPreOrder().size(); + } + + @Override + public Collection<Object> getItemIds() { + return Collections.unmodifiableCollection(getPreOrder()); + } + + @Override + public boolean isLastId(Object itemId) { + if (itemId == null) { + return false; + } + + return itemId.equals(lastItemId()); + } + + @Override + public Object lastItemId() { + if (getPreOrder().size() > 0) { + return getPreOrder().get(getPreOrder().size() - 1); + } else { + return null; + } + } + + @Override + public Object nextItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + if (indexOf == -1) { + return null; + } + indexOf++; + if (indexOf == getPreOrder().size()) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + @Override + public Object prevItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + indexOf--; + if (indexOf < 0) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + @Override + public void toggleChildVisibility(Object itemId) { + boolean removed = openItems.remove(itemId); + if (!removed) { + openItems.add(itemId); + getLogger().finest("Item " + itemId + " is now expanded"); + } else { + getLogger().finest("Item " + itemId + " is now collapsed"); + } + clearPreorderCache(); + } + + private void clearPreorderCache() { + preOrder = null; // clear preorder cache + } + + List<Object> preOrder; + + /** + * Preorder of ids currently visible + * + * @return + */ + private List<Object> getPreOrder() { + if (preOrder == null) { + preOrder = new ArrayList<Object>(); + Collection<?> rootItemIds = getContainerDataSource() + .rootItemIds(); + for (Object id : rootItemIds) { + preOrder.add(id); + addVisibleChildTree(id); + } + } + return preOrder; + } + + private void addVisibleChildTree(Object id) { + if (isNodeOpen(id)) { + Collection<?> children = getContainerDataSource().getChildren( + id); + if (children != null) { + for (Object childId : children) { + preOrder.add(childId); + addVisibleChildTree(childId); + } + } + } + + } + + @Override + public int indexOfId(Object id) { + return getPreOrder().indexOf(id); + } + + @Override + public Object getIdByIndex(int index) { + return getPreOrder().get(index); + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + // preorder becomes invalid on sort, item additions etc. + clearPreorderCache(); + super.containerItemSetChange(event); + } + + } + + /** + * Creates an empty TreeTable with a default container. + */ + public TreeTable() { + super(null, new HierarchicalContainer()); + } + + /** + * Creates an empty TreeTable with a default container. + * + * @param caption + * the caption for the TreeTable + */ + public TreeTable(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a TreeTable instance with given captions and data source. + * + * @param caption + * the caption for the component + * @param dataSource + * the dataSource that is used to list items in the component + */ + public TreeTable(String caption, Container dataSource) { + super(caption, dataSource); + } + + private ContainerStrategy cStrategy; + private Object focusedRowId = null; + private Object hierarchyColumnId; + + /** + * The item id that was expanded or collapsed during this request. Reset at + * the end of paint and only used for determining if a partial or full paint + * should be done. + * + * Can safely be reset to null whenever a change occurs that would prevent a + * partial update from rendering the correct result, e.g. rows added or + * removed during an expand operation. + */ + private Object toggledItemId; + private boolean animationsEnabled; + private boolean clearFocusedRowPending; + + /** + * If the container does not send item set change events, always do a full + * repaint instead of a partial update when expanding/collapsing nodes. + */ + private boolean containerSupportsPartialUpdates; + + private ContainerStrategy getContainerStrategy() { + if (cStrategy == null) { + if (getContainerDataSource() instanceof Collapsible) { + cStrategy = new CollapsibleStrategy(); + } else { + cStrategy = new HierarchicalStrategy(); + } + } + return cStrategy; + } + + @Override + protected void paintRowAttributes(PaintTarget target, Object itemId) + throws PaintException { + super.paintRowAttributes(target, itemId); + target.addAttribute("depth", getContainerStrategy().getDepth(itemId)); + if (getContainerDataSource().areChildrenAllowed(itemId)) { + target.addAttribute("ca", true); + target.addAttribute("open", + getContainerStrategy().isNodeOpen(itemId)); + } + } + + @Override + protected void paintRowIcon(PaintTarget target, Object[][] cells, + int indexInRowbuffer) throws PaintException { + // always paint if present (in parent only if row headers visible) + if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) { + Resource itemIcon = getItemIcon(cells[CELL_ITEMID][indexInRowbuffer]); + if (itemIcon != null) { + target.addAttribute("icon", itemIcon); + } + } else if (cells[CELL_ICON][indexInRowbuffer] != null) { + target.addAttribute("icon", + (Resource) cells[CELL_ICON][indexInRowbuffer]); + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + super.changeVariables(source, variables); + + if (variables.containsKey("toggleCollapsed")) { + String object = (String) variables.get("toggleCollapsed"); + Object itemId = itemIdMapper.get(object); + toggledItemId = itemId; + toggleChildVisibility(itemId, false); + if (variables.containsKey("selectCollapsed")) { + // ensure collapsed is selected unless opened with selection + // head + if (isSelectable()) { + select(itemId); + } + } + } else if (variables.containsKey("focusParent")) { + String key = (String) variables.get("focusParent"); + Object refId = itemIdMapper.get(key); + Object itemId = getParent(refId); + focusParent(itemId); + } + } + + private void focusParent(Object itemId) { + boolean inView = false; + Object inPageId = getCurrentPageFirstItemId(); + for (int i = 0; inPageId != null && i < getPageLength(); i++) { + if (inPageId.equals(itemId)) { + inView = true; + break; + } + inPageId = nextItemId(inPageId); + i++; + } + if (!inView) { + setCurrentPageFirstItemId(itemId); + } + // Select the row if it is selectable. + if (isSelectable()) { + if (isMultiSelect()) { + setValue(Collections.singleton(itemId)); + } else { + setValue(itemId); + } + } + setFocusedRow(itemId); + } + + private void setFocusedRow(Object itemId) { + focusedRowId = itemId; + if (focusedRowId == null) { + // Must still inform the client that the focusParent request has + // been processed + clearFocusedRowPending = true; + } + requestRepaint(); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (focusedRowId != null) { + target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId)); + focusedRowId = null; + } else if (clearFocusedRowPending) { + // Must still inform the client that the focusParent request has + // been processed + target.addAttribute("clearFocusPending", true); + clearFocusedRowPending = false; + } + target.addAttribute("animate", animationsEnabled); + if (hierarchyColumnId != null) { + Object[] visibleColumns2 = getVisibleColumns(); + for (int i = 0; i < visibleColumns2.length; i++) { + Object object = visibleColumns2[i]; + if (hierarchyColumnId.equals(object)) { + target.addAttribute( + TreeTableConnector.ATTRIBUTE_HIERARCHY_COLUMN_INDEX, + i); + break; + } + } + } + super.paintContent(target); + toggledItemId = null; + } + + /* + * Override methods for partial row updates and additions when expanding / + * collapsing nodes. + */ + + @Override + protected boolean isPartialRowUpdate() { + return toggledItemId != null && containerSupportsPartialUpdates + && !isRowCacheInvalidated(); + } + + @Override + protected int getFirstAddedItemIndex() { + return indexOfId(toggledItemId) + 1; + } + + @Override + protected int getAddedRowCount() { + return countSubNodesRecursively(getContainerDataSource(), toggledItemId); + } + + private int countSubNodesRecursively(Hierarchical hc, Object itemId) { + int count = 0; + // we need the number of children for toggledItemId no matter if its + // collapsed or expanded. Other items' children are only counted if the + // item is expanded. + if (getContainerStrategy().isNodeOpen(itemId) + || itemId == toggledItemId) { + Collection<?> children = hc.getChildren(itemId); + if (children != null) { + count += children != null ? children.size() : 0; + for (Object id : children) { + count += countSubNodesRecursively(hc, id); + } + } + } + return count; + } + + @Override + protected int getFirstUpdatedItemIndex() { + return indexOfId(toggledItemId); + } + + @Override + protected int getUpdatedRowCount() { + return 1; + } + + @Override + protected boolean shouldHideAddedRows() { + return !getContainerStrategy().isNodeOpen(toggledItemId); + } + + private void toggleChildVisibility(Object itemId, boolean forceFullRefresh) { + getContainerStrategy().toggleChildVisibility(itemId); + // ensure that page still has first item in page, DON'T clear the + // caches. + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false); + + if (isCollapsed(itemId)) { + fireCollapseEvent(itemId); + } else { + fireExpandEvent(itemId); + } + + if (containerSupportsPartialUpdates && !forceFullRefresh) { + requestRepaint(); + } else { + // For containers that do not send item set change events, always do + // full repaint instead of partial row update. + refreshRowCache(); + } + } + + @Override + public int size() { + return getContainerStrategy().size(); + } + + @Override + public Hierarchical getContainerDataSource() { + return (Hierarchical) super.getContainerDataSource(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + cStrategy = null; + + // FIXME: This disables partial updates until TreeTable is fixed so it + // does not change component hierarchy during paint + containerSupportsPartialUpdates = (newDataSource instanceof ItemSetChangeNotifier) && false; + + if (!(newDataSource instanceof Hierarchical)) { + newDataSource = new ContainerHierarchicalWrapper(newDataSource); + } + + if (!(newDataSource instanceof Ordered)) { + newDataSource = new HierarchicalContainerOrderedWrapper( + (Hierarchical) newDataSource); + } + + super.setContainerDataSource(newDataSource); + } + + @Override + public void containerItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + // Can't do partial repaints if items are added or removed during the + // expand/collapse request + toggledItemId = null; + getContainerStrategy().containerItemSetChange(event); + super.containerItemSetChange(event); + } + + @Override + protected Object getIdByIndex(int index) { + return getContainerStrategy().getIdByIndex(index); + } + + @Override + protected int indexOfId(Object itemId) { + return getContainerStrategy().indexOfId(itemId); + } + + @Override + public Object nextItemId(Object itemId) { + return getContainerStrategy().nextItemId(itemId); + } + + @Override + public Object lastItemId() { + return getContainerStrategy().lastItemId(); + } + + @Override + public Object prevItemId(Object itemId) { + return getContainerStrategy().prevItemId(itemId); + } + + @Override + public boolean isLastId(Object itemId) { + return getContainerStrategy().isLastId(itemId); + } + + @Override + public Collection<?> getItemIds() { + return getContainerStrategy().getItemIds(); + } + + @Override + public boolean areChildrenAllowed(Object itemId) { + return getContainerDataSource().areChildrenAllowed(itemId); + } + + @Override + public Collection<?> getChildren(Object itemId) { + return getContainerDataSource().getChildren(itemId); + } + + @Override + public Object getParent(Object itemId) { + return getContainerDataSource().getParent(itemId); + } + + @Override + public boolean hasChildren(Object itemId) { + return getContainerDataSource().hasChildren(itemId); + } + + @Override + public boolean isRoot(Object itemId) { + return getContainerDataSource().isRoot(itemId); + } + + @Override + public Collection<?> rootItemIds() { + return getContainerDataSource().rootItemIds(); + } + + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + return getContainerDataSource().setChildrenAllowed(itemId, + areChildrenAllowed); + } + + @Override + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + return getContainerDataSource().setParent(itemId, newParentId); + } + + /** + * Sets the Item specified by given identifier as collapsed or expanded. If + * the Item is collapsed, its children are not displayed to the user. + * + * @param itemId + * the identifier of the Item + * @param collapsed + * true if the Item should be collapsed, false if expanded + */ + public void setCollapsed(Object itemId, boolean collapsed) { + if (isCollapsed(itemId) != collapsed) { + if (null == toggledItemId && !isRowCacheInvalidated() + && getVisibleItemIds().contains(itemId)) { + // optimization: partial refresh if only one item is + // collapsed/expanded + toggledItemId = itemId; + toggleChildVisibility(itemId, false); + } else { + // make sure a full refresh takes place - otherwise neither + // partial nor full repaint of table content is performed + toggledItemId = null; + toggleChildVisibility(itemId, true); + } + } + } + + /** + * Checks if Item with given identifier is collapsed in the UI. + * + * <p> + * + * @param itemId + * the identifier of the checked Item + * @return true if the Item with given id is collapsed + * @see Collapsible#isCollapsed(Object) + */ + public boolean isCollapsed(Object itemId) { + return !getContainerStrategy().isNodeOpen(itemId); + } + + /** + * Explicitly sets the column in which the TreeTable visualizes the + * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized + * in the first visible column. + * + * @param hierarchyColumnId + */ + public void setHierarchyColumn(Object hierarchyColumnId) { + this.hierarchyColumnId = hierarchyColumnId; + } + + /** + * @return the identifier of column into which the hierarchy will be + * visualized or null if the column is not explicitly defined. + */ + public Object getHierarchyColumnId() { + return hierarchyColumnId; + } + + /** + * Adds an expand listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * Removes an expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * Emits an expand event. + * + * @param itemId + * the item id. + */ + protected void fireExpandEvent(Object itemId) { + fireEvent(new ExpandEvent(this, itemId)); + } + + /** + * Adds a collapse listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Removes a collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Emits a collapse event. + * + * @param itemId + * the item id. + */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /** + * @return true if animations are enabled + */ + public boolean isAnimationsEnabled() { + return animationsEnabled; + } + + /** + * Animations can be enabled by passing true to this method. Currently + * expanding rows slide in from the top and collapsing rows slide out the + * same way. NOTE! not supported in Internet Explorer 6 or 7. + * + * @param animationsEnabled + * true or false whether to enable animations or not. + */ + public void setAnimationsEnabled(boolean animationsEnabled) { + this.animationsEnabled = animationsEnabled; + requestRepaint(); + } + + private static final Logger getLogger() { + return Logger.getLogger(TreeTable.class.getName()); + } + +} diff --git a/server/src/com/vaadin/ui/TwinColSelect.java b/server/src/com/vaadin/ui/TwinColSelect.java new file mode 100644 index 0000000000..5539236f77 --- /dev/null +++ b/server/src/com/vaadin/ui/TwinColSelect.java @@ -0,0 +1,180 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.twincolselect.VTwinColSelect; + +/** + * Multiselect component with two lists: left side for available items and right + * side for selected items. + */ +@SuppressWarnings("serial") +public class TwinColSelect extends AbstractSelect { + + private int columns = 0; + private int rows = 0; + + private String leftColumnCaption; + private String rightColumnCaption; + + /** + * + */ + public TwinColSelect() { + super(); + setMultiSelect(true); + } + + /** + * @param caption + */ + public TwinColSelect(String caption) { + super(caption); + setMultiSelect(true); + } + + /** + * @param caption + * @param dataSource + */ + public TwinColSelect(String caption, Container dataSource) { + super(caption, dataSource); + setMultiSelect(true); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * <p> + * The number of columns overrides the value set by setWidth. Only if + * columns are set to 0 (default) the width set using + * {@link #setWidth(float, int)} or {@link #setWidth(String)} is used. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + requestRepaint(); + } + } + + public int getColumns() { + return columns; + } + + public int getRows() { + return rows; + } + + /** + * Sets the number of rows in the editor. If the number of rows is set to 0, + * the actual number of displayed rows is determined implicitly by the + * adapter. + * <p> + * If a height if set (using {@link #setHeight(String)} or + * {@link #setHeight(float, int)}) it overrides the number of rows. Leave + * the height undefined to use this method. This is the opposite of how + * {@link #setColumns(int)} work. + * + * + * @param rows + * the number of rows to set. + */ + public void setRows(int rows) { + if (rows < 0) { + rows = 0; + } + if (this.rows != rows) { + this.rows = rows; + requestRepaint(); + } + } + + /** + * @param caption + * @param options + */ + public TwinColSelect(String caption, Collection<?> options) { + super(caption, options); + setMultiSelect(true); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "twincol"); + // Adds the number of columns + if (columns != 0) { + target.addAttribute("cols", columns); + } + // Adds the number of rows + if (rows != 0) { + target.addAttribute("rows", rows); + } + + // Right and left column captions and/or icons (if set) + String lc = getLeftColumnCaption(); + String rc = getRightColumnCaption(); + if (lc != null) { + target.addAttribute(VTwinColSelect.ATTRIBUTE_LEFT_CAPTION, lc); + } + if (rc != null) { + target.addAttribute(VTwinColSelect.ATTRIBUTE_RIGHT_CAPTION, rc); + } + + super.paintContent(target); + } + + /** + * Sets the text shown above the right column. + * + * @param caption + * The text to show + */ + public void setRightColumnCaption(String rightColumnCaption) { + this.rightColumnCaption = rightColumnCaption; + requestRepaint(); + } + + /** + * Returns the text shown above the right column. + * + * @return The text shown or null if not set. + */ + public String getRightColumnCaption() { + return rightColumnCaption; + } + + /** + * Sets the text shown above the left column. + * + * @param caption + * The text to show + */ + public void setLeftColumnCaption(String leftColumnCaption) { + this.leftColumnCaption = leftColumnCaption; + requestRepaint(); + } + + /** + * Returns the text shown above the left column. + * + * @return The text shown or null if not set. + */ + public String getLeftColumnCaption() { + return leftColumnCaption; + } + +} diff --git a/server/src/com/vaadin/ui/UniqueSerializable.java b/server/src/com/vaadin/ui/UniqueSerializable.java new file mode 100644 index 0000000000..828b285538 --- /dev/null +++ b/server/src/com/vaadin/ui/UniqueSerializable.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +/** + * A base class for generating an unique object that is serializable. + * <p> + * This class is abstract but has no abstract methods to force users to create + * an anonymous inner class. Otherwise each instance will not be unique. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0 + * + */ +public abstract class UniqueSerializable implements Serializable { + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return getClass() == obj.getClass(); + } +} diff --git a/server/src/com/vaadin/ui/Upload.java b/server/src/com/vaadin/ui/Upload.java new file mode 100644 index 0000000000..9d533b67f6 --- /dev/null +++ b/server/src/com/vaadin/ui/Upload.java @@ -0,0 +1,1055 @@ +/* + * @VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.StreamVariable.StreamingProgressEvent; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.server.NoInputStreamException; +import com.vaadin.terminal.gwt.server.NoOutputStreamException; + +/** + * Component for uploading files from client to server. + * + * <p> + * The visible component consists of a file name input box and a browse button + * and an upload submit button to start uploading. + * + * <p> + * The Upload component needs a java.io.OutputStream to write the uploaded data. + * You need to implement the Upload.Receiver interface and return the output + * stream in the receiveUpload() method. + * + * <p> + * You can get an event regarding starting (StartedEvent), progress + * (ProgressEvent), and finishing (FinishedEvent) of upload by implementing + * StartedListener, ProgressListener, and FinishedListener, respectively. The + * FinishedListener is called for both failed and succeeded uploads. If you wish + * to separate between these two cases, you can use SucceededListener + * (SucceededEvenet) and FailedListener (FailedEvent). + * + * <p> + * The upload component does not itself show upload progress, but you can use + * the ProgressIndicator for providing progress feedback by implementing + * ProgressListener and updating the indicator in updateProgress(). + * + * <p> + * Setting upload component immediate initiates the upload as soon as a file is + * selected, instead of the common pattern of file selection field and upload + * button. + * + * <p> + * Note! Because of browser dependent implementations of <input type="file"> + * element, setting size for Upload component is not supported. For some + * browsers setting size may work to some extend. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Upload extends AbstractComponent implements Component.Focusable, + Vaadin6Component { + + /** + * Should the field be focused on next repaint? + */ + private final boolean focus = false; + + /** + * The tab order number of this field. + */ + private int tabIndex = 0; + + /** + * The output of the upload is redirected to this receiver. + */ + private Receiver receiver; + + private boolean isUploading; + + private long contentLength = -1; + + private int totalBytes; + + private String buttonCaption = "Upload"; + + /** + * ProgressListeners to which information about progress is sent during + * upload + */ + private LinkedHashSet<ProgressListener> progressListeners; + + private boolean interrupted = false; + + private boolean notStarted; + + private int nextid; + + /** + * Flag to indicate that submitting file has been requested. + */ + private boolean forceSubmit; + + /** + * Creates a new instance of Upload. + * + * The receiver must be set before performing an upload. + */ + public Upload() { + } + + public Upload(String caption, Receiver uploadReceiver) { + setCaption(caption); + receiver = uploadReceiver; + } + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("pollForStart")) { + int id = (Integer) variables.get("pollForStart"); + if (!isUploading && id == nextid) { + notStarted = true; + requestRepaint(); + } else { + } + } + } + + /** + * Paints the content of this component. + * + * @param target + * Target to paint the content on. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (notStarted) { + target.addAttribute("notStarted", true); + notStarted = false; + return; + } + if (forceSubmit) { + target.addAttribute("forceSubmit", true); + forceSubmit = true; + return; + } + // The field should be focused + if (focus) { + target.addAttribute("focus", true); + } + + // The tab ordering number + if (tabIndex >= 0) { + target.addAttribute("tabindex", tabIndex); + } + + target.addAttribute("state", isUploading); + + if (buttonCaption != null) { + target.addAttribute("buttoncaption", buttonCaption); + } + + target.addAttribute("nextid", nextid); + + // Post file to this strean variable + target.addVariable(this, "action", getStreamVariable()); + + } + + /** + * Interface that must be implemented by the upload receivers to provide the + * Upload component an output stream to write the uploaded data. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Receiver extends Serializable { + + /** + * Invoked when a new upload arrives. + * + * @param filename + * the desired filename of the upload, usually as specified + * by the client. + * @param mimeType + * the MIME type of the uploaded file. + * @return Stream to which the uploaded file should be written. + */ + public OutputStream receiveUpload(String filename, String mimeType); + + } + + /* Upload events */ + + private static final Method UPLOAD_FINISHED_METHOD; + + private static final Method UPLOAD_FAILED_METHOD; + + private static final Method UPLOAD_SUCCEEDED_METHOD; + + private static final Method UPLOAD_STARTED_METHOD; + + static { + try { + UPLOAD_FINISHED_METHOD = FinishedListener.class.getDeclaredMethod( + "uploadFinished", new Class[] { FinishedEvent.class }); + UPLOAD_FAILED_METHOD = FailedListener.class.getDeclaredMethod( + "uploadFailed", new Class[] { FailedEvent.class }); + UPLOAD_STARTED_METHOD = StartedListener.class.getDeclaredMethod( + "uploadStarted", new Class[] { StartedEvent.class }); + UPLOAD_SUCCEEDED_METHOD = SucceededListener.class + .getDeclaredMethod("uploadSucceeded", + new Class[] { SucceededEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in Upload"); + } + } + + /** + * Upload.FinishedEvent is sent when the upload receives a file, regardless + * of whether the reception was successful or failed. If you wish to + * distinguish between the two cases, use either SucceededEvent or + * FailedEvent, which are both subclasses of the FinishedEvent. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class FinishedEvent extends Component.Event { + + /** + * Length of the received file. + */ + private final long length; + + /** + * MIME type of the received file. + */ + private final String type; + + /** + * Received file name. + */ + private final String filename; + + /** + * + * @param source + * the source of the file. + * @param filename + * the received file name. + * @param MIMEType + * the MIME type of the received file. + * @param length + * the length of the received file. + */ + public FinishedEvent(Upload source, String filename, String MIMEType, + long length) { + super(source); + type = MIMEType; + this.filename = filename; + this.length = length; + } + + /** + * Uploads where the event occurred. + * + * @return the Source of the event. + */ + public Upload getUpload() { + return (Upload) getSource(); + } + + /** + * Gets the file name. + * + * @return the filename. + */ + public String getFilename() { + return filename; + } + + /** + * Gets the MIME Type of the file. + * + * @return the MIME type. + */ + public String getMIMEType() { + return type; + } + + /** + * Gets the length of the file. + * + * @return the length. + */ + public long getLength() { + return length; + } + + } + + /** + * Upload.FailedEvent event is sent when the upload is received, but the + * reception is interrupted for some reason. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class FailedEvent extends FinishedEvent { + + private Exception reason = null; + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + * @param exception + */ + public FailedEvent(Upload source, String filename, String MIMEType, + long length, Exception reason) { + this(source, filename, MIMEType, length); + this.reason = reason; + } + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + * @param exception + */ + public FailedEvent(Upload source, String filename, String MIMEType, + long length) { + super(source, filename, MIMEType, length); + } + + /** + * Gets the exception that caused the failure. + * + * @return the exception that caused the failure, null if n/a + */ + public Exception getReason() { + return reason; + } + + } + + /** + * FailedEvent that indicates that an output stream could not be obtained. + */ + public static class NoOutputStreamEvent extends FailedEvent { + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public NoOutputStreamEvent(Upload source, String filename, + String MIMEType, long length) { + super(source, filename, MIMEType, length); + } + } + + /** + * FailedEvent that indicates that an input stream could not be obtained. + */ + public static class NoInputStreamEvent extends FailedEvent { + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public NoInputStreamEvent(Upload source, String filename, + String MIMEType, long length) { + super(source, filename, MIMEType, length); + } + + } + + /** + * Upload.SucceededEvent event is sent when the upload is received + * successfully. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class SucceededEvent extends FinishedEvent { + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public SucceededEvent(Upload source, String filename, String MIMEType, + long length) { + super(source, filename, MIMEType, length); + } + + } + + /** + * Upload.StartedEvent event is sent when the upload is started to received. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + public static class StartedEvent extends Component.Event { + + private final String filename; + private final String type; + /** + * Length of the received file. + */ + private final long length; + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public StartedEvent(Upload source, String filename, String MIMEType, + long contentLength) { + super(source); + this.filename = filename; + type = MIMEType; + length = contentLength; + } + + /** + * Uploads where the event occurred. + * + * @return the Source of the event. + */ + public Upload getUpload() { + return (Upload) getSource(); + } + + /** + * Gets the file name. + * + * @return the filename. + */ + public String getFilename() { + return filename; + } + + /** + * Gets the MIME Type of the file. + * + * @return the MIME type. + */ + public String getMIMEType() { + return type; + } + + /** + * @return the length of the file that is being uploaded + */ + public long getContentLength() { + return length; + } + + } + + /** + * Receives the events when the upload starts. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + public interface StartedListener extends Serializable { + + /** + * Upload has started. + * + * @param event + * the Upload started event. + */ + public void uploadStarted(StartedEvent event); + } + + /** + * Receives the events when the uploads are ready. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface FinishedListener extends Serializable { + + /** + * Upload has finished. + * + * @param event + * the Upload finished event. + */ + public void uploadFinished(FinishedEvent event); + } + + /** + * Receives events when the uploads are finished, but unsuccessful. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface FailedListener extends Serializable { + + /** + * Upload has finished unsuccessfully. + * + * @param event + * the Upload failed event. + */ + public void uploadFailed(FailedEvent event); + } + + /** + * Receives events when the uploads are successfully finished. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface SucceededListener extends Serializable { + + /** + * Upload successfull.. + * + * @param event + * the Upload successfull event. + */ + public void uploadSucceeded(SucceededEvent event); + } + + /** + * Adds the upload started event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(StartedListener listener) { + addListener(StartedEvent.class, listener, UPLOAD_STARTED_METHOD); + } + + /** + * Removes the upload started event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(StartedListener listener) { + removeListener(StartedEvent.class, listener, UPLOAD_STARTED_METHOD); + } + + /** + * Adds the upload received event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(FinishedListener listener) { + addListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD); + } + + /** + * Removes the upload received event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(FinishedListener listener) { + removeListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD); + } + + /** + * Adds the upload interrupted event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(FailedListener listener) { + addListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD); + } + + /** + * Removes the upload interrupted event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(FailedListener listener) { + removeListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD); + } + + /** + * Adds the upload success event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(SucceededListener listener) { + addListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD); + } + + /** + * Removes the upload success event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(SucceededListener listener) { + removeListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD); + } + + /** + * Adds the upload success event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ProgressListener listener) { + if (progressListeners == null) { + progressListeners = new LinkedHashSet<ProgressListener>(); + } + progressListeners.add(listener); + } + + /** + * Removes the upload success event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ProgressListener listener) { + if (progressListeners != null) { + progressListeners.remove(listener); + } + } + + /** + * Emit upload received event. + * + * @param filename + * @param MIMEType + * @param length + */ + protected void fireStarted(String filename, String MIMEType) { + fireEvent(new Upload.StartedEvent(this, filename, MIMEType, + contentLength)); + } + + /** + * Emits the upload failed event. + * + * @param filename + * @param MIMEType + * @param length + */ + protected void fireUploadInterrupted(String filename, String MIMEType, + long length) { + fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length)); + } + + protected void fireNoInputStream(String filename, String MIMEType, + long length) { + fireEvent(new Upload.NoInputStreamEvent(this, filename, MIMEType, + length)); + } + + protected void fireNoOutputStream(String filename, String MIMEType, + long length) { + fireEvent(new Upload.NoOutputStreamEvent(this, filename, MIMEType, + length)); + } + + protected void fireUploadInterrupted(String filename, String MIMEType, + long length, Exception e) { + fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length, e)); + } + + /** + * Emits the upload success event. + * + * @param filename + * @param MIMEType + * @param length + * + */ + protected void fireUploadSuccess(String filename, String MIMEType, + long length) { + fireEvent(new Upload.SucceededEvent(this, filename, MIMEType, length)); + } + + /** + * Emits the progress event. + * + * @param totalBytes + * bytes received so far + * @param contentLength + * actual size of the file being uploaded, if known + * + */ + protected void fireUpdateProgress(long totalBytes, long contentLength) { + // this is implemented differently than other listeners to maintain + // backwards compatibility + if (progressListeners != null) { + for (Iterator<ProgressListener> it = progressListeners.iterator(); it + .hasNext();) { + ProgressListener l = it.next(); + l.updateProgress(totalBytes, contentLength); + } + } + } + + /** + * Returns the current receiver. + * + * @return the StreamVariable. + */ + public Receiver getReceiver() { + return receiver; + } + + /** + * Sets the receiver. + * + * @param receiver + * the receiver to set. + */ + public void setReceiver(Receiver receiver) { + this.receiver = receiver; + } + + /** + * {@inheritDoc} + */ + @Override + public void focus() { + super.focus(); + } + + /** + * Gets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + @Override + public int getTabIndex() { + return tabIndex; + } + + /** + * Sets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + /** + * Go into upload state. This is to prevent double uploading on same + * component. + * + * Warning: this is an internal method used by the framework and should not + * be used by user of the Upload component. Using it results in the Upload + * component going in wrong state and not working. It is currently public + * because it is used by another class. + */ + public void startUpload() { + if (isUploading) { + throw new IllegalStateException("uploading already started"); + } + isUploading = true; + nextid++; + } + + /** + * Interrupts the upload currently being received. The interruption will be + * done by the receiving tread so this method will return immediately and + * the actual interrupt will happen a bit later. + */ + public void interruptUpload() { + if (isUploading) { + interrupted = true; + } + } + + /** + * Go into state where new uploading can begin. + * + * Warning: this is an internal method used by the framework and should not + * be used by user of the Upload component. + */ + private void endUpload() { + isUploading = false; + contentLength = -1; + interrupted = false; + requestRepaint(); + } + + public boolean isUploading() { + return isUploading; + } + + /** + * Gets read bytes of the file currently being uploaded. + * + * @return bytes + */ + public long getBytesRead() { + return totalBytes; + } + + /** + * Returns size of file currently being uploaded. Value sane only during + * upload. + * + * @return size in bytes + */ + public long getUploadSize() { + return contentLength; + } + + /** + * This method is deprecated, use addListener(ProgressListener) instead. + * + * @deprecated Use addListener(ProgressListener) instead. + * @param progressListener + */ + @Deprecated + public void setProgressListener(ProgressListener progressListener) { + addListener(progressListener); + } + + /** + * This method is deprecated. + * + * @deprecated Replaced with addListener/removeListener + * @return listener + * + */ + @Deprecated + public ProgressListener getProgressListener() { + if (progressListeners == null || progressListeners.isEmpty()) { + return null; + } else { + return progressListeners.iterator().next(); + } + } + + /** + * ProgressListener receives events to track progress of upload. + */ + public interface ProgressListener extends Serializable { + /** + * Updates progress to listener + * + * @param readBytes + * bytes transferred + * @param contentLength + * total size of file currently being uploaded, -1 if unknown + */ + public void updateProgress(long readBytes, long contentLength); + } + + /** + * @return String to be rendered into button that fires uploading + */ + public String getButtonCaption() { + return buttonCaption; + } + + /** + * In addition to the actual file chooser, upload components have button + * that starts actual upload progress. This method is used to set text in + * that button. + * <p> + * In case the button text is set to null, the button is hidden. In this + * case developer must explicitly initiate the upload process with + * {@link #submitUpload()}. + * <p> + * In case the Upload is used in immediate mode using + * {@link #setImmediate(boolean)}, the file choose (html input with type + * "file") is hidden and only the button with this text is shown. + * <p> + * + * <p> + * <strong>Note</strong> the string given is set as is to the button. HTML + * formatting is not stripped. Be sure to properly validate your value + * according to your needs. + * + * @param buttonCaption + * text for upload components button. + */ + public void setButtonCaption(String buttonCaption) { + this.buttonCaption = buttonCaption; + requestRepaint(); + } + + /** + * Forces the upload the send selected file to the server. + * <p> + * In case developer wants to use this feature, he/she will most probably + * want to hide the uploads internal submit button by setting its caption to + * null with {@link #setButtonCaption(String)} method. + * <p> + * Note, that the upload runs asynchronous. Developer should use normal + * upload listeners to trac the process of upload. If the field is empty + * uploaded the file name will be empty string and file length 0 in the + * upload finished event. + * <p> + * Also note, that the developer should not remove or modify the upload in + * the same user transaction where the upload submit is requested. The + * upload may safely be hidden or removed once the upload started event is + * fired. + */ + public void submitUpload() { + requestRepaint(); + forceSubmit = true; + } + + @Override + public void requestRepaint() { + forceSubmit = false; + super.requestRepaint(); + } + + /* + * Handle to terminal via Upload monitors and controls the upload during it + * is being streamed. + */ + private com.vaadin.terminal.StreamVariable streamVariable; + + protected com.vaadin.terminal.StreamVariable getStreamVariable() { + if (streamVariable == null) { + streamVariable = new com.vaadin.terminal.StreamVariable() { + private StreamingStartEvent lastStartedEvent; + + @Override + public boolean listenProgress() { + return (progressListeners != null && !progressListeners + .isEmpty()); + } + + @Override + public void onProgress(StreamingProgressEvent event) { + fireUpdateProgress(event.getBytesReceived(), + event.getContentLength()); + } + + @Override + public boolean isInterrupted() { + return interrupted; + } + + @Override + public OutputStream getOutputStream() { + OutputStream receiveUpload = receiver.receiveUpload( + lastStartedEvent.getFileName(), + lastStartedEvent.getMimeType()); + lastStartedEvent = null; + return receiveUpload; + } + + @Override + public void streamingStarted(StreamingStartEvent event) { + startUpload(); + contentLength = event.getContentLength(); + fireStarted(event.getFileName(), event.getMimeType()); + lastStartedEvent = event; + } + + @Override + public void streamingFinished(StreamingEndEvent event) { + fireUploadSuccess(event.getFileName(), event.getMimeType(), + event.getContentLength()); + endUpload(); + requestRepaint(); + } + + @Override + public void streamingFailed(StreamingErrorEvent event) { + Exception exception = event.getException(); + if (exception instanceof NoInputStreamException) { + fireNoInputStream(event.getFileName(), + event.getMimeType(), 0); + } else if (exception instanceof NoOutputStreamException) { + fireNoOutputStream(event.getFileName(), + event.getMimeType(), 0); + } else { + fireUploadInterrupted(event.getFileName(), + event.getMimeType(), 0, exception); + } + endUpload(); + } + }; + } + return streamVariable; + } + + @Override + public java.util.Collection<?> getListeners(java.lang.Class<?> eventType) { + if (StreamingProgressEvent.class.isAssignableFrom(eventType)) { + if (progressListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections.unmodifiableCollection(progressListeners); + } + + } + return super.getListeners(eventType); + }; +} diff --git a/server/src/com/vaadin/ui/VerticalLayout.java b/server/src/com/vaadin/ui/VerticalLayout.java new file mode 100644 index 0000000000..a04d052d98 --- /dev/null +++ b/server/src/com/vaadin/ui/VerticalLayout.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * Vertical layout + * + * <code>VerticalLayout</code> is a component container, which shows the + * subcomponents in the order of their addition (vertically). A vertical layout + * is by default 100% wide. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.3 + */ +@SuppressWarnings("serial") +public class VerticalLayout extends AbstractOrderedLayout { + + public VerticalLayout() { + setWidth("100%"); + } + +} diff --git a/server/src/com/vaadin/ui/VerticalSplitPanel.java b/server/src/com/vaadin/ui/VerticalSplitPanel.java new file mode 100644 index 0000000000..0630240e9c --- /dev/null +++ b/server/src/com/vaadin/ui/VerticalSplitPanel.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * A vertical split panel contains two components and lays them vertically. The + * first component is above the second component. + * + * <pre> + * +--------------------------+ + * | | + * | The first component | + * | | + * +==========================+ <-- splitter + * | | + * | The second component | + * | | + * +--------------------------+ + * </pre> + * + */ +public class VerticalSplitPanel extends AbstractSplitPanel { + + public VerticalSplitPanel() { + super(); + setSizeFull(); + } + +} diff --git a/server/src/com/vaadin/ui/Video.java b/server/src/com/vaadin/ui/Video.java new file mode 100644 index 0000000000..d4f95a5be3 --- /dev/null +++ b/server/src/com/vaadin/ui/Video.java @@ -0,0 +1,81 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.shared.ui.video.VideoState; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.server.ResourceReference; + +/** + * The Video component translates into an HTML5 <video> element and as + * such is only supported in browsers that support HTML5 media markup. Browsers + * that do not support HTML5 display the text or HTML set by calling + * {@link #setAltText(String)}. + * + * A flash-player fallback can be implemented by setting HTML content allowed ( + * {@link #setHtmlContentAllowed(boolean)} and calling + * {@link #setAltText(String)} with the flash player markup. An example of flash + * fallback can be found at the <a href= + * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash" + * >Mozilla Developer Network</a>. + * + * Multiple sources can be specified. Which of the sources is used is selected + * by the browser depending on which file formats it supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @author Vaadin Ltd + * @since 6.7.0 + */ +public class Video extends AbstractMedia { + + @Override + public VideoState getState() { + return (VideoState) super.getState(); + } + + public Video() { + this("", null); + } + + /** + * @param caption + * The caption for this video. + */ + public Video(String caption) { + this(caption, null); + } + + /** + * @param caption + * The caption for this video. + * @param source + * The Resource containing the video to play. + */ + public Video(String caption, Resource source) { + setCaption(caption); + setSource(source); + setShowControls(true); + } + + /** + * Sets the poster image, which is shown in place of the video before the + * user presses play. + * + * @param poster + */ + public void setPoster(Resource poster) { + getState().setPoster(ResourceReference.create(poster)); + requestRepaint(); + } + + /** + * @return The poster image. + */ + public Resource getPoster() { + return ResourceReference.getResource(getState().getPoster()); + } + +} diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java new file mode 100644 index 0000000000..e413d35e6d --- /dev/null +++ b/server/src/com/vaadin/ui/Window.java @@ -0,0 +1,853 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; + +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.ShortcutAction; +import com.vaadin.event.ShortcutAction.KeyCode; +import com.vaadin.event.ShortcutAction.ModifierKey; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.window.WindowServerRpc; +import com.vaadin.shared.ui.window.WindowState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; + +/** + * A component that represents a floating popup window that can be added to a + * {@link Root}. A window is added to a {@code Root} using + * {@link Root#addWindow(Window)}. </p> + * <p> + * The contents of a window is set using {@link #setContent(ComponentContainer)} + * or by using the {@link #Window(String, ComponentContainer)} constructor. The + * contents can in turn contain other components. By default, a + * {@link VerticalLayout} is used as content. + * </p> + * <p> + * A window can be positioned on the screen using absolute coordinates (pixels) + * or set to be centered using {@link #center()} + * </p> + * <p> + * The caption is displayed in the window header. + * </p> + * <p> + * In Vaadin versions prior to 7.0.0, Window was also used as application level + * windows. This function is now covered by the {@link Root} class. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Window extends Panel implements FocusNotifier, BlurNotifier, + Vaadin6Component { + + private WindowServerRpc rpc = new WindowServerRpc() { + + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Window.this, mouseDetails)); + } + }; + + private int browserWindowWidth = -1; + + private int browserWindowHeight = -1; + + /** + * Creates a new unnamed window with a default layout. + */ + public Window() { + this("", null); + } + + /** + * Creates a new unnamed window with a default layout and given title. + * + * @param caption + * the title of the window. + */ + public Window(String caption) { + this(caption, null); + } + + /** + * Creates a new unnamed window with the given content and title. + * + * @param caption + * the title of the window. + * @param content + * the contents of the window + */ + public Window(String caption, ComponentContainer content) { + super(caption, content); + registerRpc(rpc); + setSizeUndefined(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Panel#addComponent(com.vaadin.ui.Component) + */ + + @Override + public void addComponent(Component c) { + if (c instanceof Window) { + throw new IllegalArgumentException( + "Window cannot be added to another via addComponent. " + + "Use addWindow(Window) instead."); + } + super.addComponent(c); + } + + /* ********************************************************************* */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Panel#paintContent(com.vaadin.terminal.PaintTarget) + */ + + @Override + public synchronized void paintContent(PaintTarget target) + throws PaintException { + if (bringToFront != null) { + target.addAttribute("bringToFront", bringToFront.intValue()); + bringToFront = null; + } + + // Contents of the window panel is painted + super.paintContent(target); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Panel#changeVariables(java.lang.Object, java.util.Map) + */ + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + // TODO Are these for top level windows or sub windows? + boolean sizeHasChanged = false; + // size is handled in super class, but resize events only in windows -> + // so detect if size change occurs before super.changeVariables() + if (variables.containsKey("height") + && (getHeightUnits() != Unit.PIXELS || (Integer) variables + .get("height") != getHeight())) { + sizeHasChanged = true; + } + if (variables.containsKey("width") + && (getWidthUnits() != Unit.PIXELS || (Integer) variables + .get("width") != getWidth())) { + sizeHasChanged = true; + } + Integer browserHeightVar = (Integer) variables + .get(VRoot.BROWSER_HEIGHT_VAR); + if (browserHeightVar != null + && browserHeightVar.intValue() != browserWindowHeight) { + browserWindowHeight = browserHeightVar.intValue(); + sizeHasChanged = true; + } + Integer browserWidthVar = (Integer) variables + .get(VRoot.BROWSER_WIDTH_VAR); + if (browserWidthVar != null + && browserWidthVar.intValue() != browserWindowWidth) { + browserWindowWidth = browserWidthVar.intValue(); + sizeHasChanged = true; + } + + super.changeVariables(source, variables); + + // Positioning + final Integer positionx = (Integer) variables.get("positionx"); + if (positionx != null) { + final int x = positionx.intValue(); + // This is information from the client so it is already using the + // position. No need to repaint. + setPositionX(x < 0 ? -1 : x, false); + } + final Integer positiony = (Integer) variables.get("positiony"); + if (positiony != null) { + final int y = positiony.intValue(); + // This is information from the client so it is already using the + // position. No need to repaint. + setPositionY(y < 0 ? -1 : y, false); + } + + if (isClosable()) { + // Closing + final Boolean close = (Boolean) variables.get("close"); + if (close != null && close.booleanValue()) { + close(); + } + } + + // fire event if size has really changed + if (sizeHasChanged) { + fireResize(); + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } else if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + + } + + /** + * Method that handles window closing (from UI). + * + * <p> + * By default, sub-windows are removed from their respective parent windows + * and thus visually closed on browser-side. Browser-level windows also + * closed on the client-side, but they are not implicitly removed from the + * application. + * </p> + * + * <p> + * To explicitly close a sub-window, use {@link #removeWindow(Window)}. To + * react to a window being closed (after it is closed), register a + * {@link CloseListener}. + * </p> + */ + public void close() { + Root root = getRoot(); + + // Don't do anything if not attached to a root + if (root != null) { + // focus is restored to the parent window + root.focus(); + // subwindow is removed from the root + root.removeWindow(this); + } + } + + /** + * Gets the distance of Window left border in pixels from left border of the + * containing (main window). + * + * @return the Distance of Window left border in pixels from left border of + * the containing (main window). or -1 if unspecified. + * @since 4.0.0 + */ + public int getPositionX() { + return getState().getPositionX(); + } + + /** + * Sets the distance of Window left border in pixels from left border of the + * containing (main window). + * + * @param positionX + * the Distance of Window left border in pixels from left border + * of the containing (main window). or -1 if unspecified. + * @since 4.0.0 + */ + public void setPositionX(int positionX) { + setPositionX(positionX, true); + } + + /** + * Sets the distance of Window left border in pixels from left border of the + * containing (main window). + * + * @param positionX + * the Distance of Window left border in pixels from left border + * of the containing (main window). or -1 if unspecified. + * @param repaintRequired + * true if the window needs to be repainted, false otherwise + * @since 6.3.4 + */ + private void setPositionX(int positionX, boolean repaintRequired) { + getState().setPositionX(positionX); + getState().setCentered(false); + if (repaintRequired) { + requestRepaint(); + } + } + + /** + * Gets the distance of Window top border in pixels from top border of the + * containing (main window). + * + * @return Distance of Window top border in pixels from top border of the + * containing (main window). or -1 if unspecified . + * + * @since 4.0.0 + */ + public int getPositionY() { + return getState().getPositionY(); + } + + /** + * Sets the distance of Window top border in pixels from top border of the + * containing (main window). + * + * @param positionY + * the Distance of Window top border in pixels from top border of + * the containing (main window). or -1 if unspecified + * + * @since 4.0.0 + */ + public void setPositionY(int positionY) { + setPositionY(positionY, true); + } + + /** + * Sets the distance of Window top border in pixels from top border of the + * containing (main window). + * + * @param positionY + * the Distance of Window top border in pixels from top border of + * the containing (main window). or -1 if unspecified + * @param repaintRequired + * true if the window needs to be repainted, false otherwise + * + * @since 6.3.4 + */ + private void setPositionY(int positionY, boolean repaintRequired) { + getState().setPositionY(positionY); + getState().setCentered(false); + if (repaintRequired) { + requestRepaint(); + } + } + + private static final Method WINDOW_CLOSE_METHOD; + static { + try { + WINDOW_CLOSE_METHOD = CloseListener.class.getDeclaredMethod( + "windowClose", new Class[] { CloseEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error, window close method not found"); + } + } + + public class CloseEvent extends Component.Event { + + /** + * + * @param source + */ + public CloseEvent(Component source) { + super(source); + } + + /** + * Gets the Window. + * + * @return the window. + */ + public Window getWindow() { + return (Window) getSource(); + } + } + + /** + * An interface used for listening to Window close events. Add the + * CloseListener to a browser level window or a sub window and + * {@link CloseListener#windowClose(CloseEvent)} will be called whenever the + * user closes the window. + * + * <p> + * Since Vaadin 6.5, removing a window using {@link #removeWindow(Window)} + * fires the CloseListener. + * </p> + */ + public interface CloseListener extends Serializable { + /** + * Called when the user closes a window. Use + * {@link CloseEvent#getWindow()} to get a reference to the + * {@link Window} that was closed. + * + * @param e + * Event containing + */ + public void windowClose(CloseEvent e); + } + + /** + * Adds a CloseListener to the window. + * + * For a sub window the CloseListener is fired when the user closes it + * (clicks on the close button). + * + * For a browser level window the CloseListener is fired when the browser + * level window is closed. Note that closing a browser level window does not + * mean it will be destroyed. Also note that Opera does not send events like + * all other browsers and therefore the close listener might not be called + * if Opera is used. + * + * <p> + * Since Vaadin 6.5, removing windows using {@link #removeWindow(Window)} + * does fire the CloseListener. + * </p> + * + * @param listener + * the CloseListener to add. + */ + public void addListener(CloseListener listener) { + addListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD); + } + + /** + * Removes the CloseListener from the window. + * + * <p> + * For more information on CloseListeners see {@link CloseListener}. + * </p> + * + * @param listener + * the CloseListener to remove. + */ + public void removeListener(CloseListener listener) { + removeListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD); + } + + protected void fireClose() { + fireEvent(new Window.CloseEvent(this)); + } + + /** + * Method for the resize event. + */ + private static final Method WINDOW_RESIZE_METHOD; + static { + try { + WINDOW_RESIZE_METHOD = ResizeListener.class.getDeclaredMethod( + "windowResized", new Class[] { ResizeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error, window resized method not found"); + } + } + + /** + * Resize events are fired whenever the client-side fires a resize-event + * (e.g. the browser window is resized). The frequency may vary across + * browsers. + */ + public class ResizeEvent extends Component.Event { + + /** + * + * @param source + */ + public ResizeEvent(Component source) { + super(source); + } + + /** + * Get the window form which this event originated + * + * @return the window + */ + public Window getWindow() { + return (Window) getSource(); + } + } + + /** + * Listener for window resize events. + * + * @see com.vaadin.ui.Window.ResizeEvent + */ + public interface ResizeListener extends Serializable { + public void windowResized(ResizeEvent e); + } + + /** + * Add a resize listener. + * + * @param listener + */ + public void addListener(ResizeListener listener) { + addListener(ResizeEvent.class, listener, WINDOW_RESIZE_METHOD); + } + + /** + * Remove a resize listener. + * + * @param listener + */ + public void removeListener(ResizeListener listener) { + removeListener(ResizeEvent.class, listener); + } + + /** + * Fire the resize event. + */ + protected void fireResize() { + fireEvent(new ResizeEvent(this)); + } + + /** + * Used to keep the right order of windows if multiple windows are brought + * to front in a single changeset. If this is not used, the order is quite + * random (depends on the order getting to dirty list. e.g. which window got + * variable changes). + */ + private Integer bringToFront = null; + + /** + * If there are currently several windows visible, calling this method makes + * this window topmost. + * <p> + * This method can only be called if this window connected a root. Else an + * illegal state exception is thrown. Also if there are modal windows and + * this window is not modal, and illegal state exception is thrown. + * <p> + */ + public void bringToFront() { + Root root = getRoot(); + if (root == null) { + throw new IllegalStateException( + "Window must be attached to parent before calling bringToFront method."); + } + int maxBringToFront = -1; + for (Window w : root.getWindows()) { + if (!isModal() && w.isModal()) { + throw new IllegalStateException( + "The root contains modal windows, non-modal window cannot be brought to front."); + } + if (w.bringToFront != null) { + maxBringToFront = Math.max(maxBringToFront, + w.bringToFront.intValue()); + } + } + bringToFront = Integer.valueOf(maxBringToFront + 1); + requestRepaint(); + } + + /** + * Sets sub-window modal, so that widgets behind it cannot be accessed. + * <b>Note:</b> affects sub-windows only. + * + * @param modal + * true if modality is to be turned on + */ + public void setModal(boolean modal) { + getState().setModal(modal); + center(); + requestRepaint(); + } + + /** + * @return true if this window is modal. + */ + public boolean isModal() { + return getState().isModal(); + } + + /** + * Sets sub-window resizable. <b>Note:</b> affects sub-windows only. + * + * @param resizable + * true if resizability is to be turned on + */ + public void setResizable(boolean resizable) { + getState().setResizable(resizable); + requestRepaint(); + } + + /** + * + * @return true if window is resizable by the end-user, otherwise false. + */ + public boolean isResizable() { + return getState().isResizable(); + } + + /** + * + * @return true if a delay is used before recalculating sizes, false if + * sizes are recalculated immediately. + */ + public boolean isResizeLazy() { + return getState().isResizeLazy(); + } + + /** + * Should resize operations be lazy, i.e. should there be a delay before + * layout sizes are recalculated. Speeds up resize operations in slow UIs + * with the penalty of slightly decreased usability. + * + * Note, some browser send false resize events for the browser window and + * are therefore always lazy. + * + * @param resizeLazy + * true to use a delay before recalculating sizes, false to + * calculate immediately. + */ + public void setResizeLazy(boolean resizeLazy) { + getState().setResizeLazy(resizeLazy); + requestRepaint(); + } + + /** + * Sets this window to be centered relative to its parent window. Affects + * sub-windows only. If the window is resized as a result of the size of its + * content changing, it will keep itself centered as long as its position is + * not explicitly changed programmatically or by the user. + * <p> + * <b>NOTE:</b> This method has several issues as currently implemented. + * Please refer to http://dev.vaadin.com/ticket/8971 for details. + */ + public void center() { + getState().setCentered(true); + requestRepaint(); + } + + /** + * Returns the closable status of the sub window. If a sub window is + * closable it typically shows an X in the upper right corner. Clicking on + * the X sends a close event to the server. Setting closable to false will + * remove the X from the sub window and prevent the user from closing the + * window. + * + * Note! For historical reasons readonly controls the closability of the sub + * window and therefore readonly and closable affect each other. Setting + * readonly to true will set closable to false and vice versa. + * <p/> + * Closable only applies to sub windows, not to browser level windows. + * + * @return true if the sub window can be closed by the user. + */ + public boolean isClosable() { + return !isReadOnly(); + } + + /** + * Sets the closable status for the sub window. If a sub window is closable + * it typically shows an X in the upper right corner. Clicking on the X + * sends a close event to the server. Setting closable to false will remove + * the X from the sub window and prevent the user from closing the window. + * + * Note! For historical reasons readonly controls the closability of the sub + * window and therefore readonly and closable affect each other. Setting + * readonly to true will set closable to false and vice versa. + * <p/> + * Closable only applies to sub windows, not to browser level windows. + * + * @param closable + * determines if the sub window can be closed by the user. + */ + public void setClosable(boolean closable) { + setReadOnly(!closable); + } + + /** + * Indicates whether a sub window can be dragged or not. By default a sub + * window is draggable. + * <p/> + * Draggable only applies to sub windows, not to browser level windows. + * + * @param draggable + * true if the sub window can be dragged by the user + */ + public boolean isDraggable() { + return getState().isDraggable(); + } + + /** + * Enables or disables that a sub window can be dragged (moved) by the user. + * By default a sub window is draggable. + * <p/> + * Draggable only applies to sub windows, not to browser level windows. + * + * @param draggable + * true if the sub window can be dragged by the user + */ + public void setDraggable(boolean draggable) { + getState().setDraggable(draggable); + requestRepaint(); + } + + /* + * Actions + */ + protected CloseShortcut closeShortcut; + + /** + * Makes is possible to close the window by pressing the given + * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/> + * Note that this shortcut only reacts while the window has focus, closing + * itself - if you want to close a subwindow from a parent window, use + * {@link #addAction(com.vaadin.event.Action)} of the parent window instead. + * + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut, null for + * none + */ + public void setCloseShortcut(int keyCode, int... modifiers) { + if (closeShortcut != null) { + removeAction(closeShortcut); + } + closeShortcut = new CloseShortcut(this, keyCode, modifiers); + addAction(closeShortcut); + } + + /** + * Removes the keyboard shortcut previously set with + * {@link #setCloseShortcut(int, int...)}. + */ + public void removeCloseShortcut() { + if (closeShortcut != null) { + removeAction(closeShortcut); + closeShortcut = null; + } + } + + /** + * A {@link ShortcutListener} specifically made to define a keyboard + * shortcut that closes the window. + * + * <pre> + * <code> + * // within the window using helper + * subWindow.setCloseShortcut(KeyCode.ESCAPE, null); + * + * // or globally + * getWindow().addAction(new Window.CloseShortcut(subWindow, KeyCode.ESCAPE)); + * </code> + * </pre> + * + */ + public static class CloseShortcut extends ShortcutListener { + protected Window window; + + /** + * Creates a keyboard shortcut for closing the given window using the + * shorthand notation defined in {@link ShortcutAction}. + * + * @param window + * to be closed when the shortcut is invoked + * @param shorthandCaption + * the caption with shortcut keycode and modifiers indicated + */ + public CloseShortcut(Window window, String shorthandCaption) { + super(shorthandCaption); + this.window = window; + } + + /** + * Creates a keyboard shortcut for closing the given window using the + * given {@link KeyCode} and {@link ModifierKey}s. + * + * @param window + * to be closed when the shortcut is invoked + * @param keyCode + * KeyCode to react to + * @param modifiers + * optional modifiers for shortcut + */ + public CloseShortcut(Window window, int keyCode, int... modifiers) { + super(null, keyCode, modifiers); + this.window = window; + } + + /** + * Creates a keyboard shortcut for closing the given window using the + * given {@link KeyCode}. + * + * @param window + * to be closed when the shortcut is invoked + * @param keyCode + * KeyCode to react to + */ + public CloseShortcut(Window window, int keyCode) { + this(window, keyCode, null); + } + + @Override + public void handleAction(Object sender, Object target) { + window.close(); + } + } + + /** + * Note, that focus/blur listeners in Window class are only supported by sub + * windows. Also note that Window is not considered focused if its contained + * component currently has focus. + * + * @see com.vaadin.event.FieldEvents.FocusNotifier#addListener(com.vaadin.event.FieldEvents.FocusListener) + */ + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + /** + * Note, that focus/blur listeners in Window class are only supported by sub + * windows. Also note that Window is not considered focused if its contained + * component currently has focus. + * + * @see com.vaadin.event.FieldEvents.BlurNotifier#addListener(com.vaadin.event.FieldEvents.BlurListener) + */ + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + /** + * {@inheritDoc} + * + * If the window is a sub-window focusing will cause the sub-window to be + * brought on top of other sub-windows on gain keyboard focus. + */ + + @Override + public void focus() { + /* + * When focusing a sub-window it basically means it should be brought to + * the front. Instead of just moving the keyboard focus we focus the + * window and bring it top-most. + */ + super.focus(); + bringToFront(); + } + + @Override + public WindowState getState() { + return (WindowState) super.getState(); + } +} diff --git a/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif b/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif Binary files differnew file mode 100644 index 0000000000..936c220d11 --- /dev/null +++ b/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif diff --git a/server/src/com/vaadin/ui/doc-files/component_interfaces.gif b/server/src/com/vaadin/ui/doc-files/component_interfaces.gif Binary files differnew file mode 100644 index 0000000000..44c99826bb --- /dev/null +++ b/server/src/com/vaadin/ui/doc-files/component_interfaces.gif diff --git a/server/src/com/vaadin/ui/package.html b/server/src/com/vaadin/ui/package.html new file mode 100644 index 0000000000..6b19a28fe7 --- /dev/null +++ b/server/src/com/vaadin/ui/package.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides interfaces and classes in Vaadin.</p> + +<h2>Package Specification</h2> + +<p><strong>Interface hierarchy</strong></p> + +<p>The general interface hierarchy looks like this:</p> + +<p style="text-align: center;"><img + src="doc-files/component_interfaces.gif" /></p> + +<p><i>Note that the above picture includes only the main +interfaces. This package includes several other lesser sub-interfaces +which are not significant in this scope. The interfaces not appearing +here are documented with the classes that define them.</i></p> + +<p>The {@link com.vaadin.ui.Component} interface is the top-level +interface which must be implemented by all user interface components in +Vaadin. It defines the common properties of the components and how the +framework will handle them. Most simple components, such as {@link +com.vaadin.ui.Button}, for example, do not need to implement the +lower-level interfaces described below. Notice that also the classes and +interfaces required by the component event framework are defined in +{@link com.vaadin.ui.Component}.</p> + +<p>The next level in the component hierarchy are the classes +implementing the {@link com.vaadin.ui.ComponentContainer} interface. It +adds the capacity to contain other components to {@link +com.vaadin.ui.Component} with a simple API.</p> + +<p>The third and last level is the {@link com.vaadin.ui.Layout}, +which adds the concept of location to the components contained in a +{@link com.vaadin.ui.ComponentContainer}. It can be used to create +containers which contents can be positioned.</p> + +<p><strong>Component class hierarchy</strong></p> + +<p>The actual component classes form a hierarchy like this:</p> + +<center><img src="doc-files/component_class_hierarchy.gif" /></center> +<br /> + +<center><i>Underlined classes are abstract.</i></center> + +<p>At the top level is {@link com.vaadin.ui.AbstractComponent} which +implements the {@link com.vaadin.ui.Component} interface. As the name +suggests it is abstract, but it does include a default implementation +for all methods defined in <code>Component</code> so that a component is +free to override only those functionalities it needs.</p> + +<p>As seen in the picture, <code>AbstractComponent</code> serves as +the superclass for several "real" components, but it also has a some +abstract extensions. {@link com.vaadin.ui.AbstractComponentContainer} +serves as the root class for all components (for example, panels and +windows) who can contain other components. {@link +com.vaadin.ui.AbstractField}, on the other hand, implements several +interfaces to provide a base class for components that are used for data +display and manipulation.</p> + + +<!-- Package spec here --> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> diff --git a/server/src/com/vaadin/ui/themes/BaseTheme.java b/server/src/com/vaadin/ui/themes/BaseTheme.java new file mode 100644 index 0000000000..6f448746bf --- /dev/null +++ b/server/src/com/vaadin/ui/themes/BaseTheme.java @@ -0,0 +1,59 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +/** + * <p> + * The Base theme is the foundation for all Vaadin themes. Although it is not + * necessary to use it as the starting point for all other themes, it is heavily + * encouraged, since it abstracts and hides away many necessary style properties + * that the Vaadin terminal expects and needs. + * </p> + * <p> + * When creating your own theme, either extend this class and specify the styles + * implemented in your theme here, or extend some other theme that has a class + * file specified (e.g. Reindeer or Runo). + * </p> + * <p> + * All theme class files should follow the convention of specifying the theme + * name as a string constant <code>THEME_NAME</code>. + * + * @since 6.3.0 + * + */ +public class BaseTheme { + + public static final String THEME_NAME = "base"; + + /** + * Creates a button that looks like a regular hypertext link but still acts + * like a normal button. + */ + public static final String BUTTON_LINK = "link"; + + /** + * Removes extra decorations from the panel. + * + * @deprecated Base theme does not implement this style, but it is defined + * here since it has been a part of the framework before + * multiple themes were available. Use the constant provided by + * the theme you're using instead, e.g. + * {@link Reindeer#PANEL_LIGHT} or {@link Runo#PANEL_LIGHT}. + */ + @Deprecated + public static final String PANEL_LIGHT = "light"; + + /** + * Adds the connector lines between a parent node and its child nodes to + * indicate the tree hierarchy better. + */ + public static final String TREE_CONNECTORS = "connectors"; + + /** + * Clips the component so it will be constrained to its given size and not + * overflow. + */ + public static final String CLIP = "v-clip"; + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/themes/ChameleonTheme.java b/server/src/com/vaadin/ui/themes/ChameleonTheme.java new file mode 100644 index 0000000000..5ae8cd4e57 --- /dev/null +++ b/server/src/com/vaadin/ui/themes/ChameleonTheme.java @@ -0,0 +1,365 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +public class ChameleonTheme extends BaseTheme { + + public static final String THEME_NAME = "chameleon"; + + /*************************************************************************** + * Label styles + **************************************************************************/ + + /** + * Large font for main application headings + */ + public static final String LABEL_H1 = "h1"; + + /** + * Large font for different sections in the application + */ + public static final String LABEL_H2 = "h2"; + + /** + * Font for sub-section headers + */ + public static final String LABEL_H3 = "h3"; + + /** + * Font for paragraphs headers + */ + public static final String LABEL_H4 = "h4"; + + /** + * Big font for important or emphasized texts + */ + public static final String LABEL_BIG = "big"; + + /** + * Small and a little lighter font + */ + public static final String LABEL_SMALL = "small"; + + /** + * Very small and lighter font for things such as footnotes and component + * specific informations. Use carefully, since this style will usually + * reduce legibility. + */ + public static final String LABEL_TINY = "tiny"; + + /** + * Adds color to the text (usually the alternate color of the theme) + */ + public static final String LABEL_COLOR = "color"; + + /** + * Adds a warning icon on the left side and a yellow background to the label + */ + public static final String LABEL_WARNING = "warning"; + + /** + * Adds an error icon on the left side and a red background to the label + */ + public static final String LABEL_ERROR = "error"; + + /** + * Adds a spinner icon on the left side of the label + */ + public static final String LABEL_LOADING = "loading"; + + /*************************************************************************** + * Button styles + **************************************************************************/ + + /** + * Default action style for buttons (the button that gets activated when + * user presses 'enter' in a form). Use sparingly, only one default button + * per screen should be visible. + */ + public static final String BUTTON_DEFAULT = "default"; + + /** + * Small sized button, use for context specific actions for example + */ + public static final String BUTTON_SMALL = "small"; + + /** + * Big button, use to get more attention for the button action + */ + public static final String BUTTON_BIG = "big"; + + /** + * Adds more padding on the sides of the button. Makes it easier for the + * user to hit the button. + */ + public static final String BUTTON_WIDE = "wide"; + + /** + * Adds more padding on the top and on the bottom of the button. Makes it + * easier for the user to hit the button. + */ + public static final String BUTTON_TALL = "tall"; + + /** + * Removes all graphics from the button, leaving only the caption and the + * icon visible. Useful for making icon-only buttons and toolbar buttons. + */ + public static final String BUTTON_BORDERLESS = "borderless"; + + /** + * Places the button icon on top of the caption. By default the icon is on + * the left side of the button caption. + */ + public static final String BUTTON_ICON_ON_TOP = "icon-on-top"; + + /** + * Places the button icon on the right side of the caption. By default the + * icon is on the left side of the button caption. + */ + public static final String BUTTON_ICON_ON_RIGHT = "icon-on-right"; + + /** + * Removes the button caption and only shows its icon + */ + public static final String BUTTON_ICON_ONLY = "icon-only"; + + /** + * Makes the button look like it is pressed down. Useful for creating a + * toggle button. + */ + public static final String BUTTON_DOWN = "down"; + + /*************************************************************************** + * TextField styles + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /** + * Large sized text field with big font + */ + public static final String TEXTFIELD_BIG = "big"; + + /** + * Adds a magnifier icon on the left side of the fields text + */ + public static final String TEXTFIELD_SEARCH = "search"; + + /*************************************************************************** + * Select styles + **************************************************************************/ + + /** + * Small sized select with small font + */ + public static final String SELECT_SMALL = "small"; + + /** + * Large sized select with big font + */ + public static final String SELECT_BIG = "big"; + + /** + * Adds a magnifier icon on the left side of the fields text + */ + public static final String COMBOBOX_SEARCH = "search"; + + /** + * Adds a magnifier icon on the left side of the fields text + */ + public static final String COMBOBOX_SELECT_BUTTON = "select-button"; + + /*************************************************************************** + * DateField styles + **************************************************************************/ + + /** + * Small sized date field with small font + */ + public static final String DATEFIELD_SMALL = "small"; + + /** + * Large sized date field with big font + */ + public static final String DATEFIELD_BIG = "big"; + + /*************************************************************************** + * Panel styles + **************************************************************************/ + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_BORDERLESS = "borderless"; + + /** + * Adds a more vibrant header for the panel, using the alternate color of + * the theme, and adds slight rounded corners (not supported in all + * browsers) + */ + public static final String PANEL_BUBBLE = "bubble"; + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * SplitPanel styles + **************************************************************************/ + + /** + * Reduces the split handle to a minimal size (1 pixel) + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * TabSheet styles + **************************************************************************/ + + /** + * Removes borders and background color from the tab sheet + */ + public static final String TABSHEET_BORDERLESS = "borderless"; + + /*************************************************************************** + * Accordion styles + **************************************************************************/ + + /** + * Makes the accordion background opaque (non-transparent) + */ + public static final String ACCORDION_OPAQUE = "opaque"; + + /*************************************************************************** + * Table styles + **************************************************************************/ + + /** + * Removes borders and background color from the table + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /** + * Makes the column header and content font size smaller inside the table + */ + public static final String TABLE_SMALL = "small"; + + /** + * Makes the column header and content font size bigger inside the table + */ + public static final String TABLE_BIG = "big"; + + /** + * Adds a light alternate background color to even rows in the table. + */ + public static final String TABLE_STRIPED = "striped"; + + /*************************************************************************** + * ProgressIndicator styles + **************************************************************************/ + + /** + * Reduces the height of the progress bar + */ + public static final String PROGRESS_INDICATOR_SMALL = "small"; + + /** + * Increases the height of the progress bar. If the indicator is in + * indeterminate mode, shows a bigger spinner than the regular indeterminate + * indicator. + */ + public static final String PROGRESS_INDICATOR_BIG = "big"; + + /** + * Displays an indeterminate progress indicator as a bar with animated + * background stripes. This style can be used in combination with the + * "small" and "big" styles. + */ + public static final String PROGRESS_INDICATOR_INDETERMINATE_BAR = "bar"; + + /*************************************************************************** + * Window styles + **************************************************************************/ + + /** + * Sub-window style that makes the window background opaque (i.e. not + * semi-transparent). + */ + public static final String WINDOW_OPAQUE = "opaque"; + + /*************************************************************************** + * Compound styles + **************************************************************************/ + + /** + * Creates a context for a segment button control. Place buttons inside the + * segment, and add "<code>first</code>" and "<code>last</code>" style names + * for the first and last button in the segment. Then use the + * {@link #BUTTON_DOWN} style to indicate button states. + * + * E.g. + * + * <pre> + * HorizontalLayout ("segment") + * + Button ("first down") + * + Button ("down") + * + Button + * ... + * + Button ("last") + * </pre> + * + * You can also use most of the different button styles for the contained + * buttons (e.g. {@link #BUTTON_BIG}, {@link #BUTTON_ICON_ONLY} etc.). + */ + public static final String COMPOUND_HORIZONTAL_LAYOUT_SEGMENT = "segment"; + + /** + * Use this mixin-style in combination with the + * {@link #COMPOUND_HORIZONTAL_LAYOUT_SEGMENT} style to make buttons with + * the "down" style use the themes alternate color (e.g. blue instead of + * gray). + * + * E.g. + * + * <pre> + * HorizontalLayout ("segment segment-alternate") + * + Button ("first down") + * + Button ("down") + * + Button + * ... + * + Button ("last") + * </pre> + */ + public static final String COMPOUND_HORIZONTAL_LAYOUT_SEGMENT_ALTERNATE = "segment-alternate"; + + /** + * Creates an iTunes-like menu from a CssLayout or a VerticalLayout. Place + * plain Labels and NativeButtons inside the layout, and you're all set. + * + * E.g. + * + * <pre> + * CssLayout ("sidebar-menu") + * + Label + * + NativeButton + * + NativeButton + * ... + * + Label + * + NativeButton + * </pre> + */ + public static final String COMPOUND_LAYOUT_SIDEBAR_MENU = "sidebar-menu"; + + /** + * Adds a toolbar-like background for the layout, and aligns Buttons and + * Segments horizontally. Feel free to use different buttons styles inside + * the toolbar, like {@link #BUTTON_ICON_ON_TOP} and + * {@link #BUTTON_BORDERLESS} + */ + public static final String COMPOUND_CSSLAYOUT_TOOLBAR = "toolbar"; +} diff --git a/server/src/com/vaadin/ui/themes/LiferayTheme.java b/server/src/com/vaadin/ui/themes/LiferayTheme.java new file mode 100644 index 0000000000..9b48306ac2 --- /dev/null +++ b/server/src/com/vaadin/ui/themes/LiferayTheme.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +public class LiferayTheme extends BaseTheme { + + public static final String THEME_NAME = "liferay"; + + /*************************************************************************** + * + * Panel styles + * + **************************************************************************/ + + /** + * Removes borders and background from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the split handle to a minimal size (1 pixel) + */ + public static final String SPLITPANEL_SMALL = "small"; +} diff --git a/server/src/com/vaadin/ui/themes/Reindeer.java b/server/src/com/vaadin/ui/themes/Reindeer.java new file mode 100644 index 0000000000..7aaae8faa2 --- /dev/null +++ b/server/src/com/vaadin/ui/themes/Reindeer.java @@ -0,0 +1,217 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.FormLayout; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.HorizontalSplitPanel; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.VerticalSplitPanel; + +public class Reindeer extends BaseTheme { + + public static final String THEME_NAME = "reindeer"; + + /*************************************************************************** + * + * Label styles + * + **************************************************************************/ + + /** + * Large font for main application headings + */ + public static final String LABEL_H1 = "h1"; + + /** + * Large font for different sections in the application + */ + public static final String LABEL_H2 = "h2"; + + /** + * Small and a little lighter font + */ + public static final String LABEL_SMALL = "light"; + + /** + * @deprecated Use {@link #LABEL_SMALL} instead. + */ + @Deprecated + public static final String LABEL_LIGHT = "small"; + + /*************************************************************************** + * + * Button styles + * + **************************************************************************/ + + /** + * Default action style for buttons (the button that should get activated + * when the user presses 'enter' in a form). Use sparingly, only one default + * button per view should be visible. + */ + public static final String BUTTON_DEFAULT = "primary"; + + /** + * @deprecated Use {@link #BUTTON_DEFAULT} instead + */ + @Deprecated + public static final String BUTTON_PRIMARY = BUTTON_DEFAULT; + + /** + * Small sized button, use for context specific actions for example + */ + public static final String BUTTON_SMALL = "small"; + + /*************************************************************************** + * + * TextField styles + * + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /*************************************************************************** + * + * Panel styles + * + **************************************************************************/ + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the split handle to a minimal size (1 pixel) + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * + * TabSheet styles + * + **************************************************************************/ + + /** + * Removes borders from the default tab sheet style. + */ + public static final String TABSHEET_BORDERLESS = "borderless"; + + /** + * Removes borders and background color from the tab sheet, and shows the + * tabs as a small bar. + */ + public static final String TABSHEET_SMALL = "bar"; + + /** + * @deprecated Use {@link #TABSHEET_SMALL} instead. + */ + @Deprecated + public static final String TABSHEET_BAR = TABSHEET_SMALL; + + /** + * Removes borders and background color from the tab sheet. The tabs are + * presented with minimal lines indicating the selected tab. + */ + public static final String TABSHEET_MINIMAL = "minimal"; + + /** + * Makes the tab close buttons visible only when the user is hovering over + * the tab. + */ + public static final String TABSHEET_HOVER_CLOSABLE = "hover-closable"; + + /** + * Makes the tab close buttons visible only when the tab is selected. + */ + public static final String TABSHEET_SELECTED_CLOSABLE = "selected-closable"; + + /*************************************************************************** + * + * Table styles + * + **************************************************************************/ + + /** + * Removes borders from the table + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /** + * Makes the table headers dark and more prominent. + */ + public static final String TABLE_STRONG = "strong"; + + /*************************************************************************** + * + * Layout styles + * + **************************************************************************/ + + /** + * Changes the background of a layout to white. Applies to + * {@link VerticalLayout}, {@link HorizontalLayout}, {@link GridLayout}, + * {@link FormLayout}, {@link CssLayout}, {@link VerticalSplitPanel} and + * {@link HorizontalSplitPanel}. + * <p> + * <em>Does not revert any contained components back to normal if some + * parent layout has style {@link #LAYOUT_BLACK} applied.</em> + */ + public static final String LAYOUT_WHITE = "white"; + + /** + * Changes the background of a layout to a shade of blue. Applies to + * {@link VerticalLayout}, {@link HorizontalLayout}, {@link GridLayout}, + * {@link FormLayout}, {@link CssLayout}, {@link VerticalSplitPanel} and + * {@link HorizontalSplitPanel}. + * <p> + * <em>Does not revert any contained components back to normal if some + * parent layout has style {@link #LAYOUT_BLACK} applied.</em> + */ + public static final String LAYOUT_BLUE = "blue"; + + /** + * <p> + * Changes the background of a layout to almost black, and at the same time + * transforms contained components to their black style correspondents when + * available. At least texts, buttons, text fields, selects, date fields, + * tables and a few other component styles should change. + * </p> + * <p> + * Applies to {@link VerticalLayout}, {@link HorizontalLayout}, + * {@link GridLayout}, {@link FormLayout} and {@link CssLayout}. + * </p> + * + */ + public static final String LAYOUT_BLACK = "black"; + + /*************************************************************************** + * + * Window styles + * + **************************************************************************/ + + /** + * Makes the whole window white and increases the font size of the title. + */ + public static final String WINDOW_LIGHT = "light"; + + /** + * Makes the whole window black, and changes contained components in the + * same way as {@link #LAYOUT_BLACK} does. + */ + public static final String WINDOW_BLACK = "black"; +} diff --git a/server/src/com/vaadin/ui/themes/Runo.java b/server/src/com/vaadin/ui/themes/Runo.java new file mode 100644 index 0000000000..28a19e8dcd --- /dev/null +++ b/server/src/com/vaadin/ui/themes/Runo.java @@ -0,0 +1,183 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +public class Runo extends BaseTheme { + + public static final String THEME_NAME = "runo"; + + public static String themeName() { + return THEME_NAME.toLowerCase(); + } + + /*************************************************************************** + * + * Button styles + * + **************************************************************************/ + + /** + * Small sized button, use for context specific actions for example + */ + public static final String BUTTON_SMALL = "small"; + + /** + * Big sized button, use to gather much attention for some particular action + */ + public static final String BUTTON_BIG = "big"; + + /** + * Default action style for buttons (the button that should get activated + * when the user presses 'enter' in a form). Use sparingly, only one default + * button per view should be visible. + */ + public static final String BUTTON_DEFAULT = "default"; + + /*************************************************************************** + * + * Panel styles + * + **************************************************************************/ + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * + * TabSheet styles + * + **************************************************************************/ + + /** + * Smaller tabs, no border and background for content area + */ + public static final String TABSHEET_SMALL = "light"; + + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the width/height of the split handle. Useful when you don't want + * the split handle to touch the sides of the containing layout. + */ + public static final String SPLITPANEL_REDUCED = "rounded"; + + /** + * Reduces the visual size of the split handle to one pixel (the active drag + * size is still larger). + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * + * Label styles + * + **************************************************************************/ + + /** + * Largest title/header size. Use for main sections in your application. + */ + public static final String LABEL_H1 = "h1"; + + /** + * Similar style as in panel captions. Useful for sub-sections within a + * view. + */ + public static final String LABEL_H2 = "h2"; + + /** + * Small font size. Useful for contextual help texts and similar less + * frequently needed information. Use with modesty, since this style will be + * more harder to read due to its smaller size and contrast. + */ + public static final String LABEL_SMALL = "small"; + + /*************************************************************************** + * + * Layout styles + * + **************************************************************************/ + + /** + * An alternative background color for layouts. Use on top of white + * background (e.g. inside Panels, TabSheets and sub-windows). + */ + public static final String LAYOUT_DARKER = "darker"; + + /** + * Add a drop shadow around the layout and its contained components. + * Produces a rectangular shadow, even if the contained component would have + * a different shape. + * <p> + * Note: does not work in Internet Explorer 6 + */ + public static final String CSSLAYOUT_SHADOW = "box-shadow"; + + /** + * Adds necessary styles to the layout to make it look selectable (i.e. + * clickable). Add a click listener for the layout, and toggle the + * {@link #CSSLAYOUT_SELECTABLE_SELECTED} style for the same layout to make + * it look selected or not. + */ + public static final String CSSLAYOUT_SELECTABLE = "selectable"; + public static final String CSSLAYOUT_SELECTABLE_SELECTED = "selectable-selected"; + + /*************************************************************************** + * + * TextField styles + * + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /*************************************************************************** + * + * Table styles + * + **************************************************************************/ + + /** + * Smaller header and item fonts. + */ + public static final String TABLE_SMALL = "small"; + + /** + * Removes the border and background color from the table. Removes + * alternating row background colors as well. + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /*************************************************************************** + * + * Accordion styles + * + **************************************************************************/ + + /** + * A detached looking accordion, providing space around its captions and + * content. Doesn't necessarily need a Panel or other container to wrap it + * in order to make it look right. + */ + public static final String ACCORDION_LIGHT = "light"; + + /*************************************************************************** + * + * Window styles + * + **************************************************************************/ + + /** + * Smaller header and a darker background color for the window. Useful for + * smaller dialog-like windows. + */ + public static final String WINDOW_DIALOG = "dialog"; +} diff --git a/server/src/com/vaadin/util/SerializerHelper.java b/server/src/com/vaadin/util/SerializerHelper.java new file mode 100644 index 0000000000..5b7b388dd6 --- /dev/null +++ b/server/src/com/vaadin/util/SerializerHelper.java @@ -0,0 +1,145 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * Helper class for performing serialization. Most of the methods are here are + * workarounds for problems in Google App Engine. Used internally by Vaadin and + * should not be used by application developers. Subject to change at any time. + * + * @since 6.0 + */ +public class SerializerHelper { + + /** + * Serializes the class reference so {@link #readClass(ObjectInputStream)} + * can deserialize it. Supports null class references. + * + * @param out + * The {@link ObjectOutputStream} to serialize to. + * @param cls + * A class or null. + * @throws IOException + * Rethrows any IOExceptions from the ObjectOutputStream + */ + public static void writeClass(ObjectOutputStream out, Class<?> cls) + throws IOException { + if (cls == null) { + out.writeObject(null); + } else { + out.writeObject(cls.getName()); + } + + } + + /** + * Serializes the class references so + * {@link #readClassArray(ObjectInputStream)} can deserialize it. Supports + * null class arrays. + * + * @param out + * The {@link ObjectOutputStream} to serialize to. + * @param classes + * An array containing class references or null. + * @throws IOException + * Rethrows any IOExceptions from the ObjectOutputStream + */ + public static void writeClassArray(ObjectOutputStream out, + Class<?>[] classes) throws IOException { + if (classes == null) { + out.writeObject(null); + } else { + String[] classNames = new String[classes.length]; + for (int i = 0; i < classes.length; i++) { + classNames[i] = classes[i].getName(); + } + out.writeObject(classNames); + } + } + + /** + * Deserializes a class references serialized by + * {@link #writeClassArray(ObjectOutputStream, Class[])}. Supports null + * class arrays. + * + * @param in + * {@link ObjectInputStream} to read from. + * @return Class array with the class references or null. + * @throws ClassNotFoundException + * If one of the classes could not be resolved. + * @throws IOException + * Rethrows IOExceptions from the ObjectInputStream + */ + public static Class<?>[] readClassArray(ObjectInputStream in) + throws ClassNotFoundException, IOException { + String[] classNames = (String[]) in.readObject(); + if (classNames == null) { + return null; + } + Class<?>[] classes = new Class<?>[classNames.length]; + for (int i = 0; i < classNames.length; i++) { + classes[i] = resolveClass(classNames[i]); + } + + return classes; + } + + /** + * List of primitive classes. Google App Engine has problems + * serializing/deserializing these (#3064). + */ + private static Class<?>[] primitiveClasses = new Class<?>[] { byte.class, + short.class, int.class, long.class, float.class, double.class, + boolean.class, char.class }; + + /** + * Resolves the class given by {@code className}. + * + * @param className + * The fully qualified class name. + * @return A {@code Class} reference. + * @throws ClassNotFoundException + * If the class could not be resolved. + */ + public static Class<?> resolveClass(String className) + throws ClassNotFoundException { + for (Class<?> c : primitiveClasses) { + if (className.equals(c.getName())) { + return c; + } + } + + return Class.forName(className); + } + + /** + * Deserializes a class reference serialized by + * {@link #writeClass(ObjectOutputStream, Class)}. Supports null class + * references. + * + * @param in + * {@code ObjectInputStream} to read from. + * @return Class reference to the resolved class + * @throws ClassNotFoundException + * If the class could not be resolved. + * @throws IOException + * Rethrows IOExceptions from the ObjectInputStream + */ + public static Class<?> readClass(ObjectInputStream in) throws IOException, + ClassNotFoundException { + String className = (String) in.readObject(); + if (className == null) { + return null; + } else { + return resolveClass(className); + + } + + } + +} diff --git a/server/src/org/jsoup/Connection.java b/server/src/org/jsoup/Connection.java new file mode 100644 index 0000000000..564eeb89b7 --- /dev/null +++ b/server/src/org/jsoup/Connection.java @@ -0,0 +1,481 @@ +package org.jsoup; + +import org.jsoup.nodes.Document; +import org.jsoup.parser.Parser; + +import java.net.URL; +import java.util.Map; +import java.util.Collection; +import java.io.IOException; + +/** + * A Connection provides a convenient interface to fetch content from the web, and parse them into Documents. + * <p> + * To get a new Connection, use {@link org.jsoup.Jsoup#connect(String)}. Connections contain {@link Connection.Request} + * and {@link Connection.Response} objects. The request objects are reusable as prototype requests. + * <p> + * Request configuration can be made using either the shortcut methods in Connection (e.g. {@link #userAgent(String)}), + * or by methods in the Connection.Request object directly. All request configuration must be made before the request + * is executed. + * <p> + * The Connection interface is <b>currently in beta</b> and subject to change. Comments, suggestions, and bug reports are welcome. + */ +public interface Connection { + + /** + * GET and POST http methods. + */ + public enum Method { + GET, POST + } + + /** + * Set the request URL to fetch. The protocol must be HTTP or HTTPS. + * @param url URL to connect to + * @return this Connection, for chaining + */ + public Connection url(URL url); + + /** + * Set the request URL to fetch. The protocol must be HTTP or HTTPS. + * @param url URL to connect to + * @return this Connection, for chaining + */ + public Connection url(String url); + + /** + * Set the request user-agent header. + * @param userAgent user-agent to use + * @return this Connection, for chaining + */ + public Connection userAgent(String userAgent); + + /** + * Set the request timeouts (connect and read). If a timeout occurs, an IOException will be thrown. The default + * timeout is 3 seconds (3000 millis). A timeout of zero is treated as an infinite timeout. + * @param millis number of milliseconds (thousandths of a second) before timing out connects or reads. + * @return this Connection, for chaining + */ + public Connection timeout(int millis); + + /** + * Set the request referrer (aka "referer") header. + * @param referrer referrer to use + * @return this Connection, for chaining + */ + public Connection referrer(String referrer); + + /** + * Configures the connection to (not) follow server redirects. By default this is <b>true</b>. + * @param followRedirects true if server redirects should be followed. + * @return this Connection, for chaining + */ + public Connection followRedirects(boolean followRedirects); + + /** + * Set the request method to use, GET or POST. Default is GET. + * @param method HTTP request method + * @return this Connection, for chaining + */ + public Connection method(Method method); + + /** + * Configures the connection to not throw exceptions when a HTTP error occurs. (4xx - 5xx, e.g. 404 or 500). By + * default this is <b>false</b>; an IOException is thrown if an error is encountered. If set to <b>true</b>, the + * response is populated with the error body, and the status message will reflect the error. + * @param ignoreHttpErrors - false (default) if HTTP errors should be ignored. + * @return this Connection, for chaining + */ + public Connection ignoreHttpErrors(boolean ignoreHttpErrors); + + /** + * Ignore the document's Content-Type when parsing the response. By default this is <b>false</b>, an unrecognised + * content-type will cause an IOException to be thrown. (This is to prevent producing garbage by attempting to parse + * a JPEG binary image, for example.) Set to true to force a parse attempt regardless of content type. + * @param ignoreContentType set to true if you would like the content type ignored on parsing the response into a + * Document. + * @return this Connection, for chaining + */ + public Connection ignoreContentType(boolean ignoreContentType); + + /** + * Add a request data parameter. Request parameters are sent in the request query string for GETs, and in the request + * body for POSTs. A request may have multiple values of the same name. + * @param key data key + * @param value data value + * @return this Connection, for chaining + */ + public Connection data(String key, String value); + + /** + * Adds all of the supplied data to the request data parameters + * @param data map of data parameters + * @return this Connection, for chaining + */ + public Connection data(Map<String, String> data); + + /** + * Add a number of request data parameters. Multiple parameters may be set at once, e.g.: + * <code>.data("name", "jsoup", "language", "Java", "language", "English");</code> creates a query string like: + * <code>?name=jsoup&language=Java&language=English</code> + * @param keyvals a set of key value pairs. + * @return this Connection, for chaining + */ + public Connection data(String... keyvals); + + /** + * Set a request header. + * @param name header name + * @param value header value + * @return this Connection, for chaining + * @see org.jsoup.Connection.Request#headers() + */ + public Connection header(String name, String value); + + /** + * Set a cookie to be sent in the request. + * @param name name of cookie + * @param value value of cookie + * @return this Connection, for chaining + */ + public Connection cookie(String name, String value); + + /** + * Adds each of the supplied cookies to the request. + * @param cookies map of cookie name -> value pairs + * @return this Connection, for chaining + */ + public Connection cookies(Map<String, String> cookies); + + /** + * Provide an alternate parser to use when parsing the response to a Document. + * @param parser alternate parser + * @return this Connection, for chaining + */ + public Connection parser(Parser parser); + + /** + * Execute the request as a GET, and parse the result. + * @return parsed Document + * @throws IOException on error + */ + public Document get() throws IOException; + + /** + * Execute the request as a POST, and parse the result. + * @return parsed Document + * @throws IOException on error + */ + public Document post() throws IOException; + + /** + * Execute the request. + * @return a response object + * @throws IOException on error + */ + public Response execute() throws IOException; + + /** + * Get the request object associated with this connection + * @return request + */ + public Request request(); + + /** + * Set the connection's request + * @param request new request object + * @return this Connection, for chaining + */ + public Connection request(Request request); + + /** + * Get the response, once the request has been executed + * @return response + */ + public Response response(); + + /** + * Set the connection's response + * @param response new response + * @return this Connection, for chaining + */ + public Connection response(Response response); + + + /** + * Common methods for Requests and Responses + * @param <T> Type of Base, either Request or Response + */ + interface Base<T extends Base> { + + /** + * Get the URL + * @return URL + */ + public URL url(); + + /** + * Set the URL + * @param url new URL + * @return this, for chaining + */ + public T url(URL url); + + /** + * Get the request method + * @return method + */ + public Method method(); + + /** + * Set the request method + * @param method new method + * @return this, for chaining + */ + public T method(Method method); + + /** + * Get the value of a header. This is a simplified header model, where a header may only have one value. + * <p> + * Header names are case insensitive. + * @param name name of header (case insensitive) + * @return value of header, or null if not set. + * @see #hasHeader(String) + * @see #cookie(String) + */ + public String header(String name); + + /** + * Set a header. This method will overwrite any existing header with the same case insensitive name. + * @param name Name of header + * @param value Value of header + * @return this, for chaining + */ + public T header(String name, String value); + + /** + * Check if a header is present + * @param name name of header (case insensitive) + * @return if the header is present in this request/response + */ + public boolean hasHeader(String name); + + /** + * Remove a header by name + * @param name name of header to remove (case insensitive) + * @return this, for chaining + */ + public T removeHeader(String name); + + /** + * Retrieve all of the request/response headers as a map + * @return headers + */ + public Map<String, String> headers(); + + /** + * Get a cookie value by name from this request/response. + * <p> + * Response objects have a simplified cookie model. Each cookie set in the response is added to the response + * object's cookie key=value map. The cookie's path, domain, and expiry date are ignored. + * @param name name of cookie to retrieve. + * @return value of cookie, or null if not set + */ + public String cookie(String name); + + /** + * Set a cookie in this request/response. + * @param name name of cookie + * @param value value of cookie + * @return this, for chaining + */ + public T cookie(String name, String value); + + /** + * Check if a cookie is present + * @param name name of cookie + * @return if the cookie is present in this request/response + */ + public boolean hasCookie(String name); + + /** + * Remove a cookie by name + * @param name name of cookie to remove + * @return this, for chaining + */ + public T removeCookie(String name); + + /** + * Retrieve all of the request/response cookies as a map + * @return cookies + */ + public Map<String, String> cookies(); + + } + + /** + * Represents a HTTP request. + */ + public interface Request extends Base<Request> { + /** + * Get the request timeout, in milliseconds. + * @return the timeout in milliseconds. + */ + public int timeout(); + + /** + * Update the request timeout. + * @param millis timeout, in milliseconds + * @return this Request, for chaining + */ + public Request timeout(int millis); + + /** + * Get the current followRedirects configuration. + * @return true if followRedirects is enabled. + */ + public boolean followRedirects(); + + /** + * Configures the request to (not) follow server redirects. By default this is <b>true</b>. + * + * @param followRedirects true if server redirects should be followed. + * @return this Request, for chaining + */ + public Request followRedirects(boolean followRedirects); + + /** + * Get the current ignoreHttpErrors configuration. + * @return true if errors will be ignored; false (default) if HTTP errors will cause an IOException to be thrown. + */ + public boolean ignoreHttpErrors(); + + /** + * Configures the request to ignore HTTP errors in the response. + * @param ignoreHttpErrors set to true to ignore HTTP errors. + * @return this Request, for chaining + */ + public Request ignoreHttpErrors(boolean ignoreHttpErrors); + + /** + * Get the current ignoreContentType configuration. + * @return true if invalid content-types will be ignored; false (default) if they will cause an IOException to be thrown. + */ + public boolean ignoreContentType(); + + /** + * Configures the request to ignore the Content-Type of the response. + * @param ignoreContentType set to true to ignore the content type. + * @return this Request, for chaining + */ + public Request ignoreContentType(boolean ignoreContentType); + + /** + * Add a data parameter to the request + * @param keyval data to add. + * @return this Request, for chaining + */ + public Request data(KeyVal keyval); + + /** + * Get all of the request's data parameters + * @return collection of keyvals + */ + public Collection<KeyVal> data(); + + /** + * Specify the parser to use when parsing the document. + * @param parser parser to use. + * @return this Request, for chaining + */ + public Request parser(Parser parser); + + /** + * Get the current parser to use when parsing the document. + * @return current Parser + */ + public Parser parser(); + } + + /** + * Represents a HTTP response. + */ + public interface Response extends Base<Response> { + + /** + * Get the status code of the response. + * @return status code + */ + public int statusCode(); + + /** + * Get the status message of the response. + * @return status message + */ + public String statusMessage(); + + /** + * Get the character set name of the response. + * @return character set name + */ + public String charset(); + + /** + * Get the response content type (e.g. "text/html"); + * @return the response content type + */ + public String contentType(); + + /** + * Parse the body of the response as a Document. + * @return a parsed Document + * @throws IOException on error + */ + public Document parse() throws IOException; + + /** + * Get the body of the response as a plain string. + * @return body + */ + public String body(); + + /** + * Get the body of the response as an array of bytes. + * @return body bytes + */ + public byte[] bodyAsBytes(); + } + + /** + * A Key Value tuple. + */ + public interface KeyVal { + + /** + * Update the key of a keyval + * @param key new key + * @return this KeyVal, for chaining + */ + public KeyVal key(String key); + + /** + * Get the key of a keyval + * @return the key + */ + public String key(); + + /** + * Update the value of a keyval + * @param value the new value + * @return this KeyVal, for chaining + */ + public KeyVal value(String value); + + /** + * Get the value of a keyval + * @return the value + */ + public String value(); + } +} + diff --git a/server/src/org/jsoup/Jsoup.java b/server/src/org/jsoup/Jsoup.java new file mode 100644 index 0000000000..8c6afcee36 --- /dev/null +++ b/server/src/org/jsoup/Jsoup.java @@ -0,0 +1,229 @@ +package org.jsoup; + +import org.jsoup.nodes.Document; +import org.jsoup.parser.Parser; +import org.jsoup.safety.Cleaner; +import org.jsoup.safety.Whitelist; +import org.jsoup.helper.DataUtil; +import org.jsoup.helper.HttpConnection; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + The core public access point to the jsoup functionality. + + @author Jonathan Hedley */ +public class Jsoup { + private Jsoup() {} + + /** + Parse HTML into a Document. The parser will make a sensible, balanced document tree out of any HTML. + + @param html HTML to parse + @param baseUri The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur + before the HTML declares a {@code <base href>} tag. + @return sane HTML + */ + public static Document parse(String html, String baseUri) { + return Parser.parse(html, baseUri); + } + + /** + Parse HTML into a Document, using the provided Parser. You can provide an alternate parser, such as a simple XML + (non-HTML) parser. + + @param html HTML to parse + @param baseUri The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur + before the HTML declares a {@code <base href>} tag. + @param parser alternate {@link Parser#xmlParser() parser} to use. + @return sane HTML + */ + public static Document parse(String html, String baseUri, Parser parser) { + return parser.parseInput(html, baseUri); + } + + /** + Parse HTML into a Document. As no base URI is specified, absolute URL detection relies on the HTML including a + {@code <base href>} tag. + + @param html HTML to parse + @return sane HTML + + @see #parse(String, String) + */ + public static Document parse(String html) { + return Parser.parse(html, ""); + } + + /** + * Creates a new {@link Connection} to a URL. Use to fetch and parse a HTML page. + * <p> + * Use examples: + * <ul> + * <li><code>Document doc = Jsoup.connect("http://example.com").userAgent("Mozilla").data("name", "jsoup").get();</code></li> + * <li><code>Document doc = Jsoup.connect("http://example.com").cookie("auth", "token").post(); + * </ul> + * @param url URL to connect to. The protocol must be {@code http} or {@code https}. + * @return the connection. You can add data, cookies, and headers; set the user-agent, referrer, method; and then execute. + */ + public static Connection connect(String url) { + return HttpConnection.connect(url); + } + + /** + Parse the contents of a file as HTML. + + @param in file to load HTML from + @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if + present, or fall back to {@code UTF-8} (which is often safe to do). + @param baseUri The URL where the HTML was retrieved from, to resolve relative links against. + @return sane HTML + + @throws IOException if the file could not be found, or read, or if the charsetName is invalid. + */ + public static Document parse(File in, String charsetName, String baseUri) throws IOException { + return DataUtil.load(in, charsetName, baseUri); + } + + /** + Parse the contents of a file as HTML. The location of the file is used as the base URI to qualify relative URLs. + + @param in file to load HTML from + @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if + present, or fall back to {@code UTF-8} (which is often safe to do). + @return sane HTML + + @throws IOException if the file could not be found, or read, or if the charsetName is invalid. + @see #parse(File, String, String) + */ + public static Document parse(File in, String charsetName) throws IOException { + return DataUtil.load(in, charsetName, in.getAbsolutePath()); + } + + /** + Read an input stream, and parse it to a Document. + + @param in input stream to read. Make sure to close it after parsing. + @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if + present, or fall back to {@code UTF-8} (which is often safe to do). + @param baseUri The URL where the HTML was retrieved from, to resolve relative links against. + @return sane HTML + + @throws IOException if the file could not be found, or read, or if the charsetName is invalid. + */ + public static Document parse(InputStream in, String charsetName, String baseUri) throws IOException { + return DataUtil.load(in, charsetName, baseUri); + } + + /** + Read an input stream, and parse it to a Document. You can provide an alternate parser, such as a simple XML + (non-HTML) parser. + + @param in input stream to read. Make sure to close it after parsing. + @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if + present, or fall back to {@code UTF-8} (which is often safe to do). + @param baseUri The URL where the HTML was retrieved from, to resolve relative links against. + @param parser alternate {@link Parser#xmlParser() parser} to use. + @return sane HTML + + @throws IOException if the file could not be found, or read, or if the charsetName is invalid. + */ + public static Document parse(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException { + return DataUtil.load(in, charsetName, baseUri, parser); + } + + /** + Parse a fragment of HTML, with the assumption that it forms the {@code body} of the HTML. + + @param bodyHtml body HTML fragment + @param baseUri URL to resolve relative URLs against. + @return sane HTML document + + @see Document#body() + */ + public static Document parseBodyFragment(String bodyHtml, String baseUri) { + return Parser.parseBodyFragment(bodyHtml, baseUri); + } + + /** + Parse a fragment of HTML, with the assumption that it forms the {@code body} of the HTML. + + @param bodyHtml body HTML fragment + @return sane HTML document + + @see Document#body() + */ + public static Document parseBodyFragment(String bodyHtml) { + return Parser.parseBodyFragment(bodyHtml, ""); + } + + /** + Fetch a URL, and parse it as HTML. Provided for compatibility; in most cases use {@link #connect(String)} instead. + <p> + The encoding character set is determined by the content-type header or http-equiv meta tag, or falls back to {@code UTF-8}. + + @param url URL to fetch (with a GET). The protocol must be {@code http} or {@code https}. + @param timeoutMillis Connection and read timeout, in milliseconds. If exceeded, IOException is thrown. + @return The parsed HTML. + + @throws IOException If the final server response != 200 OK (redirects are followed), or if there's an error reading + the response stream. + + @see #connect(String) + */ + public static Document parse(URL url, int timeoutMillis) throws IOException { + Connection con = HttpConnection.connect(url); + con.timeout(timeoutMillis); + return con.get(); + } + + /** + Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted + tags and attributes. + + @param bodyHtml input untrusted HTML + @param baseUri URL to resolve relative URLs against + @param whitelist white-list of permitted HTML elements + @return safe HTML + + @see Cleaner#clean(Document) + */ + public static String clean(String bodyHtml, String baseUri, Whitelist whitelist) { + Document dirty = parseBodyFragment(bodyHtml, baseUri); + Cleaner cleaner = new Cleaner(whitelist); + Document clean = cleaner.clean(dirty); + return clean.body().html(); + } + + /** + Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted + tags and attributes. + + @param bodyHtml input untrusted HTML + @param whitelist white-list of permitted HTML elements + @return safe HTML + + @see Cleaner#clean(Document) + */ + public static String clean(String bodyHtml, Whitelist whitelist) { + return clean(bodyHtml, "", whitelist); + } + + /** + Test if the input HTML has only tags and attributes allowed by the Whitelist. Useful for form validation. The input HTML should + still be run through the cleaner to set up enforced attributes, and to tidy the output. + @param bodyHtml HTML to test + @param whitelist whitelist to test against + @return true if no tags or attributes were removed; false otherwise + @see #clean(String, org.jsoup.safety.Whitelist) + */ + public static boolean isValid(String bodyHtml, Whitelist whitelist) { + Document dirty = parseBodyFragment(bodyHtml, ""); + Cleaner cleaner = new Cleaner(whitelist); + return cleaner.isValid(dirty); + } + +} diff --git a/server/src/org/jsoup/examples/HtmlToPlainText.java b/server/src/org/jsoup/examples/HtmlToPlainText.java new file mode 100644 index 0000000000..8f563e9608 --- /dev/null +++ b/server/src/org/jsoup/examples/HtmlToPlainText.java @@ -0,0 +1,109 @@ +package org.jsoup.examples; + +import org.jsoup.Jsoup; +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; + +import java.io.IOException; + +/** + * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted + * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a + * scrape. + * <p/> + * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend. + * + * @author Jonathan Hedley, jonathan@hedley.net + */ +public class HtmlToPlainText { + public static void main(String... args) throws IOException { + Validate.isTrue(args.length == 1, "usage: supply url to fetch"); + String url = args[0]; + + // fetch the specified URL and parse to a HTML DOM + Document doc = Jsoup.connect(url).get(); + + HtmlToPlainText formatter = new HtmlToPlainText(); + String plainText = formatter.getPlainText(doc); + System.out.println(plainText); + } + + /** + * Format an Element to plain-text + * @param element the root element to format + * @return formatted text + */ + public String getPlainText(Element element) { + FormattingVisitor formatter = new FormattingVisitor(); + NodeTraversor traversor = new NodeTraversor(formatter); + traversor.traverse(element); // walk the DOM, and call .head() and .tail() for each node + + return formatter.toString(); + } + + // the formatting rules, implemented in a breadth-first DOM traverse + private class FormattingVisitor implements NodeVisitor { + private static final int maxWidth = 80; + private int width = 0; + private StringBuilder accum = new StringBuilder(); // holds the accumulated text + + // hit when the node is first seen + public void head(Node node, int depth) { + String name = node.nodeName(); + if (node instanceof TextNode) + append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM. + else if (name.equals("li")) + append("\n * "); + } + + // hit when all of the node's children (if any) have been visited + public void tail(Node node, int depth) { + String name = node.nodeName(); + if (name.equals("br")) + append("\n"); + else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5")) + append("\n\n"); + else if (name.equals("a")) + append(String.format(" <%s>", node.absUrl("href"))); + } + + // appends text to the string builder with a simple word wrap method + private void append(String text) { + if (text.startsWith("\n")) + width = 0; // reset counter if starts with a newline. only from formats above, not in natural text + if (text.equals(" ") && + (accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n"))) + return; // don't accumulate long runs of empty spaces + + if (text.length() + width > maxWidth) { // won't fit, needs to wrap + String words[] = text.split("\\s+"); + for (int i = 0; i < words.length; i++) { + String word = words[i]; + boolean last = i == words.length - 1; + if (!last) // insert a space if not the last word + word = word + " "; + if (word.length() + width > maxWidth) { // wrap and reset counter + accum.append("\n").append(word); + width = word.length(); + } else { + accum.append(word); + width += word.length(); + } + } + } else { // fits as is, without need to wrap text + accum.append(text); + width += text.length(); + } + } + + public String toString() { + return accum.toString(); + } + } +} diff --git a/server/src/org/jsoup/examples/ListLinks.java b/server/src/org/jsoup/examples/ListLinks.java new file mode 100644 index 0000000000..64b29ba107 --- /dev/null +++ b/server/src/org/jsoup/examples/ListLinks.java @@ -0,0 +1,56 @@ +package org.jsoup.examples; + +import org.jsoup.Jsoup; +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; + +/** + * Example program to list links from a URL. + */ +public class ListLinks { + public static void main(String[] args) throws IOException { + Validate.isTrue(args.length == 1, "usage: supply url to fetch"); + String url = args[0]; + print("Fetching %s...", url); + + Document doc = Jsoup.connect(url).get(); + Elements links = doc.select("a[href]"); + Elements media = doc.select("[src]"); + Elements imports = doc.select("link[href]"); + + print("\nMedia: (%d)", media.size()); + for (Element src : media) { + if (src.tagName().equals("img")) + print(" * %s: <%s> %sx%s (%s)", + src.tagName(), src.attr("abs:src"), src.attr("width"), src.attr("height"), + trim(src.attr("alt"), 20)); + else + print(" * %s: <%s>", src.tagName(), src.attr("abs:src")); + } + + print("\nImports: (%d)", imports.size()); + for (Element link : imports) { + print(" * %s <%s> (%s)", link.tagName(),link.attr("abs:href"), link.attr("rel")); + } + + print("\nLinks: (%d)", links.size()); + for (Element link : links) { + print(" * a: <%s> (%s)", link.attr("abs:href"), trim(link.text(), 35)); + } + } + + private static void print(String msg, Object... args) { + System.out.println(String.format(msg, args)); + } + + private static String trim(String s, int width) { + if (s.length() > width) + return s.substring(0, width-1) + "."; + else + return s; + } +} diff --git a/server/src/org/jsoup/examples/package-info.java b/server/src/org/jsoup/examples/package-info.java new file mode 100644 index 0000000000..c312f430d4 --- /dev/null +++ b/server/src/org/jsoup/examples/package-info.java @@ -0,0 +1,4 @@ +/** + Contains example programs and use of jsoup. See the <a href="http://jsoup.org/cookbook/">jsoup cookbook</a>. + */ +package org.jsoup.examples;
\ No newline at end of file diff --git a/server/src/org/jsoup/helper/DataUtil.java b/server/src/org/jsoup/helper/DataUtil.java new file mode 100644 index 0000000000..9adfe42153 --- /dev/null +++ b/server/src/org/jsoup/helper/DataUtil.java @@ -0,0 +1,135 @@ +package org.jsoup.helper; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Parser; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Internal static utilities for handling data. + * + */ +public class DataUtil { + private static final Pattern charsetPattern = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)"); + static final String defaultCharset = "UTF-8"; // used if not found in header or meta charset + private static final int bufferSize = 0x20000; // ~130K. + + private DataUtil() {} + + /** + * Loads a file to a Document. + * @param in file to load + * @param charsetName character set of input + * @param baseUri base URI of document, to resolve relative links against + * @return Document + * @throws IOException on IO error + */ + public static Document load(File in, String charsetName, String baseUri) throws IOException { + FileInputStream inStream = null; + try { + inStream = new FileInputStream(in); + ByteBuffer byteData = readToByteBuffer(inStream); + return parseByteData(byteData, charsetName, baseUri, Parser.htmlParser()); + } finally { + if (inStream != null) + inStream.close(); + } + } + + /** + * Parses a Document from an input steam. + * @param in input stream to parse. You will need to close it. + * @param charsetName character set of input + * @param baseUri base URI of document, to resolve relative links against + * @return Document + * @throws IOException on IO error + */ + public static Document load(InputStream in, String charsetName, String baseUri) throws IOException { + ByteBuffer byteData = readToByteBuffer(in); + return parseByteData(byteData, charsetName, baseUri, Parser.htmlParser()); + } + + /** + * Parses a Document from an input steam, using the provided Parser. + * @param in input stream to parse. You will need to close it. + * @param charsetName character set of input + * @param baseUri base URI of document, to resolve relative links against + * @param parser alternate {@link Parser#xmlParser() parser} to use. + * @return Document + * @throws IOException on IO error + */ + public static Document load(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException { + ByteBuffer byteData = readToByteBuffer(in); + return parseByteData(byteData, charsetName, baseUri, parser); + } + + // reads bytes first into a buffer, then decodes with the appropriate charset. done this way to support + // switching the chartset midstream when a meta http-equiv tag defines the charset. + static Document parseByteData(ByteBuffer byteData, String charsetName, String baseUri, Parser parser) { + String docData; + Document doc = null; + if (charsetName == null) { // determine from meta. safe parse as UTF-8 + // look for <meta http-equiv="Content-Type" content="text/html;charset=gb2312"> or HTML5 <meta charset="gb2312"> + docData = Charset.forName(defaultCharset).decode(byteData).toString(); + doc = parser.parseInput(docData, baseUri); + Element meta = doc.select("meta[http-equiv=content-type], meta[charset]").first(); + if (meta != null) { // if not found, will keep utf-8 as best attempt + String foundCharset = meta.hasAttr("http-equiv") ? getCharsetFromContentType(meta.attr("content")) : meta.attr("charset"); + if (foundCharset != null && foundCharset.length() != 0 && !foundCharset.equals(defaultCharset)) { // need to re-decode + charsetName = foundCharset; + byteData.rewind(); + docData = Charset.forName(foundCharset).decode(byteData).toString(); + doc = null; + } + } + } else { // specified by content type header (or by user on file load) + Validate.notEmpty(charsetName, "Must set charset arg to character set of file to parse. Set to null to attempt to detect from HTML"); + docData = Charset.forName(charsetName).decode(byteData).toString(); + } + if (doc == null) { + // there are times where there is a spurious byte-order-mark at the start of the text. Shouldn't be present + // in utf-8. If after decoding, there is a BOM, strip it; otherwise will cause the parser to go straight + // into head mode + if (docData.charAt(0) == 65279) + docData = docData.substring(1); + + doc = parser.parseInput(docData, baseUri); + doc.outputSettings().charset(charsetName); + } + return doc; + } + + static ByteBuffer readToByteBuffer(InputStream inStream) throws IOException { + byte[] buffer = new byte[bufferSize]; + ByteArrayOutputStream outStream = new ByteArrayOutputStream(bufferSize); + int read; + while(true) { + read = inStream.read(buffer); + if (read == -1) break; + outStream.write(buffer, 0, read); + } + ByteBuffer byteData = ByteBuffer.wrap(outStream.toByteArray()); + return byteData; + } + + /** + * Parse out a charset from a content type header. + * @param contentType e.g. "text/html; charset=EUC-JP" + * @return "EUC-JP", or null if not found. Charset is trimmed and uppercased. + */ + static String getCharsetFromContentType(String contentType) { + if (contentType == null) return null; + Matcher m = charsetPattern.matcher(contentType); + if (m.find()) { + return m.group(1).trim().toUpperCase(); + } + return null; + } + + +} diff --git a/server/src/org/jsoup/helper/DescendableLinkedList.java b/server/src/org/jsoup/helper/DescendableLinkedList.java new file mode 100644 index 0000000000..28ca1971eb --- /dev/null +++ b/server/src/org/jsoup/helper/DescendableLinkedList.java @@ -0,0 +1,82 @@ +package org.jsoup.helper; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.ListIterator; + +/** + * Provides a descending iterator and other 1.6 methods to allow support on the 1.5 JRE. + */ +public class DescendableLinkedList<E> extends LinkedList<E> { + + /** + * Create a new DescendableLinkedList. + */ + public DescendableLinkedList() { + super(); + } + + /** + * Add a new element to the start of the list. + * @param e element to add + */ + public void push(E e) { + addFirst(e); + } + + /** + * Look at the last element, if there is one. + * @return the last element, or null + */ + public E peekLast() { + return size() == 0 ? null : getLast(); + } + + /** + * Remove and return the last element, if there is one + * @return the last element, or null + */ + public E pollLast() { + return size() == 0 ? null : removeLast(); + } + + /** + * Get an iterator that starts and the end of the list and works towards the start. + * @return an iterator that starts and the end of the list and works towards the start. + */ + public Iterator<E> descendingIterator() { + return new DescendingIterator<E>(size()); + } + + private class DescendingIterator<E> implements Iterator<E> { + private final ListIterator<E> iter; + + @SuppressWarnings("unchecked") + private DescendingIterator(int index) { + iter = (ListIterator<E>) listIterator(index); + } + + /** + * Check if there is another element on the list. + * @return if another element + */ + public boolean hasNext() { + return iter.hasPrevious(); + } + + /** + * Get the next element. + * @return the next element. + */ + public E next() { + return iter.previous(); + } + + /** + * Remove the current element. + */ + public void remove() { + iter.remove(); + } + } +} diff --git a/server/src/org/jsoup/helper/HttpConnection.java b/server/src/org/jsoup/helper/HttpConnection.java new file mode 100644 index 0000000000..06200a2547 --- /dev/null +++ b/server/src/org/jsoup/helper/HttpConnection.java @@ -0,0 +1,658 @@ +package org.jsoup.helper; + +import org.jsoup.Connection; +import org.jsoup.nodes.Document; +import org.jsoup.parser.Parser; +import org.jsoup.parser.TokenQueue; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.*; +import java.util.zip.GZIPInputStream; + +/** + * Implementation of {@link Connection}. + * @see org.jsoup.Jsoup#connect(String) + */ +public class HttpConnection implements Connection { + public static Connection connect(String url) { + Connection con = new HttpConnection(); + con.url(url); + return con; + } + + public static Connection connect(URL url) { + Connection con = new HttpConnection(); + con.url(url); + return con; + } + + private Connection.Request req; + private Connection.Response res; + + private HttpConnection() { + req = new Request(); + res = new Response(); + } + + public Connection url(URL url) { + req.url(url); + return this; + } + + public Connection url(String url) { + Validate.notEmpty(url, "Must supply a valid URL"); + try { + req.url(new URL(url)); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Malformed URL: " + url, e); + } + return this; + } + + public Connection userAgent(String userAgent) { + Validate.notNull(userAgent, "User agent must not be null"); + req.header("User-Agent", userAgent); + return this; + } + + public Connection timeout(int millis) { + req.timeout(millis); + return this; + } + + public Connection followRedirects(boolean followRedirects) { + req.followRedirects(followRedirects); + return this; + } + + public Connection referrer(String referrer) { + Validate.notNull(referrer, "Referrer must not be null"); + req.header("Referer", referrer); + return this; + } + + public Connection method(Method method) { + req.method(method); + return this; + } + + public Connection ignoreHttpErrors(boolean ignoreHttpErrors) { + req.ignoreHttpErrors(ignoreHttpErrors); + return this; + } + + public Connection ignoreContentType(boolean ignoreContentType) { + req.ignoreContentType(ignoreContentType); + return this; + } + + public Connection data(String key, String value) { + req.data(KeyVal.create(key, value)); + return this; + } + + public Connection data(Map<String, String> data) { + Validate.notNull(data, "Data map must not be null"); + for (Map.Entry<String, String> entry : data.entrySet()) { + req.data(KeyVal.create(entry.getKey(), entry.getValue())); + } + return this; + } + + public Connection data(String... keyvals) { + Validate.notNull(keyvals, "Data key value pairs must not be null"); + Validate.isTrue(keyvals.length %2 == 0, "Must supply an even number of key value pairs"); + for (int i = 0; i < keyvals.length; i += 2) { + String key = keyvals[i]; + String value = keyvals[i+1]; + Validate.notEmpty(key, "Data key must not be empty"); + Validate.notNull(value, "Data value must not be null"); + req.data(KeyVal.create(key, value)); + } + return this; + } + + public Connection header(String name, String value) { + req.header(name, value); + return this; + } + + public Connection cookie(String name, String value) { + req.cookie(name, value); + return this; + } + + public Connection cookies(Map<String, String> cookies) { + Validate.notNull(cookies, "Cookie map must not be null"); + for (Map.Entry<String, String> entry : cookies.entrySet()) { + req.cookie(entry.getKey(), entry.getValue()); + } + return this; + } + + public Connection parser(Parser parser) { + req.parser(parser); + return this; + } + + public Document get() throws IOException { + req.method(Method.GET); + execute(); + return res.parse(); + } + + public Document post() throws IOException { + req.method(Method.POST); + execute(); + return res.parse(); + } + + public Connection.Response execute() throws IOException { + res = Response.execute(req); + return res; + } + + public Connection.Request request() { + return req; + } + + public Connection request(Connection.Request request) { + req = request; + return this; + } + + public Connection.Response response() { + return res; + } + + public Connection response(Connection.Response response) { + res = response; + return this; + } + + @SuppressWarnings({"unchecked"}) + private static abstract class Base<T extends Connection.Base> implements Connection.Base<T> { + URL url; + Method method; + Map<String, String> headers; + Map<String, String> cookies; + + private Base() { + headers = new LinkedHashMap<String, String>(); + cookies = new LinkedHashMap<String, String>(); + } + + public URL url() { + return url; + } + + public T url(URL url) { + Validate.notNull(url, "URL must not be null"); + this.url = url; + return (T) this; + } + + public Method method() { + return method; + } + + public T method(Method method) { + Validate.notNull(method, "Method must not be null"); + this.method = method; + return (T) this; + } + + public String header(String name) { + Validate.notNull(name, "Header name must not be null"); + return getHeaderCaseInsensitive(name); + } + + public T header(String name, String value) { + Validate.notEmpty(name, "Header name must not be empty"); + Validate.notNull(value, "Header value must not be null"); + removeHeader(name); // ensures we don't get an "accept-encoding" and a "Accept-Encoding" + headers.put(name, value); + return (T) this; + } + + public boolean hasHeader(String name) { + Validate.notEmpty(name, "Header name must not be empty"); + return getHeaderCaseInsensitive(name) != null; + } + + public T removeHeader(String name) { + Validate.notEmpty(name, "Header name must not be empty"); + Map.Entry<String, String> entry = scanHeaders(name); // remove is case insensitive too + if (entry != null) + headers.remove(entry.getKey()); // ensures correct case + return (T) this; + } + + public Map<String, String> headers() { + return headers; + } + + private String getHeaderCaseInsensitive(String name) { + Validate.notNull(name, "Header name must not be null"); + // quick evals for common case of title case, lower case, then scan for mixed + String value = headers.get(name); + if (value == null) + value = headers.get(name.toLowerCase()); + if (value == null) { + Map.Entry<String, String> entry = scanHeaders(name); + if (entry != null) + value = entry.getValue(); + } + return value; + } + + private Map.Entry<String, String> scanHeaders(String name) { + String lc = name.toLowerCase(); + for (Map.Entry<String, String> entry : headers.entrySet()) { + if (entry.getKey().toLowerCase().equals(lc)) + return entry; + } + return null; + } + + public String cookie(String name) { + Validate.notNull(name, "Cookie name must not be null"); + return cookies.get(name); + } + + public T cookie(String name, String value) { + Validate.notEmpty(name, "Cookie name must not be empty"); + Validate.notNull(value, "Cookie value must not be null"); + cookies.put(name, value); + return (T) this; + } + + public boolean hasCookie(String name) { + Validate.notEmpty("Cookie name must not be empty"); + return cookies.containsKey(name); + } + + public T removeCookie(String name) { + Validate.notEmpty("Cookie name must not be empty"); + cookies.remove(name); + return (T) this; + } + + public Map<String, String> cookies() { + return cookies; + } + } + + public static class Request extends Base<Connection.Request> implements Connection.Request { + private int timeoutMilliseconds; + private boolean followRedirects; + private Collection<Connection.KeyVal> data; + private boolean ignoreHttpErrors = false; + private boolean ignoreContentType = false; + private Parser parser; + + private Request() { + timeoutMilliseconds = 3000; + followRedirects = true; + data = new ArrayList<Connection.KeyVal>(); + method = Connection.Method.GET; + headers.put("Accept-Encoding", "gzip"); + parser = Parser.htmlParser(); + } + + public int timeout() { + return timeoutMilliseconds; + } + + public Request timeout(int millis) { + Validate.isTrue(millis >= 0, "Timeout milliseconds must be 0 (infinite) or greater"); + timeoutMilliseconds = millis; + return this; + } + + public boolean followRedirects() { + return followRedirects; + } + + public Connection.Request followRedirects(boolean followRedirects) { + this.followRedirects = followRedirects; + return this; + } + + public boolean ignoreHttpErrors() { + return ignoreHttpErrors; + } + + public Connection.Request ignoreHttpErrors(boolean ignoreHttpErrors) { + this.ignoreHttpErrors = ignoreHttpErrors; + return this; + } + + public boolean ignoreContentType() { + return ignoreContentType; + } + + public Connection.Request ignoreContentType(boolean ignoreContentType) { + this.ignoreContentType = ignoreContentType; + return this; + } + + public Request data(Connection.KeyVal keyval) { + Validate.notNull(keyval, "Key val must not be null"); + data.add(keyval); + return this; + } + + public Collection<Connection.KeyVal> data() { + return data; + } + + public Request parser(Parser parser) { + this.parser = parser; + return this; + } + + public Parser parser() { + return parser; + } + } + + public static class Response extends Base<Connection.Response> implements Connection.Response { + private static final int MAX_REDIRECTS = 20; + private int statusCode; + private String statusMessage; + private ByteBuffer byteData; + private String charset; + private String contentType; + private boolean executed = false; + private int numRedirects = 0; + private Connection.Request req; + + Response() { + super(); + } + + private Response(Response previousResponse) throws IOException { + super(); + if (previousResponse != null) { + numRedirects = previousResponse.numRedirects + 1; + if (numRedirects >= MAX_REDIRECTS) + throw new IOException(String.format("Too many redirects occurred trying to load URL %s", previousResponse.url())); + } + } + + static Response execute(Connection.Request req) throws IOException { + return execute(req, null); + } + + static Response execute(Connection.Request req, Response previousResponse) throws IOException { + Validate.notNull(req, "Request must not be null"); + String protocol = req.url().getProtocol(); + Validate + .isTrue(protocol.equals("http") || protocol.equals("https"), "Only http & https protocols supported"); + + // set up the request for execution + if (req.method() == Connection.Method.GET && req.data().size() > 0) + serialiseRequestUrl(req); // appends query string + HttpURLConnection conn = createConnection(req); + conn.connect(); + if (req.method() == Connection.Method.POST) + writePost(req.data(), conn.getOutputStream()); + + int status = conn.getResponseCode(); + boolean needsRedirect = false; + if (status != HttpURLConnection.HTTP_OK) { + if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER) + needsRedirect = true; + else if (!req.ignoreHttpErrors()) + throw new IOException(status + " error loading URL " + req.url().toString()); + } + Response res = new Response(previousResponse); + res.setupFromConnection(conn, previousResponse); + if (needsRedirect && req.followRedirects()) { + req.method(Method.GET); // always redirect with a get. any data param from original req are dropped. + req.data().clear(); + req.url(new URL(req.url(), res.header("Location"))); + for (Map.Entry<String, String> cookie : res.cookies.entrySet()) { // add response cookies to request (for e.g. login posts) + req.cookie(cookie.getKey(), cookie.getValue()); + } + return execute(req, res); + } + res.req = req; + + InputStream bodyStream = null; + InputStream dataStream = null; + try { + dataStream = conn.getErrorStream() != null ? conn.getErrorStream() : conn.getInputStream(); + bodyStream = res.hasHeader("Content-Encoding") && res.header("Content-Encoding").equalsIgnoreCase("gzip") ? + new BufferedInputStream(new GZIPInputStream(dataStream)) : + new BufferedInputStream(dataStream); + + res.byteData = DataUtil.readToByteBuffer(bodyStream); + res.charset = DataUtil.getCharsetFromContentType(res.contentType); // may be null, readInputStream deals with it + } finally { + if (bodyStream != null) bodyStream.close(); + if (dataStream != null) dataStream.close(); + } + + res.executed = true; + return res; + } + + public int statusCode() { + return statusCode; + } + + public String statusMessage() { + return statusMessage; + } + + public String charset() { + return charset; + } + + public String contentType() { + return contentType; + } + + public Document parse() throws IOException { + Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before parsing response"); + if (!req.ignoreContentType() && (contentType == null || !(contentType.startsWith("text/") || contentType.startsWith("application/xml") || contentType.startsWith("application/xhtml+xml")))) + throw new IOException(String.format("Unhandled content type \"%s\" on URL %s. Must be text/*, application/xml, or application/xhtml+xml", + contentType, url.toString())); + Document doc = DataUtil.parseByteData(byteData, charset, url.toExternalForm(), req.parser()); + byteData.rewind(); + charset = doc.outputSettings().charset().name(); // update charset from meta-equiv, possibly + return doc; + } + + public String body() { + Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before getting response body"); + // charset gets set from header on execute, and from meta-equiv on parse. parse may not have happened yet + String body; + if (charset == null) + body = Charset.forName(DataUtil.defaultCharset).decode(byteData).toString(); + else + body = Charset.forName(charset).decode(byteData).toString(); + byteData.rewind(); + return body; + } + + public byte[] bodyAsBytes() { + Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before getting response body"); + return byteData.array(); + } + + // set up connection defaults, and details from request + private static HttpURLConnection createConnection(Connection.Request req) throws IOException { + HttpURLConnection conn = (HttpURLConnection) req.url().openConnection(); + conn.setRequestMethod(req.method().name()); + conn.setInstanceFollowRedirects(false); // don't rely on native redirection support + conn.setConnectTimeout(req.timeout()); + conn.setReadTimeout(req.timeout()); + if (req.method() == Method.POST) + conn.setDoOutput(true); + if (req.cookies().size() > 0) + conn.addRequestProperty("Cookie", getRequestCookieString(req)); + for (Map.Entry<String, String> header : req.headers().entrySet()) { + conn.addRequestProperty(header.getKey(), header.getValue()); + } + return conn; + } + + // set up url, method, header, cookies + private void setupFromConnection(HttpURLConnection conn, Connection.Response previousResponse) throws IOException { + method = Connection.Method.valueOf(conn.getRequestMethod()); + url = conn.getURL(); + statusCode = conn.getResponseCode(); + statusMessage = conn.getResponseMessage(); + contentType = conn.getContentType(); + + Map<String, List<String>> resHeaders = conn.getHeaderFields(); + processResponseHeaders(resHeaders); + + // if from a redirect, map previous response cookies into this response + if (previousResponse != null) { + for (Map.Entry<String, String> prevCookie : previousResponse.cookies().entrySet()) { + if (!hasCookie(prevCookie.getKey())) + cookie(prevCookie.getKey(), prevCookie.getValue()); + } + } + } + + void processResponseHeaders(Map<String, List<String>> resHeaders) { + for (Map.Entry<String, List<String>> entry : resHeaders.entrySet()) { + String name = entry.getKey(); + if (name == null) + continue; // http/1.1 line + + List<String> values = entry.getValue(); + if (name.equalsIgnoreCase("Set-Cookie")) { + for (String value : values) { + if (value == null) + continue; + TokenQueue cd = new TokenQueue(value); + String cookieName = cd.chompTo("=").trim(); + String cookieVal = cd.consumeTo(";").trim(); + if (cookieVal == null) + cookieVal = ""; + // ignores path, date, domain, secure et al. req'd? + // name not blank, value not null + if (cookieName != null && cookieName.length() > 0) + cookie(cookieName, cookieVal); + } + } else { // only take the first instance of each header + if (!values.isEmpty()) + header(name, values.get(0)); + } + } + } + + private static void writePost(Collection<Connection.KeyVal> data, OutputStream outputStream) throws IOException { + OutputStreamWriter w = new OutputStreamWriter(outputStream, DataUtil.defaultCharset); + boolean first = true; + for (Connection.KeyVal keyVal : data) { + if (!first) + w.append('&'); + else + first = false; + + w.write(URLEncoder.encode(keyVal.key(), DataUtil.defaultCharset)); + w.write('='); + w.write(URLEncoder.encode(keyVal.value(), DataUtil.defaultCharset)); + } + w.close(); + } + + private static String getRequestCookieString(Connection.Request req) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry<String, String> cookie : req.cookies().entrySet()) { + if (!first) + sb.append("; "); + else + first = false; + sb.append(cookie.getKey()).append('=').append(cookie.getValue()); + // todo: spec says only ascii, no escaping / encoding defined. validate on set? or escape somehow here? + } + return sb.toString(); + } + + // for get url reqs, serialise the data map into the url + private static void serialiseRequestUrl(Connection.Request req) throws IOException { + URL in = req.url(); + StringBuilder url = new StringBuilder(); + boolean first = true; + // reconstitute the query, ready for appends + url + .append(in.getProtocol()) + .append("://") + .append(in.getAuthority()) // includes host, port + .append(in.getPath()) + .append("?"); + if (in.getQuery() != null) { + url.append(in.getQuery()); + first = false; + } + for (Connection.KeyVal keyVal : req.data()) { + if (!first) + url.append('&'); + else + first = false; + url + .append(URLEncoder.encode(keyVal.key(), DataUtil.defaultCharset)) + .append('=') + .append(URLEncoder.encode(keyVal.value(), DataUtil.defaultCharset)); + } + req.url(new URL(url.toString())); + req.data().clear(); // moved into url as get params + } + } + + public static class KeyVal implements Connection.KeyVal { + private String key; + private String value; + + public static KeyVal create(String key, String value) { + Validate.notEmpty(key, "Data key must not be empty"); + Validate.notNull(value, "Data value must not be null"); + return new KeyVal(key, value); + } + + private KeyVal(String key, String value) { + this.key = key; + this.value = value; + } + + public KeyVal key(String key) { + Validate.notEmpty(key, "Data key must not be empty"); + this.key = key; + return this; + } + + public String key() { + return key; + } + + public KeyVal value(String value) { + Validate.notNull(value, "Data value must not be null"); + this.value = value; + return this; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return key + "=" + value; + } + } +} diff --git a/server/src/org/jsoup/helper/StringUtil.java b/server/src/org/jsoup/helper/StringUtil.java new file mode 100644 index 0000000000..071a92c7a5 --- /dev/null +++ b/server/src/org/jsoup/helper/StringUtil.java @@ -0,0 +1,140 @@ +package org.jsoup.helper; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A minimal String utility class. Designed for internal jsoup use only. + */ +public final class StringUtil { + // memoised padding up to 10 + private static final String[] padding = {"", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "}; + + /** + * Join a collection of strings by a seperator + * @param strings collection of string objects + * @param sep string to place between strings + * @return joined string + */ + public static String join(Collection strings, String sep) { + return join(strings.iterator(), sep); + } + + /** + * Join a collection of strings by a seperator + * @param strings iterator of string objects + * @param sep string to place between strings + * @return joined string + */ + public static String join(Iterator strings, String sep) { + if (!strings.hasNext()) + return ""; + + String start = strings.next().toString(); + if (!strings.hasNext()) // only one, avoid builder + return start; + + StringBuilder sb = new StringBuilder(64).append(start); + while (strings.hasNext()) { + sb.append(sep); + sb.append(strings.next()); + } + return sb.toString(); + } + + /** + * Returns space padding + * @param width amount of padding desired + * @return string of spaces * width + */ + public static String padding(int width) { + if (width < 0) + throw new IllegalArgumentException("width must be > 0"); + + if (width < padding.length) + return padding[width]; + + char[] out = new char[width]; + for (int i = 0; i < width; i++) + out[i] = ' '; + return String.valueOf(out); + } + + /** + * Tests if a string is blank: null, emtpy, or only whitespace (" ", \r\n, \t, etc) + * @param string string to test + * @return if string is blank + */ + public static boolean isBlank(String string) { + if (string == null || string.length() == 0) + return true; + + int l = string.length(); + for (int i = 0; i < l; i++) { + if (!StringUtil.isWhitespace(string.codePointAt(i))) + return false; + } + return true; + } + + /** + * Tests if a string is numeric, i.e. contains only digit characters + * @param string string to test + * @return true if only digit chars, false if empty or null or contains non-digit chrs + */ + public static boolean isNumeric(String string) { + if (string == null || string.length() == 0) + return false; + + int l = string.length(); + for (int i = 0; i < l; i++) { + if (!Character.isDigit(string.codePointAt(i))) + return false; + } + return true; + } + + /** + * Tests if a code point is "whitespace" as defined in the HTML spec. + * @param c code point to test + * @return true if code point is whitespace, false otherwise + */ + public static boolean isWhitespace(int c){ + return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r'; + } + + public static String normaliseWhitespace(String string) { + StringBuilder sb = new StringBuilder(string.length()); + + boolean lastWasWhite = false; + boolean modified = false; + + int l = string.length(); + for (int i = 0; i < l; i++) { + int c = string.codePointAt(i); + if (isWhitespace(c)) { + if (lastWasWhite) { + modified = true; + continue; + } + if (c != ' ') + modified = true; + sb.append(' '); + lastWasWhite = true; + } + else { + sb.appendCodePoint(c); + lastWasWhite = false; + } + } + return modified ? sb.toString() : string; + } + + public static boolean in(String needle, String... haystack) { + for (String hay : haystack) { + if (hay.equals(needle)) + return true; + } + return false; + } +} diff --git a/server/src/org/jsoup/helper/Validate.java b/server/src/org/jsoup/helper/Validate.java new file mode 100644 index 0000000000..814bcc3a40 --- /dev/null +++ b/server/src/org/jsoup/helper/Validate.java @@ -0,0 +1,112 @@ +package org.jsoup.helper; + +/** + * Simple validation methods. Designed for jsoup internal use + */ +public final class Validate { + + private Validate() {} + + /** + * Validates that the object is not null + * @param obj object to test + */ + public static void notNull(Object obj) { + if (obj == null) + throw new IllegalArgumentException("Object must not be null"); + } + + /** + * Validates that the object is not null + * @param obj object to test + * @param msg message to output if validation fails + */ + public static void notNull(Object obj, String msg) { + if (obj == null) + throw new IllegalArgumentException(msg); + } + + /** + * Validates that the value is true + * @param val object to test + */ + public static void isTrue(boolean val) { + if (!val) + throw new IllegalArgumentException("Must be true"); + } + + /** + * Validates that the value is true + * @param val object to test + * @param msg message to output if validation fails + */ + public static void isTrue(boolean val, String msg) { + if (!val) + throw new IllegalArgumentException(msg); + } + + /** + * Validates that the value is false + * @param val object to test + */ + public static void isFalse(boolean val) { + if (val) + throw new IllegalArgumentException("Must be false"); + } + + /** + * Validates that the value is false + * @param val object to test + * @param msg message to output if validation fails + */ + public static void isFalse(boolean val, String msg) { + if (val) + throw new IllegalArgumentException(msg); + } + + /** + * Validates that the array contains no null elements + * @param objects the array to test + */ + public static void noNullElements(Object[] objects) { + noNullElements(objects, "Array must not contain any null objects"); + } + + /** + * Validates that the array contains no null elements + * @param objects the array to test + * @param msg message to output if validation fails + */ + public static void noNullElements(Object[] objects, String msg) { + for (Object obj : objects) + if (obj == null) + throw new IllegalArgumentException(msg); + } + + /** + * Validates that the string is not empty + * @param string the string to test + */ + public static void notEmpty(String string) { + if (string == null || string.length() == 0) + throw new IllegalArgumentException("String must not be empty"); + } + + /** + * Validates that the string is not empty + * @param string the string to test + * @param msg message to output if validation fails + */ + public static void notEmpty(String string, String msg) { + if (string == null || string.length() == 0) + throw new IllegalArgumentException(msg); + } + + /** + Cause a failure. + @param msg message to output. + */ + public static void fail(String msg) { + throw new IllegalArgumentException(msg); + } +} diff --git a/server/src/org/jsoup/nodes/Attribute.java b/server/src/org/jsoup/nodes/Attribute.java new file mode 100644 index 0000000000..02eb29db83 --- /dev/null +++ b/server/src/org/jsoup/nodes/Attribute.java @@ -0,0 +1,131 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.Validate; + +import java.util.Map; + +/** + A single key + value attribute. Keys are trimmed and normalised to lower-case. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Attribute implements Map.Entry<String, String>, Cloneable { + private String key; + private String value; + + /** + * Create a new attribute from unencoded (raw) key and value. + * @param key attribute key + * @param value attribute value + * @see #createFromEncoded + */ + public Attribute(String key, String value) { + Validate.notEmpty(key); + Validate.notNull(value); + this.key = key.trim().toLowerCase(); + this.value = value; + } + + /** + Get the attribute key. + @return the attribute key + */ + public String getKey() { + return key; + } + + /** + Set the attribute key. Gets normalised as per the constructor method. + @param key the new key; must not be null + */ + public void setKey(String key) { + Validate.notEmpty(key); + this.key = key.trim().toLowerCase(); + } + + /** + Get the attribute value. + @return the attribute value + */ + public String getValue() { + return value; + } + + /** + Set the attribute value. + @param value the new attribute value; must not be null + */ + public String setValue(String value) { + Validate.notNull(value); + String old = this.value; + this.value = value; + return old; + } + + /** + Get the HTML representation of this attribute; e.g. {@code href="index.html"}. + @return HTML + */ + public String html() { + return key + "=\"" + Entities.escape(value, (new Document("")).outputSettings()) + "\""; + } + + protected void html(StringBuilder accum, Document.OutputSettings out) { + accum + .append(key) + .append("=\"") + .append(Entities.escape(value, out)) + .append("\""); + } + + /** + Get the string representation of this attribute, implemented as {@link #html()}. + @return string + */ + public String toString() { + return html(); + } + + /** + * Create a new Attribute from an unencoded key and a HTML attribute encoded value. + * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars. + * @param encodedValue HTML attribute encoded value + * @return attribute + */ + public static Attribute createFromEncoded(String unencodedKey, String encodedValue) { + String value = Entities.unescape(encodedValue, true); + return new Attribute(unencodedKey, value); + } + + protected boolean isDataAttribute() { + return key.startsWith(Attributes.dataPrefix) && key.length() > Attributes.dataPrefix.length(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Attribute)) return false; + + Attribute attribute = (Attribute) o; + + if (key != null ? !key.equals(attribute.key) : attribute.key != null) return false; + if (value != null ? !value.equals(attribute.value) : attribute.value != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = key != null ? key.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public Attribute clone() { + try { + return (Attribute) super.clone(); // only fields are immutable strings key and value, so no more deep copy required + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/server/src/org/jsoup/nodes/Attributes.java b/server/src/org/jsoup/nodes/Attributes.java new file mode 100644 index 0000000000..9436750fc9 --- /dev/null +++ b/server/src/org/jsoup/nodes/Attributes.java @@ -0,0 +1,249 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.Validate; + +import java.util.*; + +/** + * The attributes of an Element. + * <p/> + * Attributes are treated as a map: there can be only one value associated with an attribute key. + * <p/> + * Attribute key and value comparisons are done case insensitively, and keys are normalised to + * lower-case. + * + * @author Jonathan Hedley, jonathan@hedley.net + */ +public class Attributes implements Iterable<Attribute>, Cloneable { + protected static final String dataPrefix = "data-"; + + private LinkedHashMap<String, Attribute> attributes = null; + // linked hash map to preserve insertion order. + // null be default as so many elements have no attributes -- saves a good chunk of memory + + /** + Get an attribute value by key. + @param key the attribute key + @return the attribute value if set; or empty string if not set. + @see #hasKey(String) + */ + public String get(String key) { + Validate.notEmpty(key); + + if (attributes == null) + return ""; + + Attribute attr = attributes.get(key.toLowerCase()); + return attr != null ? attr.getValue() : ""; + } + + /** + Set a new attribute, or replace an existing one by key. + @param key attribute key + @param value attribute value + */ + public void put(String key, String value) { + Attribute attr = new Attribute(key, value); + put(attr); + } + + /** + Set a new attribute, or replace an existing one by key. + @param attribute attribute + */ + public void put(Attribute attribute) { + Validate.notNull(attribute); + if (attributes == null) + attributes = new LinkedHashMap<String, Attribute>(2); + attributes.put(attribute.getKey(), attribute); + } + + /** + Remove an attribute by key. + @param key attribute key to remove + */ + public void remove(String key) { + Validate.notEmpty(key); + if (attributes == null) + return; + attributes.remove(key.toLowerCase()); + } + + /** + Tests if these attributes contain an attribute with this key. + @param key key to check for + @return true if key exists, false otherwise + */ + public boolean hasKey(String key) { + return attributes != null && attributes.containsKey(key.toLowerCase()); + } + + /** + Get the number of attributes in this set. + @return size + */ + public int size() { + if (attributes == null) + return 0; + return attributes.size(); + } + + /** + Add all the attributes from the incoming set to this set. + @param incoming attributes to add to these attributes. + */ + public void addAll(Attributes incoming) { + if (incoming.size() == 0) + return; + if (attributes == null) + attributes = new LinkedHashMap<String, Attribute>(incoming.size()); + attributes.putAll(incoming.attributes); + } + + public Iterator<Attribute> iterator() { + return asList().iterator(); + } + + /** + Get the attributes as a List, for iteration. Do not modify the keys of the attributes via this view, as changes + to keys will not be recognised in the containing set. + @return an view of the attributes as a List. + */ + public List<Attribute> asList() { + if (attributes == null) + return Collections.emptyList(); + + List<Attribute> list = new ArrayList<Attribute>(attributes.size()); + for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { + list.add(entry.getValue()); + } + return Collections.unmodifiableList(list); + } + + /** + * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys + * starting with {@code data-}. + * @return map of custom data attributes. + */ + public Map<String, String> dataset() { + return new Dataset(); + } + + /** + Get the HTML representation of these attributes. + @return HTML + */ + public String html() { + StringBuilder accum = new StringBuilder(); + html(accum, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used + return accum.toString(); + } + + void html(StringBuilder accum, Document.OutputSettings out) { + if (attributes == null) + return; + + for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { + Attribute attribute = entry.getValue(); + accum.append(" "); + attribute.html(accum, out); + } + } + + public String toString() { + return html(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Attributes)) return false; + + Attributes that = (Attributes) o; + + if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) return false; + + return true; + } + + @Override + public int hashCode() { + return attributes != null ? attributes.hashCode() : 0; + } + + @Override + public Attributes clone() { + if (attributes == null) + return new Attributes(); + + Attributes clone; + try { + clone = (Attributes) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + clone.attributes = new LinkedHashMap<String, Attribute>(attributes.size()); + for (Attribute attribute: this) + clone.attributes.put(attribute.getKey(), attribute.clone()); + return clone; + } + + private class Dataset extends AbstractMap<String, String> { + + private Dataset() { + if (attributes == null) + attributes = new LinkedHashMap<String, Attribute>(2); + } + + public Set<Entry<String, String>> entrySet() { + return new EntrySet(); + } + + @Override + public String put(String key, String value) { + String dataKey = dataKey(key); + String oldValue = hasKey(dataKey) ? attributes.get(dataKey).getValue() : null; + Attribute attr = new Attribute(dataKey, value); + attributes.put(dataKey, attr); + return oldValue; + } + + private class EntrySet extends AbstractSet<Map.Entry<String, String>> { + public Iterator<Map.Entry<String, String>> iterator() { + return new DatasetIterator(); + } + + public int size() { + int count = 0; + Iterator iter = new DatasetIterator(); + while (iter.hasNext()) + count++; + return count; + } + } + + private class DatasetIterator implements Iterator<Map.Entry<String, String>> { + private Iterator<Attribute> attrIter = attributes.values().iterator(); + private Attribute attr; + public boolean hasNext() { + while (attrIter.hasNext()) { + attr = attrIter.next(); + if (attr.isDataAttribute()) return true; + } + return false; + } + + public Entry<String, String> next() { + return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue()); + } + + public void remove() { + attributes.remove(attr.getKey()); + } + } + } + + private static String dataKey(String key) { + return dataPrefix + key; + } +} diff --git a/server/src/org/jsoup/nodes/Comment.java b/server/src/org/jsoup/nodes/Comment.java new file mode 100644 index 0000000000..37fd4368fa --- /dev/null +++ b/server/src/org/jsoup/nodes/Comment.java @@ -0,0 +1,46 @@ +package org.jsoup.nodes; + +/** + A comment node. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Comment extends Node { + private static final String COMMENT_KEY = "comment"; + + /** + Create a new comment node. + @param data The contents of the comment + @param baseUri base URI + */ + public Comment(String data, String baseUri) { + super(baseUri); + attributes.put(COMMENT_KEY, data); + } + + public String nodeName() { + return "#comment"; + } + + /** + Get the contents of the comment. + @return comment content + */ + public String getData() { + return attributes.get(COMMENT_KEY); + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + if (out.prettyPrint()) + indent(accum, depth, out); + accum + .append("<!--") + .append(getData()) + .append("-->"); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } +} diff --git a/server/src/org/jsoup/nodes/DataNode.java b/server/src/org/jsoup/nodes/DataNode.java new file mode 100644 index 0000000000..a64f56f0a4 --- /dev/null +++ b/server/src/org/jsoup/nodes/DataNode.java @@ -0,0 +1,62 @@ +package org.jsoup.nodes; + +/** + A data node, for contents of style, script tags etc, where contents should not show in text(). + + @author Jonathan Hedley, jonathan@hedley.net */ +public class DataNode extends Node{ + private static final String DATA_KEY = "data"; + + /** + Create a new DataNode. + @param data data contents + @param baseUri base URI + */ + public DataNode(String data, String baseUri) { + super(baseUri); + attributes.put(DATA_KEY, data); + } + + public String nodeName() { + return "#data"; + } + + /** + Get the data contents of this node. Will be unescaped and with original new lines, space etc. + @return data + */ + public String getWholeData() { + return attributes.get(DATA_KEY); + } + + /** + * Set the data contents of this node. + * @param data unencoded data + * @return this node, for chaining + */ + public DataNode setWholeData(String data) { + attributes.put(DATA_KEY, data); + return this; + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + accum.append(getWholeData()); // data is not escaped in return from data nodes, so " in script, style is plain + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } + + /** + Create a new DataNode from HTML encoded data. + @param encodedData encoded data + @param baseUri bass URI + @return new DataNode + */ + public static DataNode createFromEncoded(String encodedData, String baseUri) { + String data = Entities.unescape(encodedData); + return new DataNode(data, baseUri); + } +} diff --git a/server/src/org/jsoup/nodes/Document.java b/server/src/org/jsoup/nodes/Document.java new file mode 100644 index 0000000000..adb371ce14 --- /dev/null +++ b/server/src/org/jsoup/nodes/Document.java @@ -0,0 +1,350 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.Validate; +import org.jsoup.parser.Tag; +import org.jsoup.select.Elements; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.List; + +/** + A HTML Document. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Document extends Element { + private OutputSettings outputSettings = new OutputSettings(); + private QuirksMode quirksMode = QuirksMode.noQuirks; + + /** + Create a new, empty Document. + @param baseUri base URI of document + @see org.jsoup.Jsoup#parse + @see #createShell + */ + public Document(String baseUri) { + super(Tag.valueOf("#root"), baseUri); + } + + /** + Create a valid, empty shell of a document, suitable for adding more elements to. + @param baseUri baseUri of document + @return document with html, head, and body elements. + */ + static public Document createShell(String baseUri) { + Validate.notNull(baseUri); + + Document doc = new Document(baseUri); + Element html = doc.appendElement("html"); + html.appendElement("head"); + html.appendElement("body"); + + return doc; + } + + /** + Accessor to the document's {@code head} element. + @return {@code head} + */ + public Element head() { + return findFirstElementByTagName("head", this); + } + + /** + Accessor to the document's {@code body} element. + @return {@code body} + */ + public Element body() { + return findFirstElementByTagName("body", this); + } + + /** + Get the string contents of the document's {@code title} element. + @return Trimmed title, or empty string if none set. + */ + public String title() { + Element titleEl = getElementsByTag("title").first(); + return titleEl != null ? titleEl.text().trim() : ""; + } + + /** + Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if + not present + @param title string to set as title + */ + public void title(String title) { + Validate.notNull(title); + Element titleEl = getElementsByTag("title").first(); + if (titleEl == null) { // add to head + head().appendElement("title").text(title); + } else { + titleEl.text(title); + } + } + + /** + Create a new Element, with this document's base uri. Does not make the new element a child of this document. + @param tagName element tag name (e.g. {@code a}) + @return new element + */ + public Element createElement(String tagName) { + return new Element(Tag.valueOf(tagName), this.baseUri()); + } + + /** + Normalise the document. This happens after the parse phase so generally does not need to be called. + Moves any text content that is not in the body element into the body. + @return this document after normalisation + */ + public Document normalise() { + Element htmlEl = findFirstElementByTagName("html", this); + if (htmlEl == null) + htmlEl = appendElement("html"); + if (head() == null) + htmlEl.prependElement("head"); + if (body() == null) + htmlEl.appendElement("body"); + + // pull text nodes out of root, html, and head els, and push into body. non-text nodes are already taken care + // of. do in inverse order to maintain text order. + normaliseTextNodes(head()); + normaliseTextNodes(htmlEl); + normaliseTextNodes(this); + + normaliseStructure("head", htmlEl); + normaliseStructure("body", htmlEl); + + return this; + } + + // does not recurse. + private void normaliseTextNodes(Element element) { + List<Node> toMove = new ArrayList<Node>(); + for (Node node: element.childNodes) { + if (node instanceof TextNode) { + TextNode tn = (TextNode) node; + if (!tn.isBlank()) + toMove.add(tn); + } + } + + for (int i = toMove.size()-1; i >= 0; i--) { + Node node = toMove.get(i); + element.removeChild(node); + body().prependChild(new TextNode(" ", "")); + body().prependChild(node); + } + } + + // merge multiple <head> or <body> contents into one, delete the remainder, and ensure they are owned by <html> + private void normaliseStructure(String tag, Element htmlEl) { + Elements elements = this.getElementsByTag(tag); + Element master = elements.first(); // will always be available as created above if not existent + if (elements.size() > 1) { // dupes, move contents to master + List<Node> toMove = new ArrayList<Node>(); + for (int i = 1; i < elements.size(); i++) { + Node dupe = elements.get(i); + for (Node node : dupe.childNodes) + toMove.add(node); + dupe.remove(); + } + + for (Node dupe : toMove) + master.appendChild(dupe); + } + // ensure parented by <html> + if (!master.parent().equals(htmlEl)) { + htmlEl.appendChild(master); // includes remove() + } + } + + // fast method to get first by tag name, used for html, head, body finders + private Element findFirstElementByTagName(String tag, Node node) { + if (node.nodeName().equals(tag)) + return (Element) node; + else { + for (Node child: node.childNodes) { + Element found = findFirstElementByTagName(tag, child); + if (found != null) + return found; + } + } + return null; + } + + @Override + public String outerHtml() { + return super.html(); // no outer wrapper tag + } + + /** + Set the text of the {@code body} of this document. Any existing nodes within the body will be cleared. + @param text unencoded text + @return this document + */ + @Override + public Element text(String text) { + body().text(text); // overridden to not nuke doc structure + return this; + } + + @Override + public String nodeName() { + return "#document"; + } + + @Override + public Document clone() { + Document clone = (Document) super.clone(); + clone.outputSettings = this.outputSettings.clone(); + return clone; + } + + /** + * A Document's output settings control the form of the text() and html() methods. + */ + public static class OutputSettings implements Cloneable { + private Entities.EscapeMode escapeMode = Entities.EscapeMode.base; + private Charset charset = Charset.forName("UTF-8"); + private CharsetEncoder charsetEncoder = charset.newEncoder(); + private boolean prettyPrint = true; + private int indentAmount = 1; + + public OutputSettings() {} + + /** + * Get the document's current HTML escape mode: <code>base</code>, which provides a limited set of named HTML + * entities and escapes other characters as numbered entities for maximum compatibility; or <code>extended</code>, + * which uses the complete set of HTML named entities. + * <p> + * The default escape mode is <code>base</code>. + * @return the document's current escape mode + */ + public Entities.EscapeMode escapeMode() { + return escapeMode; + } + + /** + * Set the document's escape mode + * @param escapeMode the new escape mode to use + * @return the document's output settings, for chaining + */ + public OutputSettings escapeMode(Entities.EscapeMode escapeMode) { + this.escapeMode = escapeMode; + return this; + } + + /** + * Get the document's current output charset, which is used to control which characters are escaped when + * generating HTML (via the <code>html()</code> methods), and which are kept intact. + * <p> + * Where possible (when parsing from a URL or File), the document's output charset is automatically set to the + * input charset. Otherwise, it defaults to UTF-8. + * @return the document's current charset. + */ + public Charset charset() { + return charset; + } + + /** + * Update the document's output charset. + * @param charset the new charset to use. + * @return the document's output settings, for chaining + */ + public OutputSettings charset(Charset charset) { + // todo: this should probably update the doc's meta charset + this.charset = charset; + charsetEncoder = charset.newEncoder(); + return this; + } + + /** + * Update the document's output charset. + * @param charset the new charset (by name) to use. + * @return the document's output settings, for chaining + */ + public OutputSettings charset(String charset) { + charset(Charset.forName(charset)); + return this; + } + + CharsetEncoder encoder() { + return charsetEncoder; + } + + /** + * Get if pretty printing is enabled. Default is true. If disabled, the HTML output methods will not re-format + * the output, and the output will generally look like the input. + * @return if pretty printing is enabled. + */ + public boolean prettyPrint() { + return prettyPrint; + } + + /** + * Enable or disable pretty printing. + * @param pretty new pretty print setting + * @return this, for chaining + */ + public OutputSettings prettyPrint(boolean pretty) { + prettyPrint = pretty; + return this; + } + + /** + * Get the current tag indent amount, used when pretty printing. + * @return the current indent amount + */ + public int indentAmount() { + return indentAmount; + } + + /** + * Set the indent amount for pretty printing + * @param indentAmount number of spaces to use for indenting each level. Must be >= 0. + * @return this, for chaining + */ + public OutputSettings indentAmount(int indentAmount) { + Validate.isTrue(indentAmount >= 0); + this.indentAmount = indentAmount; + return this; + } + + @Override + public OutputSettings clone() { + OutputSettings clone; + try { + clone = (OutputSettings) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + clone.charset(charset.name()); // new charset and charset encoder + clone.escapeMode = Entities.EscapeMode.valueOf(escapeMode.name()); + // indentAmount, prettyPrint are primitives so object.clone() will handle + return clone; + } + } + + /** + * Get the document's current output settings. + * @return the document's current output settings. + */ + public OutputSettings outputSettings() { + return outputSettings; + } + + public enum QuirksMode { + noQuirks, quirks, limitedQuirks; + } + + public QuirksMode quirksMode() { + return quirksMode; + } + + public Document quirksMode(QuirksMode quirksMode) { + this.quirksMode = quirksMode; + return this; + } +} + diff --git a/server/src/org/jsoup/nodes/DocumentType.java b/server/src/org/jsoup/nodes/DocumentType.java new file mode 100644 index 0000000000..f8c79f0d18 --- /dev/null +++ b/server/src/org/jsoup/nodes/DocumentType.java @@ -0,0 +1,46 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; + +/** + * A {@code <!DOCTPYE>} node. + */ +public class DocumentType extends Node { + // todo: quirk mode from publicId and systemId + + /** + * Create a new doctype element. + * @param name the doctype's name + * @param publicId the doctype's public ID + * @param systemId the doctype's system ID + * @param baseUri the doctype's base URI + */ + public DocumentType(String name, String publicId, String systemId, String baseUri) { + super(baseUri); + + Validate.notEmpty(name); + attr("name", name); + attr("publicId", publicId); + attr("systemId", systemId); + } + + @Override + public String nodeName() { + return "#doctype"; + } + + @Override + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + accum.append("<!DOCTYPE ").append(attr("name")); + if (!StringUtil.isBlank(attr("publicId"))) + accum.append(" PUBLIC \"").append(attr("publicId")).append("\""); + if (!StringUtil.isBlank(attr("systemId"))) + accum.append(" \"").append(attr("systemId")).append("\""); + accum.append('>'); + } + + @Override + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) { + } +} diff --git a/server/src/org/jsoup/nodes/Element.java b/server/src/org/jsoup/nodes/Element.java new file mode 100644 index 0000000000..5c1894c934 --- /dev/null +++ b/server/src/org/jsoup/nodes/Element.java @@ -0,0 +1,1119 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.parser.Parser; +import org.jsoup.parser.Tag; +import org.jsoup.select.Collector; +import org.jsoup.select.Elements; +import org.jsoup.select.Evaluator; +import org.jsoup.select.Selector; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A HTML element consists of a tag name, attributes, and child nodes (including text nodes and + * other elements). + * + * From an Element, you can extract data, traverse the node graph, and manipulate the HTML. + * + * @author Jonathan Hedley, jonathan@hedley.net + */ +public class Element extends Node { + private Tag tag; + private Set<String> classNames; + + /** + * Create a new, standalone Element. (Standalone in that is has no parent.) + * + * @param tag tag of this element + * @param baseUri the base URI + * @param attributes initial attributes + * @see #appendChild(Node) + * @see #appendElement(String) + */ + public Element(Tag tag, String baseUri, Attributes attributes) { + super(baseUri, attributes); + + Validate.notNull(tag); + this.tag = tag; + } + + /** + * Create a new Element from a tag and a base URI. + * + * @param tag element tag + * @param baseUri the base URI of this element. It is acceptable for the base URI to be an empty + * string, but not null. + * @see Tag#valueOf(String) + */ + public Element(Tag tag, String baseUri) { + this(tag, baseUri, new Attributes()); + } + + @Override + public String nodeName() { + return tag.getName(); + } + + /** + * Get the name of the tag for this element. E.g. {@code div} + * + * @return the tag name + */ + public String tagName() { + return tag.getName(); + } + + /** + * Change the tag of this element. For example, convert a {@code <span>} to a {@code <div>} with + * {@code el.tagName("div");}. + * + * @param tagName new tag name for this element + * @return this element, for chaining + */ + public Element tagName(String tagName) { + Validate.notEmpty(tagName, "Tag name must not be empty."); + tag = Tag.valueOf(tagName); + return this; + } + + /** + * Get the Tag for this element. + * + * @return the tag object + */ + public Tag tag() { + return tag; + } + + /** + * Test if this element is a block-level element. (E.g. {@code <div> == true} or an inline element + * {@code <p> == false}). + * + * @return true if block, false if not (and thus inline) + */ + public boolean isBlock() { + return tag.isBlock(); + } + + /** + * Get the {@code id} attribute of this element. + * + * @return The id attribute, if present, or an empty string if not. + */ + public String id() { + String id = attr("id"); + return id == null ? "" : id; + } + + /** + * Set an attribute value on this element. If this element already has an attribute with the + * key, its value is updated; otherwise, a new attribute is added. + * + * @return this element + */ + public Element attr(String attributeKey, String attributeValue) { + super.attr(attributeKey, attributeValue); + return this; + } + + /** + * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key + * starting with "data-" is included the dataset. + * <p> + * E.g., the element {@code <div data-package="jsoup" data-language="Java" class="group">...} has the dataset + * {@code package=jsoup, language=java}. + * <p> + * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected + * in the other map. + * <p> + * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector. + * @return a map of {@code key=value} custom data attributes. + */ + public Map<String, String> dataset() { + return attributes.dataset(); + } + + @Override + public final Element parent() { + return (Element) parentNode; + } + + /** + * Get this element's parent and ancestors, up to the document root. + * @return this element's stack of parents, closest first. + */ + public Elements parents() { + Elements parents = new Elements(); + accumulateParents(this, parents); + return parents; + } + + private static void accumulateParents(Element el, Elements parents) { + Element parent = el.parent(); + if (parent != null && !parent.tagName().equals("#root")) { + parents.add(parent); + accumulateParents(parent, parents); + } + } + + /** + * Get a child element of this element, by its 0-based index number. + * <p/> + * Note that an element can have both mixed Nodes and Elements as children. This method inspects + * a filtered list of children that are elements, and the index is based on that filtered list. + * + * @param index the index number of the element to retrieve + * @return the child element, if it exists, or {@code null} if absent. + * @see #childNode(int) + */ + public Element child(int index) { + return children().get(index); + } + + /** + * Get this element's child elements. + * <p/> + * This is effectively a filter on {@link #childNodes()} to get Element nodes. + * @return child elements. If this element has no children, returns an + * empty list. + * @see #childNodes() + */ + public Elements children() { + // create on the fly rather than maintaining two lists. if gets slow, memoize, and mark dirty on change + List<Element> elements = new ArrayList<Element>(); + for (Node node : childNodes) { + if (node instanceof Element) + elements.add((Element) node); + } + return new Elements(elements); + } + + /** + * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated. + * <p/> + * This is effectively a filter on {@link #childNodes()} to get Text nodes. + * @return child text nodes. If this element has no text nodes, returns an + * empty list. + * <p/> + * For example, with the input HTML: {@code <p>One <span>Two</span> Three <br> Four</p>} with the {@code p} element selected: + * <ul> + * <li>{@code p.text()} = {@code "One Two Three Four"}</li> + * <li>{@code p.ownText()} = {@code "One Three Four"}</li> + * <li>{@code p.children()} = {@code Elements[<span>, <br>]}</li> + * <li>{@code p.childNodes()} = {@code List<Node>["One ", <span>, " Three ", <br>, " Four"]}</li> + * <li>{@code p.textNodes()} = {@code List<TextNode>["One ", " Three ", " Four"]}</li> + * </ul> + */ + public List<TextNode> textNodes() { + List<TextNode> textNodes = new ArrayList<TextNode>(); + for (Node node : childNodes) { + if (node instanceof TextNode) + textNodes.add((TextNode) node); + } + return Collections.unmodifiableList(textNodes); + } + + /** + * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated. + * <p/> + * This is effectively a filter on {@link #childNodes()} to get Data nodes. + * @return child data nodes. If this element has no data nodes, returns an + * empty list. + * @see #data() + */ + public List<DataNode> dataNodes() { + List<DataNode> dataNodes = new ArrayList<DataNode>(); + for (Node node : childNodes) { + if (node instanceof DataNode) + dataNodes.add((DataNode) node); + } + return Collections.unmodifiableList(dataNodes); + } + + /** + * Find elements that match the {@link Selector} CSS query, with this element as the starting context. Matched elements + * may include this element, or any of its children. + * <p/> + * This method is generally more powerful to use than the DOM-type {@code getElementBy*} methods, because + * multiple filters can be combined, e.g.: + * <ul> + * <li>{@code el.select("a[href]")} - finds links ({@code a} tags with {@code href} attributes) + * <li>{@code el.select("a[href*=example.com]")} - finds links pointing to example.com (loosely) + * </ul> + * <p/> + * See the query syntax documentation in {@link org.jsoup.select.Selector}. + * + * @param cssQuery a {@link Selector} CSS-like query + * @return elements that match the query (empty if none match) + * @see org.jsoup.select.Selector + */ + public Elements select(String cssQuery) { + return Selector.select(cssQuery, this); + } + + /** + * Add a node child node to this element. + * + * @param child node to add. Must not already have a parent. + * @return this element, so that you can add more child nodes or elements. + */ + public Element appendChild(Node child) { + Validate.notNull(child); + + addChildren(child); + return this; + } + + /** + * Add a node to the start of this element's children. + * + * @param child node to add. Must not already have a parent. + * @return this element, so that you can add more child nodes or elements. + */ + public Element prependChild(Node child) { + Validate.notNull(child); + + addChildren(0, child); + return this; + } + + /** + * Create a new element by tag name, and add it as the last child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.appendElement("h1").attr("id", "header").text("Welcome");} + */ + public Element appendElement(String tagName) { + Element child = new Element(Tag.valueOf(tagName), baseUri()); + appendChild(child); + return child; + } + + /** + * Create a new element by tag name, and add it as the first child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.prependElement("h1").attr("id", "header").text("Welcome");} + */ + public Element prependElement(String tagName) { + Element child = new Element(Tag.valueOf(tagName), baseUri()); + prependChild(child); + return child; + } + + /** + * Create and append a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + public Element appendText(String text) { + TextNode node = new TextNode(text, baseUri()); + appendChild(node); + return this; + } + + /** + * Create and prepend a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + public Element prependText(String text) { + TextNode node = new TextNode(text, baseUri()); + prependChild(node); + return this; + } + + /** + * Add inner HTML to this element. The supplied HTML will be parsed, and each node appended to the end of the children. + * @param html HTML to add inside this element, after the existing HTML + * @return this element + * @see #html(String) + */ + public Element append(String html) { + Validate.notNull(html); + + List<Node> nodes = Parser.parseFragment(html, this, baseUri()); + addChildren(nodes.toArray(new Node[nodes.size()])); + return this; + } + + /** + * Add inner HTML into this element. The supplied HTML will be parsed, and each node prepended to the start of the element's children. + * @param html HTML to add inside this element, before the existing HTML + * @return this element + * @see #html(String) + */ + public Element prepend(String html) { + Validate.notNull(html); + + List<Node> nodes = Parser.parseFragment(html, this, baseUri()); + addChildren(0, nodes.toArray(new Node[nodes.size()])); + return this; + } + + /** + * Insert the specified HTML into the DOM before this element (i.e. as a preceding sibling). + * + * @param html HTML to add before this element + * @return this element, for chaining + * @see #after(String) + */ + @Override + public Element before(String html) { + return (Element) super.before(html); + } + + /** + * Insert the specified node into the DOM before this node (i.e. as a preceding sibling). + * @param node to add before this element + * @return this Element, for chaining + * @see #after(Node) + */ + @Override + public Element before(Node node) { + return (Element) super.before(node); + } + + /** + * Insert the specified HTML into the DOM after this element (i.e. as a following sibling). + * + * @param html HTML to add after this element + * @return this element, for chaining + * @see #before(String) + */ + @Override + public Element after(String html) { + return (Element) super.after(html); + } + + /** + * Insert the specified node into the DOM after this node (i.e. as a following sibling). + * @param node to add after this element + * @return this element, for chaining + * @see #before(Node) + */ + @Override + public Element after(Node node) { + return (Element) super.after(node); + } + + /** + * Remove all of the element's child nodes. Any attributes are left as-is. + * @return this element + */ + public Element empty() { + childNodes.clear(); + return this; + } + + /** + * Wrap the supplied HTML around this element. + * + * @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. + * @return this element, for chaining. + */ + @Override + public Element wrap(String html) { + return (Element) super.wrap(html); + } + + /** + * Get sibling elements. If the element has no sibling elements, returns an empty list. An element is not a sibling + * of itself, so will not be included in the returned list. + * @return sibling elements + */ + public Elements siblingElements() { + if (parentNode == null) + return new Elements(0); + + List<Element> elements = parent().children(); + Elements siblings = new Elements(elements.size() - 1); + for (Element el: elements) + if (el != this) + siblings.add(el); + return siblings; + } + + /** + * Gets the next sibling element of this element. E.g., if a {@code div} contains two {@code p}s, + * the {@code nextElementSibling} of the first {@code p} is the second {@code p}. + * <p/> + * This is similar to {@link #nextSibling()}, but specifically finds only Elements + * @return the next element, or null if there is no next element + * @see #previousElementSibling() + */ + public Element nextElementSibling() { + if (parentNode == null) return null; + List<Element> siblings = parent().children(); + Integer index = indexInList(this, siblings); + Validate.notNull(index); + if (siblings.size() > index+1) + return siblings.get(index+1); + else + return null; + } + + /** + * Gets the previous element sibling of this element. + * @return the previous element, or null if there is no previous element + * @see #nextElementSibling() + */ + public Element previousElementSibling() { + if (parentNode == null) return null; + List<Element> siblings = parent().children(); + Integer index = indexInList(this, siblings); + Validate.notNull(index); + if (index > 0) + return siblings.get(index-1); + else + return null; + } + + /** + * Gets the first element sibling of this element. + * @return the first sibling that is an element (aka the parent's first element child) + */ + public Element firstElementSibling() { + // todo: should firstSibling() exclude this? + List<Element> siblings = parent().children(); + return siblings.size() > 1 ? siblings.get(0) : null; + } + + /** + * Get the list index of this element in its element sibling list. I.e. if this is the first element + * sibling, returns 0. + * @return position in element sibling list + */ + public Integer elementSiblingIndex() { + if (parent() == null) return 0; + return indexInList(this, parent().children()); + } + + /** + * Gets the last element sibling of this element + * @return the last sibling that is an element (aka the parent's last element child) + */ + public Element lastElementSibling() { + List<Element> siblings = parent().children(); + return siblings.size() > 1 ? siblings.get(siblings.size() - 1) : null; + } + + private static <E extends Element> Integer indexInList(Element search, List<E> elements) { + Validate.notNull(search); + Validate.notNull(elements); + + for (int i = 0; i < elements.size(); i++) { + E element = elements.get(i); + if (element.equals(search)) + return i; + } + return null; + } + + // DOM type methods + + /** + * Finds elements, including and recursively under this element, with the specified tag name. + * @param tagName The tag name to search for (case insensitively). + * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match. + */ + public Elements getElementsByTag(String tagName) { + Validate.notEmpty(tagName); + tagName = tagName.toLowerCase().trim(); + + return Collector.collect(new Evaluator.Tag(tagName), this); + } + + /** + * Find an element by ID, including or under this element. + * <p> + * Note that this finds the first matching ID, starting with this element. If you search down from a different + * starting point, it is possible to find a different element by ID. For unique element by ID within a Document, + * use {@link Document#getElementById(String)} + * @param id The ID to search for. + * @return The first matching element by ID, starting with this element, or null if none found. + */ + public Element getElementById(String id) { + Validate.notEmpty(id); + + Elements elements = Collector.collect(new Evaluator.Id(id), this); + if (elements.size() > 0) + return elements.get(0); + else + return null; + } + + /** + * Find elements that have this class, including or under this element. Case insensitive. + * <p> + * Elements can have multiple classes (e.g. {@code <div class="header round first">}. This method + * checks each class, so you can find the above with {@code el.getElementsByClass("header");}. + * + * @param className the name of the class to search for. + * @return elements with the supplied class name, empty if none + * @see #hasClass(String) + * @see #classNames() + */ + public Elements getElementsByClass(String className) { + Validate.notEmpty(className); + + return Collector.collect(new Evaluator.Class(className), this); + } + + /** + * Find elements that have a named attribute set. Case insensitive. + * + * @param key name of the attribute, e.g. {@code href} + * @return elements that have this attribute, empty if none + */ + public Elements getElementsByAttribute(String key) { + Validate.notEmpty(key); + key = key.trim().toLowerCase(); + + return Collector.collect(new Evaluator.Attribute(key), this); + } + + /** + * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements + * that have HTML5 datasets. + * @param keyPrefix name prefix of the attribute e.g. {@code data-} + * @return elements that have attribute names that start with with the prefix, empty if none. + */ + public Elements getElementsByAttributeStarting(String keyPrefix) { + Validate.notEmpty(keyPrefix); + keyPrefix = keyPrefix.trim().toLowerCase(); + + return Collector.collect(new Evaluator.AttributeStarting(keyPrefix), this); + } + + /** + * Find elements that have an attribute with the specific value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that have this attribute with this value, empty if none + */ + public Elements getElementsByAttributeValue(String key, String value) { + return Collector.collect(new Evaluator.AttributeWithValue(key, value), this); + } + + /** + * Find elements that either do not have this attribute, or have it with a different value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that do not have a matching attribute + */ + public Elements getElementsByAttributeValueNot(String key, String value) { + return Collector.collect(new Evaluator.AttributeWithValueNot(key, value), this); + } + + /** + * Find elements that have attributes that start with the value prefix. Case insensitive. + * + * @param key name of the attribute + * @param valuePrefix start of attribute value + * @return elements that have attributes that start with the value prefix + */ + public Elements getElementsByAttributeValueStarting(String key, String valuePrefix) { + return Collector.collect(new Evaluator.AttributeWithValueStarting(key, valuePrefix), this); + } + + /** + * Find elements that have attributes that end with the value suffix. Case insensitive. + * + * @param key name of the attribute + * @param valueSuffix end of the attribute value + * @return elements that have attributes that end with the value suffix + */ + public Elements getElementsByAttributeValueEnding(String key, String valueSuffix) { + return Collector.collect(new Evaluator.AttributeWithValueEnding(key, valueSuffix), this); + } + + /** + * Find elements that have attributes whose value contains the match string. Case insensitive. + * + * @param key name of the attribute + * @param match substring of value to search for + * @return elements that have attributes containing this text + */ + public Elements getElementsByAttributeValueContaining(String key, String match) { + return Collector.collect(new Evaluator.AttributeWithValueContaining(key, match), this); + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param pattern compiled regular expression to match against attribute values + * @return elements that have attributes matching this regular expression + */ + public Elements getElementsByAttributeValueMatching(String key, Pattern pattern) { + return Collector.collect(new Evaluator.AttributeWithValueMatching(key, pattern), this); + + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param regex regular expression to match against attribute values. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options. + * @return elements that have attributes matching this regular expression + */ + public Elements getElementsByAttributeValueMatching(String key, String regex) { + Pattern pattern; + try { + pattern = Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Pattern syntax error: " + regex, e); + } + return getElementsByAttributeValueMatching(key, pattern); + } + + /** + * Find elements whose sibling index is less than the supplied index. + * @param index 0-based index + * @return elements less than index + */ + public Elements getElementsByIndexLessThan(int index) { + return Collector.collect(new Evaluator.IndexLessThan(index), this); + } + + /** + * Find elements whose sibling index is greater than the supplied index. + * @param index 0-based index + * @return elements greater than index + */ + public Elements getElementsByIndexGreaterThan(int index) { + return Collector.collect(new Evaluator.IndexGreaterThan(index), this); + } + + /** + * Find elements whose sibling index is equal to the supplied index. + * @param index 0-based index + * @return elements equal to index + */ + public Elements getElementsByIndexEquals(int index) { + return Collector.collect(new Evaluator.IndexEquals(index), this); + } + + /** + * Find elements that contain the specified string. The search is case insensitive. The text may appear directly + * in the element, or in any of its descendants. + * @param searchText to look for in the element's text + * @return elements that contain the string, case insensitive. + * @see Element#text() + */ + public Elements getElementsContainingText(String searchText) { + return Collector.collect(new Evaluator.ContainsText(searchText), this); + } + + /** + * Find elements that directly contain the specified string. The search is case insensitive. The text must appear directly + * in the element, not in any of its descendants. + * @param searchText to look for in the element's own text + * @return elements that contain the string, case insensitive. + * @see Element#ownText() + */ + public Elements getElementsContainingOwnText(String searchText) { + return Collector.collect(new Evaluator.ContainsOwnText(searchText), this); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see Element#text() + */ + public Elements getElementsMatchingText(Pattern pattern) { + return Collector.collect(new Evaluator.Matches(pattern), this); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see Element#text() + */ + public Elements getElementsMatchingText(String regex) { + Pattern pattern; + try { + pattern = Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Pattern syntax error: " + regex, e); + } + return getElementsMatchingText(pattern); + } + + /** + * Find elements whose own text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see Element#ownText() + */ + public Elements getElementsMatchingOwnText(Pattern pattern) { + return Collector.collect(new Evaluator.MatchesOwn(pattern), this); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see Element#ownText() + */ + public Elements getElementsMatchingOwnText(String regex) { + Pattern pattern; + try { + pattern = Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Pattern syntax error: " + regex, e); + } + return getElementsMatchingOwnText(pattern); + } + + /** + * Find all elements under this element (including self, and children of children). + * + * @return all elements + */ + public Elements getAllElements() { + return Collector.collect(new Evaluator.AllElements(), this); + } + + /** + * Gets the combined text of this element and all its children. + * <p> + * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.text()} returns {@code "Hello there now!"} + * + * @return unencoded text, or empty string if none. + * @see #ownText() + * @see #textNodes() + */ + public String text() { + StringBuilder sb = new StringBuilder(); + text(sb); + return sb.toString().trim(); + } + + private void text(StringBuilder accum) { + appendWhitespaceIfBr(this, accum); + + for (Node child : childNodes) { + if (child instanceof TextNode) { + TextNode textNode = (TextNode) child; + appendNormalisedText(accum, textNode); + } else if (child instanceof Element) { + Element element = (Element) child; + if (accum.length() > 0 && element.isBlock() && !TextNode.lastCharIsWhitespace(accum)) + accum.append(" "); + element.text(accum); + } + } + } + + /** + * Gets the text owned by this element only; does not get the combined text of all children. + * <p> + * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.ownText()} returns {@code "Hello now!"}, + * whereas {@code p.text()} returns {@code "Hello there now!"}. + * Note that the text within the {@code b} element is not returned, as it is not a direct child of the {@code p} element. + * + * @return unencoded text, or empty string if none. + * @see #text() + * @see #textNodes() + */ + public String ownText() { + StringBuilder sb = new StringBuilder(); + ownText(sb); + return sb.toString().trim(); + } + + private void ownText(StringBuilder accum) { + for (Node child : childNodes) { + if (child instanceof TextNode) { + TextNode textNode = (TextNode) child; + appendNormalisedText(accum, textNode); + } else if (child instanceof Element) { + appendWhitespaceIfBr((Element) child, accum); + } + } + } + + private void appendNormalisedText(StringBuilder accum, TextNode textNode) { + String text = textNode.getWholeText(); + + if (!preserveWhitespace()) { + text = TextNode.normaliseWhitespace(text); + if (TextNode.lastCharIsWhitespace(accum)) + text = TextNode.stripLeadingWhitespace(text); + } + accum.append(text); + } + + private static void appendWhitespaceIfBr(Element element, StringBuilder accum) { + if (element.tag.getName().equals("br") && !TextNode.lastCharIsWhitespace(accum)) + accum.append(" "); + } + + boolean preserveWhitespace() { + return tag.preserveWhitespace() || parent() != null && parent().preserveWhitespace(); + } + + /** + * Set the text of this element. Any existing contents (text or elements) will be cleared + * @param text unencoded text + * @return this element + */ + public Element text(String text) { + Validate.notNull(text); + + empty(); + TextNode textNode = new TextNode(text, baseUri); + appendChild(textNode); + + return this; + } + + /** + Test if this element has any text content (that is not just whitespace). + @return true if element has non-blank text content. + */ + public boolean hasText() { + for (Node child: childNodes) { + if (child instanceof TextNode) { + TextNode textNode = (TextNode) child; + if (!textNode.isBlank()) + return true; + } else if (child instanceof Element) { + Element el = (Element) child; + if (el.hasText()) + return true; + } + } + return false; + } + + /** + * Get the combined data of this element. Data is e.g. the inside of a {@code script} tag. + * @return the data, or empty string if none + * + * @see #dataNodes() + */ + public String data() { + StringBuilder sb = new StringBuilder(); + + for (Node childNode : childNodes) { + if (childNode instanceof DataNode) { + DataNode data = (DataNode) childNode; + sb.append(data.getWholeData()); + } else if (childNode instanceof Element) { + Element element = (Element) childNode; + String elementData = element.data(); + sb.append(elementData); + } + } + return sb.toString(); + } + + /** + * Gets the literal value of this element's "class" attribute, which may include multiple class names, space + * separated. (E.g. on <code><div class="header gray"></code> returns, "<code>header gray</code>") + * @return The literal class attribute, or <b>empty string</b> if no class attribute set. + */ + public String className() { + return attr("class"); + } + + /** + * Get all of the element's class names. E.g. on element {@code <div class="header gray"}>}, + * returns a set of two elements {@code "header", "gray"}. Note that modifications to this set are not pushed to + * the backing {@code class} attribute; use the {@link #classNames(java.util.Set)} method to persist them. + * @return set of classnames, empty if no class attribute + */ + public Set<String> classNames() { + if (classNames == null) { + String[] names = className().split("\\s+"); + classNames = new LinkedHashSet<String>(Arrays.asList(names)); + } + return classNames; + } + + /** + Set the element's {@code class} attribute to the supplied class names. + @param classNames set of classes + @return this element, for chaining + */ + public Element classNames(Set<String> classNames) { + Validate.notNull(classNames); + attributes.put("class", StringUtil.join(classNames, " ")); + return this; + } + + /** + * Tests if this element has a class. Case insensitive. + * @param className name of class to check for + * @return true if it does, false if not + */ + public boolean hasClass(String className) { + Set<String> classNames = classNames(); + for (String name : classNames) { + if (className.equalsIgnoreCase(name)) + return true; + } + return false; + } + + /** + Add a class name to this element's {@code class} attribute. + @param className class name to add + @return this element + */ + public Element addClass(String className) { + Validate.notNull(className); + + Set<String> classes = classNames(); + classes.add(className); + classNames(classes); + + return this; + } + + /** + Remove a class name from this element's {@code class} attribute. + @param className class name to remove + @return this element + */ + public Element removeClass(String className) { + Validate.notNull(className); + + Set<String> classes = classNames(); + classes.remove(className); + classNames(classes); + + return this; + } + + /** + Toggle a class name on this element's {@code class} attribute: if present, remove it; otherwise add it. + @param className class name to toggle + @return this element + */ + public Element toggleClass(String className) { + Validate.notNull(className); + + Set<String> classes = classNames(); + if (classes.contains(className)) + classes.remove(className); + else + classes.add(className); + classNames(classes); + + return this; + } + + /** + * Get the value of a form element (input, textarea, etc). + * @return the value of the form element, or empty string if not set. + */ + public String val() { + if (tagName().equals("textarea")) + return text(); + else + return attr("value"); + } + + /** + * Set the value of a form element (input, textarea, etc). + * @param value value to set + * @return this element (for chaining) + */ + public Element val(String value) { + if (tagName().equals("textarea")) + text(value); + else + attr("value", value); + return this; + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + if (accum.length() > 0 && out.prettyPrint() && (tag.formatAsBlock() || (parent() != null && parent().tag().formatAsBlock()))) + indent(accum, depth, out); + accum + .append("<") + .append(tagName()); + attributes.html(accum, out); + + if (childNodes.isEmpty() && tag.isSelfClosing()) + accum.append(" />"); + else + accum.append(">"); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) { + if (!(childNodes.isEmpty() && tag.isSelfClosing())) { + if (out.prettyPrint() && !childNodes.isEmpty() && tag.formatAsBlock()) + indent(accum, depth, out); + accum.append("</").append(tagName()).append(">"); + } + } + + /** + * Retrieves the element's inner HTML. E.g. on a {@code <div>} with one empty {@code <p>}, would return + * {@code <p></p>}. (Whereas {@link #outerHtml()} would return {@code <div><p></p></div>}.) + * + * @return String of HTML. + * @see #outerHtml() + */ + public String html() { + StringBuilder accum = new StringBuilder(); + html(accum); + return accum.toString().trim(); + } + + private void html(StringBuilder accum) { + for (Node node : childNodes) + node.outerHtml(accum); + } + + /** + * Set this element's inner HTML. Clears the existing HTML first. + * @param html HTML to parse and set into this element + * @return this element + * @see #append(String) + */ + public Element html(String html) { + empty(); + append(html); + return this; + } + + public String toString() { + return outerHtml(); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + // todo: fixup, not very useful + int result = super.hashCode(); + result = 31 * result + (tag != null ? tag.hashCode() : 0); + return result; + } + + @Override + public Element clone() { + Element clone = (Element) super.clone(); + clone.classNames(); // creates linked set of class names from class attribute + return clone; + } +} diff --git a/server/src/org/jsoup/nodes/Entities.java b/server/src/org/jsoup/nodes/Entities.java new file mode 100644 index 0000000000..0ae83e1fc0 --- /dev/null +++ b/server/src/org/jsoup/nodes/Entities.java @@ -0,0 +1,184 @@ +package org.jsoup.nodes; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.CharsetEncoder; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML entities, and escape routines. + * Source: <a href="http://www.w3.org/TR/html5/named-character-references.html#named-character-references">W3C HTML + * named character references</a>. + */ +public class Entities { + public enum EscapeMode { + /** Restricted entities suitable for XHTML output: lt, gt, amp, apos, and quot only. */ + xhtml(xhtmlByVal), + /** Default HTML output entities. */ + base(baseByVal), + /** Complete HTML entities. */ + extended(fullByVal); + + private Map<Character, String> map; + + EscapeMode(Map<Character, String> map) { + this.map = map; + } + + public Map<Character, String> getMap() { + return map; + } + } + + private static final Map<String, Character> full; + private static final Map<Character, String> xhtmlByVal; + private static final Map<Character, String> baseByVal; + private static final Map<Character, String> fullByVal; + private static final Pattern unescapePattern = Pattern.compile("&(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]+\\d*);?"); + private static final Pattern strictUnescapePattern = Pattern.compile("&(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]+\\d*);"); + + private Entities() {} + + /** + * Check if the input is a known named entity + * @param name the possible entity name (e.g. "lt" or "amp" + * @return true if a known named entity + */ + public static boolean isNamedEntity(String name) { + return full.containsKey(name); + } + + /** + * Get the Character value of the named entity + * @param name named entity (e.g. "lt" or "amp") + * @return the Character value of the named entity (e.g. '<' or '&') + */ + public static Character getCharacterByName(String name) { + return full.get(name); + } + + static String escape(String string, Document.OutputSettings out) { + return escape(string, out.encoder(), out.escapeMode()); + } + + static String escape(String string, CharsetEncoder encoder, EscapeMode escapeMode) { + StringBuilder accum = new StringBuilder(string.length() * 2); + Map<Character, String> map = escapeMode.getMap(); + + for (int pos = 0; pos < string.length(); pos++) { + Character c = string.charAt(pos); + if (map.containsKey(c)) + accum.append('&').append(map.get(c)).append(';'); + else if (encoder.canEncode(c)) + accum.append(c.charValue()); + else + accum.append("&#").append((int) c).append(';'); + } + + return accum.toString(); + } + + static String unescape(String string) { + return unescape(string, false); + } + + /** + * Unescape the input string. + * @param string + * @param strict if "strict" (that is, requires trailing ';' char, otherwise that's optional) + * @return + */ + static String unescape(String string, boolean strict) { + // todo: change this method to use Tokeniser.consumeCharacterReference + if (!string.contains("&")) + return string; + + Matcher m = strict? strictUnescapePattern.matcher(string) : unescapePattern.matcher(string); // &(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]\\d*);? + StringBuffer accum = new StringBuffer(string.length()); // pity matcher can't use stringbuilder, avoid syncs + // todo: replace m.appendReplacement with own impl, so StringBuilder and quoteReplacement not required + + while (m.find()) { + int charval = -1; + String num = m.group(3); + if (num != null) { + try { + int base = m.group(2) != null ? 16 : 10; // 2 is hex indicator + charval = Integer.valueOf(num, base); + } catch (NumberFormatException e) { + } // skip + } else { + String name = m.group(1); + if (full.containsKey(name)) + charval = full.get(name); + } + + if (charval != -1 || charval > 0xFFFF) { // out of range + String c = Character.toString((char) charval); + m.appendReplacement(accum, Matcher.quoteReplacement(c)); + } else { + m.appendReplacement(accum, Matcher.quoteReplacement(m.group(0))); // replace with original string + } + } + m.appendTail(accum); + return accum.toString(); + } + + // xhtml has restricted entities + private static final Object[][] xhtmlArray = { + {"quot", 0x00022}, + {"amp", 0x00026}, + {"apos", 0x00027}, + {"lt", 0x0003C}, + {"gt", 0x0003E} + }; + + static { + xhtmlByVal = new HashMap<Character, String>(); + baseByVal = toCharacterKey(loadEntities("entities-base.properties")); // most common / default + full = loadEntities("entities-full.properties"); // extended and overblown. + fullByVal = toCharacterKey(full); + + for (Object[] entity : xhtmlArray) { + Character c = Character.valueOf((char) ((Integer) entity[1]).intValue()); + xhtmlByVal.put(c, ((String) entity[0])); + } + } + + private static Map<String, Character> loadEntities(String filename) { + Properties properties = new Properties(); + Map<String, Character> entities = new HashMap<String, Character>(); + try { + InputStream in = Entities.class.getResourceAsStream(filename); + properties.load(in); + in.close(); + } catch (IOException e) { + throw new MissingResourceException("Error loading entities resource: " + e.getMessage(), "Entities", filename); + } + + for (Map.Entry entry: properties.entrySet()) { + Character val = Character.valueOf((char) Integer.parseInt((String) entry.getValue(), 16)); + String name = (String) entry.getKey(); + entities.put(name, val); + } + return entities; + } + + private static Map<Character, String> toCharacterKey(Map<String, Character> inMap) { + Map<Character, String> outMap = new HashMap<Character, String>(); + for (Map.Entry<String, Character> entry: inMap.entrySet()) { + Character character = entry.getValue(); + String name = entry.getKey(); + + if (outMap.containsKey(character)) { + // dupe, prefer the lower case version + if (name.toLowerCase().equals(name)) + outMap.put(character, name); + } else { + outMap.put(character, name); + } + } + return outMap; + } +} diff --git a/server/src/org/jsoup/nodes/Node.java b/server/src/org/jsoup/nodes/Node.java new file mode 100644 index 0000000000..eb2b40ee73 --- /dev/null +++ b/server/src/org/jsoup/nodes/Node.java @@ -0,0 +1,615 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.parser.Parser; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + The base, abstract Node model. Elements, Documents, Comments etc are all Node instances. + + @author Jonathan Hedley, jonathan@hedley.net */ +public abstract class Node implements Cloneable { + Node parentNode; + List<Node> childNodes; + Attributes attributes; + String baseUri; + int siblingIndex; + + /** + Create a new Node. + @param baseUri base URI + @param attributes attributes (not null, but may be empty) + */ + protected Node(String baseUri, Attributes attributes) { + Validate.notNull(baseUri); + Validate.notNull(attributes); + + childNodes = new ArrayList<Node>(4); + this.baseUri = baseUri.trim(); + this.attributes = attributes; + } + + protected Node(String baseUri) { + this(baseUri, new Attributes()); + } + + /** + * Default constructor. Doesn't setup base uri, children, or attributes; use with caution. + */ + protected Node() { + childNodes = Collections.emptyList(); + attributes = null; + } + + /** + Get the node name of this node. Use for debugging purposes and not logic switching (for that, use instanceof). + @return node name + */ + public abstract String nodeName(); + + /** + * Get an attribute's value by its key. + * <p/> + * To get an absolute URL from an attribute that may be a relative URL, prefix the key with <code><b>abs</b></code>, + * which is a shortcut to the {@link #absUrl} method. + * E.g.: <blockquote><code>String url = a.attr("abs:href");</code></blockquote> + * @param attributeKey The attribute key. + * @return The attribute, or empty string if not present (to avoid nulls). + * @see #attributes() + * @see #hasAttr(String) + * @see #absUrl(String) + */ + public String attr(String attributeKey) { + Validate.notNull(attributeKey); + + if (attributes.hasKey(attributeKey)) + return attributes.get(attributeKey); + else if (attributeKey.toLowerCase().startsWith("abs:")) + return absUrl(attributeKey.substring("abs:".length())); + else return ""; + } + + /** + * Get all of the element's attributes. + * @return attributes (which implements iterable, in same order as presented in original HTML). + */ + public Attributes attributes() { + return attributes; + } + + /** + * Set an attribute (key=value). If the attribute already exists, it is replaced. + * @param attributeKey The attribute key. + * @param attributeValue The attribute value. + * @return this (for chaining) + */ + public Node attr(String attributeKey, String attributeValue) { + attributes.put(attributeKey, attributeValue); + return this; + } + + /** + * Test if this element has an attribute. + * @param attributeKey The attribute key to check. + * @return true if the attribute exists, false if not. + */ + public boolean hasAttr(String attributeKey) { + Validate.notNull(attributeKey); + + if (attributeKey.toLowerCase().startsWith("abs:")) { + String key = attributeKey.substring("abs:".length()); + if (attributes.hasKey(key) && !absUrl(key).equals("")) + return true; + } + return attributes.hasKey(attributeKey); + } + + /** + * Remove an attribute from this element. + * @param attributeKey The attribute to remove. + * @return this (for chaining) + */ + public Node removeAttr(String attributeKey) { + Validate.notNull(attributeKey); + attributes.remove(attributeKey); + return this; + } + + /** + Get the base URI of this node. + @return base URI + */ + public String baseUri() { + return baseUri; + } + + /** + Update the base URI of this node and all of its descendants. + @param baseUri base URI to set + */ + public void setBaseUri(final String baseUri) { + Validate.notNull(baseUri); + + traverse(new NodeVisitor() { + public void head(Node node, int depth) { + node.baseUri = baseUri; + } + + public void tail(Node node, int depth) { + } + }); + } + + /** + * Get an absolute URL from a URL attribute that may be relative (i.e. an <code><a href></code> or + * <code><img src></code>). + * <p/> + * E.g.: <code>String absUrl = linkEl.absUrl("href");</code> + * <p/> + * If the attribute value is already absolute (i.e. it starts with a protocol, like + * <code>http://</code> or <code>https://</code> etc), and it successfully parses as a URL, the attribute is + * returned directly. Otherwise, it is treated as a URL relative to the element's {@link #baseUri}, and made + * absolute using that. + * <p/> + * As an alternate, you can use the {@link #attr} method with the <code>abs:</code> prefix, e.g.: + * <code>String absUrl = linkEl.attr("abs:href");</code> + * + * @param attributeKey The attribute key + * @return An absolute URL if one could be made, or an empty string (not null) if the attribute was missing or + * could not be made successfully into a URL. + * @see #attr + * @see java.net.URL#URL(java.net.URL, String) + */ + public String absUrl(String attributeKey) { + Validate.notEmpty(attributeKey); + + String relUrl = attr(attributeKey); + if (!hasAttr(attributeKey)) { + return ""; // nothing to make absolute with + } else { + URL base; + try { + try { + base = new URL(baseUri); + } catch (MalformedURLException e) { + // the base is unsuitable, but the attribute may be abs on its own, so try that + URL abs = new URL(relUrl); + return abs.toExternalForm(); + } + // workaround: java resolves '//path/file + ?foo' to '//path/?foo', not '//path/file?foo' as desired + if (relUrl.startsWith("?")) + relUrl = base.getPath() + relUrl; + URL abs = new URL(base, relUrl); + return abs.toExternalForm(); + } catch (MalformedURLException e) { + return ""; + } + } + } + + /** + Get a child node by index + @param index index of child node + @return the child node at this index. + */ + public Node childNode(int index) { + return childNodes.get(index); + } + + /** + Get this node's children. Presented as an unmodifiable list: new children can not be added, but the child nodes + themselves can be manipulated. + @return list of children. If no children, returns an empty list. + */ + public List<Node> childNodes() { + return Collections.unmodifiableList(childNodes); + } + + protected Node[] childNodesAsArray() { + return childNodes.toArray(new Node[childNodes().size()]); + } + + /** + Gets this node's parent node. + @return parent node; or null if no parent. + */ + public Node parent() { + return parentNode; + } + + /** + * Gets the Document associated with this Node. + * @return the Document associated with this Node, or null if there is no such Document. + */ + public Document ownerDocument() { + if (this instanceof Document) + return (Document) this; + else if (parentNode == null) + return null; + else + return parentNode.ownerDocument(); + } + + /** + * Remove (delete) this node from the DOM tree. If this node has children, they are also removed. + */ + public void remove() { + Validate.notNull(parentNode); + parentNode.removeChild(this); + } + + /** + * Insert the specified HTML into the DOM before this node (i.e. as a preceding sibling). + * @param html HTML to add before this node + * @return this node, for chaining + * @see #after(String) + */ + public Node before(String html) { + addSiblingHtml(siblingIndex(), html); + return this; + } + + /** + * Insert the specified node into the DOM before this node (i.e. as a preceding sibling). + * @param node to add before this node + * @return this node, for chaining + * @see #after(Node) + */ + public Node before(Node node) { + Validate.notNull(node); + Validate.notNull(parentNode); + + parentNode.addChildren(siblingIndex(), node); + return this; + } + + /** + * Insert the specified HTML into the DOM after this node (i.e. as a following sibling). + * @param html HTML to add after this node + * @return this node, for chaining + * @see #before(String) + */ + public Node after(String html) { + addSiblingHtml(siblingIndex()+1, html); + return this; + } + + /** + * Insert the specified node into the DOM after this node (i.e. as a following sibling). + * @param node to add after this node + * @return this node, for chaining + * @see #before(Node) + */ + public Node after(Node node) { + Validate.notNull(node); + Validate.notNull(parentNode); + + parentNode.addChildren(siblingIndex()+1, node); + return this; + } + + private void addSiblingHtml(int index, String html) { + Validate.notNull(html); + Validate.notNull(parentNode); + + Element context = parent() instanceof Element ? (Element) parent() : null; + List<Node> nodes = Parser.parseFragment(html, context, baseUri()); + parentNode.addChildren(index, nodes.toArray(new Node[nodes.size()])); + } + + /** + Wrap the supplied HTML around this node. + @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. + @return this node, for chaining. + */ + public Node wrap(String html) { + Validate.notEmpty(html); + + Element context = parent() instanceof Element ? (Element) parent() : null; + List<Node> wrapChildren = Parser.parseFragment(html, context, baseUri()); + Node wrapNode = wrapChildren.get(0); + if (wrapNode == null || !(wrapNode instanceof Element)) // nothing to wrap with; noop + return null; + + Element wrap = (Element) wrapNode; + Element deepest = getDeepChild(wrap); + parentNode.replaceChild(this, wrap); + deepest.addChildren(this); + + // remainder (unbalanced wrap, like <div></div><p></p> -- The <p> is remainder + if (wrapChildren.size() > 0) { + for (int i = 0; i < wrapChildren.size(); i++) { + Node remainder = wrapChildren.get(i); + remainder.parentNode.removeChild(remainder); + wrap.appendChild(remainder); + } + } + return this; + } + + /** + * Removes this node from the DOM, and moves its children up into the node's parent. This has the effect of dropping + * the node but keeping its children. + * <p/> + * For example, with the input html:<br/> + * {@code <div>One <span>Two <b>Three</b></span></div>}<br/> + * Calling {@code element.unwrap()} on the {@code span} element will result in the html:<br/> + * {@code <div>One Two <b>Three</b></div>}<br/> + * and the {@code "Two "} {@link TextNode} being returned. + * @return the first child of this node, after the node has been unwrapped. Null if the node had no children. + * @see #remove() + * @see #wrap(String) + */ + public Node unwrap() { + Validate.notNull(parentNode); + + int index = siblingIndex; + Node firstChild = childNodes.size() > 0 ? childNodes.get(0) : null; + parentNode.addChildren(index, this.childNodesAsArray()); + this.remove(); + + return firstChild; + } + + private Element getDeepChild(Element el) { + List<Element> children = el.children(); + if (children.size() > 0) + return getDeepChild(children.get(0)); + else + return el; + } + + /** + * Replace this node in the DOM with the supplied node. + * @param in the node that will will replace the existing node. + */ + public void replaceWith(Node in) { + Validate.notNull(in); + Validate.notNull(parentNode); + parentNode.replaceChild(this, in); + } + + protected void setParentNode(Node parentNode) { + if (this.parentNode != null) + this.parentNode.removeChild(this); + this.parentNode = parentNode; + } + + protected void replaceChild(Node out, Node in) { + Validate.isTrue(out.parentNode == this); + Validate.notNull(in); + if (in.parentNode != null) + in.parentNode.removeChild(in); + + Integer index = out.siblingIndex(); + childNodes.set(index, in); + in.parentNode = this; + in.setSiblingIndex(index); + out.parentNode = null; + } + + protected void removeChild(Node out) { + Validate.isTrue(out.parentNode == this); + int index = out.siblingIndex(); + childNodes.remove(index); + reindexChildren(); + out.parentNode = null; + } + + protected void addChildren(Node... children) { + //most used. short circuit addChildren(int), which hits reindex children and array copy + for (Node child: children) { + reparentChild(child); + childNodes.add(child); + child.setSiblingIndex(childNodes.size()-1); + } + } + + protected void addChildren(int index, Node... children) { + Validate.noNullElements(children); + for (int i = children.length - 1; i >= 0; i--) { + Node in = children[i]; + reparentChild(in); + childNodes.add(index, in); + } + reindexChildren(); + } + + private void reparentChild(Node child) { + if (child.parentNode != null) + child.parentNode.removeChild(child); + child.setParentNode(this); + } + + private void reindexChildren() { + for (int i = 0; i < childNodes.size(); i++) { + childNodes.get(i).setSiblingIndex(i); + } + } + + /** + Retrieves this node's sibling nodes. Similar to {@link #childNodes() node.parent.childNodes()}, but does not + include this node (a node is not a sibling of itself). + @return node siblings. If the node has no parent, returns an empty list. + */ + public List<Node> siblingNodes() { + if (parentNode == null) + return Collections.emptyList(); + + List<Node> nodes = parentNode.childNodes; + List<Node> siblings = new ArrayList<Node>(nodes.size() - 1); + for (Node node: nodes) + if (node != this) + siblings.add(node); + return siblings; + } + + /** + Get this node's next sibling. + @return next sibling, or null if this is the last sibling + */ + public Node nextSibling() { + if (parentNode == null) + return null; // root + + List<Node> siblings = parentNode.childNodes; + Integer index = siblingIndex(); + Validate.notNull(index); + if (siblings.size() > index+1) + return siblings.get(index+1); + else + return null; + } + + /** + Get this node's previous sibling. + @return the previous sibling, or null if this is the first sibling + */ + public Node previousSibling() { + if (parentNode == null) + return null; // root + + List<Node> siblings = parentNode.childNodes; + Integer index = siblingIndex(); + Validate.notNull(index); + if (index > 0) + return siblings.get(index-1); + else + return null; + } + + /** + * Get the list index of this node in its node sibling list. I.e. if this is the first node + * sibling, returns 0. + * @return position in node sibling list + * @see org.jsoup.nodes.Element#elementSiblingIndex() + */ + public int siblingIndex() { + return siblingIndex; + } + + protected void setSiblingIndex(int siblingIndex) { + this.siblingIndex = siblingIndex; + } + + /** + * Perform a depth-first traversal through this node and its descendants. + * @param nodeVisitor the visitor callbacks to perform on each node + * @return this node, for chaining + */ + public Node traverse(NodeVisitor nodeVisitor) { + Validate.notNull(nodeVisitor); + NodeTraversor traversor = new NodeTraversor(nodeVisitor); + traversor.traverse(this); + return this; + } + + /** + Get the outer HTML of this node. + @return HTML + */ + public String outerHtml() { + StringBuilder accum = new StringBuilder(128); + outerHtml(accum); + return accum.toString(); + } + + protected void outerHtml(StringBuilder accum) { + new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this); + } + + // if this node has no document (or parent), retrieve the default output settings + private Document.OutputSettings getOutputSettings() { + return ownerDocument() != null ? ownerDocument().outputSettings() : (new Document("")).outputSettings(); + } + + /** + Get the outer HTML of this node. + @param accum accumulator to place HTML into + */ + abstract void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out); + + abstract void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out); + + public String toString() { + return outerHtml(); + } + + protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) { + accum.append("\n").append(StringUtil.padding(depth * out.indentAmount())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + // todo: have nodes hold a child index, compare against that and parent (not children) + return false; + } + + @Override + public int hashCode() { + int result = parentNode != null ? parentNode.hashCode() : 0; + // not children, or will block stack as they go back up to parent) + result = 31 * result + (attributes != null ? attributes.hashCode() : 0); + return result; + } + + /** + * Create a stand-alone, deep copy of this node, and all of its children. The cloned node will have no siblings or + * parent node. As a stand-alone object, any changes made to the clone or any of its children will not impact the + * original node. + * <p> + * The cloned node may be adopted into another Document or node structure using {@link Element#appendChild(Node)}. + * @return stand-alone cloned node + */ + @Override + public Node clone() { + return doClone(null); // splits for orphan + } + + protected Node doClone(Node parent) { + Node clone; + try { + clone = (Node) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + + clone.parentNode = parent; // can be null, to create an orphan split + clone.siblingIndex = parent == null ? 0 : siblingIndex; + clone.attributes = attributes != null ? attributes.clone() : null; + clone.baseUri = baseUri; + clone.childNodes = new ArrayList<Node>(childNodes.size()); + for (Node child: childNodes) + clone.childNodes.add(child.doClone(clone)); // clone() creates orphans, doClone() keeps parent + + return clone; + } + + private static class OuterHtmlVisitor implements NodeVisitor { + private StringBuilder accum; + private Document.OutputSettings out; + + OuterHtmlVisitor(StringBuilder accum, Document.OutputSettings out) { + this.accum = accum; + this.out = out; + } + + public void head(Node node, int depth) { + node.outerHtmlHead(accum, depth, out); + } + + public void tail(Node node, int depth) { + if (!node.nodeName().equals("#text")) // saves a void hit. + node.outerHtmlTail(accum, depth, out); + } + } +} diff --git a/server/src/org/jsoup/nodes/TextNode.java b/server/src/org/jsoup/nodes/TextNode.java new file mode 100644 index 0000000000..9fd0feac8f --- /dev/null +++ b/server/src/org/jsoup/nodes/TextNode.java @@ -0,0 +1,175 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; + +/** + A text node. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class TextNode extends Node { + /* + TextNode is a node, and so by default comes with attributes and children. The attributes are seldom used, but use + memory, and the child nodes are never used. So we don't have them, and override accessors to attributes to create + them as needed on the fly. + */ + private static final String TEXT_KEY = "text"; + String text; + + /** + Create a new TextNode representing the supplied (unencoded) text). + + @param text raw text + @param baseUri base uri + @see #createFromEncoded(String, String) + */ + public TextNode(String text, String baseUri) { + this.baseUri = baseUri; + this.text = text; + } + + public String nodeName() { + return "#text"; + } + + /** + * Get the text content of this text node. + * @return Unencoded, normalised text. + * @see TextNode#getWholeText() + */ + public String text() { + return normaliseWhitespace(getWholeText()); + } + + /** + * Set the text content of this text node. + * @param text unencoded text + * @return this, for chaining + */ + public TextNode text(String text) { + this.text = text; + if (attributes != null) + attributes.put(TEXT_KEY, text); + return this; + } + + /** + Get the (unencoded) text of this text node, including any newlines and spaces present in the original. + @return text + */ + public String getWholeText() { + return attributes == null ? text : attributes.get(TEXT_KEY); + } + + /** + Test if this text node is blank -- that is, empty or only whitespace (including newlines). + @return true if this document is empty or only whitespace, false if it contains any text content. + */ + public boolean isBlank() { + return StringUtil.isBlank(getWholeText()); + } + + /** + * Split this text node into two nodes at the specified string offset. After splitting, this node will contain the + * original text up to the offset, and will have a new text node sibling containing the text after the offset. + * @param offset string offset point to split node at. + * @return the newly created text node containing the text after the offset. + */ + public TextNode splitText(int offset) { + Validate.isTrue(offset >= 0, "Split offset must be not be negative"); + Validate.isTrue(offset < text.length(), "Split offset must not be greater than current text length"); + + String head = getWholeText().substring(0, offset); + String tail = getWholeText().substring(offset); + text(head); + TextNode tailNode = new TextNode(tail, this.baseUri()); + if (parent() != null) + parent().addChildren(siblingIndex()+1, tailNode); + + return tailNode; + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + String html = Entities.escape(getWholeText(), out); + if (out.prettyPrint() && parent() instanceof Element && !((Element) parent()).preserveWhitespace()) { + html = normaliseWhitespace(html); + } + + if (out.prettyPrint() && siblingIndex() == 0 && parentNode instanceof Element && ((Element) parentNode).tag().formatAsBlock() && !isBlank()) + indent(accum, depth, out); + accum.append(html); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } + + /** + * Create a new TextNode from HTML encoded (aka escaped) data. + * @param encodedText Text containing encoded HTML (e.g. &lt;) + * @return TextNode containing unencoded data (e.g. <) + */ + public static TextNode createFromEncoded(String encodedText, String baseUri) { + String text = Entities.unescape(encodedText); + return new TextNode(text, baseUri); + } + + static String normaliseWhitespace(String text) { + text = StringUtil.normaliseWhitespace(text); + return text; + } + + static String stripLeadingWhitespace(String text) { + return text.replaceFirst("^\\s+", ""); + } + + static boolean lastCharIsWhitespace(StringBuilder sb) { + return sb.length() != 0 && sb.charAt(sb.length() - 1) == ' '; + } + + // attribute fiddling. create on first access. + private void ensureAttributes() { + if (attributes == null) { + attributes = new Attributes(); + attributes.put(TEXT_KEY, text); + } + } + + @Override + public String attr(String attributeKey) { + ensureAttributes(); + return super.attr(attributeKey); + } + + @Override + public Attributes attributes() { + ensureAttributes(); + return super.attributes(); + } + + @Override + public Node attr(String attributeKey, String attributeValue) { + ensureAttributes(); + return super.attr(attributeKey, attributeValue); + } + + @Override + public boolean hasAttr(String attributeKey) { + ensureAttributes(); + return super.hasAttr(attributeKey); + } + + @Override + public Node removeAttr(String attributeKey) { + ensureAttributes(); + return super.removeAttr(attributeKey); + } + + @Override + public String absUrl(String attributeKey) { + ensureAttributes(); + return super.absUrl(attributeKey); + } +} diff --git a/server/src/org/jsoup/nodes/XmlDeclaration.java b/server/src/org/jsoup/nodes/XmlDeclaration.java new file mode 100644 index 0000000000..80d4a0152f --- /dev/null +++ b/server/src/org/jsoup/nodes/XmlDeclaration.java @@ -0,0 +1,48 @@ +package org.jsoup.nodes; + +/** + An XML Declaration. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class XmlDeclaration extends Node { + private static final String DECL_KEY = "declaration"; + private final boolean isProcessingInstruction; // <! if true, <? if false, declaration (and last data char should be ?) + + /** + Create a new XML declaration + @param data data + @param baseUri base uri + @param isProcessingInstruction is processing instruction + */ + public XmlDeclaration(String data, String baseUri, boolean isProcessingInstruction) { + super(baseUri); + attributes.put(DECL_KEY, data); + this.isProcessingInstruction = isProcessingInstruction; + } + + public String nodeName() { + return "#declaration"; + } + + /** + Get the unencoded XML declaration. + @return XML declaration + */ + public String getWholeDeclaration() { + return attributes.get(DECL_KEY); + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + accum + .append("<") + .append(isProcessingInstruction ? "!" : "?") + .append(getWholeDeclaration()) + .append(">"); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } +} diff --git a/server/src/org/jsoup/nodes/entities-base.properties b/server/src/org/jsoup/nodes/entities-base.properties new file mode 100644 index 0000000000..3d1d11e6c4 --- /dev/null +++ b/server/src/org/jsoup/nodes/entities-base.properties @@ -0,0 +1,106 @@ +AElig=000C6 +AMP=00026 +Aacute=000C1 +Acirc=000C2 +Agrave=000C0 +Aring=000C5 +Atilde=000C3 +Auml=000C4 +COPY=000A9 +Ccedil=000C7 +ETH=000D0 +Eacute=000C9 +Ecirc=000CA +Egrave=000C8 +Euml=000CB +GT=0003E +Iacute=000CD +Icirc=000CE +Igrave=000CC +Iuml=000CF +LT=0003C +Ntilde=000D1 +Oacute=000D3 +Ocirc=000D4 +Ograve=000D2 +Oslash=000D8 +Otilde=000D5 +Ouml=000D6 +QUOT=00022 +REG=000AE +THORN=000DE +Uacute=000DA +Ucirc=000DB +Ugrave=000D9 +Uuml=000DC +Yacute=000DD +aacute=000E1 +acirc=000E2 +acute=000B4 +aelig=000E6 +agrave=000E0 +amp=00026 +aring=000E5 +atilde=000E3 +auml=000E4 +brvbar=000A6 +ccedil=000E7 +cedil=000B8 +cent=000A2 +copy=000A9 +curren=000A4 +deg=000B0 +divide=000F7 +eacute=000E9 +ecirc=000EA +egrave=000E8 +eth=000F0 +euml=000EB +frac12=000BD +frac14=000BC +frac34=000BE +gt=0003E +iacute=000ED +icirc=000EE +iexcl=000A1 +igrave=000EC +iquest=000BF +iuml=000EF +laquo=000AB +lt=0003C +macr=000AF +micro=000B5 +middot=000B7 +nbsp=000A0 +not=000AC +ntilde=000F1 +oacute=000F3 +ocirc=000F4 +ograve=000F2 +ordf=000AA +ordm=000BA +oslash=000F8 +otilde=000F5 +ouml=000F6 +para=000B6 +plusmn=000B1 +pound=000A3 +quot=00022 +raquo=000BB +reg=000AE +sect=000A7 +shy=000AD +sup1=000B9 +sup2=000B2 +sup3=000B3 +szlig=000DF +thorn=000FE +times=000D7 +uacute=000FA +ucirc=000FB +ugrave=000F9 +uml=000A8 +uuml=000FC +yacute=000FD +yen=000A5 +yuml=000FF diff --git a/server/src/org/jsoup/nodes/entities-full.properties b/server/src/org/jsoup/nodes/entities-full.properties new file mode 100644 index 0000000000..92f124f408 --- /dev/null +++ b/server/src/org/jsoup/nodes/entities-full.properties @@ -0,0 +1,2032 @@ +AElig=000C6 +AMP=00026 +Aacute=000C1 +Abreve=00102 +Acirc=000C2 +Acy=00410 +Afr=1D504 +Agrave=000C0 +Alpha=00391 +Amacr=00100 +And=02A53 +Aogon=00104 +Aopf=1D538 +ApplyFunction=02061 +Aring=000C5 +Ascr=1D49C +Assign=02254 +Atilde=000C3 +Auml=000C4 +Backslash=02216 +Barv=02AE7 +Barwed=02306 +Bcy=00411 +Because=02235 +Bernoullis=0212C +Beta=00392 +Bfr=1D505 +Bopf=1D539 +Breve=002D8 +Bscr=0212C +Bumpeq=0224E +CHcy=00427 +COPY=000A9 +Cacute=00106 +Cap=022D2 +CapitalDifferentialD=02145 +Cayleys=0212D +Ccaron=0010C +Ccedil=000C7 +Ccirc=00108 +Cconint=02230 +Cdot=0010A +Cedilla=000B8 +CenterDot=000B7 +Cfr=0212D +Chi=003A7 +CircleDot=02299 +CircleMinus=02296 +CirclePlus=02295 +CircleTimes=02297 +ClockwiseContourIntegral=02232 +CloseCurlyDoubleQuote=0201D +CloseCurlyQuote=02019 +Colon=02237 +Colone=02A74 +Congruent=02261 +Conint=0222F +ContourIntegral=0222E +Copf=02102 +Coproduct=02210 +CounterClockwiseContourIntegral=02233 +Cross=02A2F +Cscr=1D49E +Cup=022D3 +CupCap=0224D +DD=02145 +DDotrahd=02911 +DJcy=00402 +DScy=00405 +DZcy=0040F +Dagger=02021 +Darr=021A1 +Dashv=02AE4 +Dcaron=0010E +Dcy=00414 +Del=02207 +Delta=00394 +Dfr=1D507 +DiacriticalAcute=000B4 +DiacriticalDot=002D9 +DiacriticalDoubleAcute=002DD +DiacriticalGrave=00060 +DiacriticalTilde=002DC +Diamond=022C4 +DifferentialD=02146 +Dopf=1D53B +Dot=000A8 +DotDot=020DC +DotEqual=02250 +DoubleContourIntegral=0222F +DoubleDot=000A8 +DoubleDownArrow=021D3 +DoubleLeftArrow=021D0 +DoubleLeftRightArrow=021D4 +DoubleLeftTee=02AE4 +DoubleLongLeftArrow=027F8 +DoubleLongLeftRightArrow=027FA +DoubleLongRightArrow=027F9 +DoubleRightArrow=021D2 +DoubleRightTee=022A8 +DoubleUpArrow=021D1 +DoubleUpDownArrow=021D5 +DoubleVerticalBar=02225 +DownArrow=02193 +DownArrowBar=02913 +DownArrowUpArrow=021F5 +DownBreve=00311 +DownLeftRightVector=02950 +DownLeftTeeVector=0295E +DownLeftVector=021BD +DownLeftVectorBar=02956 +DownRightTeeVector=0295F +DownRightVector=021C1 +DownRightVectorBar=02957 +DownTee=022A4 +DownTeeArrow=021A7 +Downarrow=021D3 +Dscr=1D49F +Dstrok=00110 +ENG=0014A +ETH=000D0 +Eacute=000C9 +Ecaron=0011A +Ecirc=000CA +Ecy=0042D +Edot=00116 +Efr=1D508 +Egrave=000C8 +Element=02208 +Emacr=00112 +EmptySmallSquare=025FB +EmptyVerySmallSquare=025AB +Eogon=00118 +Eopf=1D53C +Epsilon=00395 +Equal=02A75 +EqualTilde=02242 +Equilibrium=021CC +Escr=02130 +Esim=02A73 +Eta=00397 +Euml=000CB +Exists=02203 +ExponentialE=02147 +Fcy=00424 +Ffr=1D509 +FilledSmallSquare=025FC +FilledVerySmallSquare=025AA +Fopf=1D53D +ForAll=02200 +Fouriertrf=02131 +Fscr=02131 +GJcy=00403 +GT=0003E +Gamma=00393 +Gammad=003DC +Gbreve=0011E +Gcedil=00122 +Gcirc=0011C +Gcy=00413 +Gdot=00120 +Gfr=1D50A +Gg=022D9 +Gopf=1D53E +GreaterEqual=02265 +GreaterEqualLess=022DB +GreaterFullEqual=02267 +GreaterGreater=02AA2 +GreaterLess=02277 +GreaterSlantEqual=02A7E +GreaterTilde=02273 +Gscr=1D4A2 +Gt=0226B +HARDcy=0042A +Hacek=002C7 +Hat=0005E +Hcirc=00124 +Hfr=0210C +HilbertSpace=0210B +Hopf=0210D +HorizontalLine=02500 +Hscr=0210B +Hstrok=00126 +HumpDownHump=0224E +HumpEqual=0224F +IEcy=00415 +IJlig=00132 +IOcy=00401 +Iacute=000CD +Icirc=000CE +Icy=00418 +Idot=00130 +Ifr=02111 +Igrave=000CC +Im=02111 +Imacr=0012A +ImaginaryI=02148 +Implies=021D2 +Int=0222C +Integral=0222B +Intersection=022C2 +InvisibleComma=02063 +InvisibleTimes=02062 +Iogon=0012E +Iopf=1D540 +Iota=00399 +Iscr=02110 +Itilde=00128 +Iukcy=00406 +Iuml=000CF +Jcirc=00134 +Jcy=00419 +Jfr=1D50D +Jopf=1D541 +Jscr=1D4A5 +Jsercy=00408 +Jukcy=00404 +KHcy=00425 +KJcy=0040C +Kappa=0039A +Kcedil=00136 +Kcy=0041A +Kfr=1D50E +Kopf=1D542 +Kscr=1D4A6 +LJcy=00409 +LT=0003C +Lacute=00139 +Lambda=0039B +Lang=027EA +Laplacetrf=02112 +Larr=0219E +Lcaron=0013D +Lcedil=0013B +Lcy=0041B +LeftAngleBracket=027E8 +LeftArrow=02190 +LeftArrowBar=021E4 +LeftArrowRightArrow=021C6 +LeftCeiling=02308 +LeftDoubleBracket=027E6 +LeftDownTeeVector=02961 +LeftDownVector=021C3 +LeftDownVectorBar=02959 +LeftFloor=0230A +LeftRightArrow=02194 +LeftRightVector=0294E +LeftTee=022A3 +LeftTeeArrow=021A4 +LeftTeeVector=0295A +LeftTriangle=022B2 +LeftTriangleBar=029CF +LeftTriangleEqual=022B4 +LeftUpDownVector=02951 +LeftUpTeeVector=02960 +LeftUpVector=021BF +LeftUpVectorBar=02958 +LeftVector=021BC +LeftVectorBar=02952 +Leftarrow=021D0 +Leftrightarrow=021D4 +LessEqualGreater=022DA +LessFullEqual=02266 +LessGreater=02276 +LessLess=02AA1 +LessSlantEqual=02A7D +LessTilde=02272 +Lfr=1D50F +Ll=022D8 +Lleftarrow=021DA +Lmidot=0013F +LongLeftArrow=027F5 +LongLeftRightArrow=027F7 +LongRightArrow=027F6 +Longleftarrow=027F8 +Longleftrightarrow=027FA +Longrightarrow=027F9 +Lopf=1D543 +LowerLeftArrow=02199 +LowerRightArrow=02198 +Lscr=02112 +Lsh=021B0 +Lstrok=00141 +Lt=0226A +Map=02905 +Mcy=0041C +MediumSpace=0205F +Mellintrf=02133 +Mfr=1D510 +MinusPlus=02213 +Mopf=1D544 +Mscr=02133 +Mu=0039C +NJcy=0040A +Nacute=00143 +Ncaron=00147 +Ncedil=00145 +Ncy=0041D +NegativeMediumSpace=0200B +NegativeThickSpace=0200B +NegativeThinSpace=0200B +NegativeVeryThinSpace=0200B +NestedGreaterGreater=0226B +NestedLessLess=0226A +NewLine=0000A +Nfr=1D511 +NoBreak=02060 +NonBreakingSpace=000A0 +Nopf=02115 +Not=02AEC +NotCongruent=02262 +NotCupCap=0226D +NotDoubleVerticalBar=02226 +NotElement=02209 +NotEqual=02260 +NotExists=02204 +NotGreater=0226F +NotGreaterEqual=02271 +NotGreaterLess=02279 +NotGreaterTilde=02275 +NotLeftTriangle=022EA +NotLeftTriangleEqual=022EC +NotLess=0226E +NotLessEqual=02270 +NotLessGreater=02278 +NotLessTilde=02274 +NotPrecedes=02280 +NotPrecedesSlantEqual=022E0 +NotReverseElement=0220C +NotRightTriangle=022EB +NotRightTriangleEqual=022ED +NotSquareSubsetEqual=022E2 +NotSquareSupersetEqual=022E3 +NotSubsetEqual=02288 +NotSucceeds=02281 +NotSucceedsSlantEqual=022E1 +NotSupersetEqual=02289 +NotTilde=02241 +NotTildeEqual=02244 +NotTildeFullEqual=02247 +NotTildeTilde=02249 +NotVerticalBar=02224 +Nscr=1D4A9 +Ntilde=000D1 +Nu=0039D +OElig=00152 +Oacute=000D3 +Ocirc=000D4 +Ocy=0041E +Odblac=00150 +Ofr=1D512 +Ograve=000D2 +Omacr=0014C +Omega=003A9 +Omicron=0039F +Oopf=1D546 +OpenCurlyDoubleQuote=0201C +OpenCurlyQuote=02018 +Or=02A54 +Oscr=1D4AA +Oslash=000D8 +Otilde=000D5 +Otimes=02A37 +Ouml=000D6 +OverBar=0203E +OverBrace=023DE +OverBracket=023B4 +OverParenthesis=023DC +PartialD=02202 +Pcy=0041F +Pfr=1D513 +Phi=003A6 +Pi=003A0 +PlusMinus=000B1 +Poincareplane=0210C +Popf=02119 +Pr=02ABB +Precedes=0227A +PrecedesEqual=02AAF +PrecedesSlantEqual=0227C +PrecedesTilde=0227E +Prime=02033 +Product=0220F +Proportion=02237 +Proportional=0221D +Pscr=1D4AB +Psi=003A8 +QUOT=00022 +Qfr=1D514 +Qopf=0211A +Qscr=1D4AC +RBarr=02910 +REG=000AE +Racute=00154 +Rang=027EB +Rarr=021A0 +Rarrtl=02916 +Rcaron=00158 +Rcedil=00156 +Rcy=00420 +Re=0211C +ReverseElement=0220B +ReverseEquilibrium=021CB +ReverseUpEquilibrium=0296F +Rfr=0211C +Rho=003A1 +RightAngleBracket=027E9 +RightArrow=02192 +RightArrowBar=021E5 +RightArrowLeftArrow=021C4 +RightCeiling=02309 +RightDoubleBracket=027E7 +RightDownTeeVector=0295D +RightDownVector=021C2 +RightDownVectorBar=02955 +RightFloor=0230B +RightTee=022A2 +RightTeeArrow=021A6 +RightTeeVector=0295B +RightTriangle=022B3 +RightTriangleBar=029D0 +RightTriangleEqual=022B5 +RightUpDownVector=0294F +RightUpTeeVector=0295C +RightUpVector=021BE +RightUpVectorBar=02954 +RightVector=021C0 +RightVectorBar=02953 +Rightarrow=021D2 +Ropf=0211D +RoundImplies=02970 +Rrightarrow=021DB +Rscr=0211B +Rsh=021B1 +RuleDelayed=029F4 +SHCHcy=00429 +SHcy=00428 +SOFTcy=0042C +Sacute=0015A +Sc=02ABC +Scaron=00160 +Scedil=0015E +Scirc=0015C +Scy=00421 +Sfr=1D516 +ShortDownArrow=02193 +ShortLeftArrow=02190 +ShortRightArrow=02192 +ShortUpArrow=02191 +Sigma=003A3 +SmallCircle=02218 +Sopf=1D54A +Sqrt=0221A +Square=025A1 +SquareIntersection=02293 +SquareSubset=0228F +SquareSubsetEqual=02291 +SquareSuperset=02290 +SquareSupersetEqual=02292 +SquareUnion=02294 +Sscr=1D4AE +Star=022C6 +Sub=022D0 +Subset=022D0 +SubsetEqual=02286 +Succeeds=0227B +SucceedsEqual=02AB0 +SucceedsSlantEqual=0227D +SucceedsTilde=0227F +SuchThat=0220B +Sum=02211 +Sup=022D1 +Superset=02283 +SupersetEqual=02287 +Supset=022D1 +THORN=000DE +TRADE=02122 +TSHcy=0040B +TScy=00426 +Tab=00009 +Tau=003A4 +Tcaron=00164 +Tcedil=00162 +Tcy=00422 +Tfr=1D517 +Therefore=02234 +Theta=00398 +ThinSpace=02009 +Tilde=0223C +TildeEqual=02243 +TildeFullEqual=02245 +TildeTilde=02248 +Topf=1D54B +TripleDot=020DB +Tscr=1D4AF +Tstrok=00166 +Uacute=000DA +Uarr=0219F +Uarrocir=02949 +Ubrcy=0040E +Ubreve=0016C +Ucirc=000DB +Ucy=00423 +Udblac=00170 +Ufr=1D518 +Ugrave=000D9 +Umacr=0016A +UnderBar=0005F +UnderBrace=023DF +UnderBracket=023B5 +UnderParenthesis=023DD +Union=022C3 +UnionPlus=0228E +Uogon=00172 +Uopf=1D54C +UpArrow=02191 +UpArrowBar=02912 +UpArrowDownArrow=021C5 +UpDownArrow=02195 +UpEquilibrium=0296E +UpTee=022A5 +UpTeeArrow=021A5 +Uparrow=021D1 +Updownarrow=021D5 +UpperLeftArrow=02196 +UpperRightArrow=02197 +Upsi=003D2 +Upsilon=003A5 +Uring=0016E +Uscr=1D4B0 +Utilde=00168 +Uuml=000DC +VDash=022AB +Vbar=02AEB +Vcy=00412 +Vdash=022A9 +Vdashl=02AE6 +Vee=022C1 +Verbar=02016 +Vert=02016 +VerticalBar=02223 +VerticalLine=0007C +VerticalSeparator=02758 +VerticalTilde=02240 +VeryThinSpace=0200A +Vfr=1D519 +Vopf=1D54D +Vscr=1D4B1 +Vvdash=022AA +Wcirc=00174 +Wedge=022C0 +Wfr=1D51A +Wopf=1D54E +Wscr=1D4B2 +Xfr=1D51B +Xi=0039E +Xopf=1D54F +Xscr=1D4B3 +YAcy=0042F +YIcy=00407 +YUcy=0042E +Yacute=000DD +Ycirc=00176 +Ycy=0042B +Yfr=1D51C +Yopf=1D550 +Yscr=1D4B4 +Yuml=00178 +ZHcy=00416 +Zacute=00179 +Zcaron=0017D +Zcy=00417 +Zdot=0017B +ZeroWidthSpace=0200B +Zeta=00396 +Zfr=02128 +Zopf=02124 +Zscr=1D4B5 +aacute=000E1 +abreve=00103 +ac=0223E +acd=0223F +acirc=000E2 +acute=000B4 +acy=00430 +aelig=000E6 +af=02061 +afr=1D51E +agrave=000E0 +alefsym=02135 +aleph=02135 +alpha=003B1 +amacr=00101 +amalg=02A3F +amp=00026 +and=02227 +andand=02A55 +andd=02A5C +andslope=02A58 +andv=02A5A +ang=02220 +ange=029A4 +angle=02220 +angmsd=02221 +angmsdaa=029A8 +angmsdab=029A9 +angmsdac=029AA +angmsdad=029AB +angmsdae=029AC +angmsdaf=029AD +angmsdag=029AE +angmsdah=029AF +angrt=0221F +angrtvb=022BE +angrtvbd=0299D +angsph=02222 +angst=000C5 +angzarr=0237C +aogon=00105 +aopf=1D552 +ap=02248 +apE=02A70 +apacir=02A6F +ape=0224A +apid=0224B +apos=00027 +approx=02248 +approxeq=0224A +aring=000E5 +ascr=1D4B6 +ast=0002A +asymp=02248 +asympeq=0224D +atilde=000E3 +auml=000E4 +awconint=02233 +awint=02A11 +bNot=02AED +backcong=0224C +backepsilon=003F6 +backprime=02035 +backsim=0223D +backsimeq=022CD +barvee=022BD +barwed=02305 +barwedge=02305 +bbrk=023B5 +bbrktbrk=023B6 +bcong=0224C +bcy=00431 +bdquo=0201E +becaus=02235 +because=02235 +bemptyv=029B0 +bepsi=003F6 +bernou=0212C +beta=003B2 +beth=02136 +between=0226C +bfr=1D51F +bigcap=022C2 +bigcirc=025EF +bigcup=022C3 +bigodot=02A00 +bigoplus=02A01 +bigotimes=02A02 +bigsqcup=02A06 +bigstar=02605 +bigtriangledown=025BD +bigtriangleup=025B3 +biguplus=02A04 +bigvee=022C1 +bigwedge=022C0 +bkarow=0290D +blacklozenge=029EB +blacksquare=025AA +blacktriangle=025B4 +blacktriangledown=025BE +blacktriangleleft=025C2 +blacktriangleright=025B8 +blank=02423 +blk12=02592 +blk14=02591 +blk34=02593 +block=02588 +bnot=02310 +bopf=1D553 +bot=022A5 +bottom=022A5 +bowtie=022C8 +boxDL=02557 +boxDR=02554 +boxDl=02556 +boxDr=02553 +boxH=02550 +boxHD=02566 +boxHU=02569 +boxHd=02564 +boxHu=02567 +boxUL=0255D +boxUR=0255A +boxUl=0255C +boxUr=02559 +boxV=02551 +boxVH=0256C +boxVL=02563 +boxVR=02560 +boxVh=0256B +boxVl=02562 +boxVr=0255F +boxbox=029C9 +boxdL=02555 +boxdR=02552 +boxdl=02510 +boxdr=0250C +boxh=02500 +boxhD=02565 +boxhU=02568 +boxhd=0252C +boxhu=02534 +boxminus=0229F +boxplus=0229E +boxtimes=022A0 +boxuL=0255B +boxuR=02558 +boxul=02518 +boxur=02514 +boxv=02502 +boxvH=0256A +boxvL=02561 +boxvR=0255E +boxvh=0253C +boxvl=02524 +boxvr=0251C +bprime=02035 +breve=002D8 +brvbar=000A6 +bscr=1D4B7 +bsemi=0204F +bsim=0223D +bsime=022CD +bsol=0005C +bsolb=029C5 +bsolhsub=027C8 +bull=02022 +bullet=02022 +bump=0224E +bumpE=02AAE +bumpe=0224F +bumpeq=0224F +cacute=00107 +cap=02229 +capand=02A44 +capbrcup=02A49 +capcap=02A4B +capcup=02A47 +capdot=02A40 +caret=02041 +caron=002C7 +ccaps=02A4D +ccaron=0010D +ccedil=000E7 +ccirc=00109 +ccups=02A4C +ccupssm=02A50 +cdot=0010B +cedil=000B8 +cemptyv=029B2 +cent=000A2 +centerdot=000B7 +cfr=1D520 +chcy=00447 +check=02713 +checkmark=02713 +chi=003C7 +cir=025CB +cirE=029C3 +circ=002C6 +circeq=02257 +circlearrowleft=021BA +circlearrowright=021BB +circledR=000AE +circledS=024C8 +circledast=0229B +circledcirc=0229A +circleddash=0229D +cire=02257 +cirfnint=02A10 +cirmid=02AEF +cirscir=029C2 +clubs=02663 +clubsuit=02663 +colon=0003A +colone=02254 +coloneq=02254 +comma=0002C +commat=00040 +comp=02201 +compfn=02218 +complement=02201 +complexes=02102 +cong=02245 +congdot=02A6D +conint=0222E +copf=1D554 +coprod=02210 +copy=000A9 +copysr=02117 +crarr=021B5 +cross=02717 +cscr=1D4B8 +csub=02ACF +csube=02AD1 +csup=02AD0 +csupe=02AD2 +ctdot=022EF +cudarrl=02938 +cudarrr=02935 +cuepr=022DE +cuesc=022DF +cularr=021B6 +cularrp=0293D +cup=0222A +cupbrcap=02A48 +cupcap=02A46 +cupcup=02A4A +cupdot=0228D +cupor=02A45 +curarr=021B7 +curarrm=0293C +curlyeqprec=022DE +curlyeqsucc=022DF +curlyvee=022CE +curlywedge=022CF +curren=000A4 +curvearrowleft=021B6 +curvearrowright=021B7 +cuvee=022CE +cuwed=022CF +cwconint=02232 +cwint=02231 +cylcty=0232D +dArr=021D3 +dHar=02965 +dagger=02020 +daleth=02138 +darr=02193 +dash=02010 +dashv=022A3 +dbkarow=0290F +dblac=002DD +dcaron=0010F +dcy=00434 +dd=02146 +ddagger=02021 +ddarr=021CA +ddotseq=02A77 +deg=000B0 +delta=003B4 +demptyv=029B1 +dfisht=0297F +dfr=1D521 +dharl=021C3 +dharr=021C2 +diam=022C4 +diamond=022C4 +diamondsuit=02666 +diams=02666 +die=000A8 +digamma=003DD +disin=022F2 +div=000F7 +divide=000F7 +divideontimes=022C7 +divonx=022C7 +djcy=00452 +dlcorn=0231E +dlcrop=0230D +dollar=00024 +dopf=1D555 +dot=002D9 +doteq=02250 +doteqdot=02251 +dotminus=02238 +dotplus=02214 +dotsquare=022A1 +doublebarwedge=02306 +downarrow=02193 +downdownarrows=021CA +downharpoonleft=021C3 +downharpoonright=021C2 +drbkarow=02910 +drcorn=0231F +drcrop=0230C +dscr=1D4B9 +dscy=00455 +dsol=029F6 +dstrok=00111 +dtdot=022F1 +dtri=025BF +dtrif=025BE +duarr=021F5 +duhar=0296F +dwangle=029A6 +dzcy=0045F +dzigrarr=027FF +eDDot=02A77 +eDot=02251 +eacute=000E9 +easter=02A6E +ecaron=0011B +ecir=02256 +ecirc=000EA +ecolon=02255 +ecy=0044D +edot=00117 +ee=02147 +efDot=02252 +efr=1D522 +eg=02A9A +egrave=000E8 +egs=02A96 +egsdot=02A98 +el=02A99 +elinters=023E7 +ell=02113 +els=02A95 +elsdot=02A97 +emacr=00113 +empty=02205 +emptyset=02205 +emptyv=02205 +emsp13=02004 +emsp14=02005 +emsp=02003 +eng=0014B +ensp=02002 +eogon=00119 +eopf=1D556 +epar=022D5 +eparsl=029E3 +eplus=02A71 +epsi=003B5 +epsilon=003B5 +epsiv=003F5 +eqcirc=02256 +eqcolon=02255 +eqsim=02242 +eqslantgtr=02A96 +eqslantless=02A95 +equals=0003D +equest=0225F +equiv=02261 +equivDD=02A78 +eqvparsl=029E5 +erDot=02253 +erarr=02971 +escr=0212F +esdot=02250 +esim=02242 +eta=003B7 +eth=000F0 +euml=000EB +euro=020AC +excl=00021 +exist=02203 +expectation=02130 +exponentiale=02147 +fallingdotseq=02252 +fcy=00444 +female=02640 +ffilig=0FB03 +fflig=0FB00 +ffllig=0FB04 +ffr=1D523 +filig=0FB01 +flat=0266D +fllig=0FB02 +fltns=025B1 +fnof=00192 +fopf=1D557 +forall=02200 +fork=022D4 +forkv=02AD9 +fpartint=02A0D +frac12=000BD +frac13=02153 +frac14=000BC +frac15=02155 +frac16=02159 +frac18=0215B +frac23=02154 +frac25=02156 +frac34=000BE +frac35=02157 +frac38=0215C +frac45=02158 +frac56=0215A +frac58=0215D +frac78=0215E +frasl=02044 +frown=02322 +fscr=1D4BB +gE=02267 +gEl=02A8C +gacute=001F5 +gamma=003B3 +gammad=003DD +gap=02A86 +gbreve=0011F +gcirc=0011D +gcy=00433 +gdot=00121 +ge=02265 +gel=022DB +geq=02265 +geqq=02267 +geqslant=02A7E +ges=02A7E +gescc=02AA9 +gesdot=02A80 +gesdoto=02A82 +gesdotol=02A84 +gesles=02A94 +gfr=1D524 +gg=0226B +ggg=022D9 +gimel=02137 +gjcy=00453 +gl=02277 +glE=02A92 +gla=02AA5 +glj=02AA4 +gnE=02269 +gnap=02A8A +gnapprox=02A8A +gne=02A88 +gneq=02A88 +gneqq=02269 +gnsim=022E7 +gopf=1D558 +grave=00060 +gscr=0210A +gsim=02273 +gsime=02A8E +gsiml=02A90 +gt=0003E +gtcc=02AA7 +gtcir=02A7A +gtdot=022D7 +gtlPar=02995 +gtquest=02A7C +gtrapprox=02A86 +gtrarr=02978 +gtrdot=022D7 +gtreqless=022DB +gtreqqless=02A8C +gtrless=02277 +gtrsim=02273 +hArr=021D4 +hairsp=0200A +half=000BD +hamilt=0210B +hardcy=0044A +harr=02194 +harrcir=02948 +harrw=021AD +hbar=0210F +hcirc=00125 +hearts=02665 +heartsuit=02665 +hellip=02026 +hercon=022B9 +hfr=1D525 +hksearow=02925 +hkswarow=02926 +hoarr=021FF +homtht=0223B +hookleftarrow=021A9 +hookrightarrow=021AA +hopf=1D559 +horbar=02015 +hscr=1D4BD +hslash=0210F +hstrok=00127 +hybull=02043 +hyphen=02010 +iacute=000ED +ic=02063 +icirc=000EE +icy=00438 +iecy=00435 +iexcl=000A1 +iff=021D4 +ifr=1D526 +igrave=000EC +ii=02148 +iiiint=02A0C +iiint=0222D +iinfin=029DC +iiota=02129 +ijlig=00133 +imacr=0012B +image=02111 +imagline=02110 +imagpart=02111 +imath=00131 +imof=022B7 +imped=001B5 +in=02208 +incare=02105 +infin=0221E +infintie=029DD +inodot=00131 +int=0222B +intcal=022BA +integers=02124 +intercal=022BA +intlarhk=02A17 +intprod=02A3C +iocy=00451 +iogon=0012F +iopf=1D55A +iota=003B9 +iprod=02A3C +iquest=000BF +iscr=1D4BE +isin=02208 +isinE=022F9 +isindot=022F5 +isins=022F4 +isinsv=022F3 +isinv=02208 +it=02062 +itilde=00129 +iukcy=00456 +iuml=000EF +jcirc=00135 +jcy=00439 +jfr=1D527 +jmath=00237 +jopf=1D55B +jscr=1D4BF +jsercy=00458 +jukcy=00454 +kappa=003BA +kappav=003F0 +kcedil=00137 +kcy=0043A +kfr=1D528 +kgreen=00138 +khcy=00445 +kjcy=0045C +kopf=1D55C +kscr=1D4C0 +lAarr=021DA +lArr=021D0 +lAtail=0291B +lBarr=0290E +lE=02266 +lEg=02A8B +lHar=02962 +lacute=0013A +laemptyv=029B4 +lagran=02112 +lambda=003BB +lang=027E8 +langd=02991 +langle=027E8 +lap=02A85 +laquo=000AB +larr=02190 +larrb=021E4 +larrbfs=0291F +larrfs=0291D +larrhk=021A9 +larrlp=021AB +larrpl=02939 +larrsim=02973 +larrtl=021A2 +lat=02AAB +latail=02919 +late=02AAD +lbarr=0290C +lbbrk=02772 +lbrace=0007B +lbrack=0005B +lbrke=0298B +lbrksld=0298F +lbrkslu=0298D +lcaron=0013E +lcedil=0013C +lceil=02308 +lcub=0007B +lcy=0043B +ldca=02936 +ldquo=0201C +ldquor=0201E +ldrdhar=02967 +ldrushar=0294B +ldsh=021B2 +le=02264 +leftarrow=02190 +leftarrowtail=021A2 +leftharpoondown=021BD +leftharpoonup=021BC +leftleftarrows=021C7 +leftrightarrow=02194 +leftrightarrows=021C6 +leftrightharpoons=021CB +leftrightsquigarrow=021AD +leftthreetimes=022CB +leg=022DA +leq=02264 +leqq=02266 +leqslant=02A7D +les=02A7D +lescc=02AA8 +lesdot=02A7F +lesdoto=02A81 +lesdotor=02A83 +lesges=02A93 +lessapprox=02A85 +lessdot=022D6 +lesseqgtr=022DA +lesseqqgtr=02A8B +lessgtr=02276 +lesssim=02272 +lfisht=0297C +lfloor=0230A +lfr=1D529 +lg=02276 +lgE=02A91 +lhard=021BD +lharu=021BC +lharul=0296A +lhblk=02584 +ljcy=00459 +ll=0226A +llarr=021C7 +llcorner=0231E +llhard=0296B +lltri=025FA +lmidot=00140 +lmoust=023B0 +lmoustache=023B0 +lnE=02268 +lnap=02A89 +lnapprox=02A89 +lne=02A87 +lneq=02A87 +lneqq=02268 +lnsim=022E6 +loang=027EC +loarr=021FD +lobrk=027E6 +longleftarrow=027F5 +longleftrightarrow=027F7 +longmapsto=027FC +longrightarrow=027F6 +looparrowleft=021AB +looparrowright=021AC +lopar=02985 +lopf=1D55D +loplus=02A2D +lotimes=02A34 +lowast=02217 +lowbar=0005F +loz=025CA +lozenge=025CA +lozf=029EB +lpar=00028 +lparlt=02993 +lrarr=021C6 +lrcorner=0231F +lrhar=021CB +lrhard=0296D +lrm=0200E +lrtri=022BF +lsaquo=02039 +lscr=1D4C1 +lsh=021B0 +lsim=02272 +lsime=02A8D +lsimg=02A8F +lsqb=0005B +lsquo=02018 +lsquor=0201A +lstrok=00142 +lt=0003C +ltcc=02AA6 +ltcir=02A79 +ltdot=022D6 +lthree=022CB +ltimes=022C9 +ltlarr=02976 +ltquest=02A7B +ltrPar=02996 +ltri=025C3 +ltrie=022B4 +ltrif=025C2 +lurdshar=0294A +luruhar=02966 +mDDot=0223A +macr=000AF +male=02642 +malt=02720 +maltese=02720 +map=021A6 +mapsto=021A6 +mapstodown=021A7 +mapstoleft=021A4 +mapstoup=021A5 +marker=025AE +mcomma=02A29 +mcy=0043C +mdash=02014 +measuredangle=02221 +mfr=1D52A +mho=02127 +micro=000B5 +mid=02223 +midast=0002A +midcir=02AF0 +middot=000B7 +minus=02212 +minusb=0229F +minusd=02238 +minusdu=02A2A +mlcp=02ADB +mldr=02026 +mnplus=02213 +models=022A7 +mopf=1D55E +mp=02213 +mscr=1D4C2 +mstpos=0223E +mu=003BC +multimap=022B8 +mumap=022B8 +nLeftarrow=021CD +nLeftrightarrow=021CE +nRightarrow=021CF +nVDash=022AF +nVdash=022AE +nabla=02207 +nacute=00144 +nap=02249 +napos=00149 +napprox=02249 +natur=0266E +natural=0266E +naturals=02115 +nbsp=000A0 +ncap=02A43 +ncaron=00148 +ncedil=00146 +ncong=02247 +ncup=02A42 +ncy=0043D +ndash=02013 +ne=02260 +neArr=021D7 +nearhk=02924 +nearr=02197 +nearrow=02197 +nequiv=02262 +nesear=02928 +nexist=02204 +nexists=02204 +nfr=1D52B +nge=02271 +ngeq=02271 +ngsim=02275 +ngt=0226F +ngtr=0226F +nhArr=021CE +nharr=021AE +nhpar=02AF2 +ni=0220B +nis=022FC +nisd=022FA +niv=0220B +njcy=0045A +nlArr=021CD +nlarr=0219A +nldr=02025 +nle=02270 +nleftarrow=0219A +nleftrightarrow=021AE +nleq=02270 +nless=0226E +nlsim=02274 +nlt=0226E +nltri=022EA +nltrie=022EC +nmid=02224 +nopf=1D55F +not=000AC +notin=02209 +notinva=02209 +notinvb=022F7 +notinvc=022F6 +notni=0220C +notniva=0220C +notnivb=022FE +notnivc=022FD +npar=02226 +nparallel=02226 +npolint=02A14 +npr=02280 +nprcue=022E0 +nprec=02280 +nrArr=021CF +nrarr=0219B +nrightarrow=0219B +nrtri=022EB +nrtrie=022ED +nsc=02281 +nsccue=022E1 +nscr=1D4C3 +nshortmid=02224 +nshortparallel=02226 +nsim=02241 +nsime=02244 +nsimeq=02244 +nsmid=02224 +nspar=02226 +nsqsube=022E2 +nsqsupe=022E3 +nsub=02284 +nsube=02288 +nsubseteq=02288 +nsucc=02281 +nsup=02285 +nsupe=02289 +nsupseteq=02289 +ntgl=02279 +ntilde=000F1 +ntlg=02278 +ntriangleleft=022EA +ntrianglelefteq=022EC +ntriangleright=022EB +ntrianglerighteq=022ED +nu=003BD +num=00023 +numero=02116 +numsp=02007 +nvDash=022AD +nvHarr=02904 +nvdash=022AC +nvinfin=029DE +nvlArr=02902 +nvrArr=02903 +nwArr=021D6 +nwarhk=02923 +nwarr=02196 +nwarrow=02196 +nwnear=02927 +oS=024C8 +oacute=000F3 +oast=0229B +ocir=0229A +ocirc=000F4 +ocy=0043E +odash=0229D +odblac=00151 +odiv=02A38 +odot=02299 +odsold=029BC +oelig=00153 +ofcir=029BF +ofr=1D52C +ogon=002DB +ograve=000F2 +ogt=029C1 +ohbar=029B5 +ohm=003A9 +oint=0222E +olarr=021BA +olcir=029BE +olcross=029BB +oline=0203E +olt=029C0 +omacr=0014D +omega=003C9 +omicron=003BF +omid=029B6 +ominus=02296 +oopf=1D560 +opar=029B7 +operp=029B9 +oplus=02295 +or=02228 +orarr=021BB +ord=02A5D +order=02134 +orderof=02134 +ordf=000AA +ordm=000BA +origof=022B6 +oror=02A56 +orslope=02A57 +orv=02A5B +oscr=02134 +oslash=000F8 +osol=02298 +otilde=000F5 +otimes=02297 +otimesas=02A36 +ouml=000F6 +ovbar=0233D +par=02225 +para=000B6 +parallel=02225 +parsim=02AF3 +parsl=02AFD +part=02202 +pcy=0043F +percnt=00025 +period=0002E +permil=02030 +perp=022A5 +pertenk=02031 +pfr=1D52D +phi=003C6 +phiv=003D5 +phmmat=02133 +phone=0260E +pi=003C0 +pitchfork=022D4 +piv=003D6 +planck=0210F +planckh=0210E +plankv=0210F +plus=0002B +plusacir=02A23 +plusb=0229E +pluscir=02A22 +plusdo=02214 +plusdu=02A25 +pluse=02A72 +plusmn=000B1 +plussim=02A26 +plustwo=02A27 +pm=000B1 +pointint=02A15 +popf=1D561 +pound=000A3 +pr=0227A +prE=02AB3 +prap=02AB7 +prcue=0227C +pre=02AAF +prec=0227A +precapprox=02AB7 +preccurlyeq=0227C +preceq=02AAF +precnapprox=02AB9 +precneqq=02AB5 +precnsim=022E8 +precsim=0227E +prime=02032 +primes=02119 +prnE=02AB5 +prnap=02AB9 +prnsim=022E8 +prod=0220F +profalar=0232E +profline=02312 +profsurf=02313 +prop=0221D +propto=0221D +prsim=0227E +prurel=022B0 +pscr=1D4C5 +psi=003C8 +puncsp=02008 +qfr=1D52E +qint=02A0C +qopf=1D562 +qprime=02057 +qscr=1D4C6 +quaternions=0210D +quatint=02A16 +quest=0003F +questeq=0225F +quot=00022 +rAarr=021DB +rArr=021D2 +rAtail=0291C +rBarr=0290F +rHar=02964 +racute=00155 +radic=0221A +raemptyv=029B3 +rang=027E9 +rangd=02992 +range=029A5 +rangle=027E9 +raquo=000BB +rarr=02192 +rarrap=02975 +rarrb=021E5 +rarrbfs=02920 +rarrc=02933 +rarrfs=0291E +rarrhk=021AA +rarrlp=021AC +rarrpl=02945 +rarrsim=02974 +rarrtl=021A3 +rarrw=0219D +ratail=0291A +ratio=02236 +rationals=0211A +rbarr=0290D +rbbrk=02773 +rbrace=0007D +rbrack=0005D +rbrke=0298C +rbrksld=0298E +rbrkslu=02990 +rcaron=00159 +rcedil=00157 +rceil=02309 +rcub=0007D +rcy=00440 +rdca=02937 +rdldhar=02969 +rdquo=0201D +rdquor=0201D +rdsh=021B3 +real=0211C +realine=0211B +realpart=0211C +reals=0211D +rect=025AD +reg=000AE +rfisht=0297D +rfloor=0230B +rfr=1D52F +rhard=021C1 +rharu=021C0 +rharul=0296C +rho=003C1 +rhov=003F1 +rightarrow=02192 +rightarrowtail=021A3 +rightharpoondown=021C1 +rightharpoonup=021C0 +rightleftarrows=021C4 +rightleftharpoons=021CC +rightrightarrows=021C9 +rightsquigarrow=0219D +rightthreetimes=022CC +ring=002DA +risingdotseq=02253 +rlarr=021C4 +rlhar=021CC +rlm=0200F +rmoust=023B1 +rmoustache=023B1 +rnmid=02AEE +roang=027ED +roarr=021FE +robrk=027E7 +ropar=02986 +ropf=1D563 +roplus=02A2E +rotimes=02A35 +rpar=00029 +rpargt=02994 +rppolint=02A12 +rrarr=021C9 +rsaquo=0203A +rscr=1D4C7 +rsh=021B1 +rsqb=0005D +rsquo=02019 +rsquor=02019 +rthree=022CC +rtimes=022CA +rtri=025B9 +rtrie=022B5 +rtrif=025B8 +rtriltri=029CE +ruluhar=02968 +rx=0211E +sacute=0015B +sbquo=0201A +sc=0227B +scE=02AB4 +scap=02AB8 +scaron=00161 +sccue=0227D +sce=02AB0 +scedil=0015F +scirc=0015D +scnE=02AB6 +scnap=02ABA +scnsim=022E9 +scpolint=02A13 +scsim=0227F +scy=00441 +sdot=022C5 +sdotb=022A1 +sdote=02A66 +seArr=021D8 +searhk=02925 +searr=02198 +searrow=02198 +sect=000A7 +semi=0003B +seswar=02929 +setminus=02216 +setmn=02216 +sext=02736 +sfr=1D530 +sfrown=02322 +sharp=0266F +shchcy=00449 +shcy=00448 +shortmid=02223 +shortparallel=02225 +shy=000AD +sigma=003C3 +sigmaf=003C2 +sigmav=003C2 +sim=0223C +simdot=02A6A +sime=02243 +simeq=02243 +simg=02A9E +simgE=02AA0 +siml=02A9D +simlE=02A9F +simne=02246 +simplus=02A24 +simrarr=02972 +slarr=02190 +smallsetminus=02216 +smashp=02A33 +smeparsl=029E4 +smid=02223 +smile=02323 +smt=02AAA +smte=02AAC +softcy=0044C +sol=0002F +solb=029C4 +solbar=0233F +sopf=1D564 +spades=02660 +spadesuit=02660 +spar=02225 +sqcap=02293 +sqcup=02294 +sqsub=0228F +sqsube=02291 +sqsubset=0228F +sqsubseteq=02291 +sqsup=02290 +sqsupe=02292 +sqsupset=02290 +sqsupseteq=02292 +squ=025A1 +square=025A1 +squarf=025AA +squf=025AA +srarr=02192 +sscr=1D4C8 +ssetmn=02216 +ssmile=02323 +sstarf=022C6 +star=02606 +starf=02605 +straightepsilon=003F5 +straightphi=003D5 +strns=000AF +sub=02282 +subE=02AC5 +subdot=02ABD +sube=02286 +subedot=02AC3 +submult=02AC1 +subnE=02ACB +subne=0228A +subplus=02ABF +subrarr=02979 +subset=02282 +subseteq=02286 +subseteqq=02AC5 +subsetneq=0228A +subsetneqq=02ACB +subsim=02AC7 +subsub=02AD5 +subsup=02AD3 +succ=0227B +succapprox=02AB8 +succcurlyeq=0227D +succeq=02AB0 +succnapprox=02ABA +succneqq=02AB6 +succnsim=022E9 +succsim=0227F +sum=02211 +sung=0266A +sup1=000B9 +sup2=000B2 +sup3=000B3 +sup=02283 +supE=02AC6 +supdot=02ABE +supdsub=02AD8 +supe=02287 +supedot=02AC4 +suphsol=027C9 +suphsub=02AD7 +suplarr=0297B +supmult=02AC2 +supnE=02ACC +supne=0228B +supplus=02AC0 +supset=02283 +supseteq=02287 +supseteqq=02AC6 +supsetneq=0228B +supsetneqq=02ACC +supsim=02AC8 +supsub=02AD4 +supsup=02AD6 +swArr=021D9 +swarhk=02926 +swarr=02199 +swarrow=02199 +swnwar=0292A +szlig=000DF +target=02316 +tau=003C4 +tbrk=023B4 +tcaron=00165 +tcedil=00163 +tcy=00442 +tdot=020DB +telrec=02315 +tfr=1D531 +there4=02234 +therefore=02234 +theta=003B8 +thetasym=003D1 +thetav=003D1 +thickapprox=02248 +thicksim=0223C +thinsp=02009 +thkap=02248 +thksim=0223C +thorn=000FE +tilde=002DC +times=000D7 +timesb=022A0 +timesbar=02A31 +timesd=02A30 +tint=0222D +toea=02928 +top=022A4 +topbot=02336 +topcir=02AF1 +topf=1D565 +topfork=02ADA +tosa=02929 +tprime=02034 +trade=02122 +triangle=025B5 +triangledown=025BF +triangleleft=025C3 +trianglelefteq=022B4 +triangleq=0225C +triangleright=025B9 +trianglerighteq=022B5 +tridot=025EC +trie=0225C +triminus=02A3A +triplus=02A39 +trisb=029CD +tritime=02A3B +trpezium=023E2 +tscr=1D4C9 +tscy=00446 +tshcy=0045B +tstrok=00167 +twixt=0226C +twoheadleftarrow=0219E +twoheadrightarrow=021A0 +uArr=021D1 +uHar=02963 +uacute=000FA +uarr=02191 +ubrcy=0045E +ubreve=0016D +ucirc=000FB +ucy=00443 +udarr=021C5 +udblac=00171 +udhar=0296E +ufisht=0297E +ufr=1D532 +ugrave=000F9 +uharl=021BF +uharr=021BE +uhblk=02580 +ulcorn=0231C +ulcorner=0231C +ulcrop=0230F +ultri=025F8 +umacr=0016B +uml=000A8 +uogon=00173 +uopf=1D566 +uparrow=02191 +updownarrow=02195 +upharpoonleft=021BF +upharpoonright=021BE +uplus=0228E +upsi=003C5 +upsih=003D2 +upsilon=003C5 +upuparrows=021C8 +urcorn=0231D +urcorner=0231D +urcrop=0230E +uring=0016F +urtri=025F9 +uscr=1D4CA +utdot=022F0 +utilde=00169 +utri=025B5 +utrif=025B4 +uuarr=021C8 +uuml=000FC +uwangle=029A7 +vArr=021D5 +vBar=02AE8 +vBarv=02AE9 +vDash=022A8 +vangrt=0299C +varepsilon=003F5 +varkappa=003F0 +varnothing=02205 +varphi=003D5 +varpi=003D6 +varpropto=0221D +varr=02195 +varrho=003F1 +varsigma=003C2 +vartheta=003D1 +vartriangleleft=022B2 +vartriangleright=022B3 +vcy=00432 +vdash=022A2 +vee=02228 +veebar=022BB +veeeq=0225A +vellip=022EE +verbar=0007C +vert=0007C +vfr=1D533 +vltri=022B2 +vopf=1D567 +vprop=0221D +vrtri=022B3 +vscr=1D4CB +vzigzag=0299A +wcirc=00175 +wedbar=02A5F +wedge=02227 +wedgeq=02259 +weierp=02118 +wfr=1D534 +wopf=1D568 +wp=02118 +wr=02240 +wreath=02240 +wscr=1D4CC +xcap=022C2 +xcirc=025EF +xcup=022C3 +xdtri=025BD +xfr=1D535 +xhArr=027FA +xharr=027F7 +xi=003BE +xlArr=027F8 +xlarr=027F5 +xmap=027FC +xnis=022FB +xodot=02A00 +xopf=1D569 +xoplus=02A01 +xotime=02A02 +xrArr=027F9 +xrarr=027F6 +xscr=1D4CD +xsqcup=02A06 +xuplus=02A04 +xutri=025B3 +xvee=022C1 +xwedge=022C0 +yacute=000FD +yacy=0044F +ycirc=00177 +ycy=0044B +yen=000A5 +yfr=1D536 +yicy=00457 +yopf=1D56A +yscr=1D4CE +yucy=0044E +yuml=000FF +zacute=0017A +zcaron=0017E +zcy=00437 +zdot=0017C +zeetrf=02128 +zeta=003B6 +zfr=1D537 +zhcy=00436 +zigrarr=021DD +zopf=1D56B +zscr=1D4CF +zwj=0200D +zwnj=0200C diff --git a/server/src/org/jsoup/nodes/package-info.java b/server/src/org/jsoup/nodes/package-info.java new file mode 100644 index 0000000000..24b12803ff --- /dev/null +++ b/server/src/org/jsoup/nodes/package-info.java @@ -0,0 +1,4 @@ +/** + HTML document structure nodes. + */ +package org.jsoup.nodes;
\ No newline at end of file diff --git a/server/src/org/jsoup/package-info.java b/server/src/org/jsoup/package-info.java new file mode 100644 index 0000000000..49526116b4 --- /dev/null +++ b/server/src/org/jsoup/package-info.java @@ -0,0 +1,4 @@ +/** + Contains the main {@link org.jsoup.Jsoup} class, which provides convenient static access to the jsoup functionality. + */ +package org.jsoup;
\ No newline at end of file diff --git a/server/src/org/jsoup/parser/CharacterReader.java b/server/src/org/jsoup/parser/CharacterReader.java new file mode 100644 index 0000000000..b549a571a0 --- /dev/null +++ b/server/src/org/jsoup/parser/CharacterReader.java @@ -0,0 +1,230 @@ +package org.jsoup.parser; + +import org.jsoup.helper.Validate; + +/** + CharacterReader consumes tokens off a string. To replace the old TokenQueue. + */ +class CharacterReader { + static final char EOF = (char) -1; + + private final String input; + private final int length; + private int pos = 0; + private int mark = 0; + + CharacterReader(String input) { + Validate.notNull(input); + input = input.replaceAll("\r\n?", "\n"); // normalise carriage returns to newlines + + this.input = input; + this.length = input.length(); + } + + int pos() { + return pos; + } + + boolean isEmpty() { + return pos >= length; + } + + char current() { + return isEmpty() ? EOF : input.charAt(pos); + } + + char consume() { + char val = isEmpty() ? EOF : input.charAt(pos); + pos++; + return val; + } + + void unconsume() { + pos--; + } + + void advance() { + pos++; + } + + void mark() { + mark = pos; + } + + void rewindToMark() { + pos = mark; + } + + String consumeAsString() { + return input.substring(pos, pos++); + } + + String consumeTo(char c) { + int offset = input.indexOf(c, pos); + if (offset != -1) { + String consumed = input.substring(pos, offset); + pos += consumed.length(); + return consumed; + } else { + return consumeToEnd(); + } + } + + String consumeTo(String seq) { + int offset = input.indexOf(seq, pos); + if (offset != -1) { + String consumed = input.substring(pos, offset); + pos += consumed.length(); + return consumed; + } else { + return consumeToEnd(); + } + } + + String consumeToAny(char... seq) { + int start = pos; + + OUTER: while (!isEmpty()) { + char c = input.charAt(pos); + for (char seek : seq) { + if (seek == c) + break OUTER; + } + pos++; + } + + return pos > start ? input.substring(start, pos) : ""; + } + + String consumeToEnd() { + String data = input.substring(pos, input.length()); + pos = input.length(); + return data; + } + + String consumeLetterSequence() { + int start = pos; + while (!isEmpty()) { + char c = input.charAt(pos); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + pos++; + else + break; + } + + return input.substring(start, pos); + } + + String consumeLetterThenDigitSequence() { + int start = pos; + while (!isEmpty()) { + char c = input.charAt(pos); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + pos++; + else + break; + } + while (!isEmpty()) { + char c = input.charAt(pos); + if (c >= '0' && c <= '9') + pos++; + else + break; + } + + return input.substring(start, pos); + } + + String consumeHexSequence() { + int start = pos; + while (!isEmpty()) { + char c = input.charAt(pos); + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + pos++; + else + break; + } + return input.substring(start, pos); + } + + String consumeDigitSequence() { + int start = pos; + while (!isEmpty()) { + char c = input.charAt(pos); + if (c >= '0' && c <= '9') + pos++; + else + break; + } + return input.substring(start, pos); + } + + boolean matches(char c) { + return !isEmpty() && input.charAt(pos) == c; + + } + + boolean matches(String seq) { + return input.startsWith(seq, pos); + } + + boolean matchesIgnoreCase(String seq) { + return input.regionMatches(true, pos, seq, 0, seq.length()); + } + + boolean matchesAny(char... seq) { + if (isEmpty()) + return false; + + char c = input.charAt(pos); + for (char seek : seq) { + if (seek == c) + return true; + } + return false; + } + + boolean matchesLetter() { + if (isEmpty()) + return false; + char c = input.charAt(pos); + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + boolean matchesDigit() { + if (isEmpty()) + return false; + char c = input.charAt(pos); + return (c >= '0' && c <= '9'); + } + + boolean matchConsume(String seq) { + if (matches(seq)) { + pos += seq.length(); + return true; + } else { + return false; + } + } + + boolean matchConsumeIgnoreCase(String seq) { + if (matchesIgnoreCase(seq)) { + pos += seq.length(); + return true; + } else { + return false; + } + } + + boolean containsIgnoreCase(String seq) { + // used to check presence of </title>, </style>. only finds consistent case. + String loScan = seq.toLowerCase(); + String hiScan = seq.toUpperCase(); + return (input.indexOf(loScan, pos) > -1) || (input.indexOf(hiScan, pos) > -1); + } + + @Override + public String toString() { + return input.substring(pos); + } +} diff --git a/server/src/org/jsoup/parser/HtmlTreeBuilder.java b/server/src/org/jsoup/parser/HtmlTreeBuilder.java new file mode 100644 index 0000000000..457a4c3249 --- /dev/null +++ b/server/src/org/jsoup/parser/HtmlTreeBuilder.java @@ -0,0 +1,672 @@ +package org.jsoup.parser; + +import org.jsoup.helper.DescendableLinkedList; +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.nodes.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * HTML Tree Builder; creates a DOM from Tokens. + */ +class HtmlTreeBuilder extends TreeBuilder { + + private HtmlTreeBuilderState state; // the current state + private HtmlTreeBuilderState originalState; // original / marked state + + private boolean baseUriSetFromDoc = false; + private Element headElement; // the current head element + private Element formElement; // the current form element + private Element contextElement; // fragment parse context -- could be null even if fragment parsing + private DescendableLinkedList<Element> formattingElements = new DescendableLinkedList<Element>(); // active (open) formatting elements + private List<Token.Character> pendingTableCharacters = new ArrayList<Token.Character>(); // chars in table to be shifted out + + private boolean framesetOk = true; // if ok to go into frameset + private boolean fosterInserts = false; // if next inserts should be fostered + private boolean fragmentParsing = false; // if parsing a fragment of html + + HtmlTreeBuilder() {} + + @Override + Document parse(String input, String baseUri, ParseErrorList errors) { + state = HtmlTreeBuilderState.Initial; + return super.parse(input, baseUri, errors); + } + + List<Node> parseFragment(String inputFragment, Element context, String baseUri, ParseErrorList errors) { + // context may be null + state = HtmlTreeBuilderState.Initial; + initialiseParse(inputFragment, baseUri, errors); + contextElement = context; + fragmentParsing = true; + Element root = null; + + if (context != null) { + if (context.ownerDocument() != null) // quirks setup: + doc.quirksMode(context.ownerDocument().quirksMode()); + + // initialise the tokeniser state: + String contextTag = context.tagName(); + if (StringUtil.in(contextTag, "title", "textarea")) + tokeniser.transition(TokeniserState.Rcdata); + else if (StringUtil.in(contextTag, "iframe", "noembed", "noframes", "style", "xmp")) + tokeniser.transition(TokeniserState.Rawtext); + else if (contextTag.equals("script")) + tokeniser.transition(TokeniserState.ScriptData); + else if (contextTag.equals(("noscript"))) + tokeniser.transition(TokeniserState.Data); // if scripting enabled, rawtext + else if (contextTag.equals("plaintext")) + tokeniser.transition(TokeniserState.Data); + else + tokeniser.transition(TokeniserState.Data); // default + + root = new Element(Tag.valueOf("html"), baseUri); + doc.appendChild(root); + stack.push(root); + resetInsertionMode(); + // todo: setup form element to nearest form on context (up ancestor chain) + } + + runParser(); + if (context != null) + return root.childNodes(); + else + return doc.childNodes(); + } + + @Override + protected boolean process(Token token) { + currentToken = token; + return this.state.process(token, this); + } + + boolean process(Token token, HtmlTreeBuilderState state) { + currentToken = token; + return state.process(token, this); + } + + void transition(HtmlTreeBuilderState state) { + this.state = state; + } + + HtmlTreeBuilderState state() { + return state; + } + + void markInsertionMode() { + originalState = state; + } + + HtmlTreeBuilderState originalState() { + return originalState; + } + + void framesetOk(boolean framesetOk) { + this.framesetOk = framesetOk; + } + + boolean framesetOk() { + return framesetOk; + } + + Document getDocument() { + return doc; + } + + String getBaseUri() { + return baseUri; + } + + void maybeSetBaseUri(Element base) { + if (baseUriSetFromDoc) // only listen to the first <base href> in parse + return; + + String href = base.absUrl("href"); + if (href.length() != 0) { // ignore <base target> etc + baseUri = href; + baseUriSetFromDoc = true; + doc.setBaseUri(href); // set on the doc so doc.createElement(Tag) will get updated base, and to update all descendants + } + } + + boolean isFragmentParsing() { + return fragmentParsing; + } + + void error(HtmlTreeBuilderState state) { + if (errors.canAddError()) + errors.add(new ParseError(reader.pos(), "Unexpected token [%s] when in state [%s]", currentToken.tokenType(), state)); + } + + Element insert(Token.StartTag startTag) { + // handle empty unknown tags + // when the spec expects an empty tag, will directly hit insertEmpty, so won't generate fake end tag. + if (startTag.isSelfClosing() && !Tag.isKnownTag(startTag.name())) { + Element el = insertEmpty(startTag); + process(new Token.EndTag(el.tagName())); // ensure we get out of whatever state we are in + return el; + } + + Element el = new Element(Tag.valueOf(startTag.name()), baseUri, startTag.attributes); + insert(el); + return el; + } + + Element insert(String startTagName) { + Element el = new Element(Tag.valueOf(startTagName), baseUri); + insert(el); + return el; + } + + void insert(Element el) { + insertNode(el); + stack.add(el); + } + + Element insertEmpty(Token.StartTag startTag) { + Tag tag = Tag.valueOf(startTag.name()); + Element el = new Element(tag, baseUri, startTag.attributes); + insertNode(el); + if (startTag.isSelfClosing()) { + tokeniser.acknowledgeSelfClosingFlag(); + if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output + tag.setSelfClosing(); + } + return el; + } + + void insert(Token.Comment commentToken) { + Comment comment = new Comment(commentToken.getData(), baseUri); + insertNode(comment); + } + + void insert(Token.Character characterToken) { + Node node; + // characters in script and style go in as datanodes, not text nodes + if (StringUtil.in(currentElement().tagName(), "script", "style")) + node = new DataNode(characterToken.getData(), baseUri); + else + node = new TextNode(characterToken.getData(), baseUri); + currentElement().appendChild(node); // doesn't use insertNode, because we don't foster these; and will always have a stack. + } + + private void insertNode(Node node) { + // if the stack hasn't been set up yet, elements (doctype, comments) go into the doc + if (stack.size() == 0) + doc.appendChild(node); + else if (isFosterInserts()) + insertInFosterParent(node); + else + currentElement().appendChild(node); + } + + Element pop() { + // todo - dev, remove validation check + if (stack.peekLast().nodeName().equals("td") && !state.name().equals("InCell")) + Validate.isFalse(true, "pop td not in cell"); + if (stack.peekLast().nodeName().equals("html")) + Validate.isFalse(true, "popping html!"); + return stack.pollLast(); + } + + void push(Element element) { + stack.add(element); + } + + DescendableLinkedList<Element> getStack() { + return stack; + } + + boolean onStack(Element el) { + return isElementInQueue(stack, el); + } + + private boolean isElementInQueue(DescendableLinkedList<Element> queue, Element element) { + Iterator<Element> it = queue.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next == element) { + return true; + } + } + return false; + } + + Element getFromStack(String elName) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next.nodeName().equals(elName)) { + return next; + } + } + return null; + } + + boolean removeFromStack(Element el) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next == el) { + it.remove(); + return true; + } + } + return false; + } + + void popStackToClose(String elName) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next.nodeName().equals(elName)) { + it.remove(); + break; + } else { + it.remove(); + } + } + } + + void popStackToClose(String... elNames) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (StringUtil.in(next.nodeName(), elNames)) { + it.remove(); + break; + } else { + it.remove(); + } + } + } + + void popStackToBefore(String elName) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next.nodeName().equals(elName)) { + break; + } else { + it.remove(); + } + } + } + + void clearStackToTableContext() { + clearStackToContext("table"); + } + + void clearStackToTableBodyContext() { + clearStackToContext("tbody", "tfoot", "thead"); + } + + void clearStackToTableRowContext() { + clearStackToContext("tr"); + } + + private void clearStackToContext(String... nodeNames) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (StringUtil.in(next.nodeName(), nodeNames) || next.nodeName().equals("html")) + break; + else + it.remove(); + } + } + + Element aboveOnStack(Element el) { + assert onStack(el); + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next == el) { + return it.next(); + } + } + return null; + } + + void insertOnStackAfter(Element after, Element in) { + int i = stack.lastIndexOf(after); + Validate.isTrue(i != -1); + stack.add(i+1, in); + } + + void replaceOnStack(Element out, Element in) { + replaceInQueue(stack, out, in); + } + + private void replaceInQueue(LinkedList<Element> queue, Element out, Element in) { + int i = queue.lastIndexOf(out); + Validate.isTrue(i != -1); + queue.remove(i); + queue.add(i, in); + } + + void resetInsertionMode() { + boolean last = false; + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element node = it.next(); + if (!it.hasNext()) { + last = true; + node = contextElement; + } + String name = node.nodeName(); + if ("select".equals(name)) { + transition(HtmlTreeBuilderState.InSelect); + break; // frag + } else if (("td".equals(name) || "td".equals(name) && !last)) { + transition(HtmlTreeBuilderState.InCell); + break; + } else if ("tr".equals(name)) { + transition(HtmlTreeBuilderState.InRow); + break; + } else if ("tbody".equals(name) || "thead".equals(name) || "tfoot".equals(name)) { + transition(HtmlTreeBuilderState.InTableBody); + break; + } else if ("caption".equals(name)) { + transition(HtmlTreeBuilderState.InCaption); + break; + } else if ("colgroup".equals(name)) { + transition(HtmlTreeBuilderState.InColumnGroup); + break; // frag + } else if ("table".equals(name)) { + transition(HtmlTreeBuilderState.InTable); + break; + } else if ("head".equals(name)) { + transition(HtmlTreeBuilderState.InBody); + break; // frag + } else if ("body".equals(name)) { + transition(HtmlTreeBuilderState.InBody); + break; + } else if ("frameset".equals(name)) { + transition(HtmlTreeBuilderState.InFrameset); + break; // frag + } else if ("html".equals(name)) { + transition(HtmlTreeBuilderState.BeforeHead); + break; // frag + } else if (last) { + transition(HtmlTreeBuilderState.InBody); + break; // frag + } + } + } + + // todo: tidy up in specific scope methods + private boolean inSpecificScope(String targetName, String[] baseTypes, String[] extraTypes) { + return inSpecificScope(new String[]{targetName}, baseTypes, extraTypes); + } + + private boolean inSpecificScope(String[] targetNames, String[] baseTypes, String[] extraTypes) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element el = it.next(); + String elName = el.nodeName(); + if (StringUtil.in(elName, targetNames)) + return true; + if (StringUtil.in(elName, baseTypes)) + return false; + if (extraTypes != null && StringUtil.in(elName, extraTypes)) + return false; + } + Validate.fail("Should not be reachable"); + return false; + } + + boolean inScope(String[] targetNames) { + return inSpecificScope(targetNames, new String[]{"applet", "caption", "html", "table", "td", "th", "marquee", "object"}, null); + } + + boolean inScope(String targetName) { + return inScope(targetName, null); + } + + boolean inScope(String targetName, String[] extras) { + return inSpecificScope(targetName, new String[]{"applet", "caption", "html", "table", "td", "th", "marquee", "object"}, extras); + // todo: in mathml namespace: mi, mo, mn, ms, mtext annotation-xml + // todo: in svg namespace: forignOjbect, desc, title + } + + boolean inListItemScope(String targetName) { + return inScope(targetName, new String[]{"ol", "ul"}); + } + + boolean inButtonScope(String targetName) { + return inScope(targetName, new String[]{"button"}); + } + + boolean inTableScope(String targetName) { + return inSpecificScope(targetName, new String[]{"html", "table"}, null); + } + + boolean inSelectScope(String targetName) { + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element el = it.next(); + String elName = el.nodeName(); + if (elName.equals(targetName)) + return true; + if (!StringUtil.in(elName, "optgroup", "option")) // all elements except + return false; + } + Validate.fail("Should not be reachable"); + return false; + } + + void setHeadElement(Element headElement) { + this.headElement = headElement; + } + + Element getHeadElement() { + return headElement; + } + + boolean isFosterInserts() { + return fosterInserts; + } + + void setFosterInserts(boolean fosterInserts) { + this.fosterInserts = fosterInserts; + } + + Element getFormElement() { + return formElement; + } + + void setFormElement(Element formElement) { + this.formElement = formElement; + } + + void newPendingTableCharacters() { + pendingTableCharacters = new ArrayList<Token.Character>(); + } + + List<Token.Character> getPendingTableCharacters() { + return pendingTableCharacters; + } + + void setPendingTableCharacters(List<Token.Character> pendingTableCharacters) { + this.pendingTableCharacters = pendingTableCharacters; + } + + /** + 11.2.5.2 Closing elements that have implied end tags<p/> + When the steps below require the UA to generate implied end tags, then, while the current node is a dd element, a + dt element, an li element, an option element, an optgroup element, a p element, an rp element, or an rt element, + the UA must pop the current node off the stack of open elements. + + @param excludeTag If a step requires the UA to generate implied end tags but lists an element to exclude from the + process, then the UA must perform the above steps as if that element was not in the above list. + */ + void generateImpliedEndTags(String excludeTag) { + while ((excludeTag != null && !currentElement().nodeName().equals(excludeTag)) && + StringUtil.in(currentElement().nodeName(), "dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) + pop(); + } + + void generateImpliedEndTags() { + generateImpliedEndTags(null); + } + + boolean isSpecial(Element el) { + // todo: mathml's mi, mo, mn + // todo: svg's foreigObject, desc, title + String name = el.nodeName(); + return StringUtil.in(name, "address", "applet", "area", "article", "aside", "base", "basefont", "bgsound", + "blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "command", "dd", + "details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form", + "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", + "iframe", "img", "input", "isindex", "li", "link", "listing", "marquee", "menu", "meta", "nav", + "noembed", "noframes", "noscript", "object", "ol", "p", "param", "plaintext", "pre", "script", + "section", "select", "style", "summary", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", + "title", "tr", "ul", "wbr", "xmp"); + } + + // active formatting elements + void pushActiveFormattingElements(Element in) { + int numSeen = 0; + Iterator<Element> iter = formattingElements.descendingIterator(); + while (iter.hasNext()) { + Element el = iter.next(); + if (el == null) // marker + break; + + if (isSameFormattingElement(in, el)) + numSeen++; + + if (numSeen == 3) { + iter.remove(); + break; + } + } + formattingElements.add(in); + } + + private boolean isSameFormattingElement(Element a, Element b) { + // same if: same namespace, tag, and attributes. Element.equals only checks tag, might in future check children + return a.nodeName().equals(b.nodeName()) && + // a.namespace().equals(b.namespace()) && + a.attributes().equals(b.attributes()); + // todo: namespaces + } + + void reconstructFormattingElements() { + int size = formattingElements.size(); + if (size == 0 || formattingElements.getLast() == null || onStack(formattingElements.getLast())) + return; + + Element entry = formattingElements.getLast(); + int pos = size - 1; + boolean skip = false; + while (true) { + if (pos == 0) { // step 4. if none before, skip to 8 + skip = true; + break; + } + entry = formattingElements.get(--pos); // step 5. one earlier than entry + if (entry == null || onStack(entry)) // step 6 - neither marker nor on stack + break; // jump to 8, else continue back to 4 + } + while(true) { + if (!skip) // step 7: on later than entry + entry = formattingElements.get(++pos); + Validate.notNull(entry); // should not occur, as we break at last element + + // 8. create new element from element, 9 insert into current node, onto stack + skip = false; // can only skip increment from 4. + Element newEl = insert(entry.nodeName()); // todo: avoid fostering here? + // newEl.namespace(entry.namespace()); // todo: namespaces + newEl.attributes().addAll(entry.attributes()); + + // 10. replace entry with new entry + formattingElements.add(pos, newEl); + formattingElements.remove(pos + 1); + + // 11 + if (pos == size-1) // if not last entry in list, jump to 7 + break; + } + } + + void clearFormattingElementsToLastMarker() { + while (!formattingElements.isEmpty()) { + Element el = formattingElements.peekLast(); + formattingElements.removeLast(); + if (el == null) + break; + } + } + + void removeFromActiveFormattingElements(Element el) { + Iterator<Element> it = formattingElements.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next == el) { + it.remove(); + break; + } + } + } + + boolean isInActiveFormattingElements(Element el) { + return isElementInQueue(formattingElements, el); + } + + Element getActiveFormattingElement(String nodeName) { + Iterator<Element> it = formattingElements.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next == null) // scope marker + break; + else if (next.nodeName().equals(nodeName)) + return next; + } + return null; + } + + void replaceActiveFormattingElement(Element out, Element in) { + replaceInQueue(formattingElements, out, in); + } + + void insertMarkerToFormattingElements() { + formattingElements.add(null); + } + + void insertInFosterParent(Node in) { + Element fosterParent = null; + Element lastTable = getFromStack("table"); + boolean isLastTableParent = false; + if (lastTable != null) { + if (lastTable.parent() != null) { + fosterParent = lastTable.parent(); + isLastTableParent = true; + } else + fosterParent = aboveOnStack(lastTable); + } else { // no table == frag + fosterParent = stack.get(0); + } + + if (isLastTableParent) { + Validate.notNull(lastTable); // last table cannot be null by this point. + lastTable.before(in); + } + else + fosterParent.appendChild(in); + } + + @Override + public String toString() { + return "TreeBuilder{" + + "currentToken=" + currentToken + + ", state=" + state + + ", currentElement=" + currentElement() + + '}'; + } +} diff --git a/server/src/org/jsoup/parser/HtmlTreeBuilderState.java b/server/src/org/jsoup/parser/HtmlTreeBuilderState.java new file mode 100644 index 0000000000..ceab9faa5a --- /dev/null +++ b/server/src/org/jsoup/parser/HtmlTreeBuilderState.java @@ -0,0 +1,1482 @@ +package org.jsoup.parser; + +import org.jsoup.helper.DescendableLinkedList; +import org.jsoup.helper.StringUtil; +import org.jsoup.nodes.*; + +import java.util.Iterator; +import java.util.LinkedList; + +/** + * The Tree Builder's current state. Each state embodies the processing for the state, and transitions to other states. + */ +enum HtmlTreeBuilderState { + Initial { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + return true; // ignore whitespace + } else if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype()) { + // todo: parse error check on expected doctypes + // todo: quirk state check on doctype ids + Token.Doctype d = t.asDoctype(); + DocumentType doctype = new DocumentType(d.getName(), d.getPublicIdentifier(), d.getSystemIdentifier(), tb.getBaseUri()); + tb.getDocument().appendChild(doctype); + if (d.isForceQuirks()) + tb.getDocument().quirksMode(Document.QuirksMode.quirks); + tb.transition(BeforeHtml); + } else { + // todo: check not iframe srcdoc + tb.transition(BeforeHtml); + return tb.process(t); // re-process token + } + return true; + } + }, + BeforeHtml { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isDoctype()) { + tb.error(this); + return false; + } else if (t.isComment()) { + tb.insert(t.asComment()); + } else if (isWhitespace(t)) { + return true; // ignore whitespace + } else if (t.isStartTag() && t.asStartTag().name().equals("html")) { + tb.insert(t.asStartTag()); + tb.transition(BeforeHead); + } else if (t.isEndTag() && (StringUtil.in(t.asEndTag().name(), "head", "body", "html", "br"))) { + return anythingElse(t, tb); + } else if (t.isEndTag()) { + tb.error(this); + return false; + } else { + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + tb.insert("html"); + tb.transition(BeforeHead); + return tb.process(t); + } + }, + BeforeHead { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + return true; + } else if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype()) { + tb.error(this); + return false; + } else if (t.isStartTag() && t.asStartTag().name().equals("html")) { + return InBody.process(t, tb); // does not transition + } else if (t.isStartTag() && t.asStartTag().name().equals("head")) { + Element head = tb.insert(t.asStartTag()); + tb.setHeadElement(head); + tb.transition(InHead); + } else if (t.isEndTag() && (StringUtil.in(t.asEndTag().name(), "head", "body", "html", "br"))) { + tb.process(new Token.StartTag("head")); + return tb.process(t); + } else if (t.isEndTag()) { + tb.error(this); + return false; + } else { + tb.process(new Token.StartTag("head")); + return tb.process(t); + } + return true; + } + }, + InHead { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + tb.insert(t.asCharacter()); + return true; + } + switch (t.type) { + case Comment: + tb.insert(t.asComment()); + break; + case Doctype: + tb.error(this); + return false; + case StartTag: + Token.StartTag start = t.asStartTag(); + String name = start.name(); + if (name.equals("html")) { + return InBody.process(t, tb); + } else if (StringUtil.in(name, "base", "basefont", "bgsound", "command", "link")) { + Element el = tb.insertEmpty(start); + // jsoup special: update base the frist time it is seen + if (name.equals("base") && el.hasAttr("href")) + tb.maybeSetBaseUri(el); + } else if (name.equals("meta")) { + Element meta = tb.insertEmpty(start); + // todo: charset switches + } else if (name.equals("title")) { + handleRcData(start, tb); + } else if (StringUtil.in(name, "noframes", "style")) { + handleRawtext(start, tb); + } else if (name.equals("noscript")) { + // else if noscript && scripting flag = true: rawtext (jsoup doesn't run script, to handle as noscript) + tb.insert(start); + tb.transition(InHeadNoscript); + } else if (name.equals("script")) { + // skips some script rules as won't execute them + tb.insert(start); + tb.tokeniser.transition(TokeniserState.ScriptData); + tb.markInsertionMode(); + tb.transition(Text); + } else if (name.equals("head")) { + tb.error(this); + return false; + } else { + return anythingElse(t, tb); + } + break; + case EndTag: + Token.EndTag end = t.asEndTag(); + name = end.name(); + if (name.equals("head")) { + tb.pop(); + tb.transition(AfterHead); + } else if (StringUtil.in(name, "body", "html", "br")) { + return anythingElse(t, tb); + } else { + tb.error(this); + return false; + } + break; + default: + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, TreeBuilder tb) { + tb.process(new Token.EndTag("head")); + return tb.process(t); + } + }, + InHeadNoscript { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isDoctype()) { + tb.error(this); + } else if (t.isStartTag() && t.asStartTag().name().equals("html")) { + return tb.process(t, InBody); + } else if (t.isEndTag() && t.asEndTag().name().equals("noscript")) { + tb.pop(); + tb.transition(InHead); + } else if (isWhitespace(t) || t.isComment() || (t.isStartTag() && StringUtil.in(t.asStartTag().name(), + "basefont", "bgsound", "link", "meta", "noframes", "style"))) { + return tb.process(t, InHead); + } else if (t.isEndTag() && t.asEndTag().name().equals("br")) { + return anythingElse(t, tb); + } else if ((t.isStartTag() && StringUtil.in(t.asStartTag().name(), "head", "noscript")) || t.isEndTag()) { + tb.error(this); + return false; + } else { + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + tb.error(this); + tb.process(new Token.EndTag("noscript")); + return tb.process(t); + } + }, + AfterHead { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + tb.insert(t.asCharacter()); + } else if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype()) { + tb.error(this); + } else if (t.isStartTag()) { + Token.StartTag startTag = t.asStartTag(); + String name = startTag.name(); + if (name.equals("html")) { + return tb.process(t, InBody); + } else if (name.equals("body")) { + tb.insert(startTag); + tb.framesetOk(false); + tb.transition(InBody); + } else if (name.equals("frameset")) { + tb.insert(startTag); + tb.transition(InFrameset); + } else if (StringUtil.in(name, "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title")) { + tb.error(this); + Element head = tb.getHeadElement(); + tb.push(head); + tb.process(t, InHead); + tb.removeFromStack(head); + } else if (name.equals("head")) { + tb.error(this); + return false; + } else { + anythingElse(t, tb); + } + } else if (t.isEndTag()) { + if (StringUtil.in(t.asEndTag().name(), "body", "html")) { + anythingElse(t, tb); + } else { + tb.error(this); + return false; + } + } else { + anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + tb.process(new Token.StartTag("body")); + tb.framesetOk(true); + return tb.process(t); + } + }, + InBody { + boolean process(Token t, HtmlTreeBuilder tb) { + switch (t.type) { + case Character: { + Token.Character c = t.asCharacter(); + if (c.getData().equals(nullString)) { + // todo confirm that check + tb.error(this); + return false; + } else if (isWhitespace(c)) { + tb.reconstructFormattingElements(); + tb.insert(c); + } else { + tb.reconstructFormattingElements(); + tb.insert(c); + tb.framesetOk(false); + } + break; + } + case Comment: { + tb.insert(t.asComment()); + break; + } + case Doctype: { + tb.error(this); + return false; + } + case StartTag: + Token.StartTag startTag = t.asStartTag(); + String name = startTag.name(); + if (name.equals("html")) { + tb.error(this); + // merge attributes onto real html + Element html = tb.getStack().getFirst(); + for (Attribute attribute : startTag.getAttributes()) { + if (!html.hasAttr(attribute.getKey())) + html.attributes().put(attribute); + } + } else if (StringUtil.in(name, "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title")) { + return tb.process(t, InHead); + } else if (name.equals("body")) { + tb.error(this); + LinkedList<Element> stack = tb.getStack(); + if (stack.size() == 1 || (stack.size() > 2 && !stack.get(1).nodeName().equals("body"))) { + // only in fragment case + return false; // ignore + } else { + tb.framesetOk(false); + Element body = stack.get(1); + for (Attribute attribute : startTag.getAttributes()) { + if (!body.hasAttr(attribute.getKey())) + body.attributes().put(attribute); + } + } + } else if (name.equals("frameset")) { + tb.error(this); + LinkedList<Element> stack = tb.getStack(); + if (stack.size() == 1 || (stack.size() > 2 && !stack.get(1).nodeName().equals("body"))) { + // only in fragment case + return false; // ignore + } else if (!tb.framesetOk()) { + return false; // ignore frameset + } else { + Element second = stack.get(1); + if (second.parent() != null) + second.remove(); + // pop up to html element + while (stack.size() > 1) + stack.removeLast(); + tb.insert(startTag); + tb.transition(InFrameset); + } + } else if (StringUtil.in(name, + "address", "article", "aside", "blockquote", "center", "details", "dir", "div", "dl", + "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu", "nav", "ol", + "p", "section", "summary", "ul")) { + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insert(startTag); + } else if (StringUtil.in(name, "h1", "h2", "h3", "h4", "h5", "h6")) { + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + if (StringUtil.in(tb.currentElement().nodeName(), "h1", "h2", "h3", "h4", "h5", "h6")) { + tb.error(this); + tb.pop(); + } + tb.insert(startTag); + } else if (StringUtil.in(name, "pre", "listing")) { + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insert(startTag); + // todo: ignore LF if next token + tb.framesetOk(false); + } else if (name.equals("form")) { + if (tb.getFormElement() != null) { + tb.error(this); + return false; + } + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + Element form = tb.insert(startTag); + tb.setFormElement(form); + } else if (name.equals("li")) { + tb.framesetOk(false); + LinkedList<Element> stack = tb.getStack(); + for (int i = stack.size() - 1; i > 0; i--) { + Element el = stack.get(i); + if (el.nodeName().equals("li")) { + tb.process(new Token.EndTag("li")); + break; + } + if (tb.isSpecial(el) && !StringUtil.in(el.nodeName(), "address", "div", "p")) + break; + } + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insert(startTag); + } else if (StringUtil.in(name, "dd", "dt")) { + tb.framesetOk(false); + LinkedList<Element> stack = tb.getStack(); + for (int i = stack.size() - 1; i > 0; i--) { + Element el = stack.get(i); + if (StringUtil.in(el.nodeName(), "dd", "dt")) { + tb.process(new Token.EndTag(el.nodeName())); + break; + } + if (tb.isSpecial(el) && !StringUtil.in(el.nodeName(), "address", "div", "p")) + break; + } + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insert(startTag); + } else if (name.equals("plaintext")) { + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insert(startTag); + tb.tokeniser.transition(TokeniserState.PLAINTEXT); // once in, never gets out + } else if (name.equals("button")) { + if (tb.inButtonScope("button")) { + // close and reprocess + tb.error(this); + tb.process(new Token.EndTag("button")); + tb.process(startTag); + } else { + tb.reconstructFormattingElements(); + tb.insert(startTag); + tb.framesetOk(false); + } + } else if (name.equals("a")) { + if (tb.getActiveFormattingElement("a") != null) { + tb.error(this); + tb.process(new Token.EndTag("a")); + + // still on stack? + Element remainingA = tb.getFromStack("a"); + if (remainingA != null) { + tb.removeFromActiveFormattingElements(remainingA); + tb.removeFromStack(remainingA); + } + } + tb.reconstructFormattingElements(); + Element a = tb.insert(startTag); + tb.pushActiveFormattingElements(a); + } else if (StringUtil.in(name, + "b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u")) { + tb.reconstructFormattingElements(); + Element el = tb.insert(startTag); + tb.pushActiveFormattingElements(el); + } else if (name.equals("nobr")) { + tb.reconstructFormattingElements(); + if (tb.inScope("nobr")) { + tb.error(this); + tb.process(new Token.EndTag("nobr")); + tb.reconstructFormattingElements(); + } + Element el = tb.insert(startTag); + tb.pushActiveFormattingElements(el); + } else if (StringUtil.in(name, "applet", "marquee", "object")) { + tb.reconstructFormattingElements(); + tb.insert(startTag); + tb.insertMarkerToFormattingElements(); + tb.framesetOk(false); + } else if (name.equals("table")) { + if (tb.getDocument().quirksMode() != Document.QuirksMode.quirks && tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insert(startTag); + tb.framesetOk(false); + tb.transition(InTable); + } else if (StringUtil.in(name, "area", "br", "embed", "img", "keygen", "wbr")) { + tb.reconstructFormattingElements(); + tb.insertEmpty(startTag); + tb.framesetOk(false); + } else if (name.equals("input")) { + tb.reconstructFormattingElements(); + Element el = tb.insertEmpty(startTag); + if (!el.attr("type").equalsIgnoreCase("hidden")) + tb.framesetOk(false); + } else if (StringUtil.in(name, "param", "source", "track")) { + tb.insertEmpty(startTag); + } else if (name.equals("hr")) { + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.insertEmpty(startTag); + tb.framesetOk(false); + } else if (name.equals("image")) { + // we're not supposed to ask. + startTag.name("img"); + return tb.process(startTag); + } else if (name.equals("isindex")) { + // how much do we care about the early 90s? + tb.error(this); + if (tb.getFormElement() != null) + return false; + + tb.tokeniser.acknowledgeSelfClosingFlag(); + tb.process(new Token.StartTag("form")); + if (startTag.attributes.hasKey("action")) { + Element form = tb.getFormElement(); + form.attr("action", startTag.attributes.get("action")); + } + tb.process(new Token.StartTag("hr")); + tb.process(new Token.StartTag("label")); + // hope you like english. + String prompt = startTag.attributes.hasKey("prompt") ? + startTag.attributes.get("prompt") : + "This is a searchable index. Enter search keywords: "; + + tb.process(new Token.Character(prompt)); + + // input + Attributes inputAttribs = new Attributes(); + for (Attribute attr : startTag.attributes) { + if (!StringUtil.in(attr.getKey(), "name", "action", "prompt")) + inputAttribs.put(attr); + } + inputAttribs.put("name", "isindex"); + tb.process(new Token.StartTag("input", inputAttribs)); + tb.process(new Token.EndTag("label")); + tb.process(new Token.StartTag("hr")); + tb.process(new Token.EndTag("form")); + } else if (name.equals("textarea")) { + tb.insert(startTag); + // todo: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move on to the next one. (Newlines at the start of textarea elements are ignored as an authoring convenience.) + tb.tokeniser.transition(TokeniserState.Rcdata); + tb.markInsertionMode(); + tb.framesetOk(false); + tb.transition(Text); + } else if (name.equals("xmp")) { + if (tb.inButtonScope("p")) { + tb.process(new Token.EndTag("p")); + } + tb.reconstructFormattingElements(); + tb.framesetOk(false); + handleRawtext(startTag, tb); + } else if (name.equals("iframe")) { + tb.framesetOk(false); + handleRawtext(startTag, tb); + } else if (name.equals("noembed")) { + // also handle noscript if script enabled + handleRawtext(startTag, tb); + } else if (name.equals("select")) { + tb.reconstructFormattingElements(); + tb.insert(startTag); + tb.framesetOk(false); + + HtmlTreeBuilderState state = tb.state(); + if (state.equals(InTable) || state.equals(InCaption) || state.equals(InTableBody) || state.equals(InRow) || state.equals(InCell)) + tb.transition(InSelectInTable); + else + tb.transition(InSelect); + } else if (StringUtil.in("optgroup", "option")) { + if (tb.currentElement().nodeName().equals("option")) + tb.process(new Token.EndTag("option")); + tb.reconstructFormattingElements(); + tb.insert(startTag); + } else if (StringUtil.in("rp", "rt")) { + if (tb.inScope("ruby")) { + tb.generateImpliedEndTags(); + if (!tb.currentElement().nodeName().equals("ruby")) { + tb.error(this); + tb.popStackToBefore("ruby"); // i.e. close up to but not include name + } + tb.insert(startTag); + } + } else if (name.equals("math")) { + tb.reconstructFormattingElements(); + // todo: handle A start tag whose tag name is "math" (i.e. foreign, mathml) + tb.insert(startTag); + tb.tokeniser.acknowledgeSelfClosingFlag(); + } else if (name.equals("svg")) { + tb.reconstructFormattingElements(); + // todo: handle A start tag whose tag name is "svg" (xlink, svg) + tb.insert(startTag); + tb.tokeniser.acknowledgeSelfClosingFlag(); + } else if (StringUtil.in(name, + "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr")) { + tb.error(this); + return false; + } else { + tb.reconstructFormattingElements(); + tb.insert(startTag); + } + break; + + case EndTag: + Token.EndTag endTag = t.asEndTag(); + name = endTag.name(); + if (name.equals("body")) { + if (!tb.inScope("body")) { + tb.error(this); + return false; + } else { + // todo: error if stack contains something not dd, dt, li, optgroup, option, p, rp, rt, tbody, td, tfoot, th, thead, tr, body, html + tb.transition(AfterBody); + } + } else if (name.equals("html")) { + boolean notIgnored = tb.process(new Token.EndTag("body")); + if (notIgnored) + return tb.process(endTag); + } else if (StringUtil.in(name, + "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", + "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", + "nav", "ol", "pre", "section", "summary", "ul")) { + // todo: refactor these lookups + if (!tb.inScope(name)) { + // nothing to close + tb.error(this); + return false; + } else { + tb.generateImpliedEndTags(); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose(name); + } + } else if (name.equals("form")) { + Element currentForm = tb.getFormElement(); + tb.setFormElement(null); + if (currentForm == null || !tb.inScope(name)) { + tb.error(this); + return false; + } else { + tb.generateImpliedEndTags(); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + // remove currentForm from stack. will shift anything under up. + tb.removeFromStack(currentForm); + } + } else if (name.equals("p")) { + if (!tb.inButtonScope(name)) { + tb.error(this); + tb.process(new Token.StartTag(name)); // if no p to close, creates an empty <p></p> + return tb.process(endTag); + } else { + tb.generateImpliedEndTags(name); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose(name); + } + } else if (name.equals("li")) { + if (!tb.inListItemScope(name)) { + tb.error(this); + return false; + } else { + tb.generateImpliedEndTags(name); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose(name); + } + } else if (StringUtil.in(name, "dd", "dt")) { + if (!tb.inScope(name)) { + tb.error(this); + return false; + } else { + tb.generateImpliedEndTags(name); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose(name); + } + } else if (StringUtil.in(name, "h1", "h2", "h3", "h4", "h5", "h6")) { + if (!tb.inScope(new String[]{"h1", "h2", "h3", "h4", "h5", "h6"})) { + tb.error(this); + return false; + } else { + tb.generateImpliedEndTags(name); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose("h1", "h2", "h3", "h4", "h5", "h6"); + } + } else if (name.equals("sarcasm")) { + // *sigh* + return anyOtherEndTag(t, tb); + } else if (StringUtil.in(name, + "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u")) { + // Adoption Agency Algorithm. + OUTER: + for (int i = 0; i < 8; i++) { + Element formatEl = tb.getActiveFormattingElement(name); + if (formatEl == null) + return anyOtherEndTag(t, tb); + else if (!tb.onStack(formatEl)) { + tb.error(this); + tb.removeFromActiveFormattingElements(formatEl); + return true; + } else if (!tb.inScope(formatEl.nodeName())) { + tb.error(this); + return false; + } else if (tb.currentElement() != formatEl) + tb.error(this); + + Element furthestBlock = null; + Element commonAncestor = null; + boolean seenFormattingElement = false; + LinkedList<Element> stack = tb.getStack(); + for (int si = 0; si < stack.size(); si++) { + Element el = stack.get(si); + if (el == formatEl) { + commonAncestor = stack.get(si - 1); + seenFormattingElement = true; + } else if (seenFormattingElement && tb.isSpecial(el)) { + furthestBlock = el; + break; + } + } + if (furthestBlock == null) { + tb.popStackToClose(formatEl.nodeName()); + tb.removeFromActiveFormattingElements(formatEl); + return true; + } + + // todo: Let a bookmark note the position of the formatting element in the list of active formatting elements relative to the elements on either side of it in the list. + // does that mean: int pos of format el in list? + Element node = furthestBlock; + Element lastNode = furthestBlock; + INNER: + for (int j = 0; j < 3; j++) { + if (tb.onStack(node)) + node = tb.aboveOnStack(node); + if (!tb.isInActiveFormattingElements(node)) { // note no bookmark check + tb.removeFromStack(node); + continue INNER; + } else if (node == formatEl) + break INNER; + + Element replacement = new Element(Tag.valueOf(node.nodeName()), tb.getBaseUri()); + tb.replaceActiveFormattingElement(node, replacement); + tb.replaceOnStack(node, replacement); + node = replacement; + + if (lastNode == furthestBlock) { + // todo: move the aforementioned bookmark to be immediately after the new node in the list of active formatting elements. + // not getting how this bookmark both straddles the element above, but is inbetween here... + } + if (lastNode.parent() != null) + lastNode.remove(); + node.appendChild(lastNode); + + lastNode = node; + } + + if (StringUtil.in(commonAncestor.nodeName(), "table", "tbody", "tfoot", "thead", "tr")) { + if (lastNode.parent() != null) + lastNode.remove(); + tb.insertInFosterParent(lastNode); + } else { + if (lastNode.parent() != null) + lastNode.remove(); + commonAncestor.appendChild(lastNode); + } + + Element adopter = new Element(Tag.valueOf(name), tb.getBaseUri()); + Node[] childNodes = furthestBlock.childNodes().toArray(new Node[furthestBlock.childNodes().size()]); + for (Node childNode : childNodes) { + adopter.appendChild(childNode); // append will reparent. thus the clone to avoid concurrent mod. + } + furthestBlock.appendChild(adopter); + tb.removeFromActiveFormattingElements(formatEl); + // todo: insert the new element into the list of active formatting elements at the position of the aforementioned bookmark. + tb.removeFromStack(formatEl); + tb.insertOnStackAfter(furthestBlock, adopter); + } + } else if (StringUtil.in(name, "applet", "marquee", "object")) { + if (!tb.inScope("name")) { + if (!tb.inScope(name)) { + tb.error(this); + return false; + } + tb.generateImpliedEndTags(); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose(name); + tb.clearFormattingElementsToLastMarker(); + } + } else if (name.equals("br")) { + tb.error(this); + tb.process(new Token.StartTag("br")); + return false; + } else { + return anyOtherEndTag(t, tb); + } + + break; + case EOF: + // todo: error if stack contains something not dd, dt, li, p, tbody, td, tfoot, th, thead, tr, body, html + // stop parsing + break; + } + return true; + } + + boolean anyOtherEndTag(Token t, HtmlTreeBuilder tb) { + String name = t.asEndTag().name(); + DescendableLinkedList<Element> stack = tb.getStack(); + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element node = it.next(); + if (node.nodeName().equals(name)) { + tb.generateImpliedEndTags(name); + if (!name.equals(tb.currentElement().nodeName())) + tb.error(this); + tb.popStackToClose(name); + break; + } else { + if (tb.isSpecial(node)) { + tb.error(this); + return false; + } + } + } + return true; + } + }, + Text { + // in script, style etc. normally treated as data tags + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isCharacter()) { + tb.insert(t.asCharacter()); + } else if (t.isEOF()) { + tb.error(this); + // if current node is script: already started + tb.pop(); + tb.transition(tb.originalState()); + return tb.process(t); + } else if (t.isEndTag()) { + // if: An end tag whose tag name is "script" -- scripting nesting level, if evaluating scripts + tb.pop(); + tb.transition(tb.originalState()); + } + return true; + } + }, + InTable { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isCharacter()) { + tb.newPendingTableCharacters(); + tb.markInsertionMode(); + tb.transition(InTableText); + return tb.process(t); + } else if (t.isComment()) { + tb.insert(t.asComment()); + return true; + } else if (t.isDoctype()) { + tb.error(this); + return false; + } else if (t.isStartTag()) { + Token.StartTag startTag = t.asStartTag(); + String name = startTag.name(); + if (name.equals("caption")) { + tb.clearStackToTableContext(); + tb.insertMarkerToFormattingElements(); + tb.insert(startTag); + tb.transition(InCaption); + } else if (name.equals("colgroup")) { + tb.clearStackToTableContext(); + tb.insert(startTag); + tb.transition(InColumnGroup); + } else if (name.equals("col")) { + tb.process(new Token.StartTag("colgroup")); + return tb.process(t); + } else if (StringUtil.in(name, "tbody", "tfoot", "thead")) { + tb.clearStackToTableContext(); + tb.insert(startTag); + tb.transition(InTableBody); + } else if (StringUtil.in(name, "td", "th", "tr")) { + tb.process(new Token.StartTag("tbody")); + return tb.process(t); + } else if (name.equals("table")) { + tb.error(this); + boolean processed = tb.process(new Token.EndTag("table")); + if (processed) // only ignored if in fragment + return tb.process(t); + } else if (StringUtil.in(name, "style", "script")) { + return tb.process(t, InHead); + } else if (name.equals("input")) { + if (!startTag.attributes.get("type").equalsIgnoreCase("hidden")) { + return anythingElse(t, tb); + } else { + tb.insertEmpty(startTag); + } + } else if (name.equals("form")) { + tb.error(this); + if (tb.getFormElement() != null) + return false; + else { + Element form = tb.insertEmpty(startTag); + tb.setFormElement(form); + } + } else { + return anythingElse(t, tb); + } + } else if (t.isEndTag()) { + Token.EndTag endTag = t.asEndTag(); + String name = endTag.name(); + + if (name.equals("table")) { + if (!tb.inTableScope(name)) { + tb.error(this); + return false; + } else { + tb.popStackToClose("table"); + } + tb.resetInsertionMode(); + } else if (StringUtil.in(name, + "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr")) { + tb.error(this); + return false; + } else { + return anythingElse(t, tb); + } + } else if (t.isEOF()) { + if (tb.currentElement().nodeName().equals("html")) + tb.error(this); + return true; // stops parsing + } + return anythingElse(t, tb); + } + + boolean anythingElse(Token t, HtmlTreeBuilder tb) { + tb.error(this); + boolean processed = true; + if (StringUtil.in(tb.currentElement().nodeName(), "table", "tbody", "tfoot", "thead", "tr")) { + tb.setFosterInserts(true); + processed = tb.process(t, InBody); + tb.setFosterInserts(false); + } else { + processed = tb.process(t, InBody); + } + return processed; + } + }, + InTableText { + boolean process(Token t, HtmlTreeBuilder tb) { + switch (t.type) { + case Character: + Token.Character c = t.asCharacter(); + if (c.getData().equals(nullString)) { + tb.error(this); + return false; + } else { + tb.getPendingTableCharacters().add(c); + } + break; + default: + if (tb.getPendingTableCharacters().size() > 0) { + for (Token.Character character : tb.getPendingTableCharacters()) { + if (!isWhitespace(character)) { + // InTable anything else section: + tb.error(this); + if (StringUtil.in(tb.currentElement().nodeName(), "table", "tbody", "tfoot", "thead", "tr")) { + tb.setFosterInserts(true); + tb.process(character, InBody); + tb.setFosterInserts(false); + } else { + tb.process(character, InBody); + } + } else + tb.insert(character); + } + tb.newPendingTableCharacters(); + } + tb.transition(tb.originalState()); + return tb.process(t); + } + return true; + } + }, + InCaption { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isEndTag() && t.asEndTag().name().equals("caption")) { + Token.EndTag endTag = t.asEndTag(); + String name = endTag.name(); + if (!tb.inTableScope(name)) { + tb.error(this); + return false; + } else { + tb.generateImpliedEndTags(); + if (!tb.currentElement().nodeName().equals("caption")) + tb.error(this); + tb.popStackToClose("caption"); + tb.clearFormattingElementsToLastMarker(); + tb.transition(InTable); + } + } else if (( + t.isStartTag() && StringUtil.in(t.asStartTag().name(), + "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr") || + t.isEndTag() && t.asEndTag().name().equals("table")) + ) { + tb.error(this); + boolean processed = tb.process(new Token.EndTag("caption")); + if (processed) + return tb.process(t); + } else if (t.isEndTag() && StringUtil.in(t.asEndTag().name(), + "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr")) { + tb.error(this); + return false; + } else { + return tb.process(t, InBody); + } + return true; + } + }, + InColumnGroup { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + tb.insert(t.asCharacter()); + return true; + } + switch (t.type) { + case Comment: + tb.insert(t.asComment()); + break; + case Doctype: + tb.error(this); + break; + case StartTag: + Token.StartTag startTag = t.asStartTag(); + String name = startTag.name(); + if (name.equals("html")) + return tb.process(t, InBody); + else if (name.equals("col")) + tb.insertEmpty(startTag); + else + return anythingElse(t, tb); + break; + case EndTag: + Token.EndTag endTag = t.asEndTag(); + name = endTag.name(); + if (name.equals("colgroup")) { + if (tb.currentElement().nodeName().equals("html")) { // frag case + tb.error(this); + return false; + } else { + tb.pop(); + tb.transition(InTable); + } + } else + return anythingElse(t, tb); + break; + case EOF: + if (tb.currentElement().nodeName().equals("html")) + return true; // stop parsing; frag case + else + return anythingElse(t, tb); + default: + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, TreeBuilder tb) { + boolean processed = tb.process(new Token.EndTag("colgroup")); + if (processed) // only ignored in frag case + return tb.process(t); + return true; + } + }, + InTableBody { + boolean process(Token t, HtmlTreeBuilder tb) { + switch (t.type) { + case StartTag: + Token.StartTag startTag = t.asStartTag(); + String name = startTag.name(); + if (name.equals("tr")) { + tb.clearStackToTableBodyContext(); + tb.insert(startTag); + tb.transition(InRow); + } else if (StringUtil.in(name, "th", "td")) { + tb.error(this); + tb.process(new Token.StartTag("tr")); + return tb.process(startTag); + } else if (StringUtil.in(name, "caption", "col", "colgroup", "tbody", "tfoot", "thead")) { + return exitTableBody(t, tb); + } else + return anythingElse(t, tb); + break; + case EndTag: + Token.EndTag endTag = t.asEndTag(); + name = endTag.name(); + if (StringUtil.in(name, "tbody", "tfoot", "thead")) { + if (!tb.inTableScope(name)) { + tb.error(this); + return false; + } else { + tb.clearStackToTableBodyContext(); + tb.pop(); + tb.transition(InTable); + } + } else if (name.equals("table")) { + return exitTableBody(t, tb); + } else if (StringUtil.in(name, "body", "caption", "col", "colgroup", "html", "td", "th", "tr")) { + tb.error(this); + return false; + } else + return anythingElse(t, tb); + break; + default: + return anythingElse(t, tb); + } + return true; + } + + private boolean exitTableBody(Token t, HtmlTreeBuilder tb) { + if (!(tb.inTableScope("tbody") || tb.inTableScope("thead") || tb.inScope("tfoot"))) { + // frag case + tb.error(this); + return false; + } + tb.clearStackToTableBodyContext(); + tb.process(new Token.EndTag(tb.currentElement().nodeName())); // tbody, tfoot, thead + return tb.process(t); + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + return tb.process(t, InTable); + } + }, + InRow { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isStartTag()) { + Token.StartTag startTag = t.asStartTag(); + String name = startTag.name(); + + if (StringUtil.in(name, "th", "td")) { + tb.clearStackToTableRowContext(); + tb.insert(startTag); + tb.transition(InCell); + tb.insertMarkerToFormattingElements(); + } else if (StringUtil.in(name, "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr")) { + return handleMissingTr(t, tb); + } else { + return anythingElse(t, tb); + } + } else if (t.isEndTag()) { + Token.EndTag endTag = t.asEndTag(); + String name = endTag.name(); + + if (name.equals("tr")) { + if (!tb.inTableScope(name)) { + tb.error(this); // frag + return false; + } + tb.clearStackToTableRowContext(); + tb.pop(); // tr + tb.transition(InTableBody); + } else if (name.equals("table")) { + return handleMissingTr(t, tb); + } else if (StringUtil.in(name, "tbody", "tfoot", "thead")) { + if (!tb.inTableScope(name)) { + tb.error(this); + return false; + } + tb.process(new Token.EndTag("tr")); + return tb.process(t); + } else if (StringUtil.in(name, "body", "caption", "col", "colgroup", "html", "td", "th")) { + tb.error(this); + return false; + } else { + return anythingElse(t, tb); + } + } else { + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + return tb.process(t, InTable); + } + + private boolean handleMissingTr(Token t, TreeBuilder tb) { + boolean processed = tb.process(new Token.EndTag("tr")); + if (processed) + return tb.process(t); + else + return false; + } + }, + InCell { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isEndTag()) { + Token.EndTag endTag = t.asEndTag(); + String name = endTag.name(); + + if (StringUtil.in(name, "td", "th")) { + if (!tb.inTableScope(name)) { + tb.error(this); + tb.transition(InRow); // might not be in scope if empty: <td /> and processing fake end tag + return false; + } + tb.generateImpliedEndTags(); + if (!tb.currentElement().nodeName().equals(name)) + tb.error(this); + tb.popStackToClose(name); + tb.clearFormattingElementsToLastMarker(); + tb.transition(InRow); + } else if (StringUtil.in(name, "body", "caption", "col", "colgroup", "html")) { + tb.error(this); + return false; + } else if (StringUtil.in(name, "table", "tbody", "tfoot", "thead", "tr")) { + if (!tb.inTableScope(name)) { + tb.error(this); + return false; + } + closeCell(tb); + return tb.process(t); + } else { + return anythingElse(t, tb); + } + } else if (t.isStartTag() && + StringUtil.in(t.asStartTag().name(), + "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr")) { + if (!(tb.inTableScope("td") || tb.inTableScope("th"))) { + tb.error(this); + return false; + } + closeCell(tb); + return tb.process(t); + } else { + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + return tb.process(t, InBody); + } + + private void closeCell(HtmlTreeBuilder tb) { + if (tb.inTableScope("td")) + tb.process(new Token.EndTag("td")); + else + tb.process(new Token.EndTag("th")); // only here if th or td in scope + } + }, + InSelect { + boolean process(Token t, HtmlTreeBuilder tb) { + switch (t.type) { + case Character: + Token.Character c = t.asCharacter(); + if (c.getData().equals(nullString)) { + tb.error(this); + return false; + } else { + tb.insert(c); + } + break; + case Comment: + tb.insert(t.asComment()); + break; + case Doctype: + tb.error(this); + return false; + case StartTag: + Token.StartTag start = t.asStartTag(); + String name = start.name(); + if (name.equals("html")) + return tb.process(start, InBody); + else if (name.equals("option")) { + tb.process(new Token.EndTag("option")); + tb.insert(start); + } else if (name.equals("optgroup")) { + if (tb.currentElement().nodeName().equals("option")) + tb.process(new Token.EndTag("option")); + else if (tb.currentElement().nodeName().equals("optgroup")) + tb.process(new Token.EndTag("optgroup")); + tb.insert(start); + } else if (name.equals("select")) { + tb.error(this); + return tb.process(new Token.EndTag("select")); + } else if (StringUtil.in(name, "input", "keygen", "textarea")) { + tb.error(this); + if (!tb.inSelectScope("select")) + return false; // frag + tb.process(new Token.EndTag("select")); + return tb.process(start); + } else if (name.equals("script")) { + return tb.process(t, InHead); + } else { + return anythingElse(t, tb); + } + break; + case EndTag: + Token.EndTag end = t.asEndTag(); + name = end.name(); + if (name.equals("optgroup")) { + if (tb.currentElement().nodeName().equals("option") && tb.aboveOnStack(tb.currentElement()) != null && tb.aboveOnStack(tb.currentElement()).nodeName().equals("optgroup")) + tb.process(new Token.EndTag("option")); + if (tb.currentElement().nodeName().equals("optgroup")) + tb.pop(); + else + tb.error(this); + } else if (name.equals("option")) { + if (tb.currentElement().nodeName().equals("option")) + tb.pop(); + else + tb.error(this); + } else if (name.equals("select")) { + if (!tb.inSelectScope(name)) { + tb.error(this); + return false; + } else { + tb.popStackToClose(name); + tb.resetInsertionMode(); + } + } else + return anythingElse(t, tb); + break; + case EOF: + if (!tb.currentElement().nodeName().equals("html")) + tb.error(this); + break; + default: + return anythingElse(t, tb); + } + return true; + } + + private boolean anythingElse(Token t, HtmlTreeBuilder tb) { + tb.error(this); + return false; + } + }, + InSelectInTable { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isStartTag() && StringUtil.in(t.asStartTag().name(), "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th")) { + tb.error(this); + tb.process(new Token.EndTag("select")); + return tb.process(t); + } else if (t.isEndTag() && StringUtil.in(t.asEndTag().name(), "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th")) { + tb.error(this); + if (tb.inTableScope(t.asEndTag().name())) { + tb.process(new Token.EndTag("select")); + return (tb.process(t)); + } else + return false; + } else { + return tb.process(t, InSelect); + } + } + }, + AfterBody { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + return tb.process(t, InBody); + } else if (t.isComment()) { + tb.insert(t.asComment()); // into html node + } else if (t.isDoctype()) { + tb.error(this); + return false; + } else if (t.isStartTag() && t.asStartTag().name().equals("html")) { + return tb.process(t, InBody); + } else if (t.isEndTag() && t.asEndTag().name().equals("html")) { + if (tb.isFragmentParsing()) { + tb.error(this); + return false; + } else { + tb.transition(AfterAfterBody); + } + } else if (t.isEOF()) { + // chillax! we're done + } else { + tb.error(this); + tb.transition(InBody); + return tb.process(t); + } + return true; + } + }, + InFrameset { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + tb.insert(t.asCharacter()); + } else if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype()) { + tb.error(this); + return false; + } else if (t.isStartTag()) { + Token.StartTag start = t.asStartTag(); + String name = start.name(); + if (name.equals("html")) { + return tb.process(start, InBody); + } else if (name.equals("frameset")) { + tb.insert(start); + } else if (name.equals("frame")) { + tb.insertEmpty(start); + } else if (name.equals("noframes")) { + return tb.process(start, InHead); + } else { + tb.error(this); + return false; + } + } else if (t.isEndTag() && t.asEndTag().name().equals("frameset")) { + if (tb.currentElement().nodeName().equals("html")) { // frag + tb.error(this); + return false; + } else { + tb.pop(); + if (!tb.isFragmentParsing() && !tb.currentElement().nodeName().equals("frameset")) { + tb.transition(AfterFrameset); + } + } + } else if (t.isEOF()) { + if (!tb.currentElement().nodeName().equals("html")) { + tb.error(this); + return true; + } + } else { + tb.error(this); + return false; + } + return true; + } + }, + AfterFrameset { + boolean process(Token t, HtmlTreeBuilder tb) { + if (isWhitespace(t)) { + tb.insert(t.asCharacter()); + } else if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype()) { + tb.error(this); + return false; + } else if (t.isStartTag() && t.asStartTag().name().equals("html")) { + return tb.process(t, InBody); + } else if (t.isEndTag() && t.asEndTag().name().equals("html")) { + tb.transition(AfterAfterFrameset); + } else if (t.isStartTag() && t.asStartTag().name().equals("noframes")) { + return tb.process(t, InHead); + } else if (t.isEOF()) { + // cool your heels, we're complete + } else { + tb.error(this); + return false; + } + return true; + } + }, + AfterAfterBody { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype() || isWhitespace(t) || (t.isStartTag() && t.asStartTag().name().equals("html"))) { + return tb.process(t, InBody); + } else if (t.isEOF()) { + // nice work chuck + } else { + tb.error(this); + tb.transition(InBody); + return tb.process(t); + } + return true; + } + }, + AfterAfterFrameset { + boolean process(Token t, HtmlTreeBuilder tb) { + if (t.isComment()) { + tb.insert(t.asComment()); + } else if (t.isDoctype() || isWhitespace(t) || (t.isStartTag() && t.asStartTag().name().equals("html"))) { + return tb.process(t, InBody); + } else if (t.isEOF()) { + // nice work chuck + } else if (t.isStartTag() && t.asStartTag().name().equals("noframes")) { + return tb.process(t, InHead); + } else { + tb.error(this); + return false; + } + return true; + } + }, + ForeignContent { + boolean process(Token t, HtmlTreeBuilder tb) { + return true; + // todo: implement. Also; how do we get here? + } + }; + + private static String nullString = String.valueOf('\u0000'); + + abstract boolean process(Token t, HtmlTreeBuilder tb); + + private static boolean isWhitespace(Token t) { + if (t.isCharacter()) { + String data = t.asCharacter().getData(); + // todo: this checks more than spec - "\t", "\n", "\f", "\r", " " + for (int i = 0; i < data.length(); i++) { + char c = data.charAt(i); + if (!StringUtil.isWhitespace(c)) + return false; + } + return true; + } + return false; + } + + private static void handleRcData(Token.StartTag startTag, HtmlTreeBuilder tb) { + tb.insert(startTag); + tb.tokeniser.transition(TokeniserState.Rcdata); + tb.markInsertionMode(); + tb.transition(Text); + } + + private static void handleRawtext(Token.StartTag startTag, HtmlTreeBuilder tb) { + tb.insert(startTag); + tb.tokeniser.transition(TokeniserState.Rawtext); + tb.markInsertionMode(); + tb.transition(Text); + } +} diff --git a/server/src/org/jsoup/parser/ParseError.java b/server/src/org/jsoup/parser/ParseError.java new file mode 100644 index 0000000000..dfa090051b --- /dev/null +++ b/server/src/org/jsoup/parser/ParseError.java @@ -0,0 +1,40 @@ +package org.jsoup.parser; + +/** + * A Parse Error records an error in the input HTML that occurs in either the tokenisation or the tree building phase. + */ +public class ParseError { + private int pos; + private String errorMsg; + + ParseError(int pos, String errorMsg) { + this.pos = pos; + this.errorMsg = errorMsg; + } + + ParseError(int pos, String errorFormat, Object... args) { + this.errorMsg = String.format(errorFormat, args); + this.pos = pos; + } + + /** + * Retrieve the error message. + * @return the error message. + */ + public String getErrorMessage() { + return errorMsg; + } + + /** + * Retrieves the offset of the error. + * @return error offset within input + */ + public int getPosition() { + return pos; + } + + @Override + public String toString() { + return pos + ": " + errorMsg; + } +} diff --git a/server/src/org/jsoup/parser/ParseErrorList.java b/server/src/org/jsoup/parser/ParseErrorList.java new file mode 100644 index 0000000000..3824ffbc4e --- /dev/null +++ b/server/src/org/jsoup/parser/ParseErrorList.java @@ -0,0 +1,34 @@ +package org.jsoup.parser; + +import java.util.ArrayList; + +/** + * A container for ParseErrors. + * + * @author Jonathan Hedley + */ +class ParseErrorList extends ArrayList<ParseError>{ + private static final int INITIAL_CAPACITY = 16; + private final int maxSize; + + ParseErrorList(int initialCapacity, int maxSize) { + super(initialCapacity); + this.maxSize = maxSize; + } + + boolean canAddError() { + return size() < maxSize; + } + + int getMaxSize() { + return maxSize; + } + + static ParseErrorList noTracking() { + return new ParseErrorList(0, 0); + } + + static ParseErrorList tracking(int maxSize) { + return new ParseErrorList(INITIAL_CAPACITY, maxSize); + } +} diff --git a/server/src/org/jsoup/parser/Parser.java b/server/src/org/jsoup/parser/Parser.java new file mode 100644 index 0000000000..2236219c06 --- /dev/null +++ b/server/src/org/jsoup/parser/Parser.java @@ -0,0 +1,157 @@ +package org.jsoup.parser; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; + +import java.util.List; + +/** + * Parses HTML into a {@link org.jsoup.nodes.Document}. Generally best to use one of the more convenient parse methods + * in {@link org.jsoup.Jsoup}. + */ +public class Parser { + private static final int DEFAULT_MAX_ERRORS = 0; // by default, error tracking is disabled. + + private TreeBuilder treeBuilder; + private int maxErrors = DEFAULT_MAX_ERRORS; + private ParseErrorList errors; + + /** + * Create a new Parser, using the specified TreeBuilder + * @param treeBuilder TreeBuilder to use to parse input into Documents. + */ + public Parser(TreeBuilder treeBuilder) { + this.treeBuilder = treeBuilder; + } + + public Document parseInput(String html, String baseUri) { + errors = isTrackErrors() ? ParseErrorList.tracking(maxErrors) : ParseErrorList.noTracking(); + Document doc = treeBuilder.parse(html, baseUri, errors); + return doc; + } + + // gets & sets + /** + * Get the TreeBuilder currently in use. + * @return current TreeBuilder. + */ + public TreeBuilder getTreeBuilder() { + return treeBuilder; + } + + /** + * Update the TreeBuilder used when parsing content. + * @param treeBuilder current TreeBuilder + * @return this, for chaining + */ + public Parser setTreeBuilder(TreeBuilder treeBuilder) { + this.treeBuilder = treeBuilder; + return this; + } + + /** + * Check if parse error tracking is enabled. + * @return current track error state. + */ + public boolean isTrackErrors() { + return maxErrors > 0; + } + + /** + * Enable or disable parse error tracking for the next parse. + * @param maxErrors the maximum number of errors to track. Set to 0 to disable. + * @return this, for chaining + */ + public Parser setTrackErrors(int maxErrors) { + this.maxErrors = maxErrors; + return this; + } + + /** + * Retrieve the parse errors, if any, from the last parse. + * @return list of parse errors, up to the size of the maximum errors tracked. + */ + public List<ParseError> getErrors() { + return errors; + } + + // static parse functions below + /** + * Parse HTML into a Document. + * + * @param html HTML to parse + * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. + * + * @return parsed Document + */ + public static Document parse(String html, String baseUri) { + TreeBuilder treeBuilder = new HtmlTreeBuilder(); + return treeBuilder.parse(html, baseUri, ParseErrorList.noTracking()); + } + + /** + * Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context. + * + * @param fragmentHtml the fragment of HTML to parse + * @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). This + * provides stack context (for implicit element creation). + * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. + * + * @return list of nodes parsed from the input HTML. Note that the context element, if supplied, is not modified. + */ + public static List<Node> parseFragment(String fragmentHtml, Element context, String baseUri) { + HtmlTreeBuilder treeBuilder = new HtmlTreeBuilder(); + return treeBuilder.parseFragment(fragmentHtml, context, baseUri, ParseErrorList.noTracking()); + } + + /** + * Parse a fragment of HTML into the {@code body} of a Document. + * + * @param bodyHtml fragment of HTML + * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. + * + * @return Document, with empty head, and HTML parsed into body + */ + public static Document parseBodyFragment(String bodyHtml, String baseUri) { + Document doc = Document.createShell(baseUri); + Element body = doc.body(); + List<Node> nodeList = parseFragment(bodyHtml, body, baseUri); + Node[] nodes = nodeList.toArray(new Node[nodeList.size()]); // the node list gets modified when re-parented + for (Node node : nodes) { + body.appendChild(node); + } + return doc; + } + + /** + * @param bodyHtml HTML to parse + * @param baseUri baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. + * + * @return parsed Document + * @deprecated Use {@link #parseBodyFragment} or {@link #parseFragment} instead. + */ + public static Document parseBodyFragmentRelaxed(String bodyHtml, String baseUri) { + return parse(bodyHtml, baseUri); + } + + // builders + + /** + * Create a new HTML parser. This parser treats input as HTML5, and enforces the creation of a normalised document, + * based on a knowledge of the semantics of the incoming tags. + * @return a new HTML parser. + */ + public static Parser htmlParser() { + return new Parser(new HtmlTreeBuilder()); + } + + /** + * Create a new XML parser. This parser assumes no knowledge of the incoming tags and does not treat it as HTML, + * rather creates a simple tree directly from the input. + * @return a new simple XML parser. + */ + public static Parser xmlParser() { + return new Parser(new XmlTreeBuilder()); + } +} diff --git a/server/src/org/jsoup/parser/Tag.java b/server/src/org/jsoup/parser/Tag.java new file mode 100644 index 0000000000..40b7557b39 --- /dev/null +++ b/server/src/org/jsoup/parser/Tag.java @@ -0,0 +1,262 @@ +package org.jsoup.parser; + +import org.jsoup.helper.Validate; + +import java.util.HashMap; +import java.util.Map; + +/** + * HTML Tag capabilities. + * + * @author Jonathan Hedley, jonathan@hedley.net + */ +public class Tag { + private static final Map<String, Tag> tags = new HashMap<String, Tag>(); // map of known tags + + private String tagName; + private boolean isBlock = true; // block or inline + private boolean formatAsBlock = true; // should be formatted as a block + private boolean canContainBlock = true; // Can this tag hold block level tags? + private boolean canContainInline = true; // only pcdata if not + private boolean empty = false; // can hold nothing; e.g. img + private boolean selfClosing = false; // can self close (<foo />). used for unknown tags that self close, without forcing them as empty. + private boolean preserveWhitespace = false; // for pre, textarea, script etc + + private Tag(String tagName) { + this.tagName = tagName.toLowerCase(); + } + + /** + * Get this tag's name. + * + * @return the tag's name + */ + public String getName() { + return tagName; + } + + /** + * Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything. + * <p/> + * Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals(). + * + * @param tagName Name of tag, e.g. "p". Case insensitive. + * @return The tag, either defined or new generic. + */ + public static Tag valueOf(String tagName) { + Validate.notNull(tagName); + tagName = tagName.trim().toLowerCase(); + Validate.notEmpty(tagName); + + synchronized (tags) { + Tag tag = tags.get(tagName); + if (tag == null) { + // not defined: create default; go anywhere, do anything! (incl be inside a <p>) + tag = new Tag(tagName); + tag.isBlock = false; + tag.canContainBlock = true; + } + return tag; + } + } + + /** + * Gets if this is a block tag. + * + * @return if block tag + */ + public boolean isBlock() { + return isBlock; + } + + /** + * Gets if this tag should be formatted as a block (or as inline) + * + * @return if should be formatted as block or inline + */ + public boolean formatAsBlock() { + return formatAsBlock; + } + + /** + * Gets if this tag can contain block tags. + * + * @return if tag can contain block tags + */ + public boolean canContainBlock() { + return canContainBlock; + } + + /** + * Gets if this tag is an inline tag. + * + * @return if this tag is an inline tag. + */ + public boolean isInline() { + return !isBlock; + } + + /** + * Gets if this tag is a data only tag. + * + * @return if this tag is a data only tag + */ + public boolean isData() { + return !canContainInline && !isEmpty(); + } + + /** + * Get if this is an empty tag + * + * @return if this is an empty tag + */ + public boolean isEmpty() { + return empty; + } + + /** + * Get if this tag is self closing. + * + * @return if this tag should be output as self closing. + */ + public boolean isSelfClosing() { + return empty || selfClosing; + } + + /** + * Get if this is a pre-defined tag, or was auto created on parsing. + * + * @return if a known tag + */ + public boolean isKnownTag() { + return tags.containsKey(tagName); + } + + /** + * Check if this tagname is a known tag. + * + * @param tagName name of tag + * @return if known HTML tag + */ + public static boolean isKnownTag(String tagName) { + return tags.containsKey(tagName); + } + + /** + * Get if this tag should preserve whitespace within child text nodes. + * + * @return if preserve whitepace + */ + public boolean preserveWhitespace() { + return preserveWhitespace; + } + + Tag setSelfClosing() { + selfClosing = true; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tag)) return false; + + Tag tag = (Tag) o; + + if (canContainBlock != tag.canContainBlock) return false; + if (canContainInline != tag.canContainInline) return false; + if (empty != tag.empty) return false; + if (formatAsBlock != tag.formatAsBlock) return false; + if (isBlock != tag.isBlock) return false; + if (preserveWhitespace != tag.preserveWhitespace) return false; + if (selfClosing != tag.selfClosing) return false; + if (!tagName.equals(tag.tagName)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = tagName.hashCode(); + result = 31 * result + (isBlock ? 1 : 0); + result = 31 * result + (formatAsBlock ? 1 : 0); + result = 31 * result + (canContainBlock ? 1 : 0); + result = 31 * result + (canContainInline ? 1 : 0); + result = 31 * result + (empty ? 1 : 0); + result = 31 * result + (selfClosing ? 1 : 0); + result = 31 * result + (preserveWhitespace ? 1 : 0); + return result; + } + + public String toString() { + return tagName; + } + + // internal static initialisers: + // prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources + private static final String[] blockTags = { + "html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame", + "noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6", + "ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", "ins", + "del", "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th", + "td", "video", "audio", "canvas", "details", "menu", "plaintext" + }; + private static final String[] inlineTags = { + "object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd", + "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "a", "img", "br", "wbr", "map", "q", + "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup", + "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track", + "summary", "command", "device" + }; + private static final String[] emptyTags = { + "meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command", + "device" + }; + private static final String[] formatAsInlineTags = { + "title", "a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style" + }; + private static final String[] preserveWhitespaceTags = {"pre", "plaintext", "title"}; + + static { + // creates + for (String tagName : blockTags) { + Tag tag = new Tag(tagName); + register(tag); + } + for (String tagName : inlineTags) { + Tag tag = new Tag(tagName); + tag.isBlock = false; + tag.canContainBlock = false; + tag.formatAsBlock = false; + register(tag); + } + + // mods: + for (String tagName : emptyTags) { + Tag tag = tags.get(tagName); + Validate.notNull(tag); + tag.canContainBlock = false; + tag.canContainInline = false; + tag.empty = true; + } + + for (String tagName : formatAsInlineTags) { + Tag tag = tags.get(tagName); + Validate.notNull(tag); + tag.formatAsBlock = false; + } + + for (String tagName : preserveWhitespaceTags) { + Tag tag = tags.get(tagName); + Validate.notNull(tag); + tag.preserveWhitespace = true; + } + } + + private static Tag register(Tag tag) { + synchronized (tags) { + tags.put(tag.tagName, tag); + } + return tag; + } +} diff --git a/server/src/org/jsoup/parser/Token.java b/server/src/org/jsoup/parser/Token.java new file mode 100644 index 0000000000..9f4f9e250d --- /dev/null +++ b/server/src/org/jsoup/parser/Token.java @@ -0,0 +1,252 @@ +package org.jsoup.parser; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Attribute; +import org.jsoup.nodes.Attributes; + +/** + * Parse tokens for the Tokeniser. + */ +abstract class Token { + TokenType type; + + private Token() { + } + + String tokenType() { + return this.getClass().getSimpleName(); + } + + static class Doctype extends Token { + final StringBuilder name = new StringBuilder(); + final StringBuilder publicIdentifier = new StringBuilder(); + final StringBuilder systemIdentifier = new StringBuilder(); + boolean forceQuirks = false; + + Doctype() { + type = TokenType.Doctype; + } + + String getName() { + return name.toString(); + } + + String getPublicIdentifier() { + return publicIdentifier.toString(); + } + + public String getSystemIdentifier() { + return systemIdentifier.toString(); + } + + public boolean isForceQuirks() { + return forceQuirks; + } + } + + static abstract class Tag extends Token { + protected String tagName; + private String pendingAttributeName; + private String pendingAttributeValue; + + boolean selfClosing = false; + Attributes attributes = new Attributes(); // todo: allow nodes to not have attributes + + void newAttribute() { + if (pendingAttributeName != null) { + if (pendingAttributeValue == null) + pendingAttributeValue = ""; + Attribute attribute = new Attribute(pendingAttributeName, pendingAttributeValue); + attributes.put(attribute); + } + pendingAttributeName = null; + pendingAttributeValue = null; + } + + void finaliseTag() { + // finalises for emit + if (pendingAttributeName != null) { + // todo: check if attribute name exists; if so, drop and error + newAttribute(); + } + } + + String name() { + Validate.isFalse(tagName.length() == 0); + return tagName; + } + + Tag name(String name) { + tagName = name; + return this; + } + + boolean isSelfClosing() { + return selfClosing; + } + + @SuppressWarnings({"TypeMayBeWeakened"}) + Attributes getAttributes() { + return attributes; + } + + // these appenders are rarely hit in not null state-- caused by null chars. + void appendTagName(String append) { + tagName = tagName == null ? append : tagName.concat(append); + } + + void appendTagName(char append) { + appendTagName(String.valueOf(append)); + } + + void appendAttributeName(String append) { + pendingAttributeName = pendingAttributeName == null ? append : pendingAttributeName.concat(append); + } + + void appendAttributeName(char append) { + appendAttributeName(String.valueOf(append)); + } + + void appendAttributeValue(String append) { + pendingAttributeValue = pendingAttributeValue == null ? append : pendingAttributeValue.concat(append); + } + + void appendAttributeValue(char append) { + appendAttributeValue(String.valueOf(append)); + } + } + + static class StartTag extends Tag { + StartTag() { + super(); + type = TokenType.StartTag; + } + + StartTag(String name) { + this(); + this.tagName = name; + } + + StartTag(String name, Attributes attributes) { + this(); + this.tagName = name; + this.attributes = attributes; + } + + @Override + public String toString() { + return "<" + name() + " " + attributes.toString() + ">"; + } + } + + static class EndTag extends Tag{ + EndTag() { + super(); + type = TokenType.EndTag; + } + + EndTag(String name) { + this(); + this.tagName = name; + } + + @Override + public String toString() { + return "</" + name() + " " + attributes.toString() + ">"; + } + } + + static class Comment extends Token { + final StringBuilder data = new StringBuilder(); + + Comment() { + type = TokenType.Comment; + } + + String getData() { + return data.toString(); + } + + @Override + public String toString() { + return "<!--" + getData() + "-->"; + } + } + + static class Character extends Token { + private final String data; + + Character(String data) { + type = TokenType.Character; + this.data = data; + } + + String getData() { + return data; + } + + @Override + public String toString() { + return getData(); + } + } + + static class EOF extends Token { + EOF() { + type = Token.TokenType.EOF; + } + } + + boolean isDoctype() { + return type == TokenType.Doctype; + } + + Doctype asDoctype() { + return (Doctype) this; + } + + boolean isStartTag() { + return type == TokenType.StartTag; + } + + StartTag asStartTag() { + return (StartTag) this; + } + + boolean isEndTag() { + return type == TokenType.EndTag; + } + + EndTag asEndTag() { + return (EndTag) this; + } + + boolean isComment() { + return type == TokenType.Comment; + } + + Comment asComment() { + return (Comment) this; + } + + boolean isCharacter() { + return type == TokenType.Character; + } + + Character asCharacter() { + return (Character) this; + } + + boolean isEOF() { + return type == TokenType.EOF; + } + + enum TokenType { + Doctype, + StartTag, + EndTag, + Comment, + Character, + EOF + } +} diff --git a/server/src/org/jsoup/parser/TokenQueue.java b/server/src/org/jsoup/parser/TokenQueue.java new file mode 100644 index 0000000000..a2fdfe621a --- /dev/null +++ b/server/src/org/jsoup/parser/TokenQueue.java @@ -0,0 +1,393 @@ +package org.jsoup.parser; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; + +/** + * A character queue with parsing helpers. + * + * @author Jonathan Hedley + */ +public class TokenQueue { + private String queue; + private int pos = 0; + + private static final char ESC = '\\'; // escape char for chomp balanced. + + /** + Create a new TokenQueue. + @param data string of data to back queue. + */ + public TokenQueue(String data) { + Validate.notNull(data); + queue = data; + } + + /** + * Is the queue empty? + * @return true if no data left in queue. + */ + public boolean isEmpty() { + return remainingLength() == 0; + } + + private int remainingLength() { + return queue.length() - pos; + } + + /** + * Retrieves but does not remove the first character from the queue. + * @return First character, or 0 if empty. + */ + public char peek() { + return isEmpty() ? 0 : queue.charAt(pos); + } + + /** + Add a character to the start of the queue (will be the next character retrieved). + @param c character to add + */ + public void addFirst(Character c) { + addFirst(c.toString()); + } + + /** + Add a string to the start of the queue. + @param seq string to add. + */ + public void addFirst(String seq) { + // not very performant, but an edge case + queue = seq + queue.substring(pos); + pos = 0; + } + + /** + * Tests if the next characters on the queue match the sequence. Case insensitive. + * @param seq String to check queue for. + * @return true if the next characters match. + */ + public boolean matches(String seq) { + return queue.regionMatches(true, pos, seq, 0, seq.length()); + } + + /** + * Case sensitive match test. + * @param seq string to case sensitively check for + * @return true if matched, false if not + */ + public boolean matchesCS(String seq) { + return queue.startsWith(seq, pos); + } + + + /** + Tests if the next characters match any of the sequences. Case insensitive. + @param seq list of strings to case insensitively check for + @return true of any matched, false if none did + */ + public boolean matchesAny(String... seq) { + for (String s : seq) { + if (matches(s)) + return true; + } + return false; + } + + public boolean matchesAny(char... seq) { + if (isEmpty()) + return false; + + for (char c: seq) { + if (queue.charAt(pos) == c) + return true; + } + return false; + } + + public boolean matchesStartTag() { + // micro opt for matching "<x" + return (remainingLength() >= 2 && queue.charAt(pos) == '<' && Character.isLetter(queue.charAt(pos+1))); + } + + /** + * Tests if the queue matches the sequence (as with match), and if they do, removes the matched string from the + * queue. + * @param seq String to search for, and if found, remove from queue. + * @return true if found and removed, false if not found. + */ + public boolean matchChomp(String seq) { + if (matches(seq)) { + pos += seq.length(); + return true; + } else { + return false; + } + } + + /** + Tests if queue starts with a whitespace character. + @return if starts with whitespace + */ + public boolean matchesWhitespace() { + return !isEmpty() && StringUtil.isWhitespace(queue.charAt(pos)); + } + + /** + Test if the queue matches a word character (letter or digit). + @return if matches a word character + */ + public boolean matchesWord() { + return !isEmpty() && Character.isLetterOrDigit(queue.charAt(pos)); + } + + /** + * Drops the next character off the queue. + */ + public void advance() { + if (!isEmpty()) pos++; + } + + /** + * Consume one character off queue. + * @return first character on queue. + */ + public char consume() { + return queue.charAt(pos++); + } + + /** + * Consumes the supplied sequence of the queue. If the queue does not start with the supplied sequence, will + * throw an illegal state exception -- but you should be running match() against that condition. + <p> + Case insensitive. + * @param seq sequence to remove from head of queue. + */ + public void consume(String seq) { + if (!matches(seq)) + throw new IllegalStateException("Queue did not match expected sequence"); + int len = seq.length(); + if (len > remainingLength()) + throw new IllegalStateException("Queue not long enough to consume sequence"); + + pos += len; + } + + /** + * Pulls a string off the queue, up to but exclusive of the match sequence, or to the queue running out. + * @param seq String to end on (and not include in return, but leave on queue). <b>Case sensitive.</b> + * @return The matched data consumed from queue. + */ + public String consumeTo(String seq) { + int offset = queue.indexOf(seq, pos); + if (offset != -1) { + String consumed = queue.substring(pos, offset); + pos += consumed.length(); + return consumed; + } else { + return remainder(); + } + } + + public String consumeToIgnoreCase(String seq) { + int start = pos; + String first = seq.substring(0, 1); + boolean canScan = first.toLowerCase().equals(first.toUpperCase()); // if first is not cased, use index of + while (!isEmpty()) { + if (matches(seq)) + break; + + if (canScan) { + int skip = queue.indexOf(first, pos) - pos; + if (skip == 0) // this char is the skip char, but not match, so force advance of pos + pos++; + else if (skip < 0) // no chance of finding, grab to end + pos = queue.length(); + else + pos += skip; + } + else + pos++; + } + + String data = queue.substring(start, pos); + return data; + } + + /** + Consumes to the first sequence provided, or to the end of the queue. Leaves the terminator on the queue. + @param seq any number of terminators to consume to. <b>Case insensitive.</b> + @return consumed string + */ + // todo: method name. not good that consumeTo cares for case, and consume to any doesn't. And the only use for this + // is is a case sensitive time... + public String consumeToAny(String... seq) { + int start = pos; + while (!isEmpty() && !matchesAny(seq)) { + pos++; + } + + String data = queue.substring(start, pos); + return data; + } + + /** + * Pulls a string off the queue (like consumeTo), and then pulls off the matched string (but does not return it). + * <p> + * If the queue runs out of characters before finding the seq, will return as much as it can (and queue will go + * isEmpty() == true). + * @param seq String to match up to, and not include in return, and to pull off queue. <b>Case sensitive.</b> + * @return Data matched from queue. + */ + public String chompTo(String seq) { + String data = consumeTo(seq); + matchChomp(seq); + return data; + } + + public String chompToIgnoreCase(String seq) { + String data = consumeToIgnoreCase(seq); // case insensitive scan + matchChomp(seq); + return data; + } + + /** + * Pulls a balanced string off the queue. E.g. if queue is "(one (two) three) four", (,) will return "one (two) three", + * and leave " four" on the queue. Unbalanced openers and closers can be escaped (with \). Those escapes will be left + * in the returned string, which is suitable for regexes (where we need to preserve the escape), but unsuitable for + * contains text strings; use unescape for that. + * @param open opener + * @param close closer + * @return data matched from the queue + */ + public String chompBalanced(char open, char close) { + StringBuilder accum = new StringBuilder(); + int depth = 0; + char last = 0; + + do { + if (isEmpty()) break; + Character c = consume(); + if (last == 0 || last != ESC) { + if (c.equals(open)) + depth++; + else if (c.equals(close)) + depth--; + } + + if (depth > 0 && last != 0) + accum.append(c); // don't include the outer match pair in the return + last = c; + } while (depth > 0); + return accum.toString(); + } + + /** + * Unescaped a \ escaped string. + * @param in backslash escaped string + * @return unescaped string + */ + public static String unescape(String in) { + StringBuilder out = new StringBuilder(); + char last = 0; + for (char c : in.toCharArray()) { + if (c == ESC) { + if (last != 0 && last == ESC) + out.append(c); + } + else + out.append(c); + last = c; + } + return out.toString(); + } + + /** + * Pulls the next run of whitespace characters of the queue. + */ + public boolean consumeWhitespace() { + boolean seen = false; + while (matchesWhitespace()) { + pos++; + seen = true; + } + return seen; + } + + /** + * Retrieves the next run of word type (letter or digit) off the queue. + * @return String of word characters from queue, or empty string if none. + */ + public String consumeWord() { + int start = pos; + while (matchesWord()) + pos++; + return queue.substring(start, pos); + } + + /** + * Consume an tag name off the queue (word or :, _, -) + * + * @return tag name + */ + public String consumeTagName() { + int start = pos; + while (!isEmpty() && (matchesWord() || matchesAny(':', '_', '-'))) + pos++; + + return queue.substring(start, pos); + } + + /** + * Consume a CSS element selector (tag name, but | instead of : for namespaces, to not conflict with :pseudo selects). + * + * @return tag name + */ + public String consumeElementSelector() { + int start = pos; + while (!isEmpty() && (matchesWord() || matchesAny('|', '_', '-'))) + pos++; + + return queue.substring(start, pos); + } + + /** + Consume a CSS identifier (ID or class) off the queue (letter, digit, -, _) + http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier + @return identifier + */ + public String consumeCssIdentifier() { + int start = pos; + while (!isEmpty() && (matchesWord() || matchesAny('-', '_'))) + pos++; + + return queue.substring(start, pos); + } + + /** + Consume an attribute key off the queue (letter, digit, -, _, :") + @return attribute key + */ + public String consumeAttributeKey() { + int start = pos; + while (!isEmpty() && (matchesWord() || matchesAny('-', '_', ':'))) + pos++; + + return queue.substring(start, pos); + } + + /** + Consume and return whatever is left on the queue. + @return remained of queue. + */ + public String remainder() { + StringBuilder accum = new StringBuilder(); + while (!isEmpty()) { + accum.append(consume()); + } + return accum.toString(); + } + + public String toString() { + return queue.substring(pos); + } +} diff --git a/server/src/org/jsoup/parser/Tokeniser.java b/server/src/org/jsoup/parser/Tokeniser.java new file mode 100644 index 0000000000..ce6ee690d6 --- /dev/null +++ b/server/src/org/jsoup/parser/Tokeniser.java @@ -0,0 +1,230 @@ +package org.jsoup.parser; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Entities; + +import java.util.ArrayList; +import java.util.List; + +/** + * Readers the input stream into tokens. + */ +class Tokeniser { + static final char replacementChar = '\uFFFD'; // replaces null character + + private CharacterReader reader; // html input + private ParseErrorList errors; // errors found while tokenising + + private TokeniserState state = TokeniserState.Data; // current tokenisation state + private Token emitPending; // the token we are about to emit on next read + private boolean isEmitPending = false; + private StringBuilder charBuffer = new StringBuilder(); // buffers characters to output as one token + StringBuilder dataBuffer; // buffers data looking for </script> + + Token.Tag tagPending; // tag we are building up + Token.Doctype doctypePending; // doctype building up + Token.Comment commentPending; // comment building up + private Token.StartTag lastStartTag; // the last start tag emitted, to test appropriate end tag + private boolean selfClosingFlagAcknowledged = true; + + Tokeniser(CharacterReader reader, ParseErrorList errors) { + this.reader = reader; + this.errors = errors; + } + + Token read() { + if (!selfClosingFlagAcknowledged) { + error("Self closing flag not acknowledged"); + selfClosingFlagAcknowledged = true; + } + + while (!isEmitPending) + state.read(this, reader); + + // if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next read: + if (charBuffer.length() > 0) { + String str = charBuffer.toString(); + charBuffer.delete(0, charBuffer.length()); + return new Token.Character(str); + } else { + isEmitPending = false; + return emitPending; + } + } + + void emit(Token token) { + Validate.isFalse(isEmitPending, "There is an unread token pending!"); + + emitPending = token; + isEmitPending = true; + + if (token.type == Token.TokenType.StartTag) { + Token.StartTag startTag = (Token.StartTag) token; + lastStartTag = startTag; + if (startTag.selfClosing) + selfClosingFlagAcknowledged = false; + } else if (token.type == Token.TokenType.EndTag) { + Token.EndTag endTag = (Token.EndTag) token; + if (endTag.attributes.size() > 0) + error("Attributes incorrectly present on end tag"); + } + } + + void emit(String str) { + // buffer strings up until last string token found, to emit only one token for a run of character refs etc. + // does not set isEmitPending; read checks that + charBuffer.append(str); + } + + void emit(char c) { + charBuffer.append(c); + } + + TokeniserState getState() { + return state; + } + + void transition(TokeniserState state) { + this.state = state; + } + + void advanceTransition(TokeniserState state) { + reader.advance(); + this.state = state; + } + + void acknowledgeSelfClosingFlag() { + selfClosingFlagAcknowledged = true; + } + + Character consumeCharacterReference(Character additionalAllowedCharacter, boolean inAttribute) { + if (reader.isEmpty()) + return null; + if (additionalAllowedCharacter != null && additionalAllowedCharacter == reader.current()) + return null; + if (reader.matchesAny('\t', '\n', '\f', ' ', '<', '&')) + return null; + + reader.mark(); + if (reader.matchConsume("#")) { // numbered + boolean isHexMode = reader.matchConsumeIgnoreCase("X"); + String numRef = isHexMode ? reader.consumeHexSequence() : reader.consumeDigitSequence(); + if (numRef.length() == 0) { // didn't match anything + characterReferenceError("numeric reference with no numerals"); + reader.rewindToMark(); + return null; + } + if (!reader.matchConsume(";")) + characterReferenceError("missing semicolon"); // missing semi + int charval = -1; + try { + int base = isHexMode ? 16 : 10; + charval = Integer.valueOf(numRef, base); + } catch (NumberFormatException e) { + } // skip + if (charval == -1 || (charval >= 0xD800 && charval <= 0xDFFF) || charval > 0x10FFFF) { + characterReferenceError("character outside of valid range"); + return replacementChar; + } else { + // todo: implement number replacement table + // todo: check for extra illegal unicode points as parse errors + return (char) charval; + } + } else { // named + // get as many letters as possible, and look for matching entities. unconsume backwards till a match is found + String nameRef = reader.consumeLetterThenDigitSequence(); + String origNameRef = new String(nameRef); // for error reporting. nameRef gets chomped looking for matches + boolean looksLegit = reader.matches(';'); + boolean found = false; + while (nameRef.length() > 0 && !found) { + if (Entities.isNamedEntity(nameRef)) + found = true; + else { + nameRef = nameRef.substring(0, nameRef.length()-1); + reader.unconsume(); + } + } + if (!found) { + if (looksLegit) // named with semicolon + characterReferenceError(String.format("invalid named referenece '%s'", origNameRef)); + reader.rewindToMark(); + return null; + } + if (inAttribute && (reader.matchesLetter() || reader.matchesDigit() || reader.matchesAny('=', '-', '_'))) { + // don't want that to match + reader.rewindToMark(); + return null; + } + if (!reader.matchConsume(";")) + characterReferenceError("missing semicolon"); // missing semi + return Entities.getCharacterByName(nameRef); + } + } + + Token.Tag createTagPending(boolean start) { + tagPending = start ? new Token.StartTag() : new Token.EndTag(); + return tagPending; + } + + void emitTagPending() { + tagPending.finaliseTag(); + emit(tagPending); + } + + void createCommentPending() { + commentPending = new Token.Comment(); + } + + void emitCommentPending() { + emit(commentPending); + } + + void createDoctypePending() { + doctypePending = new Token.Doctype(); + } + + void emitDoctypePending() { + emit(doctypePending); + } + + void createTempBuffer() { + dataBuffer = new StringBuilder(); + } + + boolean isAppropriateEndTagToken() { + if (lastStartTag == null) + return false; + return tagPending.tagName.equals(lastStartTag.tagName); + } + + String appropriateEndTagName() { + return lastStartTag.tagName; + } + + void error(TokeniserState state) { + if (errors.canAddError()) + errors.add(new ParseError(reader.pos(), "Unexpected character '%s' in input state [%s]", reader.current(), state)); + } + + void eofError(TokeniserState state) { + if (errors.canAddError()) + errors.add(new ParseError(reader.pos(), "Unexpectedly reached end of file (EOF) in input state [%s]", state)); + } + + private void characterReferenceError(String message) { + if (errors.canAddError()) + errors.add(new ParseError(reader.pos(), "Invalid character reference: %s", message)); + } + + private void error(String errorMsg) { + if (errors.canAddError()) + errors.add(new ParseError(reader.pos(), errorMsg)); + } + + boolean currentNodeInHtmlNS() { + // todo: implement namespaces correctly + return true; + // Element currentNode = currentNode(); + // return currentNode != null && currentNode.namespace().equals("HTML"); + } +} diff --git a/server/src/org/jsoup/parser/TokeniserState.java b/server/src/org/jsoup/parser/TokeniserState.java new file mode 100644 index 0000000000..e3013c73e9 --- /dev/null +++ b/server/src/org/jsoup/parser/TokeniserState.java @@ -0,0 +1,1778 @@ +package org.jsoup.parser; + +/** + * States and transition activations for the Tokeniser. + */ +enum TokeniserState { + Data { + // in data state, gather characters until a character reference or tag is found + void read(Tokeniser t, CharacterReader r) { + switch (r.current()) { + case '&': + t.advanceTransition(CharacterReferenceInData); + break; + case '<': + t.advanceTransition(TagOpen); + break; + case nullChar: + t.error(this); // NOT replacement character (oddly?) + t.emit(r.consume()); + break; + case eof: + t.emit(new Token.EOF()); + break; + default: + String data = r.consumeToAny('&', '<', nullChar); + t.emit(data); + break; + } + } + }, + CharacterReferenceInData { + // from & in data + void read(Tokeniser t, CharacterReader r) { + Character c = t.consumeCharacterReference(null, false); + if (c == null) + t.emit('&'); + else + t.emit(c); + t.transition(Data); + } + }, + Rcdata { + /// handles data in title, textarea etc + void read(Tokeniser t, CharacterReader r) { + switch (r.current()) { + case '&': + t.advanceTransition(CharacterReferenceInRcdata); + break; + case '<': + t.advanceTransition(RcdataLessthanSign); + break; + case nullChar: + t.error(this); + r.advance(); + t.emit(replacementChar); + break; + case eof: + t.emit(new Token.EOF()); + break; + default: + String data = r.consumeToAny('&', '<', nullChar); + t.emit(data); + break; + } + } + }, + CharacterReferenceInRcdata { + void read(Tokeniser t, CharacterReader r) { + Character c = t.consumeCharacterReference(null, false); + if (c == null) + t.emit('&'); + else + t.emit(c); + t.transition(Rcdata); + } + }, + Rawtext { + void read(Tokeniser t, CharacterReader r) { + switch (r.current()) { + case '<': + t.advanceTransition(RawtextLessthanSign); + break; + case nullChar: + t.error(this); + r.advance(); + t.emit(replacementChar); + break; + case eof: + t.emit(new Token.EOF()); + break; + default: + String data = r.consumeToAny('<', nullChar); + t.emit(data); + break; + } + } + }, + ScriptData { + void read(Tokeniser t, CharacterReader r) { + switch (r.current()) { + case '<': + t.advanceTransition(ScriptDataLessthanSign); + break; + case nullChar: + t.error(this); + r.advance(); + t.emit(replacementChar); + break; + case eof: + t.emit(new Token.EOF()); + break; + default: + String data = r.consumeToAny('<', nullChar); + t.emit(data); + break; + } + } + }, + PLAINTEXT { + void read(Tokeniser t, CharacterReader r) { + switch (r.current()) { + case nullChar: + t.error(this); + r.advance(); + t.emit(replacementChar); + break; + case eof: + t.emit(new Token.EOF()); + break; + default: + String data = r.consumeTo(nullChar); + t.emit(data); + break; + } + } + }, + TagOpen { + // from < in data + void read(Tokeniser t, CharacterReader r) { + switch (r.current()) { + case '!': + t.advanceTransition(MarkupDeclarationOpen); + break; + case '/': + t.advanceTransition(EndTagOpen); + break; + case '?': + t.advanceTransition(BogusComment); + break; + default: + if (r.matchesLetter()) { + t.createTagPending(true); + t.transition(TagName); + } else { + t.error(this); + t.emit('<'); // char that got us here + t.transition(Data); + } + break; + } + } + }, + EndTagOpen { + void read(Tokeniser t, CharacterReader r) { + if (r.isEmpty()) { + t.eofError(this); + t.emit("</"); + t.transition(Data); + } else if (r.matchesLetter()) { + t.createTagPending(false); + t.transition(TagName); + } else if (r.matches('>')) { + t.error(this); + t.advanceTransition(Data); + } else { + t.error(this); + t.advanceTransition(BogusComment); + } + } + }, + TagName { + // from < or </ in data, will have start or end tag pending + void read(Tokeniser t, CharacterReader r) { + // previous TagOpen state did NOT consume, will have a letter char in current + String tagName = r.consumeToAny('\t', '\n', '\f', ' ', '/', '>', nullChar).toLowerCase(); + t.tagPending.appendTagName(tagName); + + switch (r.consume()) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeAttributeName); + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + case nullChar: // replacement + t.tagPending.appendTagName(replacementStr); + break; + case eof: // should emit pending tag? + t.eofError(this); + t.transition(Data); + // no default, as covered with above consumeToAny + } + } + }, + RcdataLessthanSign { + // from < in rcdata + void read(Tokeniser t, CharacterReader r) { + if (r.matches('/')) { + t.createTempBuffer(); + t.advanceTransition(RCDATAEndTagOpen); + } else if (r.matchesLetter() && !r.containsIgnoreCase("</" + t.appropriateEndTagName())) { + // diverge from spec: got a start tag, but there's no appropriate end tag (</title>), so rather than + // consuming to EOF; break out here + t.tagPending = new Token.EndTag(t.appropriateEndTagName()); + t.emitTagPending(); + r.unconsume(); // undo "<" + t.transition(Data); + } else { + t.emit("<"); + t.transition(Rcdata); + } + } + }, + RCDATAEndTagOpen { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + t.createTagPending(false); + t.tagPending.appendTagName(Character.toLowerCase(r.current())); + t.dataBuffer.append(Character.toLowerCase(r.current())); + t.advanceTransition(RCDATAEndTagName); + } else { + t.emit("</"); + t.transition(Rcdata); + } + } + }, + RCDATAEndTagName { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.tagPending.appendTagName(name.toLowerCase()); + t.dataBuffer.append(name); + return; + } + + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + if (t.isAppropriateEndTagToken()) + t.transition(BeforeAttributeName); + else + anythingElse(t, r); + break; + case '/': + if (t.isAppropriateEndTagToken()) + t.transition(SelfClosingStartTag); + else + anythingElse(t, r); + break; + case '>': + if (t.isAppropriateEndTagToken()) { + t.emitTagPending(); + t.transition(Data); + } + else + anythingElse(t, r); + break; + default: + anythingElse(t, r); + } + } + + private void anythingElse(Tokeniser t, CharacterReader r) { + t.emit("</" + t.dataBuffer.toString()); + t.transition(Rcdata); + } + }, + RawtextLessthanSign { + void read(Tokeniser t, CharacterReader r) { + if (r.matches('/')) { + t.createTempBuffer(); + t.advanceTransition(RawtextEndTagOpen); + } else { + t.emit('<'); + t.transition(Rawtext); + } + } + }, + RawtextEndTagOpen { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + t.createTagPending(false); + t.transition(RawtextEndTagName); + } else { + t.emit("</"); + t.transition(Rawtext); + } + } + }, + RawtextEndTagName { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.tagPending.appendTagName(name.toLowerCase()); + t.dataBuffer.append(name); + return; + } + + if (t.isAppropriateEndTagToken() && !r.isEmpty()) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeAttributeName); + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + default: + t.dataBuffer.append(c); + anythingElse(t, r); + } + } else + anythingElse(t, r); + } + + private void anythingElse(Tokeniser t, CharacterReader r) { + t.emit("</" + t.dataBuffer.toString()); + t.transition(Rawtext); + } + }, + ScriptDataLessthanSign { + void read(Tokeniser t, CharacterReader r) { + switch (r.consume()) { + case '/': + t.createTempBuffer(); + t.transition(ScriptDataEndTagOpen); + break; + case '!': + t.emit("<!"); + t.transition(ScriptDataEscapeStart); + break; + default: + t.emit("<"); + r.unconsume(); + t.transition(ScriptData); + } + } + }, + ScriptDataEndTagOpen { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + t.createTagPending(false); + t.transition(ScriptDataEndTagName); + } else { + t.emit("</"); + t.transition(ScriptData); + } + + } + }, + ScriptDataEndTagName { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.tagPending.appendTagName(name.toLowerCase()); + t.dataBuffer.append(name); + return; + } + + if (t.isAppropriateEndTagToken() && !r.isEmpty()) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeAttributeName); + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + default: + t.dataBuffer.append(c); + anythingElse(t, r); + } + } else { + anythingElse(t, r); + } + } + + private void anythingElse(Tokeniser t, CharacterReader r) { + t.emit("</" + t.dataBuffer.toString()); + t.transition(ScriptData); + } + }, + ScriptDataEscapeStart { + void read(Tokeniser t, CharacterReader r) { + if (r.matches('-')) { + t.emit('-'); + t.advanceTransition(ScriptDataEscapeStartDash); + } else { + t.transition(ScriptData); + } + } + }, + ScriptDataEscapeStartDash { + void read(Tokeniser t, CharacterReader r) { + if (r.matches('-')) { + t.emit('-'); + t.advanceTransition(ScriptDataEscapedDashDash); + } else { + t.transition(ScriptData); + } + } + }, + ScriptDataEscaped { + void read(Tokeniser t, CharacterReader r) { + if (r.isEmpty()) { + t.eofError(this); + t.transition(Data); + return; + } + + switch (r.current()) { + case '-': + t.emit('-'); + t.advanceTransition(ScriptDataEscapedDash); + break; + case '<': + t.advanceTransition(ScriptDataEscapedLessthanSign); + break; + case nullChar: + t.error(this); + r.advance(); + t.emit(replacementChar); + break; + default: + String data = r.consumeToAny('-', '<', nullChar); + t.emit(data); + } + } + }, + ScriptDataEscapedDash { + void read(Tokeniser t, CharacterReader r) { + if (r.isEmpty()) { + t.eofError(this); + t.transition(Data); + return; + } + + char c = r.consume(); + switch (c) { + case '-': + t.emit(c); + t.transition(ScriptDataEscapedDashDash); + break; + case '<': + t.transition(ScriptDataEscapedLessthanSign); + break; + case nullChar: + t.error(this); + t.emit(replacementChar); + t.transition(ScriptDataEscaped); + break; + default: + t.emit(c); + t.transition(ScriptDataEscaped); + } + } + }, + ScriptDataEscapedDashDash { + void read(Tokeniser t, CharacterReader r) { + if (r.isEmpty()) { + t.eofError(this); + t.transition(Data); + return; + } + + char c = r.consume(); + switch (c) { + case '-': + t.emit(c); + break; + case '<': + t.transition(ScriptDataEscapedLessthanSign); + break; + case '>': + t.emit(c); + t.transition(ScriptData); + break; + case nullChar: + t.error(this); + t.emit(replacementChar); + t.transition(ScriptDataEscaped); + break; + default: + t.emit(c); + t.transition(ScriptDataEscaped); + } + } + }, + ScriptDataEscapedLessthanSign { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + t.createTempBuffer(); + t.dataBuffer.append(Character.toLowerCase(r.current())); + t.emit("<" + r.current()); + t.advanceTransition(ScriptDataDoubleEscapeStart); + } else if (r.matches('/')) { + t.createTempBuffer(); + t.advanceTransition(ScriptDataEscapedEndTagOpen); + } else { + t.emit('<'); + t.transition(ScriptDataEscaped); + } + } + }, + ScriptDataEscapedEndTagOpen { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + t.createTagPending(false); + t.tagPending.appendTagName(Character.toLowerCase(r.current())); + t.dataBuffer.append(r.current()); + t.advanceTransition(ScriptDataEscapedEndTagName); + } else { + t.emit("</"); + t.transition(ScriptDataEscaped); + } + } + }, + ScriptDataEscapedEndTagName { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.tagPending.appendTagName(name.toLowerCase()); + t.dataBuffer.append(name); + return; + } + + if (t.isAppropriateEndTagToken() && !r.isEmpty()) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeAttributeName); + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + default: + t.dataBuffer.append(c); + anythingElse(t, r); + break; + } + } else { + anythingElse(t, r); + } + } + + private void anythingElse(Tokeniser t, CharacterReader r) { + t.emit("</" + t.dataBuffer.toString()); + t.transition(ScriptDataEscaped); + } + }, + ScriptDataDoubleEscapeStart { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.dataBuffer.append(name.toLowerCase()); + t.emit(name); + return; + } + + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + case '/': + case '>': + if (t.dataBuffer.toString().equals("script")) + t.transition(ScriptDataDoubleEscaped); + else + t.transition(ScriptDataEscaped); + t.emit(c); + break; + default: + r.unconsume(); + t.transition(ScriptDataEscaped); + } + } + }, + ScriptDataDoubleEscaped { + void read(Tokeniser t, CharacterReader r) { + char c = r.current(); + switch (c) { + case '-': + t.emit(c); + t.advanceTransition(ScriptDataDoubleEscapedDash); + break; + case '<': + t.emit(c); + t.advanceTransition(ScriptDataDoubleEscapedLessthanSign); + break; + case nullChar: + t.error(this); + r.advance(); + t.emit(replacementChar); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + default: + String data = r.consumeToAny('-', '<', nullChar); + t.emit(data); + } + } + }, + ScriptDataDoubleEscapedDash { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '-': + t.emit(c); + t.transition(ScriptDataDoubleEscapedDashDash); + break; + case '<': + t.emit(c); + t.transition(ScriptDataDoubleEscapedLessthanSign); + break; + case nullChar: + t.error(this); + t.emit(replacementChar); + t.transition(ScriptDataDoubleEscaped); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + default: + t.emit(c); + t.transition(ScriptDataDoubleEscaped); + } + } + }, + ScriptDataDoubleEscapedDashDash { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '-': + t.emit(c); + break; + case '<': + t.emit(c); + t.transition(ScriptDataDoubleEscapedLessthanSign); + break; + case '>': + t.emit(c); + t.transition(ScriptData); + break; + case nullChar: + t.error(this); + t.emit(replacementChar); + t.transition(ScriptDataDoubleEscaped); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + default: + t.emit(c); + t.transition(ScriptDataDoubleEscaped); + } + } + }, + ScriptDataDoubleEscapedLessthanSign { + void read(Tokeniser t, CharacterReader r) { + if (r.matches('/')) { + t.emit('/'); + t.createTempBuffer(); + t.advanceTransition(ScriptDataDoubleEscapeEnd); + } else { + t.transition(ScriptDataDoubleEscaped); + } + } + }, + ScriptDataDoubleEscapeEnd { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.dataBuffer.append(name.toLowerCase()); + t.emit(name); + return; + } + + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + case '/': + case '>': + if (t.dataBuffer.toString().equals("script")) + t.transition(ScriptDataEscaped); + else + t.transition(ScriptDataDoubleEscaped); + t.emit(c); + break; + default: + r.unconsume(); + t.transition(ScriptDataDoubleEscaped); + } + } + }, + BeforeAttributeName { + // from tagname <xxx + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + break; // ignore whitespace + case '/': + t.transition(SelfClosingStartTag); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + case nullChar: + t.error(this); + t.tagPending.newAttribute(); + r.unconsume(); + t.transition(AttributeName); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + case '"': + case '\'': + case '<': + case '=': + t.error(this); + t.tagPending.newAttribute(); + t.tagPending.appendAttributeName(c); + t.transition(AttributeName); + break; + default: // A-Z, anything else + t.tagPending.newAttribute(); + r.unconsume(); + t.transition(AttributeName); + } + } + }, + AttributeName { + // from before attribute name + void read(Tokeniser t, CharacterReader r) { + String name = r.consumeToAny('\t', '\n', '\f', ' ', '/', '=', '>', nullChar, '"', '\'', '<'); + t.tagPending.appendAttributeName(name.toLowerCase()); + + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(AfterAttributeName); + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '=': + t.transition(BeforeAttributeValue); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + case nullChar: + t.error(this); + t.tagPending.appendAttributeName(replacementChar); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + case '"': + case '\'': + case '<': + t.error(this); + t.tagPending.appendAttributeName(c); + // no default, as covered in consumeToAny + } + } + }, + AfterAttributeName { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + // ignore + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '=': + t.transition(BeforeAttributeValue); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + case nullChar: + t.error(this); + t.tagPending.appendAttributeName(replacementChar); + t.transition(AttributeName); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + case '"': + case '\'': + case '<': + t.error(this); + t.tagPending.newAttribute(); + t.tagPending.appendAttributeName(c); + t.transition(AttributeName); + break; + default: // A-Z, anything else + t.tagPending.newAttribute(); + r.unconsume(); + t.transition(AttributeName); + } + } + }, + BeforeAttributeValue { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + // ignore + break; + case '"': + t.transition(AttributeValue_doubleQuoted); + break; + case '&': + r.unconsume(); + t.transition(AttributeValue_unquoted); + break; + case '\'': + t.transition(AttributeValue_singleQuoted); + break; + case nullChar: + t.error(this); + t.tagPending.appendAttributeValue(replacementChar); + t.transition(AttributeValue_unquoted); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + case '>': + t.error(this); + t.emitTagPending(); + t.transition(Data); + break; + case '<': + case '=': + case '`': + t.error(this); + t.tagPending.appendAttributeValue(c); + t.transition(AttributeValue_unquoted); + break; + default: + r.unconsume(); + t.transition(AttributeValue_unquoted); + } + } + }, + AttributeValue_doubleQuoted { + void read(Tokeniser t, CharacterReader r) { + String value = r.consumeToAny('"', '&', nullChar); + if (value.length() > 0) + t.tagPending.appendAttributeValue(value); + + char c = r.consume(); + switch (c) { + case '"': + t.transition(AfterAttributeValue_quoted); + break; + case '&': + Character ref = t.consumeCharacterReference('"', true); + if (ref != null) + t.tagPending.appendAttributeValue(ref); + else + t.tagPending.appendAttributeValue('&'); + break; + case nullChar: + t.error(this); + t.tagPending.appendAttributeValue(replacementChar); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + // no default, handled in consume to any above + } + } + }, + AttributeValue_singleQuoted { + void read(Tokeniser t, CharacterReader r) { + String value = r.consumeToAny('\'', '&', nullChar); + if (value.length() > 0) + t.tagPending.appendAttributeValue(value); + + char c = r.consume(); + switch (c) { + case '\'': + t.transition(AfterAttributeValue_quoted); + break; + case '&': + Character ref = t.consumeCharacterReference('\'', true); + if (ref != null) + t.tagPending.appendAttributeValue(ref); + else + t.tagPending.appendAttributeValue('&'); + break; + case nullChar: + t.error(this); + t.tagPending.appendAttributeValue(replacementChar); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + // no default, handled in consume to any above + } + } + }, + AttributeValue_unquoted { + void read(Tokeniser t, CharacterReader r) { + String value = r.consumeToAny('\t', '\n', '\f', ' ', '&', '>', nullChar, '"', '\'', '<', '=', '`'); + if (value.length() > 0) + t.tagPending.appendAttributeValue(value); + + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeAttributeName); + break; + case '&': + Character ref = t.consumeCharacterReference('>', true); + if (ref != null) + t.tagPending.appendAttributeValue(ref); + else + t.tagPending.appendAttributeValue('&'); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + case nullChar: + t.error(this); + t.tagPending.appendAttributeValue(replacementChar); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + case '"': + case '\'': + case '<': + case '=': + case '`': + t.error(this); + t.tagPending.appendAttributeValue(c); + break; + // no default, handled in consume to any above + } + + } + }, + // CharacterReferenceInAttributeValue state handled inline + AfterAttributeValue_quoted { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeAttributeName); + break; + case '/': + t.transition(SelfClosingStartTag); + break; + case '>': + t.emitTagPending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + default: + t.error(this); + r.unconsume(); + t.transition(BeforeAttributeName); + } + + } + }, + SelfClosingStartTag { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '>': + t.tagPending.selfClosing = true; + t.emitTagPending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.transition(Data); + break; + default: + t.error(this); + t.transition(BeforeAttributeName); + } + } + }, + BogusComment { + void read(Tokeniser t, CharacterReader r) { + // todo: handle bogus comment starting from eof. when does that trigger? + // rewind to capture character that lead us here + r.unconsume(); + Token.Comment comment = new Token.Comment(); + comment.data.append(r.consumeTo('>')); + // todo: replace nullChar with replaceChar + t.emit(comment); + t.advanceTransition(Data); + } + }, + MarkupDeclarationOpen { + void read(Tokeniser t, CharacterReader r) { + if (r.matchConsume("--")) { + t.createCommentPending(); + t.transition(CommentStart); + } else if (r.matchConsumeIgnoreCase("DOCTYPE")) { + t.transition(Doctype); + } else if (r.matchConsume("[CDATA[")) { + // todo: should actually check current namepspace, and only non-html allows cdata. until namespace + // is implemented properly, keep handling as cdata + //} else if (!t.currentNodeInHtmlNS() && r.matchConsume("[CDATA[")) { + t.transition(CdataSection); + } else { + t.error(this); + t.advanceTransition(BogusComment); // advance so this character gets in bogus comment data's rewind + } + } + }, + CommentStart { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '-': + t.transition(CommentStartDash); + break; + case nullChar: + t.error(this); + t.commentPending.data.append(replacementChar); + t.transition(Comment); + break; + case '>': + t.error(this); + t.emitCommentPending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.emitCommentPending(); + t.transition(Data); + break; + default: + t.commentPending.data.append(c); + t.transition(Comment); + } + } + }, + CommentStartDash { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '-': + t.transition(CommentStartDash); + break; + case nullChar: + t.error(this); + t.commentPending.data.append(replacementChar); + t.transition(Comment); + break; + case '>': + t.error(this); + t.emitCommentPending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.emitCommentPending(); + t.transition(Data); + break; + default: + t.commentPending.data.append(c); + t.transition(Comment); + } + } + }, + Comment { + void read(Tokeniser t, CharacterReader r) { + char c = r.current(); + switch (c) { + case '-': + t.advanceTransition(CommentEndDash); + break; + case nullChar: + t.error(this); + r.advance(); + t.commentPending.data.append(replacementChar); + break; + case eof: + t.eofError(this); + t.emitCommentPending(); + t.transition(Data); + break; + default: + t.commentPending.data.append(r.consumeToAny('-', nullChar)); + } + } + }, + CommentEndDash { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '-': + t.transition(CommentEnd); + break; + case nullChar: + t.error(this); + t.commentPending.data.append('-').append(replacementChar); + t.transition(Comment); + break; + case eof: + t.eofError(this); + t.emitCommentPending(); + t.transition(Data); + break; + default: + t.commentPending.data.append('-').append(c); + t.transition(Comment); + } + } + }, + CommentEnd { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '>': + t.emitCommentPending(); + t.transition(Data); + break; + case nullChar: + t.error(this); + t.commentPending.data.append("--").append(replacementChar); + t.transition(Comment); + break; + case '!': + t.error(this); + t.transition(CommentEndBang); + break; + case '-': + t.error(this); + t.commentPending.data.append('-'); + break; + case eof: + t.eofError(this); + t.emitCommentPending(); + t.transition(Data); + break; + default: + t.error(this); + t.commentPending.data.append("--").append(c); + t.transition(Comment); + } + } + }, + CommentEndBang { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '-': + t.commentPending.data.append("--!"); + t.transition(CommentEndDash); + break; + case '>': + t.emitCommentPending(); + t.transition(Data); + break; + case nullChar: + t.error(this); + t.commentPending.data.append("--!").append(replacementChar); + t.transition(Comment); + break; + case eof: + t.eofError(this); + t.emitCommentPending(); + t.transition(Data); + break; + default: + t.commentPending.data.append("--!").append(c); + t.transition(Comment); + } + } + }, + Doctype { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeDoctypeName); + break; + case eof: + t.eofError(this); + t.createDoctypePending(); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.transition(BeforeDoctypeName); + } + } + }, + BeforeDoctypeName { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + t.createDoctypePending(); + t.transition(DoctypeName); + return; + } + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + break; // ignore whitespace + case nullChar: + t.error(this); + t.doctypePending.name.append(replacementChar); + t.transition(DoctypeName); + break; + case eof: + t.eofError(this); + t.createDoctypePending(); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.createDoctypePending(); + t.doctypePending.name.append(c); + t.transition(DoctypeName); + } + } + }, + DoctypeName { + void read(Tokeniser t, CharacterReader r) { + if (r.matchesLetter()) { + String name = r.consumeLetterSequence(); + t.doctypePending.name.append(name.toLowerCase()); + return; + } + char c = r.consume(); + switch (c) { + case '>': + t.emitDoctypePending(); + t.transition(Data); + break; + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(AfterDoctypeName); + break; + case nullChar: + t.error(this); + t.doctypePending.name.append(replacementChar); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.doctypePending.name.append(c); + } + } + }, + AfterDoctypeName { + void read(Tokeniser t, CharacterReader r) { + if (r.isEmpty()) { + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + return; + } + if (r.matchesAny('\t', '\n', '\f', ' ')) + r.advance(); // ignore whitespace + else if (r.matches('>')) { + t.emitDoctypePending(); + t.advanceTransition(Data); + } else if (r.matchConsumeIgnoreCase("PUBLIC")) { + t.transition(AfterDoctypePublicKeyword); + } else if (r.matchConsumeIgnoreCase("SYSTEM")) { + t.transition(AfterDoctypeSystemKeyword); + } else { + t.error(this); + t.doctypePending.forceQuirks = true; + t.advanceTransition(BogusDoctype); + } + + } + }, + AfterDoctypePublicKeyword { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeDoctypePublicIdentifier); + break; + case '"': + t.error(this); + // set public id to empty string + t.transition(DoctypePublicIdentifier_doubleQuoted); + break; + case '\'': + t.error(this); + // set public id to empty string + t.transition(DoctypePublicIdentifier_singleQuoted); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.doctypePending.forceQuirks = true; + t.transition(BogusDoctype); + } + } + }, + BeforeDoctypePublicIdentifier { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + break; + case '"': + // set public id to empty string + t.transition(DoctypePublicIdentifier_doubleQuoted); + break; + case '\'': + // set public id to empty string + t.transition(DoctypePublicIdentifier_singleQuoted); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.doctypePending.forceQuirks = true; + t.transition(BogusDoctype); + } + } + }, + DoctypePublicIdentifier_doubleQuoted { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '"': + t.transition(AfterDoctypePublicIdentifier); + break; + case nullChar: + t.error(this); + t.doctypePending.publicIdentifier.append(replacementChar); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.doctypePending.publicIdentifier.append(c); + } + } + }, + DoctypePublicIdentifier_singleQuoted { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\'': + t.transition(AfterDoctypePublicIdentifier); + break; + case nullChar: + t.error(this); + t.doctypePending.publicIdentifier.append(replacementChar); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.doctypePending.publicIdentifier.append(c); + } + } + }, + AfterDoctypePublicIdentifier { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BetweenDoctypePublicAndSystemIdentifiers); + break; + case '>': + t.emitDoctypePending(); + t.transition(Data); + break; + case '"': + t.error(this); + // system id empty + t.transition(DoctypeSystemIdentifier_doubleQuoted); + break; + case '\'': + t.error(this); + // system id empty + t.transition(DoctypeSystemIdentifier_singleQuoted); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.doctypePending.forceQuirks = true; + t.transition(BogusDoctype); + } + } + }, + BetweenDoctypePublicAndSystemIdentifiers { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + break; + case '>': + t.emitDoctypePending(); + t.transition(Data); + break; + case '"': + t.error(this); + // system id empty + t.transition(DoctypeSystemIdentifier_doubleQuoted); + break; + case '\'': + t.error(this); + // system id empty + t.transition(DoctypeSystemIdentifier_singleQuoted); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.doctypePending.forceQuirks = true; + t.transition(BogusDoctype); + } + } + }, + AfterDoctypeSystemKeyword { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + t.transition(BeforeDoctypeSystemIdentifier); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case '"': + t.error(this); + // system id empty + t.transition(DoctypeSystemIdentifier_doubleQuoted); + break; + case '\'': + t.error(this); + // system id empty + t.transition(DoctypeSystemIdentifier_singleQuoted); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + } + } + }, + BeforeDoctypeSystemIdentifier { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + break; + case '"': + // set system id to empty string + t.transition(DoctypeSystemIdentifier_doubleQuoted); + break; + case '\'': + // set public id to empty string + t.transition(DoctypeSystemIdentifier_singleQuoted); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.doctypePending.forceQuirks = true; + t.transition(BogusDoctype); + } + } + }, + DoctypeSystemIdentifier_doubleQuoted { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '"': + t.transition(AfterDoctypeSystemIdentifier); + break; + case nullChar: + t.error(this); + t.doctypePending.systemIdentifier.append(replacementChar); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.doctypePending.systemIdentifier.append(c); + } + } + }, + DoctypeSystemIdentifier_singleQuoted { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\'': + t.transition(AfterDoctypeSystemIdentifier); + break; + case nullChar: + t.error(this); + t.doctypePending.systemIdentifier.append(replacementChar); + break; + case '>': + t.error(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.doctypePending.systemIdentifier.append(c); + } + } + }, + AfterDoctypeSystemIdentifier { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '\t': + case '\n': + case '\f': + case ' ': + break; + case '>': + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.eofError(this); + t.doctypePending.forceQuirks = true; + t.emitDoctypePending(); + t.transition(Data); + break; + default: + t.error(this); + t.transition(BogusDoctype); + // NOT force quirks + } + } + }, + BogusDoctype { + void read(Tokeniser t, CharacterReader r) { + char c = r.consume(); + switch (c) { + case '>': + t.emitDoctypePending(); + t.transition(Data); + break; + case eof: + t.emitDoctypePending(); + t.transition(Data); + break; + default: + // ignore char + break; + } + } + }, + CdataSection { + void read(Tokeniser t, CharacterReader r) { + String data = r.consumeTo("]]>"); + t.emit(data); + r.matchConsume("]]>"); + t.transition(Data); + } + }; + + + abstract void read(Tokeniser t, CharacterReader r); + + private static final char nullChar = '\u0000'; + private static final char replacementChar = Tokeniser.replacementChar; + private static final String replacementStr = String.valueOf(Tokeniser.replacementChar); + private static final char eof = CharacterReader.EOF; +} diff --git a/server/src/org/jsoup/parser/TreeBuilder.java b/server/src/org/jsoup/parser/TreeBuilder.java new file mode 100644 index 0000000000..e06caad501 --- /dev/null +++ b/server/src/org/jsoup/parser/TreeBuilder.java @@ -0,0 +1,60 @@ +package org.jsoup.parser; + +import org.jsoup.helper.DescendableLinkedList; +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Jonathan Hedley + */ +abstract class TreeBuilder { + CharacterReader reader; + Tokeniser tokeniser; + protected Document doc; // current doc we are building into + protected DescendableLinkedList<Element> stack; // the stack of open elements + protected String baseUri; // current base uri, for creating new elements + protected Token currentToken; // currentToken is used only for error tracking. + protected ParseErrorList errors; // null when not tracking errors + + protected void initialiseParse(String input, String baseUri, ParseErrorList errors) { + Validate.notNull(input, "String input must not be null"); + Validate.notNull(baseUri, "BaseURI must not be null"); + + doc = new Document(baseUri); + reader = new CharacterReader(input); + this.errors = errors; + tokeniser = new Tokeniser(reader, errors); + stack = new DescendableLinkedList<Element>(); + this.baseUri = baseUri; + } + + Document parse(String input, String baseUri) { + return parse(input, baseUri, ParseErrorList.noTracking()); + } + + Document parse(String input, String baseUri, ParseErrorList errors) { + initialiseParse(input, baseUri, errors); + runParser(); + return doc; + } + + protected void runParser() { + while (true) { + Token token = tokeniser.read(); + process(token); + + if (token.type == Token.TokenType.EOF) + break; + } + } + + protected abstract boolean process(Token token); + + protected Element currentElement() { + return stack.getLast(); + } +} diff --git a/server/src/org/jsoup/parser/XmlTreeBuilder.java b/server/src/org/jsoup/parser/XmlTreeBuilder.java new file mode 100644 index 0000000000..3f03ad26ac --- /dev/null +++ b/server/src/org/jsoup/parser/XmlTreeBuilder.java @@ -0,0 +1,111 @@ +package org.jsoup.parser; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.*; + +import java.util.Iterator; + +/** + * @author Jonathan Hedley + */ +public class XmlTreeBuilder extends TreeBuilder { + @Override + protected void initialiseParse(String input, String baseUri, ParseErrorList errors) { + super.initialiseParse(input, baseUri, errors); + stack.add(doc); // place the document onto the stack. differs from HtmlTreeBuilder (not on stack) + } + + @Override + protected boolean process(Token token) { + // start tag, end tag, doctype, comment, character, eof + switch (token.type) { + case StartTag: + insert(token.asStartTag()); + break; + case EndTag: + popStackToClose(token.asEndTag()); + break; + case Comment: + insert(token.asComment()); + break; + case Character: + insert(token.asCharacter()); + break; + case Doctype: + insert(token.asDoctype()); + break; + case EOF: // could put some normalisation here if desired + break; + default: + Validate.fail("Unexpected token type: " + token.type); + } + return true; + } + + private void insertNode(Node node) { + currentElement().appendChild(node); + } + + Element insert(Token.StartTag startTag) { + Tag tag = Tag.valueOf(startTag.name()); + // todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html. + Element el = new Element(tag, baseUri, startTag.attributes); + insertNode(el); + if (startTag.isSelfClosing()) { + tokeniser.acknowledgeSelfClosingFlag(); + if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above. + tag.setSelfClosing(); + } else { + stack.add(el); + } + return el; + } + + void insert(Token.Comment commentToken) { + Comment comment = new Comment(commentToken.getData(), baseUri); + insertNode(comment); + } + + void insert(Token.Character characterToken) { + Node node = new TextNode(characterToken.getData(), baseUri); + insertNode(node); + } + + void insert(Token.Doctype d) { + DocumentType doctypeNode = new DocumentType(d.getName(), d.getPublicIdentifier(), d.getSystemIdentifier(), baseUri); + insertNode(doctypeNode); + } + + /** + * If the stack contains an element with this tag's name, pop up the stack to remove the first occurrence. If not + * found, skips. + * + * @param endTag + */ + private void popStackToClose(Token.EndTag endTag) { + String elName = endTag.name(); + Element firstFound = null; + + Iterator<Element> it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next.nodeName().equals(elName)) { + firstFound = next; + break; + } + } + if (firstFound == null) + return; // not found, skip + + it = stack.descendingIterator(); + while (it.hasNext()) { + Element next = it.next(); + if (next == firstFound) { + it.remove(); + break; + } else { + it.remove(); + } + } + } +} diff --git a/server/src/org/jsoup/parser/package-info.java b/server/src/org/jsoup/parser/package-info.java new file mode 100644 index 0000000000..168fdf4086 --- /dev/null +++ b/server/src/org/jsoup/parser/package-info.java @@ -0,0 +1,4 @@ +/** + Contains the HTML parser, tag specifications, and HTML tokeniser. + */ +package org.jsoup.parser; diff --git a/server/src/org/jsoup/safety/Cleaner.java b/server/src/org/jsoup/safety/Cleaner.java new file mode 100644 index 0000000000..eda67df86b --- /dev/null +++ b/server/src/org/jsoup/safety/Cleaner.java @@ -0,0 +1,129 @@ +package org.jsoup.safety; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.*; +import org.jsoup.parser.Tag; + +import java.util.List; + +/** + The whitelist based HTML cleaner. Use to ensure that end-user provided HTML contains only the elements and attributes + that you are expecting; no junk, and no cross-site scripting attacks! + <p/> + The HTML cleaner parses the input as HTML and then runs it through a white-list, so the output HTML can only contain + HTML that is allowed by the whitelist. + <p/> + It is assumed that the input HTML is a body fragment; the clean methods only pull from the source's body, and the + canned white-lists only allow body contained tags. + <p/> + Rather than interacting directly with a Cleaner object, generally see the {@code clean} methods in {@link org.jsoup.Jsoup}. + */ +public class Cleaner { + private Whitelist whitelist; + + /** + Create a new cleaner, that sanitizes documents using the supplied whitelist. + @param whitelist white-list to clean with + */ + public Cleaner(Whitelist whitelist) { + Validate.notNull(whitelist); + this.whitelist = whitelist; + } + + /** + Creates a new, clean document, from the original dirty document, containing only elements allowed by the whitelist. + The original document is not modified. Only elements from the dirt document's <code>body</code> are used. + @param dirtyDocument Untrusted base document to clean. + @return cleaned document. + */ + public Document clean(Document dirtyDocument) { + Validate.notNull(dirtyDocument); + + Document clean = Document.createShell(dirtyDocument.baseUri()); + copySafeNodes(dirtyDocument.body(), clean.body()); + + return clean; + } + + /** + Determines if the input document is valid, against the whitelist. It is considered valid if all the tags and attributes + in the input HTML are allowed by the whitelist. + <p/> + This method can be used as a validator for user input forms. An invalid document will still be cleaned successfully + using the {@link #clean(Document)} document. If using as a validator, it is recommended to still clean the document + to ensure enforced attributes are set correctly, and that the output is tidied. + @param dirtyDocument document to test + @return true if no tags or attributes need to be removed; false if they do + */ + public boolean isValid(Document dirtyDocument) { + Validate.notNull(dirtyDocument); + + Document clean = Document.createShell(dirtyDocument.baseUri()); + int numDiscarded = copySafeNodes(dirtyDocument.body(), clean.body()); + return numDiscarded == 0; + } + + /** + Iterates the input and copies trusted nodes (tags, attributes, text) into the destination. + @param source source of HTML + @param dest destination element to copy into + @return number of discarded elements (that were considered unsafe) + */ + private int copySafeNodes(Element source, Element dest) { + List<Node> sourceChildren = source.childNodes(); + int numDiscarded = 0; + + for (Node sourceChild : sourceChildren) { + if (sourceChild instanceof Element) { + Element sourceEl = (Element) sourceChild; + + if (whitelist.isSafeTag(sourceEl.tagName())) { // safe, clone and copy safe attrs + ElementMeta meta = createSafeElement(sourceEl); + Element destChild = meta.el; + dest.appendChild(destChild); + + numDiscarded += meta.numAttribsDiscarded; + numDiscarded += copySafeNodes(sourceEl, destChild); // recurs + } else { // not a safe tag, but it may have children (els or text) that are, so recurse + numDiscarded++; + numDiscarded += copySafeNodes(sourceEl, dest); + } + } else if (sourceChild instanceof TextNode) { + TextNode sourceText = (TextNode) sourceChild; + TextNode destText = new TextNode(sourceText.getWholeText(), sourceChild.baseUri()); + dest.appendChild(destText); + } // else, we don't care about comments, xml proc instructions, etc + } + return numDiscarded; + } + + private ElementMeta createSafeElement(Element sourceEl) { + String sourceTag = sourceEl.tagName(); + Attributes destAttrs = new Attributes(); + Element dest = new Element(Tag.valueOf(sourceTag), sourceEl.baseUri(), destAttrs); + int numDiscarded = 0; + + Attributes sourceAttrs = sourceEl.attributes(); + for (Attribute sourceAttr : sourceAttrs) { + if (whitelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr)) + destAttrs.put(sourceAttr); + else + numDiscarded++; + } + Attributes enforcedAttrs = whitelist.getEnforcedAttributes(sourceTag); + destAttrs.addAll(enforcedAttrs); + + return new ElementMeta(dest, numDiscarded); + } + + private static class ElementMeta { + Element el; + int numAttribsDiscarded; + + ElementMeta(Element el, int numAttribsDiscarded) { + this.el = el; + this.numAttribsDiscarded = numAttribsDiscarded; + } + } + +} diff --git a/server/src/org/jsoup/safety/Whitelist.java b/server/src/org/jsoup/safety/Whitelist.java new file mode 100644 index 0000000000..2c1150ce9e --- /dev/null +++ b/server/src/org/jsoup/safety/Whitelist.java @@ -0,0 +1,451 @@ +package org.jsoup.safety; + +/* + Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired + this whitelist configuration, and the initial defaults. + */ + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Attribute; +import org.jsoup.nodes.Attributes; +import org.jsoup.nodes.Element; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + + +/** + Whitelists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed. + <p/> + Start with one of the defaults: + <ul> + <li>{@link #none} + <li>{@link #simpleText} + <li>{@link #basic} + <li>{@link #basicWithImages} + <li>{@link #relaxed} + </ul> + <p/> + If you need to allow more through (please be careful!), tweak a base whitelist with: + <ul> + <li>{@link #addTags} + <li>{@link #addAttributes} + <li>{@link #addEnforcedAttribute} + <li>{@link #addProtocols} + </ul> + <p/> + The cleaner and these whitelists assume that you want to clean a <code>body</code> fragment of HTML (to add user + supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, either wrap the + document HTML around the cleaned body HTML, or create a whitelist that allows <code>html</code> and <code>head</code> + elements as appropriate. + <p/> + If you are going to extend a whitelist, please be very careful. Make sure you understand what attributes may lead to + XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See + http://ha.ckers.org/xss.html for some XSS attack examples. + + @author Jonathan Hedley + */ +public class Whitelist { + private Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span] + private Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag. + private Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values + private Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes + private boolean preserveRelativeLinks; // option to preserve relative links + + /** + This whitelist allows only text nodes: all HTML will be stripped. + + @return whitelist + */ + public static Whitelist none() { + return new Whitelist(); + } + + /** + This whitelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and + attributes) will be removed. + + @return whitelist + */ + public static Whitelist simpleText() { + return new Whitelist() + .addTags("b", "em", "i", "strong", "u") + ; + } + + /** + This whitelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, + ol, p, pre, q, small, strike, strong, sub, sup, u, ul</code>, and appropriate attributes. + <p/> + Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced + <code>rel=nofollow</code> attribute. + <p/> + Does not allow images. + + @return whitelist + */ + public static Whitelist basic() { + return new Whitelist() + .addTags( + "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em", + "i", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", + "sup", "u", "ul") + + .addAttributes("a", "href") + .addAttributes("blockquote", "cite") + .addAttributes("q", "cite") + + .addProtocols("a", "href", "ftp", "http", "https", "mailto") + .addProtocols("blockquote", "cite", "http", "https") + .addProtocols("cite", "cite", "http", "https") + + .addEnforcedAttribute("a", "rel", "nofollow") + ; + + } + + /** + This whitelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate + attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>. + + @return whitelist + */ + public static Whitelist basicWithImages() { + return basic() + .addTags("img") + .addAttributes("img", "align", "alt", "height", "src", "title", "width") + .addProtocols("img", "src", "http", "https") + ; + } + + /** + This whitelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite, + code, col, colgroup, dd, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, strike, strong, sub, + sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code> + <p/> + Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired. + + @return whitelist + */ + public static Whitelist relaxed() { + return new Whitelist() + .addTags( + "a", "b", "blockquote", "br", "caption", "cite", "code", "col", + "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", + "i", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", + "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", + "ul") + + .addAttributes("a", "href", "title") + .addAttributes("blockquote", "cite") + .addAttributes("col", "span", "width") + .addAttributes("colgroup", "span", "width") + .addAttributes("img", "align", "alt", "height", "src", "title", "width") + .addAttributes("ol", "start", "type") + .addAttributes("q", "cite") + .addAttributes("table", "summary", "width") + .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width") + .addAttributes( + "th", "abbr", "axis", "colspan", "rowspan", "scope", + "width") + .addAttributes("ul", "type") + + .addProtocols("a", "href", "ftp", "http", "https", "mailto") + .addProtocols("blockquote", "cite", "http", "https") + .addProtocols("img", "src", "http", "https") + .addProtocols("q", "cite", "http", "https") + ; + } + + /** + Create a new, empty whitelist. Generally it will be better to start with a default prepared whitelist instead. + + @see #basic() + @see #basicWithImages() + @see #simpleText() + @see #relaxed() + */ + public Whitelist() { + tagNames = new HashSet<TagName>(); + attributes = new HashMap<TagName, Set<AttributeKey>>(); + enforcedAttributes = new HashMap<TagName, Map<AttributeKey, AttributeValue>>(); + protocols = new HashMap<TagName, Map<AttributeKey, Set<Protocol>>>(); + preserveRelativeLinks = false; + } + + /** + Add a list of allowed elements to a whitelist. (If a tag is not allowed, it will be removed from the HTML.) + + @param tags tag names to allow + @return this (for chaining) + */ + public Whitelist addTags(String... tags) { + Validate.notNull(tags); + + for (String tagName : tags) { + Validate.notEmpty(tagName); + tagNames.add(TagName.valueOf(tagName)); + } + return this; + } + + /** + Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.) + <p/> + E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes + on <code>a</code> tags. + <p/> + To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g. + <code>addAttributes(":all", "class")</code>. + + @param tag The tag the attributes are for. The tag will be added to the allowed tag list if necessary. + @param keys List of valid attributes for the tag + @return this (for chaining) + */ + public Whitelist addAttributes(String tag, String... keys) { + Validate.notEmpty(tag); + Validate.notNull(keys); + Validate.isTrue(keys.length > 0, "No attributes supplied."); + + TagName tagName = TagName.valueOf(tag); + if (!tagNames.contains(tagName)) + tagNames.add(tagName); + Set<AttributeKey> attributeSet = new HashSet<AttributeKey>(); + for (String key : keys) { + Validate.notEmpty(key); + attributeSet.add(AttributeKey.valueOf(key)); + } + if (attributes.containsKey(tagName)) { + Set<AttributeKey> currentSet = attributes.get(tagName); + currentSet.addAll(attributeSet); + } else { + attributes.put(tagName, attributeSet); + } + return this; + } + + /** + Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element + already has the attribute set, it will be overridden. + <p/> + E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as + <code><a href="..." rel="nofollow"></code> + + @param tag The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary. + @param key The attribute key + @param value The enforced attribute value + @return this (for chaining) + */ + public Whitelist addEnforcedAttribute(String tag, String key, String value) { + Validate.notEmpty(tag); + Validate.notEmpty(key); + Validate.notEmpty(value); + + TagName tagName = TagName.valueOf(tag); + if (!tagNames.contains(tagName)) + tagNames.add(tagName); + AttributeKey attrKey = AttributeKey.valueOf(key); + AttributeValue attrVal = AttributeValue.valueOf(value); + + if (enforcedAttributes.containsKey(tagName)) { + enforcedAttributes.get(tagName).put(attrKey, attrVal); + } else { + Map<AttributeKey, AttributeValue> attrMap = new HashMap<AttributeKey, AttributeValue>(); + attrMap.put(attrKey, attrVal); + enforcedAttributes.put(tagName, attrMap); + } + return this; + } + + /** + * Configure this Whitelist to preserve relative links in an element's URL attribute, or convert them to absolute + * links. By default, this is <b>false</b>: URLs will be made absolute (e.g. start with an allowed protocol, like + * e.g. {@code http://}. + * <p /> + * Note that when handling relative links, the input document must have an appropriate {@code base URI} set when + * parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative + * links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute + * will be removed. + * + * @param preserve {@code true} to allow relative links, {@code false} (default) to deny + * @return this Whitelist, for chaining. + * @see #addProtocols + */ + public Whitelist preserveRelativeLinks(boolean preserve) { + preserveRelativeLinks = preserve; + return this; + } + + /** + Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to + URLs with the defined protocol. + <p/> + E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code> + + @param tag Tag the URL protocol is for + @param key Attribute key + @param protocols List of valid protocols + @return this, for chaining + */ + public Whitelist addProtocols(String tag, String key, String... protocols) { + Validate.notEmpty(tag); + Validate.notEmpty(key); + Validate.notNull(protocols); + + TagName tagName = TagName.valueOf(tag); + AttributeKey attrKey = AttributeKey.valueOf(key); + Map<AttributeKey, Set<Protocol>> attrMap; + Set<Protocol> protSet; + + if (this.protocols.containsKey(tagName)) { + attrMap = this.protocols.get(tagName); + } else { + attrMap = new HashMap<AttributeKey, Set<Protocol>>(); + this.protocols.put(tagName, attrMap); + } + if (attrMap.containsKey(attrKey)) { + protSet = attrMap.get(attrKey); + } else { + protSet = new HashSet<Protocol>(); + attrMap.put(attrKey, protSet); + } + for (String protocol : protocols) { + Validate.notEmpty(protocol); + Protocol prot = Protocol.valueOf(protocol); + protSet.add(prot); + } + return this; + } + + boolean isSafeTag(String tag) { + return tagNames.contains(TagName.valueOf(tag)); + } + + boolean isSafeAttribute(String tagName, Element el, Attribute attr) { + TagName tag = TagName.valueOf(tagName); + AttributeKey key = AttributeKey.valueOf(attr.getKey()); + + if (attributes.containsKey(tag)) { + if (attributes.get(tag).contains(key)) { + if (protocols.containsKey(tag)) { + Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag); + // ok if not defined protocol; otherwise test + return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key)); + } else { // attribute found, no protocols defined, so OK + return true; + } + } + } + // no attributes defined for tag, try :all tag + return !tagName.equals(":all") && isSafeAttribute(":all", el, attr); + } + + private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) { + // try to resolve relative urls to abs, and optionally update the attribute so output html has abs. + // rels without a baseuri get removed + String value = el.absUrl(attr.getKey()); + if (value.length() == 0) + value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols + if (!preserveRelativeLinks) + attr.setValue(value); + + for (Protocol protocol : protocols) { + String prot = protocol.toString() + ":"; + if (value.toLowerCase().startsWith(prot)) { + return true; + } + } + return false; + } + + Attributes getEnforcedAttributes(String tagName) { + Attributes attrs = new Attributes(); + TagName tag = TagName.valueOf(tagName); + if (enforcedAttributes.containsKey(tag)) { + Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag); + for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) { + attrs.put(entry.getKey().toString(), entry.getValue().toString()); + } + } + return attrs; + } + + // named types for config. All just hold strings, but here for my sanity. + + static class TagName extends TypedValue { + TagName(String value) { + super(value); + } + + static TagName valueOf(String value) { + return new TagName(value); + } + } + + static class AttributeKey extends TypedValue { + AttributeKey(String value) { + super(value); + } + + static AttributeKey valueOf(String value) { + return new AttributeKey(value); + } + } + + static class AttributeValue extends TypedValue { + AttributeValue(String value) { + super(value); + } + + static AttributeValue valueOf(String value) { + return new AttributeValue(value); + } + } + + static class Protocol extends TypedValue { + Protocol(String value) { + super(value); + } + + static Protocol valueOf(String value) { + return new Protocol(value); + } + } + + abstract static class TypedValue { + private String value; + + TypedValue(String value) { + Validate.notNull(value); + this.value = value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + TypedValue other = (TypedValue) obj; + if (value == null) { + if (other.value != null) return false; + } else if (!value.equals(other.value)) return false; + return true; + } + + @Override + public String toString() { + return value; + } + } +} + diff --git a/server/src/org/jsoup/safety/package-info.java b/server/src/org/jsoup/safety/package-info.java new file mode 100644 index 0000000000..ac890f0607 --- /dev/null +++ b/server/src/org/jsoup/safety/package-info.java @@ -0,0 +1,4 @@ +/** + Contains the jsoup HTML cleaner, and whitelist definitions. + */ +package org.jsoup.safety; diff --git a/server/src/org/jsoup/select/Collector.java b/server/src/org/jsoup/select/Collector.java new file mode 100644 index 0000000000..8f01045768 --- /dev/null +++ b/server/src/org/jsoup/select/Collector.java @@ -0,0 +1,51 @@ +package org.jsoup.select; + +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; + +/** + * Collects a list of elements that match the supplied criteria. + * + * @author Jonathan Hedley + */ +public class Collector { + + private Collector() { + } + + /** + Build a list of elements, by visiting root and every descendant of root, and testing it against the evaluator. + @param eval Evaluator to test elements against + @param root root of tree to descend + @return list of matches; empty if none + */ + public static Elements collect (Evaluator eval, Element root) { + Elements elements = new Elements(); + new NodeTraversor(new Accumulator(root, elements, eval)).traverse(root); + return elements; + } + + private static class Accumulator implements NodeVisitor { + private final Element root; + private final Elements elements; + private final Evaluator eval; + + Accumulator(Element root, Elements elements, Evaluator eval) { + this.root = root; + this.elements = elements; + this.eval = eval; + } + + public void head(Node node, int depth) { + if (node instanceof Element) { + Element el = (Element) node; + if (eval.matches(root, el)) + elements.add(el); + } + } + + public void tail(Node node, int depth) { + // void + } + } +} diff --git a/server/src/org/jsoup/select/CombiningEvaluator.java b/server/src/org/jsoup/select/CombiningEvaluator.java new file mode 100644 index 0000000000..a31ed2636f --- /dev/null +++ b/server/src/org/jsoup/select/CombiningEvaluator.java @@ -0,0 +1,94 @@ +package org.jsoup.select; + +import org.jsoup.helper.StringUtil; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Base combining (and, or) evaluator. + */ +abstract class CombiningEvaluator extends Evaluator { + final List<Evaluator> evaluators; + + CombiningEvaluator() { + super(); + evaluators = new ArrayList<Evaluator>(); + } + + CombiningEvaluator(Collection<Evaluator> evaluators) { + this(); + this.evaluators.addAll(evaluators); + } + + Evaluator rightMostEvaluator() { + return evaluators.size() > 0 ? evaluators.get(evaluators.size() - 1) : null; + } + + void replaceRightMostEvaluator(Evaluator replacement) { + evaluators.set(evaluators.size() - 1, replacement); + } + + static final class And extends CombiningEvaluator { + And(Collection<Evaluator> evaluators) { + super(evaluators); + } + + And(Evaluator... evaluators) { + this(Arrays.asList(evaluators)); + } + + @Override + public boolean matches(Element root, Element node) { + for (Evaluator s : evaluators) { + if (!s.matches(root, node)) + return false; + } + return true; + } + + @Override + public String toString() { + return StringUtil.join(evaluators, " "); + } + } + + static final class Or extends CombiningEvaluator { + /** + * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR. + * @param evaluators initial OR clause (these are wrapped into an AND evaluator). + */ + Or(Collection<Evaluator> evaluators) { + super(); + if (evaluators.size() > 1) + this.evaluators.add(new And(evaluators)); + else // 0 or 1 + this.evaluators.addAll(evaluators); + } + + Or() { + super(); + } + + public void add(Evaluator e) { + evaluators.add(e); + } + + @Override + public boolean matches(Element root, Element node) { + for (Evaluator s : evaluators) { + if (s.matches(root, node)) + return true; + } + return false; + } + + @Override + public String toString() { + return String.format(":or%s", evaluators); + } + } +} diff --git a/server/src/org/jsoup/select/Elements.java b/server/src/org/jsoup/select/Elements.java new file mode 100644 index 0000000000..8302da1e53 --- /dev/null +++ b/server/src/org/jsoup/select/Elements.java @@ -0,0 +1,536 @@ +package org.jsoup.select; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; + +import java.util.*; + +/** + A list of {@link Element Elements}, with methods that act on every element in the list. + <p/> + To get an Elements object, use the {@link Element#select(String)} method. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Elements implements List<Element>, Cloneable { + private List<Element> contents; + + public Elements() { + contents = new ArrayList<Element>(); + } + + public Elements(int initialCapacity) { + contents = new ArrayList<Element>(initialCapacity); + } + + public Elements(Collection<Element> elements) { + contents = new ArrayList<Element>(elements); + } + + public Elements(List<Element> elements) { + contents = elements; + } + + public Elements(Element... elements) { + this(Arrays.asList(elements)); + } + + @Override + public Elements clone() { + List<Element> elements = new ArrayList<Element>(); + + for(Element e : contents) + elements.add(e.clone()); + + + return new Elements(elements); + } + + // attribute methods + /** + Get an attribute value from the first matched element that has the attribute. + @param attributeKey The attribute key. + @return The attribute value from the first matched element that has the attribute.. If no elements were matched (isEmpty() == true), + or if the no elements have the attribute, returns empty string. + @see #hasAttr(String) + */ + public String attr(String attributeKey) { + for (Element element : contents) { + if (element.hasAttr(attributeKey)) + return element.attr(attributeKey); + } + return ""; + } + + /** + Checks if any of the matched elements have this attribute set. + @param attributeKey attribute key + @return true if any of the elements have the attribute; false if none do. + */ + public boolean hasAttr(String attributeKey) { + for (Element element : contents) { + if (element.hasAttr(attributeKey)) + return true; + } + return false; + } + + /** + * Set an attribute on all matched elements. + * @param attributeKey attribute key + * @param attributeValue attribute value + * @return this + */ + public Elements attr(String attributeKey, String attributeValue) { + for (Element element : contents) { + element.attr(attributeKey, attributeValue); + } + return this; + } + + /** + * Remove an attribute from every matched element. + * @param attributeKey The attribute to remove. + * @return this (for chaining) + */ + public Elements removeAttr(String attributeKey) { + for (Element element : contents) { + element.removeAttr(attributeKey); + } + return this; + } + + /** + Add the class name to every matched element's {@code class} attribute. + @param className class name to add + @return this + */ + public Elements addClass(String className) { + for (Element element : contents) { + element.addClass(className); + } + return this; + } + + /** + Remove the class name from every matched element's {@code class} attribute, if present. + @param className class name to remove + @return this + */ + public Elements removeClass(String className) { + for (Element element : contents) { + element.removeClass(className); + } + return this; + } + + /** + Toggle the class name on every matched element's {@code class} attribute. + @param className class name to add if missing, or remove if present, from every element. + @return this + */ + public Elements toggleClass(String className) { + for (Element element : contents) { + element.toggleClass(className); + } + return this; + } + + /** + Determine if any of the matched elements have this class name set in their {@code class} attribute. + @param className class name to check for + @return true if any do, false if none do + */ + public boolean hasClass(String className) { + for (Element element : contents) { + if (element.hasClass(className)) + return true; + } + return false; + } + + /** + * Get the form element's value of the first matched element. + * @return The form element's value, or empty if not set. + * @see Element#val() + */ + public String val() { + if (size() > 0) + return first().val(); + else + return ""; + } + + /** + * Set the form element's value in each of the matched elements. + * @param value The value to set into each matched element + * @return this (for chaining) + */ + public Elements val(String value) { + for (Element element : contents) + element.val(value); + return this; + } + + /** + * Get the combined text of all the matched elements. + * <p> + * Note that it is possible to get repeats if the matched elements contain both parent elements and their own + * children, as the Element.text() method returns the combined text of a parent and all its children. + * @return string of all text: unescaped and no HTML. + * @see Element#text() + */ + public String text() { + StringBuilder sb = new StringBuilder(); + for (Element element : contents) { + if (sb.length() != 0) + sb.append(" "); + sb.append(element.text()); + } + return sb.toString(); + } + + public boolean hasText() { + for (Element element: contents) { + if (element.hasText()) + return true; + } + return false; + } + + /** + * Get the combined inner HTML of all matched elements. + * @return string of all element's inner HTML. + * @see #text() + * @see #outerHtml() + */ + public String html() { + StringBuilder sb = new StringBuilder(); + for (Element element : contents) { + if (sb.length() != 0) + sb.append("\n"); + sb.append(element.html()); + } + return sb.toString(); + } + + /** + * Get the combined outer HTML of all matched elements. + * @return string of all element's outer HTML. + * @see #text() + * @see #html() + */ + public String outerHtml() { + StringBuilder sb = new StringBuilder(); + for (Element element : contents) { + if (sb.length() != 0) + sb.append("\n"); + sb.append(element.outerHtml()); + } + return sb.toString(); + } + + /** + * Get the combined outer HTML of all matched elements. Alias of {@link #outerHtml()}. + * @return string of all element's outer HTML. + * @see #text() + * @see #html() + */ + public String toString() { + return outerHtml(); + } + + /** + * Update the tag name of each matched element. For example, to change each {@code <i>} to a {@code <em>}, do + * {@code doc.select("i").tagName("em");} + * @param tagName the new tag name + * @return this, for chaining + * @see Element#tagName(String) + */ + public Elements tagName(String tagName) { + for (Element element : contents) { + element.tagName(tagName); + } + return this; + } + + /** + * Set the inner HTML of each matched element. + * @param html HTML to parse and set into each matched element. + * @return this, for chaining + * @see Element#html(String) + */ + public Elements html(String html) { + for (Element element : contents) { + element.html(html); + } + return this; + } + + /** + * Add the supplied HTML to the start of each matched element's inner HTML. + * @param html HTML to add inside each element, before the existing HTML + * @return this, for chaining + * @see Element#prepend(String) + */ + public Elements prepend(String html) { + for (Element element : contents) { + element.prepend(html); + } + return this; + } + + /** + * Add the supplied HTML to the end of each matched element's inner HTML. + * @param html HTML to add inside each element, after the existing HTML + * @return this, for chaining + * @see Element#append(String) + */ + public Elements append(String html) { + for (Element element : contents) { + element.append(html); + } + return this; + } + + /** + * Insert the supplied HTML before each matched element's outer HTML. + * @param html HTML to insert before each element + * @return this, for chaining + * @see Element#before(String) + */ + public Elements before(String html) { + for (Element element : contents) { + element.before(html); + } + return this; + } + + /** + * Insert the supplied HTML after each matched element's outer HTML. + * @param html HTML to insert after each element + * @return this, for chaining + * @see Element#after(String) + */ + public Elements after(String html) { + for (Element element : contents) { + element.after(html); + } + return this; + } + + /** + Wrap the supplied HTML around each matched elements. For example, with HTML + {@code <p><b>This</b> is <b>Jsoup</b></p>}, + <code>doc.select("b").wrap("<i></i>");</code> + becomes {@code <p><i><b>This</b></i> is <i><b>jsoup</b></i></p>} + @param html HTML to wrap around each element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. + @return this (for chaining) + @see Element#wrap + */ + public Elements wrap(String html) { + Validate.notEmpty(html); + for (Element element : contents) { + element.wrap(html); + } + return this; + } + + /** + * Removes the matched elements from the DOM, and moves their children up into their parents. This has the effect of + * dropping the elements but keeping their children. + * <p/> + * This is useful for e.g removing unwanted formatting elements but keeping their contents. + * <p/> + * E.g. with HTML: {@code <div><font>One</font> <font><a href="/">Two</a></font></div>}<br/> + * {@code doc.select("font").unwrap();}<br/> + * HTML = {@code <div>One <a href="/">Two</a></div>} + * + * @return this (for chaining) + * @see Node#unwrap + */ + public Elements unwrap() { + for (Element element : contents) { + element.unwrap(); + } + return this; + } + + /** + * Empty (remove all child nodes from) each matched element. This is similar to setting the inner HTML of each + * element to nothing. + * <p> + * E.g. HTML: {@code <div><p>Hello <b>there</b></p> <p>now</p></div>}<br> + * <code>doc.select("p").empty();</code><br> + * HTML = {@code <div><p></p> <p></p></div>} + * @return this, for chaining + * @see Element#empty() + * @see #remove() + */ + public Elements empty() { + for (Element element : contents) { + element.empty(); + } + return this; + } + + /** + * Remove each matched element from the DOM. This is similar to setting the outer HTML of each element to nothing. + * <p> + * E.g. HTML: {@code <div><p>Hello</p> <p>there</p> <img /></div>}<br> + * <code>doc.select("p").remove();</code><br> + * HTML = {@code <div> <img /></div>} + * <p> + * Note that this method should not be used to clean user-submitted HTML; rather, use {@link org.jsoup.safety.Cleaner} to clean HTML. + * @return this, for chaining + * @see Element#empty() + * @see #empty() + */ + public Elements remove() { + for (Element element : contents) { + element.remove(); + } + return this; + } + + // filters + + /** + * Find matching elements within this element list. + * @param query A {@link Selector} query + * @return the filtered list of elements, or an empty list if none match. + */ + public Elements select(String query) { + return Selector.select(query, this); + } + + /** + * Remove elements from this list that match the {@link Selector} query. + * <p> + * E.g. HTML: {@code <div class=logo>One</div> <div>Two</div>}<br> + * <code>Elements divs = doc.select("div").not("#logo");</code><br> + * Result: {@code divs: [<div>Two</div>]} + * <p> + * @param query the selector query whose results should be removed from these elements + * @return a new elements list that contains only the filtered results + */ + public Elements not(String query) { + Elements out = Selector.select(query, this); + return Selector.filterOut(this, out); + } + + /** + * Get the <i>nth</i> matched element as an Elements object. + * <p> + * See also {@link #get(int)} to retrieve an Element. + * @param index the (zero-based) index of the element in the list to retain + * @return Elements containing only the specified element, or, if that element did not exist, an empty list. + */ + public Elements eq(int index) { + return contents.size() > index ? new Elements(get(index)) : new Elements(); + } + + /** + * Test if any of the matched elements match the supplied query. + * @param query A selector + * @return true if at least one element in the list matches the query. + */ + public boolean is(String query) { + Elements children = select(query); + return !children.isEmpty(); + } + + /** + * Get all of the parents and ancestor elements of the matched elements. + * @return all of the parents and ancestor elements of the matched elements + */ + public Elements parents() { + HashSet<Element> combo = new LinkedHashSet<Element>(); + for (Element e: contents) { + combo.addAll(e.parents()); + } + return new Elements(combo); + } + + // list-like methods + /** + Get the first matched element. + @return The first matched element, or <code>null</code> if contents is empty; + */ + public Element first() { + return contents.isEmpty() ? null : contents.get(0); + } + + /** + Get the last matched element. + @return The last matched element, or <code>null</code> if contents is empty. + */ + public Element last() { + return contents.isEmpty() ? null : contents.get(contents.size() - 1); + } + + /** + * Perform a depth-first traversal on each of the selected elements. + * @param nodeVisitor the visitor callbacks to perform on each node + * @return this, for chaining + */ + public Elements traverse(NodeVisitor nodeVisitor) { + Validate.notNull(nodeVisitor); + NodeTraversor traversor = new NodeTraversor(nodeVisitor); + for (Element el: contents) { + traversor.traverse(el); + } + return this; + } + + // implements List<Element> delegates: + public int size() {return contents.size();} + + public boolean isEmpty() {return contents.isEmpty();} + + public boolean contains(Object o) {return contents.contains(o);} + + public Iterator<Element> iterator() {return contents.iterator();} + + public Object[] toArray() {return contents.toArray();} + + public <T> T[] toArray(T[] a) {return contents.toArray(a);} + + public boolean add(Element element) {return contents.add(element);} + + public boolean remove(Object o) {return contents.remove(o);} + + public boolean containsAll(Collection<?> c) {return contents.containsAll(c);} + + public boolean addAll(Collection<? extends Element> c) {return contents.addAll(c);} + + public boolean addAll(int index, Collection<? extends Element> c) {return contents.addAll(index, c);} + + public boolean removeAll(Collection<?> c) {return contents.removeAll(c);} + + public boolean retainAll(Collection<?> c) {return contents.retainAll(c);} + + public void clear() {contents.clear();} + + public boolean equals(Object o) {return contents.equals(o);} + + public int hashCode() {return contents.hashCode();} + + public Element get(int index) {return contents.get(index);} + + public Element set(int index, Element element) {return contents.set(index, element);} + + public void add(int index, Element element) {contents.add(index, element);} + + public Element remove(int index) {return contents.remove(index);} + + public int indexOf(Object o) {return contents.indexOf(o);} + + public int lastIndexOf(Object o) {return contents.lastIndexOf(o);} + + public ListIterator<Element> listIterator() {return contents.listIterator();} + + public ListIterator<Element> listIterator(int index) {return contents.listIterator(index);} + + public List<Element> subList(int fromIndex, int toIndex) {return contents.subList(fromIndex, toIndex);} +} diff --git a/server/src/org/jsoup/select/Evaluator.java b/server/src/org/jsoup/select/Evaluator.java new file mode 100644 index 0000000000..16a083bd77 --- /dev/null +++ b/server/src/org/jsoup/select/Evaluator.java @@ -0,0 +1,454 @@ +package org.jsoup.select; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Element; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Evaluates that an element matches the selector. + */ +public abstract class Evaluator { + protected Evaluator() { + } + + /** + * Test if the element meets the evaluator's requirements. + * + * @param root Root of the matching subtree + * @param element tested element + */ + public abstract boolean matches(Element root, Element element); + + /** + * Evaluator for tag name + */ + public static final class Tag extends Evaluator { + private String tagName; + + public Tag(String tagName) { + this.tagName = tagName; + } + + @Override + public boolean matches(Element root, Element element) { + return (element.tagName().equals(tagName)); + } + + @Override + public String toString() { + return String.format("%s", tagName); + } + } + + /** + * Evaluator for element id + */ + public static final class Id extends Evaluator { + private String id; + + public Id(String id) { + this.id = id; + } + + @Override + public boolean matches(Element root, Element element) { + return (id.equals(element.id())); + } + + @Override + public String toString() { + return String.format("#%s", id); + } + + } + + /** + * Evaluator for element class + */ + public static final class Class extends Evaluator { + private String className; + + public Class(String className) { + this.className = className; + } + + @Override + public boolean matches(Element root, Element element) { + return (element.hasClass(className)); + } + + @Override + public String toString() { + return String.format(".%s", className); + } + + } + + /** + * Evaluator for attribute name matching + */ + public static final class Attribute extends Evaluator { + private String key; + + public Attribute(String key) { + this.key = key; + } + + @Override + public boolean matches(Element root, Element element) { + return element.hasAttr(key); + } + + @Override + public String toString() { + return String.format("[%s]", key); + } + + } + + /** + * Evaluator for attribute name prefix matching + */ + public static final class AttributeStarting extends Evaluator { + private String keyPrefix; + + public AttributeStarting(String keyPrefix) { + this.keyPrefix = keyPrefix; + } + + @Override + public boolean matches(Element root, Element element) { + List<org.jsoup.nodes.Attribute> values = element.attributes().asList(); + for (org.jsoup.nodes.Attribute attribute : values) { + if (attribute.getKey().startsWith(keyPrefix)) + return true; + } + return false; + } + + @Override + public String toString() { + return String.format("[^%s]", keyPrefix); + } + + } + + /** + * Evaluator for attribute name/value matching + */ + public static final class AttributeWithValue extends AttributeKeyPair { + public AttributeWithValue(String key, String value) { + super(key, value); + } + + @Override + public boolean matches(Element root, Element element) { + return element.hasAttr(key) && value.equalsIgnoreCase(element.attr(key)); + } + + @Override + public String toString() { + return String.format("[%s=%s]", key, value); + } + + } + + /** + * Evaluator for attribute name != value matching + */ + public static final class AttributeWithValueNot extends AttributeKeyPair { + public AttributeWithValueNot(String key, String value) { + super(key, value); + } + + @Override + public boolean matches(Element root, Element element) { + return !value.equalsIgnoreCase(element.attr(key)); + } + + @Override + public String toString() { + return String.format("[%s!=%s]", key, value); + } + + } + + /** + * Evaluator for attribute name/value matching (value prefix) + */ + public static final class AttributeWithValueStarting extends AttributeKeyPair { + public AttributeWithValueStarting(String key, String value) { + super(key, value); + } + + @Override + public boolean matches(Element root, Element element) { + return element.hasAttr(key) && element.attr(key).toLowerCase().startsWith(value); // value is lower case already + } + + @Override + public String toString() { + return String.format("[%s^=%s]", key, value); + } + + } + + /** + * Evaluator for attribute name/value matching (value ending) + */ + public static final class AttributeWithValueEnding extends AttributeKeyPair { + public AttributeWithValueEnding(String key, String value) { + super(key, value); + } + + @Override + public boolean matches(Element root, Element element) { + return element.hasAttr(key) && element.attr(key).toLowerCase().endsWith(value); // value is lower case + } + + @Override + public String toString() { + return String.format("[%s$=%s]", key, value); + } + + } + + /** + * Evaluator for attribute name/value matching (value containing) + */ + public static final class AttributeWithValueContaining extends AttributeKeyPair { + public AttributeWithValueContaining(String key, String value) { + super(key, value); + } + + @Override + public boolean matches(Element root, Element element) { + return element.hasAttr(key) && element.attr(key).toLowerCase().contains(value); // value is lower case + } + + @Override + public String toString() { + return String.format("[%s*=%s]", key, value); + } + + } + + /** + * Evaluator for attribute name/value matching (value regex matching) + */ + public static final class AttributeWithValueMatching extends Evaluator { + String key; + Pattern pattern; + + public AttributeWithValueMatching(String key, Pattern pattern) { + this.key = key.trim().toLowerCase(); + this.pattern = pattern; + } + + @Override + public boolean matches(Element root, Element element) { + return element.hasAttr(key) && pattern.matcher(element.attr(key)).find(); + } + + @Override + public String toString() { + return String.format("[%s~=%s]", key, pattern.toString()); + } + + } + + /** + * Abstract evaluator for attribute name/value matching + */ + public abstract static class AttributeKeyPair extends Evaluator { + String key; + String value; + + public AttributeKeyPair(String key, String value) { + Validate.notEmpty(key); + Validate.notEmpty(value); + + this.key = key.trim().toLowerCase(); + this.value = value.trim().toLowerCase(); + } + } + + /** + * Evaluator for any / all element matching + */ + public static final class AllElements extends Evaluator { + + @Override + public boolean matches(Element root, Element element) { + return true; + } + + @Override + public String toString() { + return "*"; + } + } + + /** + * Evaluator for matching by sibling index number (e < idx) + */ + public static final class IndexLessThan extends IndexEvaluator { + public IndexLessThan(int index) { + super(index); + } + + @Override + public boolean matches(Element root, Element element) { + return element.elementSiblingIndex() < index; + } + + @Override + public String toString() { + return String.format(":lt(%d)", index); + } + + } + + /** + * Evaluator for matching by sibling index number (e > idx) + */ + public static final class IndexGreaterThan extends IndexEvaluator { + public IndexGreaterThan(int index) { + super(index); + } + + @Override + public boolean matches(Element root, Element element) { + return element.elementSiblingIndex() > index; + } + + @Override + public String toString() { + return String.format(":gt(%d)", index); + } + + } + + /** + * Evaluator for matching by sibling index number (e = idx) + */ + public static final class IndexEquals extends IndexEvaluator { + public IndexEquals(int index) { + super(index); + } + + @Override + public boolean matches(Element root, Element element) { + return element.elementSiblingIndex() == index; + } + + @Override + public String toString() { + return String.format(":eq(%d)", index); + } + + } + + /** + * Abstract evaluator for sibling index matching + * + * @author ant + */ + public abstract static class IndexEvaluator extends Evaluator { + int index; + + public IndexEvaluator(int index) { + this.index = index; + } + } + + /** + * Evaluator for matching Element (and its descendants) text + */ + public static final class ContainsText extends Evaluator { + private String searchText; + + public ContainsText(String searchText) { + this.searchText = searchText.toLowerCase(); + } + + @Override + public boolean matches(Element root, Element element) { + return (element.text().toLowerCase().contains(searchText)); + } + + @Override + public String toString() { + return String.format(":contains(%s", searchText); + } + } + + /** + * Evaluator for matching Element's own text + */ + public static final class ContainsOwnText extends Evaluator { + private String searchText; + + public ContainsOwnText(String searchText) { + this.searchText = searchText.toLowerCase(); + } + + @Override + public boolean matches(Element root, Element element) { + return (element.ownText().toLowerCase().contains(searchText)); + } + + @Override + public String toString() { + return String.format(":containsOwn(%s", searchText); + } + } + + /** + * Evaluator for matching Element (and its descendants) text with regex + */ + public static final class Matches extends Evaluator { + private Pattern pattern; + + public Matches(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean matches(Element root, Element element) { + Matcher m = pattern.matcher(element.text()); + return m.find(); + } + + @Override + public String toString() { + return String.format(":matches(%s", pattern); + } + } + + /** + * Evaluator for matching Element's own text with regex + */ + public static final class MatchesOwn extends Evaluator { + private Pattern pattern; + + public MatchesOwn(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean matches(Element root, Element element) { + Matcher m = pattern.matcher(element.ownText()); + return m.find(); + } + + @Override + public String toString() { + return String.format(":matchesOwn(%s", pattern); + } + } +} diff --git a/server/src/org/jsoup/select/NodeTraversor.java b/server/src/org/jsoup/select/NodeTraversor.java new file mode 100644 index 0000000000..9bb081e56c --- /dev/null +++ b/server/src/org/jsoup/select/NodeTraversor.java @@ -0,0 +1,47 @@ +package org.jsoup.select; + +import org.jsoup.nodes.Node; + +/** + * Depth-first node traversor. Use to iterate through all nodes under and including the specified root node. + * <p/> + * This implementation does not use recursion, so a deep DOM does not risk blowing the stack. + */ +public class NodeTraversor { + private NodeVisitor visitor; + + /** + * Create a new traversor. + * @param visitor a class implementing the {@link NodeVisitor} interface, to be called when visiting each node. + */ + public NodeTraversor(NodeVisitor visitor) { + this.visitor = visitor; + } + + /** + * Start a depth-first traverse of the root and all of its descendants. + * @param root the root node point to traverse. + */ + public void traverse(Node root) { + Node node = root; + int depth = 0; + + while (node != null) { + visitor.head(node, depth); + if (node.childNodes().size() > 0) { + node = node.childNode(0); + depth++; + } else { + while (node.nextSibling() == null && depth > 0) { + visitor.tail(node, depth); + node = node.parent(); + depth--; + } + visitor.tail(node, depth); + if (node == root) + break; + node = node.nextSibling(); + } + } + } +} diff --git a/server/src/org/jsoup/select/NodeVisitor.java b/server/src/org/jsoup/select/NodeVisitor.java new file mode 100644 index 0000000000..20112e8d29 --- /dev/null +++ b/server/src/org/jsoup/select/NodeVisitor.java @@ -0,0 +1,30 @@ +package org.jsoup.select; + +import org.jsoup.nodes.Node; + +/** + * Node visitor interface. Provide an implementing class to {@link NodeTraversor} to iterate through nodes. + * <p/> + * This interface provides two methods, {@code head} and {@code tail}. The head method is called when the node is first + * seen, and the tail method when all of the node's children have been visited. As an example, head can be used to + * create a start tag for a node, and tail to create the end tag. + */ +public interface NodeVisitor { + /** + * Callback for when a node is first visited. + * + * @param node the node being visited. + * @param depth the depth of the node, relative to the root node. E.g., the root node has depth 0, and a child node + * of that will have depth 1. + */ + public void head(Node node, int depth); + + /** + * Callback for when a node is last visited, after all of its descendants have been visited. + * + * @param node the node being visited. + * @param depth the depth of the node, relative to the root node. E.g., the root node has depth 0, and a child node + * of that will have depth 1. + */ + public void tail(Node node, int depth); +} diff --git a/server/src/org/jsoup/select/QueryParser.java b/server/src/org/jsoup/select/QueryParser.java new file mode 100644 index 0000000000..d3cc36f91c --- /dev/null +++ b/server/src/org/jsoup/select/QueryParser.java @@ -0,0 +1,293 @@ +package org.jsoup.select; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.parser.TokenQueue; + +/** + * Parses a CSS selector into an Evaluator tree. + */ +class QueryParser { + private final static String[] combinators = {",", ">", "+", "~", " "}; + + private TokenQueue tq; + private String query; + private List<Evaluator> evals = new ArrayList<Evaluator>(); + + /** + * Create a new QueryParser. + * @param query CSS query + */ + private QueryParser(String query) { + this.query = query; + this.tq = new TokenQueue(query); + } + + /** + * Parse a CSS query into an Evaluator. + * @param query CSS query + * @return Evaluator + */ + public static Evaluator parse(String query) { + QueryParser p = new QueryParser(query); + return p.parse(); + } + + /** + * Parse the query + * @return Evaluator + */ + Evaluator parse() { + tq.consumeWhitespace(); + + if (tq.matchesAny(combinators)) { // if starts with a combinator, use root as elements + evals.add(new StructuralEvaluator.Root()); + combinator(tq.consume()); + } else { + findElements(); + } + + while (!tq.isEmpty()) { + // hierarchy and extras + boolean seenWhite = tq.consumeWhitespace(); + + if (tq.matchesAny(combinators)) { + combinator(tq.consume()); + } else if (seenWhite) { + combinator(' '); + } else { // E.class, E#id, E[attr] etc. AND + findElements(); // take next el, #. etc off queue + } + } + + if (evals.size() == 1) + return evals.get(0); + + return new CombiningEvaluator.And(evals); + } + + private void combinator(char combinator) { + tq.consumeWhitespace(); + String subQuery = consumeSubQuery(); // support multi > childs + + Evaluator rootEval; // the new topmost evaluator + Evaluator currentEval; // the evaluator the new eval will be combined to. could be root, or rightmost or. + Evaluator newEval = parse(subQuery); // the evaluator to add into target evaluator + boolean replaceRightMost = false; + + if (evals.size() == 1) { + rootEval = currentEval = evals.get(0); + // make sure OR (,) has precedence: + if (rootEval instanceof CombiningEvaluator.Or && combinator != ',') { + currentEval = ((CombiningEvaluator.Or) currentEval).rightMostEvaluator(); + replaceRightMost = true; + } + } + else { + rootEval = currentEval = new CombiningEvaluator.And(evals); + } + evals.clear(); + + // for most combinators: change the current eval into an AND of the current eval and the new eval + if (combinator == '>') + currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.ImmediateParent(currentEval)); + else if (combinator == ' ') + currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.Parent(currentEval)); + else if (combinator == '+') + currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.ImmediatePreviousSibling(currentEval)); + else if (combinator == '~') + currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.PreviousSibling(currentEval)); + else if (combinator == ',') { // group or. + CombiningEvaluator.Or or; + if (currentEval instanceof CombiningEvaluator.Or) { + or = (CombiningEvaluator.Or) currentEval; + or.add(newEval); + } else { + or = new CombiningEvaluator.Or(); + or.add(currentEval); + or.add(newEval); + } + currentEval = or; + } + else + throw new Selector.SelectorParseException("Unknown combinator: " + combinator); + + if (replaceRightMost) + ((CombiningEvaluator.Or) rootEval).replaceRightMostEvaluator(currentEval); + else rootEval = currentEval; + evals.add(rootEval); + } + + private String consumeSubQuery() { + StringBuilder sq = new StringBuilder(); + while (!tq.isEmpty()) { + if (tq.matches("(")) + sq.append("(").append(tq.chompBalanced('(', ')')).append(")"); + else if (tq.matches("[")) + sq.append("[").append(tq.chompBalanced('[', ']')).append("]"); + else if (tq.matchesAny(combinators)) + break; + else + sq.append(tq.consume()); + } + return sq.toString(); + } + + private void findElements() { + if (tq.matchChomp("#")) + byId(); + else if (tq.matchChomp(".")) + byClass(); + else if (tq.matchesWord()) + byTag(); + else if (tq.matches("[")) + byAttribute(); + else if (tq.matchChomp("*")) + allElements(); + else if (tq.matchChomp(":lt(")) + indexLessThan(); + else if (tq.matchChomp(":gt(")) + indexGreaterThan(); + else if (tq.matchChomp(":eq(")) + indexEquals(); + else if (tq.matches(":has(")) + has(); + else if (tq.matches(":contains(")) + contains(false); + else if (tq.matches(":containsOwn(")) + contains(true); + else if (tq.matches(":matches(")) + matches(false); + else if (tq.matches(":matchesOwn(")) + matches(true); + else if (tq.matches(":not(")) + not(); + else // unhandled + throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, tq.remainder()); + + } + + private void byId() { + String id = tq.consumeCssIdentifier(); + Validate.notEmpty(id); + evals.add(new Evaluator.Id(id)); + } + + private void byClass() { + String className = tq.consumeCssIdentifier(); + Validate.notEmpty(className); + evals.add(new Evaluator.Class(className.trim().toLowerCase())); + } + + private void byTag() { + String tagName = tq.consumeElementSelector(); + Validate.notEmpty(tagName); + + // namespaces: if element name is "abc:def", selector must be "abc|def", so flip: + if (tagName.contains("|")) + tagName = tagName.replace("|", ":"); + + evals.add(new Evaluator.Tag(tagName.trim().toLowerCase())); + } + + private void byAttribute() { + TokenQueue cq = new TokenQueue(tq.chompBalanced('[', ']')); // content queue + String key = cq.consumeToAny("=", "!=", "^=", "$=", "*=", "~="); // eq, not, start, end, contain, match, (no val) + Validate.notEmpty(key); + cq.consumeWhitespace(); + + if (cq.isEmpty()) { + if (key.startsWith("^")) + evals.add(new Evaluator.AttributeStarting(key.substring(1))); + else + evals.add(new Evaluator.Attribute(key)); + } else { + if (cq.matchChomp("=")) + evals.add(new Evaluator.AttributeWithValue(key, cq.remainder())); + + else if (cq.matchChomp("!=")) + evals.add(new Evaluator.AttributeWithValueNot(key, cq.remainder())); + + else if (cq.matchChomp("^=")) + evals.add(new Evaluator.AttributeWithValueStarting(key, cq.remainder())); + + else if (cq.matchChomp("$=")) + evals.add(new Evaluator.AttributeWithValueEnding(key, cq.remainder())); + + else if (cq.matchChomp("*=")) + evals.add(new Evaluator.AttributeWithValueContaining(key, cq.remainder())); + + else if (cq.matchChomp("~=")) + evals.add(new Evaluator.AttributeWithValueMatching(key, Pattern.compile(cq.remainder()))); + else + throw new Selector.SelectorParseException("Could not parse attribute query '%s': unexpected token at '%s'", query, cq.remainder()); + } + } + + private void allElements() { + evals.add(new Evaluator.AllElements()); + } + + // pseudo selectors :lt, :gt, :eq + private void indexLessThan() { + evals.add(new Evaluator.IndexLessThan(consumeIndex())); + } + + private void indexGreaterThan() { + evals.add(new Evaluator.IndexGreaterThan(consumeIndex())); + } + + private void indexEquals() { + evals.add(new Evaluator.IndexEquals(consumeIndex())); + } + + private int consumeIndex() { + String indexS = tq.chompTo(")").trim(); + Validate.isTrue(StringUtil.isNumeric(indexS), "Index must be numeric"); + return Integer.parseInt(indexS); + } + + // pseudo selector :has(el) + private void has() { + tq.consume(":has"); + String subQuery = tq.chompBalanced('(', ')'); + Validate.notEmpty(subQuery, ":has(el) subselect must not be empty"); + evals.add(new StructuralEvaluator.Has(parse(subQuery))); + } + + // pseudo selector :contains(text), containsOwn(text) + private void contains(boolean own) { + tq.consume(own ? ":containsOwn" : ":contains"); + String searchText = TokenQueue.unescape(tq.chompBalanced('(', ')')); + Validate.notEmpty(searchText, ":contains(text) query must not be empty"); + if (own) + evals.add(new Evaluator.ContainsOwnText(searchText)); + else + evals.add(new Evaluator.ContainsText(searchText)); + } + + // :matches(regex), matchesOwn(regex) + private void matches(boolean own) { + tq.consume(own ? ":matchesOwn" : ":matches"); + String regex = tq.chompBalanced('(', ')'); // don't unescape, as regex bits will be escaped + Validate.notEmpty(regex, ":matches(regex) query must not be empty"); + + if (own) + evals.add(new Evaluator.MatchesOwn(Pattern.compile(regex))); + else + evals.add(new Evaluator.Matches(Pattern.compile(regex))); + } + + // :not(selector) + private void not() { + tq.consume(":not"); + String subQuery = tq.chompBalanced('(', ')'); + Validate.notEmpty(subQuery, ":not(selector) subselect must not be empty"); + + evals.add(new StructuralEvaluator.Not(parse(subQuery))); + } +} diff --git a/server/src/org/jsoup/select/Selector.java b/server/src/org/jsoup/select/Selector.java new file mode 100644 index 0000000000..8fc6286798 --- /dev/null +++ b/server/src/org/jsoup/select/Selector.java @@ -0,0 +1,126 @@ +package org.jsoup.select; + +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Element; + +import java.util.Collection; +import java.util.LinkedHashSet; + +/** + * CSS-like element selector, that finds elements matching a query. + * <p/> + * <h2>Selector syntax</h2> + * A selector is a chain of simple selectors, separated by combinators. Selectors are case insensitive (including against + * elements, attributes, and attribute values). + * <p/> + * The universal selector (*) is implicit when no element selector is supplied (i.e. {@code *.header} and {@code .header} + * is equivalent). + * <p/> + * <table> + * <tr><th>Pattern</th><th>Matches</th><th>Example</th></tr> + * <tr><td><code>*</code></td><td>any element</td><td><code>*</code></td></tr> + * <tr><td><code>tag</code></td><td>elements with the given tag name</td><td><code>div</code></td></tr> + * <tr><td><code>ns|E</code></td><td>elements of type E in the namespace <i>ns</i></td><td><code>fb|name</code> finds <code><fb:name></code> elements</td></tr> + * <tr><td><code>#id</code></td><td>elements with attribute ID of "id"</td><td><code>div#wrap</code>, <code>#logo</code></td></tr> + * <tr><td><code>.class</code></td><td>elements with a class name of "class"</td><td><code>div.left</code>, <code>.result</code></td></tr> + * <tr><td><code>[attr]</code></td><td>elements with an attribute named "attr" (with any value)</td><td><code>a[href]</code>, <code>[title]</code></td></tr> + * <tr><td><code>[^attrPrefix]</code></td><td>elements with an attribute name starting with "attrPrefix". Use to find elements with HTML5 datasets</td><td><code>[^data-]</code>, <code>div[^data-]</code></td></tr> + * <tr><td><code>[attr=val]</code></td><td>elements with an attribute named "attr", and value equal to "val"</td><td><code>img[width=500]</code>, <code>a[rel=nofollow]</code></td></tr> + * <tr><td><code>[attr^=valPrefix]</code></td><td>elements with an attribute named "attr", and value starting with "valPrefix"</td><td><code>a[href^=http:]</code></code></td></tr> + * <tr><td><code>[attr$=valSuffix]</code></td><td>elements with an attribute named "attr", and value ending with "valSuffix"</td><td><code>img[src$=.png]</code></td></tr> + * <tr><td><code>[attr*=valContaining]</code></td><td>elements with an attribute named "attr", and value containing "valContaining"</td><td><code>a[href*=/search/]</code></td></tr> + * <tr><td><code>[attr~=<em>regex</em>]</code></td><td>elements with an attribute named "attr", and value matching the regular expression</td><td><code>img[src~=(?i)\\.(png|jpe?g)]</code></td></tr> + * <tr><td></td><td>The above may be combined in any order</td><td><code>div.header[title]</code></td></tr> + * <tr><td><td colspan="3"><h3>Combinators</h3></td></tr> + * <tr><td><code>E F</code></td><td>an F element descended from an E element</td><td><code>div a</code>, <code>.logo h1</code></td></tr> + * <tr><td><code>E > F</code></td><td>an F direct child of E</td><td><code>ol > li</code></td></tr> + * <tr><td><code>E + F</code></td><td>an F element immediately preceded by sibling E</td><td><code>li + li</code>, <code>div.head + div</code></td></tr> + * <tr><td><code>E ~ F</code></td><td>an F element preceded by sibling E</td><td><code>h1 ~ p</code></td></tr> + * <tr><td><code>E, F, G</code></td><td>all matching elements E, F, or G</td><td><code>a[href], div, h3</code></td></tr> + * <tr><td><td colspan="3"><h3>Pseudo selectors</h3></td></tr> + * <tr><td><code>:lt(<em>n</em>)</code></td><td>elements whose sibling index is less than <em>n</em></td><td><code>td:lt(3)</code> finds the first 2 cells of each row</td></tr> + * <tr><td><code>:gt(<em>n</em>)</code></td><td>elements whose sibling index is greater than <em>n</em></td><td><code>td:gt(1)</code> finds cells after skipping the first two</td></tr> + * <tr><td><code>:eq(<em>n</em>)</code></td><td>elements whose sibling index is equal to <em>n</em></td><td><code>td:eq(0)</code> finds the first cell of each row</td></tr> + * <tr><td><code>:has(<em>selector</em>)</code></td><td>elements that contains at least one element matching the <em>selector</em></td><td><code>div:has(p)</code> finds divs that contain p elements </td></tr> + * <tr><td><code>:not(<em>selector</em>)</code></td><td>elements that do not match the <em>selector</em>. See also {@link Elements#not(String)}</td><td><code>div:not(.logo)</code> finds all divs that do not have the "logo" class.<br /><code>div:not(:has(div))</code> finds divs that do not contain divs.</code></td></tr> + * <tr><td><code>:contains(<em>text</em>)</code></td><td>elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.</td><td><code>p:contains(jsoup)</code> finds p elements containing the text "jsoup".</td></tr> + * <tr><td><code>:matches(<em>regex</em>)</code></td><td>elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.</td><td><code>td:matches(\\d+)</code> finds table cells containing digits. <code>div:matches((?i)login)</code> finds divs containing the text, case insensitively.</td></tr> + * <tr><td><code>:containsOwn(<em>text</em>)</code></td><td>elements that directly contains the specified text. The search is case insensitive. The text must appear in the found element, not any of its descendants.</td><td><code>p:containsOwn(jsoup)</code> finds p elements with own text "jsoup".</td></tr> + * <tr><td><code>:matchesOwn(<em>regex</em>)</code></td><td>elements whose own text matches the specified regular expression. The text must appear in the found element, not any of its descendants.</td><td><code>td:matchesOwn(\\d+)</code> finds table cells directly containing digits. <code>div:matchesOwn((?i)login)</code> finds divs containing the text, case insensitively.</td></tr> + * <tr><td></td><td>The above may be combined in any order and with other selectors</td><td><code>.light:contains(name):eq(0)</code></td></tr> + * </table> + * + * @author Jonathan Hedley, jonathan@hedley.net + * @see Element#select(String) + */ +public class Selector { + private final Evaluator evaluator; + private final Element root; + + private Selector(String query, Element root) { + Validate.notNull(query); + query = query.trim(); + Validate.notEmpty(query); + Validate.notNull(root); + + this.evaluator = QueryParser.parse(query); + + this.root = root; + } + + /** + * Find elements matching selector. + * + * @param query CSS selector + * @param root root element to descend into + * @return matching elements, empty if not + */ + public static Elements select(String query, Element root) { + return new Selector(query, root).select(); + } + + /** + * Find elements matching selector. + * + * @param query CSS selector + * @param roots root elements to descend into + * @return matching elements, empty if not + */ + public static Elements select(String query, Iterable<Element> roots) { + Validate.notEmpty(query); + Validate.notNull(roots); + LinkedHashSet<Element> elements = new LinkedHashSet<Element>(); + + for (Element root : roots) { + elements.addAll(select(query, root)); + } + return new Elements(elements); + } + + private Elements select() { + return Collector.collect(evaluator, root); + } + + // exclude set. package open so that Elements can implement .not() selector. + static Elements filterOut(Collection<Element> elements, Collection<Element> outs) { + Elements output = new Elements(); + for (Element el : elements) { + boolean found = false; + for (Element out : outs) { + if (el.equals(out)) { + found = true; + break; + } + } + if (!found) + output.add(el); + } + return output; + } + + public static class SelectorParseException extends IllegalStateException { + public SelectorParseException(String msg, Object... params) { + super(String.format(msg, params)); + } + } +} diff --git a/server/src/org/jsoup/select/StructuralEvaluator.java b/server/src/org/jsoup/select/StructuralEvaluator.java new file mode 100644 index 0000000000..69e8a62e58 --- /dev/null +++ b/server/src/org/jsoup/select/StructuralEvaluator.java @@ -0,0 +1,132 @@ +package org.jsoup.select; + +import org.jsoup.nodes.Element; + +/** + * Base structural evaluator. + */ +abstract class StructuralEvaluator extends Evaluator { + Evaluator evaluator; + + static class Root extends Evaluator { + public boolean matches(Element root, Element element) { + return root == element; + } + } + + static class Has extends StructuralEvaluator { + public Has(Evaluator evaluator) { + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element element) { + for (Element e : element.getAllElements()) { + if (e != element && evaluator.matches(root, e)) + return true; + } + return false; + } + + public String toString() { + return String.format(":has(%s)", evaluator); + } + } + + static class Not extends StructuralEvaluator { + public Not(Evaluator evaluator) { + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element node) { + return !evaluator.matches(root, node); + } + + public String toString() { + return String.format(":not%s", evaluator); + } + } + + static class Parent extends StructuralEvaluator { + public Parent(Evaluator evaluator) { + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element element) { + if (root == element) + return false; + + Element parent = element.parent(); + while (parent != root) { + if (evaluator.matches(root, parent)) + return true; + parent = parent.parent(); + } + return false; + } + + public String toString() { + return String.format(":parent%s", evaluator); + } + } + + static class ImmediateParent extends StructuralEvaluator { + public ImmediateParent(Evaluator evaluator) { + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element element) { + if (root == element) + return false; + + Element parent = element.parent(); + return parent != null && evaluator.matches(root, parent); + } + + public String toString() { + return String.format(":ImmediateParent%s", evaluator); + } + } + + static class PreviousSibling extends StructuralEvaluator { + public PreviousSibling(Evaluator evaluator) { + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element element) { + if (root == element) + return false; + + Element prev = element.previousElementSibling(); + + while (prev != null) { + if (evaluator.matches(root, prev)) + return true; + + prev = prev.previousElementSibling(); + } + return false; + } + + public String toString() { + return String.format(":prev*%s", evaluator); + } + } + + static class ImmediatePreviousSibling extends StructuralEvaluator { + public ImmediatePreviousSibling(Evaluator evaluator) { + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element element) { + if (root == element) + return false; + + Element prev = element.previousElementSibling(); + return prev != null && evaluator.matches(root, prev); + } + + public String toString() { + return String.format(":prev%s", evaluator); + } + } +} diff --git a/server/src/org/jsoup/select/package-info.java b/server/src/org/jsoup/select/package-info.java new file mode 100644 index 0000000000..a6e6a2fa0f --- /dev/null +++ b/server/src/org/jsoup/select/package-info.java @@ -0,0 +1,4 @@ +/** + Packages to support the CSS-style element selector. + */ +package org.jsoup.select;
\ No newline at end of file |