zk/src/main/java/org/zkoss/zk/device/Devices.java
/* Devices.java
Purpose:
Description:
History:
Tue May 15 14:09:16 2007, Created by tomyeh
Copyright (C) 2007 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.device;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.zkoss.idom.Element;
import org.zkoss.idom.util.IDOMs;
import org.zkoss.io.Files;
import org.zkoss.lang.Classes;
import org.zkoss.zk.mesg.MZk;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.sys.ServerPush;
/**
* A manager of devices ({@link Device}).
*
* @author tomyeh
* @since 2.4.0
*/
public class Devices {
private Devices() {
}
/** Map(String type, DeviceInfo info or Device device). */
private static final Map<String, Object> _devmap = new LinkedHashMap<String, Object>(4);
/** A list of devices registered. It is a duplicated info (of _devmap)
* but used to improve the performance.
*/
private static Device[] _devs = new Device[0];
/** Returns the device for the specified desktop type.
*
* @param deviceType the device type, such as ajax, xml and mil.
* @exception DeviceNotFoundException if not found.
* @since 3.0.0
*/
public static final Device getDevice(String deviceType) throws DeviceNotFoundException {
final Object o; //null, Device or DeviceInfo
synchronized (_devmap) {
o = _devmap.get(deviceType);
}
if (o instanceof Device)
return (Device) o;
if (o == null)
throw new DeviceNotFoundException(deviceType, MZk.NOT_FOUND, deviceType);
final Device device = ((DeviceInfo) o).newDevice(deviceType);
final List<Device> devs = new LinkedList<Device>();
synchronized (_devmap) {
final Object old = _devmap.put(deviceType, device);
if (old != o)
_devmap.put(deviceType, old); //changed by someone else; so restore
for (Object d : _devmap.values()) {
if (d instanceof Device)
devs.add((Device) d);
}
}
_devs = devs.toArray(new Device[devs.size()]);
return device;
}
/** Returns the device for the specified client.
* It invokes {@link Device#isCompatible} to return the correct device.
*If all devices returns null (means unknown), one of the devices returning
*null is returned. If all devices returns Boolean.FALSE,
*DeviceNotFoundException is thrown.
*
* @param userAgent represents a client.
* It is the user-agent header for HTTP-base client.
* @see org.zkoss.zk.ui.Execution#getUserAgent
* @exception DeviceNotFoundException if not found.
* @since 3.0.0
*/
public static final Device getDeviceByClient(String userAgent) throws DeviceNotFoundException {
String[] devTypes;
synchronized (_devmap) {
Collection<String> c = _devmap.keySet();
devTypes = c.toArray(new String[c.size()]);
}
Device device = null;
for (int j = 0; j < devTypes.length; ++j) {
Device dev;
try {
dev = getDevice(devTypes[j]);
} catch (Throwable ex) {
continue; //skip
}
Boolean b;
try {
b = dev.isCompatible(userAgent);
} catch (Throwable ex) { //backward compatible
b = null;
}
if (b != null) {
if (b.booleanValue())
return dev;
} else if (device == null || "ajax".equals(devTypes[j]) //ajax highest priority
|| "xml".equals(device.getType())) { //xml lowest priority
device = dev;
}
}
if (device == null)
throw new DeviceNotFoundException(userAgent, MZk.NOT_FOUND, userAgent);
return device;
}
/** Tests whether the device for the specified type exists.
*
* @param deviceType the device type, say, ajax.
* @since 2.4.0
*/
public static final boolean exists(String deviceType) {
if (deviceType == null)
return false;
final Object o;
synchronized (_devmap) {
o = _devmap.get(deviceType);
}
return o instanceof Device || (o != null && ((DeviceInfo) o).isValid());
}
/** Adds a device based on the XML declaration.
*
* <pre><code>
<device-config>
<device-type>superajax</device-type>
<device-class>my.MyDevice</device-class>
<unavailable-message>error message</unavailable-message>
<server-push-class>my.MyServerPush</server-push-class>
</device-config>
* </code></pre>
*
* @param config the XML element called zscript-config
*/
public static final void add(Element config) {
//Spec: it is OK to declare an nonexistent device
final String deviceType = IDOMs.getRequiredElementValue(config, "device-type");
String s = config.getElementValue("device-class", true);
if (s != null)
add(deviceType, s);
s = config.getElementValue("unavailable-message", true);
if (s != null)
setUnavailableMessage(deviceType, s);
s = config.getElementValue("server-push-class", true);
if (s != null)
setServerPushClass(deviceType, s);
for (Iterator it = config.getElements("embed").iterator(); it.hasNext();) {
addEmbedded(deviceType, ((Element) it.next()).getText(true));
}
}
/** Adds a device type.
*
* @param deviceType the device type (a.k.a., the device name).
* @param clsnm the device class name
* @return the previous class of the device with the same type, if any,
* or null if no such device.
*/
public static final String add(String deviceType, String clsnm) {
return add0(deviceType, clsnm);
}
/** Adds a device type.
*
* @param deviceType the device type (a.k.a., the device name).
* @param cls the device class
* @return the previous class of the device with the same type, if any,
* or null if no such device.
* @since 3.0.0
*/
public static final String add(String deviceType, Class cls) {
return add0(deviceType, cls);
}
private static final String add0(String deviceType, Object cls) {
if (deviceType == null || deviceType.length() == 0 || cls == null)
throw new IllegalArgumentException();
synchronized (_devmap) {
final Object o = _devmap.get(deviceType);
if (o instanceof DeviceInfo) {
return ((DeviceInfo) o).setDeviceClass(cls);
} else if (o instanceof Device) {
final Device device = (Device) o;
_devmap.put(deviceType,
new DeviceInfo(cls, device.getUnavailableMessage(), device.getServerPushClass()));
return device.getClass().getName();
} else {
_devmap.put(deviceType, new DeviceInfo(cls));
return null;
}
}
}
/** Returns the unavailable message for the specified device type.
*
* <p>The result is the same as the invocation of {@link Device#getUnavailableMessage}
* against {@link #getDevice}, but this method will not load the device
* if it is not loaded yet.
*
* @see Device
* @see Device#getUnavailableMessage
*/
public static final String getUnavailableMessage(String deviceType) {
final Object o;
synchronized (_devmap) {
o = _devmap.get(deviceType);
}
return o instanceof Device ? ((Device) o).getUnavailableMessage()
: o instanceof DeviceInfo ? ((DeviceInfo) o).getUnavailableMessage() : null;
}
/** Sets the unavailable message for the specified device type.
*
* @return the previous unavailable message if any.
* @see Device
* @see Device#setUnavailableMessage
*/
public static final String setUnavailableMessage(String deviceType, String msg) {
if (deviceType == null || deviceType.length() == 0)
throw new IllegalArgumentException();
if (msg != null && msg.length() == 0)
msg = null;
synchronized (_devmap) {
final Object o = _devmap.get(deviceType);
if (o instanceof Device) {
return ((Device) o).setUnavailableMessage(msg);
} else if (o instanceof DeviceInfo) {
return ((DeviceInfo) o).setUnavailableMessage(msg);
} else {
final DeviceInfo info = new DeviceInfo();
_devmap.put(deviceType, info);
info.setUnavailableMessage(msg);
return null;
}
}
}
/** Returns the content that shall be embedded to the output being
* generated to the client, or null if no embedded content.
*
* @since 3.0.6
*/
public String getEmbedded(String deviceType) {
final Object o;
synchronized (_devmap) {
o = _devmap.get(deviceType);
}
return o instanceof Device ? ((Device) o).getEmbedded()
: o instanceof DeviceInfo ? ((DeviceInfo) o).getEmbedded() : null;
}
/** Adds the content that shall be added to the output generated and
* sent to the client, when rending a desktop.
* What content can be embedded depends on the device.
* For Ajax devices, it can be anything that can be placed inside
* HTML HEAD, such as JavaScript codes.
*
* <p>As the method name suggests, the embedded contents are accumulated
* and all generated to the output.
* @since 3.0.6
*/
public static void addEmbedded(String deviceType, String content) {
if (deviceType == null || deviceType.length() == 0)
throw new IllegalArgumentException();
if (content == null || content.length() == 0)
return; //nothing to do
synchronized (_devmap) {
final Object o = _devmap.get(deviceType);
if (o instanceof Device) {
((Device) o).addEmbedded(content);
} else if (o instanceof DeviceInfo) {
((DeviceInfo) o).addEmbedded(content);
} else {
final DeviceInfo info = new DeviceInfo();
_devmap.put(deviceType, info);
info.addEmbedded(content);
}
}
}
/** Returns the class name that implements the server push feature.
*
* <p>Default: null (the server-push feature is not available).
*
* <p>The result is the same as the invocation of {@link Device#getServerPushClass}
* against {@link #getDevice}, but this method will not load the device
* if it is not loaded yet.
* @since 3.0.0
*/
public static final String getServerPushClass(String deviceType) {
final Object o;
synchronized (_devmap) {
o = _devmap.get(deviceType);
}
if (o instanceof Device) {
final Class cls = ((Device) o).getServerPushClass();
return cls != null ? cls.getName() : null;
}
return o instanceof DeviceInfo ? ((DeviceInfo) o).getServerPushClassName() : null;
}
/** Sets the name of the class that implements the server-push feature.
*
* @param clsnm the class name that implements the server push.
* If null, it means no server push is available.
* @return the previous class name, or null if not available.
* @since 3.0.0
*/
public static final String setServerPushClass(String deviceType, String clsnm) {
return setServerPushClass0(deviceType, clsnm);
}
/** Sets the class that implements the server-push feature.
*
* @param cls the class that implements the server push.
* If null, it means no server push is available.
* @return the previous class name, or null if not available.
* @since 3.0.0
*/
public static final String setServerPushClass(String deviceType, Class cls) {
return setServerPushClass0(deviceType, cls);
}
private static final String setServerPushClass0(String deviceType, Object cls) {
if (deviceType == null || deviceType.length() == 0)
throw new IllegalArgumentException();
try {
synchronized (_devmap) {
final Object o = _devmap.get(deviceType);
if (o instanceof Device) {
final Class old = ((Device) o).setServerPushClass(cls instanceof Class ? (Class) cls
: cls != null ? Classes.forNameByThread((String) cls) : null);
return old != null ? old.getName() : null;
} else if (o instanceof DeviceInfo) {
return ((DeviceInfo) o).setServerPushClass(cls);
} else {
final DeviceInfo info = new DeviceInfo();
_devmap.put(deviceType, info);
info.setServerPushClass(cls);
return null;
}
}
} catch (ClassNotFoundException ex) {
throw new UiException("Class not found: " + cls);
}
}
/** Returns the name and version of the client if the given user agent
* matches any of the devices, or null if not matched or it is a standard
* browser request.
* <p>It iterates all devices and invokes {@link Device#matches} one-by-one,
* then returns the first non-null return, if any.
* @param userAgent represents a client (i.e., HTTP's user-agent).
* @return a pair of objects or null.
* The first element of the pair is the name of the client (String),
* the second element is the version (Double, such as 3.5).
* @since 6.0.0
*/
public static final Object[] matches(String userAgent) {
for (int j = 0; j < _devs.length; ++j) {
Object[] inf = _devs[j].matches(userAgent);
if (inf != null)
return inf;
}
return null;
}
/** Loads the content of a javascript file as a String.
* @since 5.0.11
*/
public static String loadJavaScript(Execution exec, String path) throws IOException {
return loadFileContentAsString(exec, path);
}
/**
* Loads the content of a file as a String.
*
* @since 8.5.1
*/
public static String loadFileContentAsString(Execution exec, String path) throws IOException {
path = exec.locate(path);
//ZK-1345: use WebApps.getCurrent() instead of exec.getDesktop().getWebApp()
InputStream is = WebApps.getCurrent().getResourceAsStream(path);
if (is == null)
throw new UiException("Unable to load " + path);
final byte[] bs = Files.readAll(is);
Files.close(is);
return new String(bs, "UTF-8"); //UTF-8 is assumed
}
/** Device info.
*/
private static class DeviceInfo implements DeviceConfig {
/** The class or class name of {@link Device}
* of the device's implementation.
*/
private Object _dvcls;
private String _uamsg;
/** The class name or class of {@link ServerPush}.
*/
private Object _spushcls;
private String _embed;
private DeviceInfo() {
}
private DeviceInfo(Object deviceClass) {
_dvcls = deviceClass;
}
private DeviceInfo(Object deviceClass, String unavailable, Class spushcls) {
_dvcls = deviceClass;
_uamsg = unavailable;
_spushcls = spushcls;
}
/** Returns whether this device is valid, i.e., defined with a device class.
*/
private boolean isValid() {
return _dvcls != null;
}
/** Sets the device class.
*/
private String setDeviceClass(Object cls) {
final Object old = _dvcls;
_dvcls = cls;
return old instanceof Class ? ((Class) old).getName() : (String) old;
}
public String getUnavailableMessage() {
return _uamsg;
}
public String setUnavailableMessage(String msg) {
final String old = _uamsg;
_uamsg = msg != null && msg.length() > 0 ? msg : null;
return old;
}
/**
* @param cls the class name or class of the server push.
*/
public String setServerPushClass(Object cls) {
final Object old = _spushcls;
_spushcls = cls;
return old instanceof Class ? ((Class) old).getName() : (String) old;
}
public String getServerPushClassName() {
return _spushcls instanceof Class ? ((Class) _spushcls).getName() : (String) _spushcls;
}
public Class getServerPushClass() {
try {
return _spushcls instanceof Class ? (Class) _spushcls
: _spushcls != null ? Classes.forNameByThread((String) _spushcls) : null;
} catch (ClassNotFoundException ex) {
throw new UiException("Class not found: " + _spushcls);
}
}
public void addEmbedded(String content) {
if (content != null && content.length() > 0)
_embed = _embed != null ? _embed + '\n' + content : content;
}
public String getEmbedded() {
return _embed;
}
/** Creates a device based on this device info.
*/
private Device newDevice(String deviceType) {
if (_dvcls == null) //possible
throw new DeviceNotFoundException(deviceType, MZk.NOT_FOUND, deviceType);
try {
final Class cls;
if (_dvcls instanceof Class) {
cls = (Class) _dvcls;
} else {
cls = Classes.forNameByThread((String) _dvcls);
if (!Device.class.isAssignableFrom(cls))
throw new IllegalArgumentException(cls + " must implements " + Device.class);
}
final Device device = (Device) cls.newInstance();
device.init(deviceType, this);
return device;
} catch (Exception ex) {
throw UiException.Aide.wrap(ex, "Unable to create " + _dvcls);
}
}
}
}