/* * AndFHEM - Open Source Android application to control a FHEM home automation * server. * * Copyright (c) 2011, Matthias Klass or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Inc. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GENERAL PUBLIC LICENSE * for more details. * * You should have received a copy of the GNU GENERAL PUBLIC LICENSE * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package li.klass.fhem.service.room; import android.content.Context; import android.content.Intent; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.Sets; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.inject.Inject; import li.klass.fhem.R; import li.klass.fhem.constants.Actions; import li.klass.fhem.constants.BundleExtraKeys; import li.klass.fhem.domain.GenericDevice; import li.klass.fhem.domain.core.DeviceType; import li.klass.fhem.domain.core.FhemDevice; import li.klass.fhem.domain.core.RoomDeviceList; import li.klass.fhem.domain.core.XmllistAttribute; import li.klass.fhem.error.ErrorHolder; import li.klass.fhem.fhem.RequestResult; import li.klass.fhem.fhem.RequestResultError; import li.klass.fhem.service.connection.ConnectionService; import li.klass.fhem.service.deviceConfiguration.DeviceConfiguration; import li.klass.fhem.service.deviceConfiguration.DeviceConfigurationProvider; import li.klass.fhem.service.graph.gplot.GPlotHolder; import li.klass.fhem.service.room.group.GroupProvider; import li.klass.fhem.service.room.xmllist.DeviceNode; import li.klass.fhem.service.room.xmllist.Sanitiser; import li.klass.fhem.service.room.xmllist.XmlListDevice; import li.klass.fhem.service.room.xmllist.XmlListParser; import li.klass.fhem.util.StringEscapeUtil; import li.klass.fhem.util.ValueExtractUtil; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static li.klass.fhem.domain.core.DeviceType.getDeviceTypeFor; import static li.klass.fhem.util.ReflectionUtil.getAllDeclaredFields; import static li.klass.fhem.util.ReflectionUtil.getAllDeclaredMethods; /** * Class responsible for reading the current xml list from FHEM. */ public class DeviceListParser { @Inject ConnectionService connectionService; @Inject DeviceConfigurationProvider deviceConfigurationProvider; @Inject XmlListParser parser; @Inject GPlotHolder gPlotHolder; @Inject GroupProvider groupProvider; @Inject Sanitiser sanitiser; @Inject public DeviceListParser() { } private static Logger LOG = LoggerFactory.getLogger(DeviceListParser.class); private Map<Class<?>, Map<String, Set<DeviceClassCacheEntry>>> deviceClassCache = newHashMap(); RoomDeviceList parseAndWrapExceptions(String xmlList, Context context) { try { return parseXMLListUnsafe(xmlList, context); } catch (Exception e) { LOG.error("cannot parse xmllist", e); ErrorHolder.setError(e, "cannot parse xmllist, xmllist was: \r\n" + xmlList .replaceAll("<ATTR key=\"globalpassword\" value=\"[^\"]+\"/>", "") .replaceAll("<ATTR key=\"basicAuth\" value=\"[^\"]+\"/>", "")); new RequestResult<String>(RequestResultError.DEVICE_LIST_PARSE).handleErrors(); return null; } } public RoomDeviceList parseXMLListUnsafe(String xmlList, Context context) throws Exception { if (xmlList != null) { xmlList = xmlList.trim(); } RoomDeviceList allDevicesRoom = new RoomDeviceList(RoomDeviceList.ALL_DEVICES_ROOM); if (xmlList == null || "".equals(xmlList)) { LOG.error("xmlList is null or blank"); return allDevicesRoom; } gPlotHolder.reset(); Map<String, List<XmlListDevice>> parsedDevices = parser.parse(xmlList); ReadErrorHolder errorHolder = new ReadErrorHolder(); Map<String, FhemDevice> allDevices = newHashMap(); for (Map.Entry<String, List<XmlListDevice>> entry : parsedDevices.entrySet()) { DeviceType deviceType = getDeviceTypeFor(entry.getKey()); Optional<DeviceConfiguration> deviceConfiguration = deviceConfigurationProvider.configurationFor(entry.getKey()); List<XmlListDevice> xmlListDevices = parsedDevices.get(entry.getKey()); if (xmlListDevices == null || xmlListDevices.isEmpty()) { continue; } if (connectionService.mayShowInCurrentConnectionType(deviceType, context)) { int localErrorCount = devicesFromDocument(deviceType.getDeviceClass(), xmlListDevices, allDevices, context, deviceConfiguration); if (localErrorCount > 0) { errorHolder.addErrors(deviceType, localErrorCount); } } } performAfterReadOperations(allDevices, errorHolder); RoomDeviceList roomDeviceList = buildRoomDeviceList(allDevices, context); handleErrors(errorHolder, context); LOG.info("loaded {} devices!", allDevices.size()); return roomDeviceList; } private int devicesFromDocument(Class<? extends FhemDevice> deviceClass, List<XmlListDevice> xmlListDevices, Map<String, FhemDevice> allDevices, Context context, Optional<DeviceConfiguration> deviceConfiguration) { int errorCount = 0; String errorText = ""; for (XmlListDevice xmlListDevice : xmlListDevices) { if (!deviceFromXmlListDevice(deviceClass, xmlListDevice, allDevices, context, deviceConfiguration)) { errorCount++; errorText += xmlListDevice.toString() + "\r\n\r\n"; } } if (errorCount > 0) { ErrorHolder.setError("Cannot parse xmlListDevices: \r\n {}" + errorText); } return errorCount; } private void performAfterReadOperations(Map<String, FhemDevice> allDevices, ReadErrorHolder errorHolder) { List<FhemDevice> allDevicesReadCallbacks = newArrayList(); List<FhemDevice> deviceReadCallbacks = newArrayList(); for (FhemDevice device : allDevices.values()) { try { device.afterAllXMLRead(); if (device.getDeviceReadCallback() != null) deviceReadCallbacks.add(device); if (device.getAllDeviceReadCallback() != null) allDevicesReadCallbacks.add(device); } catch (Exception e) { allDevices.remove(device.getName()); errorHolder.addError(getDeviceTypeFor(device)); LOG.error("cannot perform after read operations", e); } } List<FhemDevice> callbackDevices = newArrayList(); callbackDevices.addAll(deviceReadCallbacks); callbackDevices.addAll(allDevicesReadCallbacks); for (FhemDevice device : callbackDevices) { try { if (device.getDeviceReadCallback() != null) { device.getDeviceReadCallback().devicesRead(allDevices); } if (device.getAllDeviceReadCallback() != null) { device.getAllDeviceReadCallback().devicesRead(allDevices); } } catch (Exception e) { allDevices.remove(device.getName()); errorHolder.addError(getDeviceTypeFor(device)); LOG.error("cannot handle associated devices callbacks", e); } } } private RoomDeviceList buildRoomDeviceList(Map<String, FhemDevice> allDevices, Context context) { RoomDeviceList roomDeviceList = new RoomDeviceList(RoomDeviceList.ALL_DEVICES_ROOM); for (FhemDevice device : allDevices.values()) { roomDeviceList.addDevice(device, context); } return roomDeviceList; } private void handleErrors(ReadErrorHolder errorHolder, Context context) { if (errorHolder.hasErrors()) { String errorMessage = context.getString(R.string.errorDeviceListLoad); String deviceTypesError = Joiner.on(",").join(errorHolder.getErrorDeviceTypeNames()); errorMessage = String.format(errorMessage, "" + errorHolder.getErrorCount(), deviceTypesError); Intent intent = new Intent(Actions.SHOW_TOAST); intent.putExtra(BundleExtraKeys.CONTENT, errorMessage); context.sendBroadcast(intent); } } private <T extends FhemDevice> boolean deviceFromXmlListDevice( Class<T> deviceClass, XmlListDevice xmlListDevice, Map<String, FhemDevice> allDevices, Context context, Optional<DeviceConfiguration> deviceConfiguration) { try { T device = createAndFillDevice(deviceClass, xmlListDevice, deviceConfiguration); if (device == null) { return false; } device.setXmlListDevice(xmlListDevice); device.afterDeviceXMLRead(context); LOG.debug("loaded device with name " + device.getName()); if (device instanceof GenericDevice) { xmlListDevice.setAttribute("group", groupProvider.functionalityFor(device, context)); } allDevices.put(device.getName(), device); return true; } catch (Exception e) { LOG.error("error parsing device", e); return false; } } private <T extends FhemDevice> T createAndFillDevice(Class<T> deviceClass, XmlListDevice xmlListDevice, Optional<DeviceConfiguration> deviceConfiguration) throws Exception { T device = deviceClass.newInstance(); device.setXmlListDevice(xmlListDevice); device.setDeviceConfiguration(deviceConfiguration); Map<String, Set<DeviceClassCacheEntry>> cache = getDeviceClassCacheEntriesFor(deviceClass); Iterable<DeviceNode> children = concat(xmlListDevice.getAttributes().values(), xmlListDevice.getInternals().values(), xmlListDevice.getStates().values(), xmlListDevice.getHeader().values()); for (DeviceNode child : newArrayList(children)) { if (child.getKey() == null) continue; String sanitisedKey = child.getKey().trim().replaceAll("[-.]", "_"); if (!device.acceptXmlKey(sanitisedKey)) { continue; } String nodeContent = StringEscapeUtil.unescape(child.getValue()); if (nodeContent.length() == 0) { continue; } invokeDeviceAttributeMethod(cache, device, sanitisedKey, nodeContent, child, child.getType()); } if (device.getName() == null) { return null; // just to be sure we don't catch invalid devices ... } return device; } @SuppressWarnings("unchecked") private <T extends FhemDevice> Map<String, Set<DeviceClassCacheEntry>> getDeviceClassCacheEntriesFor(Class<T> deviceClass) { Class<FhemDevice> clazz = (Class<FhemDevice>) deviceClass; if (!deviceClassCache.containsKey(clazz)) { deviceClassCache.put(clazz, initDeviceCacheEntries(deviceClass)); } return deviceClassCache.get(clazz); } private <T extends FhemDevice> void invokeDeviceAttributeMethod(Map<String, Set<DeviceClassCacheEntry>> cache, T device, String key, String value, DeviceNode deviceNode, DeviceNode.DeviceNodeType tagName) throws Exception { device.onChildItemRead(tagName, key, value, deviceNode); handleCacheEntryFor(cache, device, key, value, deviceNode); } private <T extends FhemDevice> void handleCacheEntryFor(Map<String, Set<DeviceClassCacheEntry>> cache, T device, String key, String value, DeviceNode deviceNode) throws Exception { key = key.toLowerCase(Locale.getDefault()); if (cache.containsKey(key)) { for (DeviceClassCacheEntry entry : cache.get(key)) { entry.invoke(device, deviceNode, value); } } } private <T extends FhemDevice> Map<String, Set<DeviceClassCacheEntry>> initDeviceCacheEntries(Class<T> deviceClass) { Map<String, Set<DeviceClassCacheEntry>> cache = newHashMap(); for (Method method : getAllDeclaredMethods(deviceClass)) { if (method.isAnnotationPresent(XmllistAttribute.class)) { XmllistAttribute annotation = method.getAnnotation(XmllistAttribute.class); for (String value : annotation.value()) { addToCache(cache, method, value.toLowerCase(Locale.getDefault())); } } } for (Field field : getAllDeclaredFields(deviceClass)) { if (field.isAnnotationPresent(XmllistAttribute.class)) { XmllistAttribute annotation = field.getAnnotation(XmllistAttribute.class); checkArgument(annotation.value().length > 0); for (String value : annotation.value()) { addToCache(cache, new DeviceClassFieldEntry(field, value.toLowerCase(Locale.getDefault()))); } } } return cache; } private void addToCache(Map<String, Set<DeviceClassCacheEntry>> cache, Method method, String attribute) { addToCache(cache, new DeviceClassMethodEntry(method, attribute)); } private void addToCache(Map<String, Set<DeviceClassCacheEntry>> cache, DeviceClassCacheEntry entry) { if (!cache.containsKey(entry.getAttribute())) { cache.put(entry.getAttribute(), Sets.<DeviceClassCacheEntry>newHashSet()); } cache.get(entry.getAttribute()).add(entry); } void fillDeviceWith(FhemDevice device, Map<String, String> updates, Context context) { Map<String, Set<DeviceClassCacheEntry>> cache = getDeviceClassCacheEntriesFor(device.getClass()); if (cache == null) return; for (Map.Entry<String, String> entry : updates.entrySet()) { try { handleCacheEntryFor(cache, device, entry.getKey(), entry.getValue(), new DeviceNode(DeviceNode.DeviceNodeType.GCM_UPDATE, entry.getKey(), entry.getValue(), DateTime.now())); device.getXmlListDevice().getStates().put(entry.getKey(), sanitiser.sanitise(device.getXmlListDevice().getType(), new DeviceNode(DeviceNode.DeviceNodeType.STATE, entry.getKey(), entry.getValue(), DateTime.now()) ) ); if ("STATE".equalsIgnoreCase(entry.getKey())) { device.getXmlListDevice().getInternals().put("STATE", sanitiser.sanitise(device.getXmlListDevice().getType(), new DeviceNode(DeviceNode.DeviceNodeType.INT, "STATE", entry.getValue(), DateTime.now()) ) ); } } catch (Exception e) { LOG.error("fillDeviceWith - handle " + entry, e); } } device.afterDeviceXMLRead(context); } private class ReadErrorHolder { private Map<DeviceType, Integer> deviceTypeErrorCount = newHashMap(); int getErrorCount() { int errors = 0; for (Integer deviceTypeErrors : deviceTypeErrorCount.values()) { errors += deviceTypeErrors; } return errors; } boolean hasErrors() { return deviceTypeErrorCount.size() != 0; } void addError(DeviceType deviceType) { if (deviceType != null) { addErrors(deviceType, 1); } } void addErrors(DeviceType deviceType, int errorCount) { int count = 0; if (deviceTypeErrorCount.containsKey(deviceType)) { count = deviceTypeErrorCount.get(deviceType); } deviceTypeErrorCount.put(deviceType, count + errorCount); } List<String> getErrorDeviceTypeNames() { if (deviceTypeErrorCount.size() == 0) return Collections.emptyList(); List<String> errorDeviceTypeNames = newArrayList(); for (DeviceType deviceType : deviceTypeErrorCount.keySet()) { errorDeviceTypeNames.add(deviceType.name()); } return errorDeviceTypeNames; } } private abstract class DeviceClassCacheEntry implements Serializable { private final String attribute; DeviceClassCacheEntry(String attribute) { this.attribute = attribute; } public String getAttribute() { return attribute; } public abstract void invoke(Object object, DeviceNode node, String value) throws Exception; } private class DeviceClassMethodEntry extends DeviceClassCacheEntry { private final Method method; DeviceClassMethodEntry(Method method, String attribute) { super(attribute); this.method = method; method.setAccessible(true); } @Override public void invoke(Object object, DeviceNode node, String value) throws Exception { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1) { if (parameterTypes[0].equals(String.class)) { method.invoke(object, value); } if ((parameterTypes[0].equals(double.class) || parameterTypes[0].equals(Double.class))) { method.invoke(object, ValueExtractUtil.extractLeadingDouble(value)); } if ((parameterTypes[0].equals(int.class) || parameterTypes[0].equals(Integer.class))) { method.invoke(object, ValueExtractUtil.extractLeadingInt(value)); } } else if (parameterTypes.length == 2 && parameterTypes[0].equals(String.class) && parameterTypes[1].equals(DeviceNode.class) && node != null) { method.invoke(object, value, node); } } } private class DeviceClassFieldEntry extends DeviceClassCacheEntry { private final Field field; DeviceClassFieldEntry(Field field, String attribute) { super(attribute); this.field = field; field.setAccessible(true); } @Override public void invoke(Object object, DeviceNode node, String value) throws Exception { LOG.debug("setting {} to {}", field.getName(), value); if (field.getType().isAssignableFrom(Double.class) || field.getType().isAssignableFrom(double.class)) { field.set(object, ValueExtractUtil.extractLeadingDouble(value)); } else if (field.getType().isAssignableFrom(Integer.class) || field.getType().isAssignableFrom(int.class)) { field.set(object, ValueExtractUtil.extractLeadingInt(value)); } else { field.set(object, value); } } } }