/* * Copyright (C) 2013 4th Line GmbH, Switzerland * * The contents of this file are subject to the terms of either the GNU * Lesser General Public License Version 2 or later ("LGPL") or the * Common Development and Distribution License Version 1 or later * ("CDDL") (collectively, the "License"). You may not use this file * except in compliance with the License. See LICENSE.txt for more * information. * * 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. */ package org.fourthline.cling.binding.annotations; import org.fourthline.cling.binding.LocalServiceBinder; import org.fourthline.cling.binding.LocalServiceBindingException; import org.fourthline.cling.model.ValidationError; import org.fourthline.cling.model.ValidationException; import org.fourthline.cling.model.action.ActionExecutor; import org.fourthline.cling.model.action.QueryStateVariableExecutor; import org.fourthline.cling.model.meta.Action; import org.fourthline.cling.model.meta.LocalService; import org.fourthline.cling.model.meta.QueryStateVariableAction; import org.fourthline.cling.model.meta.StateVariable; import org.fourthline.cling.model.state.FieldStateVariableAccessor; import org.fourthline.cling.model.state.GetterStateVariableAccessor; import org.fourthline.cling.model.state.StateVariableAccessor; import org.fourthline.cling.model.types.ServiceId; import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDAServiceId; import org.fourthline.cling.model.types.UDAServiceType; import org.fourthline.cling.model.types.csv.CSV; import org.seamless.util.Reflections; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.Locale; import java.util.logging.Logger; /** * Reads {@link org.fourthline.cling.model.meta.LocalService} metadata from annotations. * * @author Christian Bauer */ public class AnnotationLocalServiceBinder implements LocalServiceBinder { private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName()); public LocalService read(Class<?> clazz) throws LocalServiceBindingException { log.fine("Reading and binding annotations of service implementation class: " + clazz); // Read the service ID and service type from the annotation if (clazz.isAnnotationPresent(UpnpService.class)) { UpnpService annotation = clazz.getAnnotation(UpnpService.class); UpnpServiceId idAnnotation = annotation.serviceId(); UpnpServiceType typeAnnotation = annotation.serviceType(); ServiceId serviceId = idAnnotation.namespace().equals(UDAServiceId.DEFAULT_NAMESPACE) ? new UDAServiceId(idAnnotation.value()) : new ServiceId(idAnnotation.namespace(), idAnnotation.value()); ServiceType serviceType = typeAnnotation.namespace().equals(UDAServiceType.DEFAULT_NAMESPACE) ? new UDAServiceType(typeAnnotation.value(), typeAnnotation.version()) : new ServiceType(typeAnnotation.namespace(), typeAnnotation.value(), typeAnnotation.version()); boolean supportsQueryStateVariables = annotation.supportsQueryStateVariables(); Set<Class> stringConvertibleTypes = readStringConvertibleTypes(annotation.stringConvertibleTypes()); return read(clazz, serviceId, serviceType, supportsQueryStateVariables, stringConvertibleTypes); } else { throw new LocalServiceBindingException("Given class is not an @UpnpService"); } } public LocalService read(Class<?> clazz, ServiceId id, ServiceType type, boolean supportsQueryStateVariables, Class[] stringConvertibleTypes) throws LocalServiceBindingException { return read(clazz, id, type, supportsQueryStateVariables, new HashSet<Class>(Arrays.asList(stringConvertibleTypes))); } public LocalService read(Class<?> clazz, ServiceId id, ServiceType type, boolean supportsQueryStateVariables, Set<Class> stringConvertibleTypes) throws LocalServiceBindingException { Map<StateVariable, StateVariableAccessor> stateVariables = readStateVariables(clazz, stringConvertibleTypes); Map<Action, ActionExecutor> actions = readActions(clazz, stateVariables, stringConvertibleTypes); // Special treatment of the state variable querying action if (supportsQueryStateVariables) { actions.put(new QueryStateVariableAction(), new QueryStateVariableExecutor()); } try { return new LocalService(type, id, actions, stateVariables, stringConvertibleTypes, supportsQueryStateVariables); } catch (ValidationException ex) { log.severe("Could not validate device model: " + ex.toString()); for (ValidationError validationError : ex.getErrors()) { log.severe(validationError.toString()); } throw new LocalServiceBindingException("Validation of model failed, check the log"); } } protected Set<Class> readStringConvertibleTypes(Class[] declaredTypes) throws LocalServiceBindingException { for (Class stringConvertibleType : declaredTypes) { if (!Modifier.isPublic(stringConvertibleType.getModifiers())) { throw new LocalServiceBindingException( "Declared string-convertible type must be public: " + stringConvertibleType ); } try { stringConvertibleType.getConstructor(String.class); } catch (NoSuchMethodException ex) { throw new LocalServiceBindingException( "Declared string-convertible type needs a public single-argument String constructor: " + stringConvertibleType ); } } Set<Class> stringConvertibleTypes = new HashSet(Arrays.asList(declaredTypes)); // Some defaults stringConvertibleTypes.add(URI.class); stringConvertibleTypes.add(URL.class); stringConvertibleTypes.add(CSV.class); return stringConvertibleTypes; } protected Map<StateVariable, StateVariableAccessor> readStateVariables(Class<?> clazz, Set<Class> stringConvertibleTypes) throws LocalServiceBindingException { Map<StateVariable, StateVariableAccessor> map = new HashMap(); // State variables declared on the class if (clazz.isAnnotationPresent(UpnpStateVariables.class)) { UpnpStateVariables variables = clazz.getAnnotation(UpnpStateVariables.class); for (UpnpStateVariable v : variables.value()) { if (v.name().length() == 0) throw new LocalServiceBindingException("Class-level @UpnpStateVariable name attribute value required"); String javaPropertyName = toJavaStateVariableName(v.name()); Method getter = Reflections.getGetterMethod(clazz, javaPropertyName); Field field = Reflections.getField(clazz, javaPropertyName); StateVariableAccessor accessor = null; if (getter != null && field != null) { accessor = variables.preferFields() ? new FieldStateVariableAccessor(field) : new GetterStateVariableAccessor(getter); } else if (field != null) { accessor = new FieldStateVariableAccessor(field); } else if (getter != null) { accessor = new GetterStateVariableAccessor(getter); } else { log.finer("No field or getter found for state variable, skipping accessor: " + v.name()); } StateVariable stateVar = new AnnotationStateVariableBinder(v, v.name(), accessor, stringConvertibleTypes) .createStateVariable(); map.put(stateVar, accessor); } } // State variables declared on fields for (Field field : Reflections.getFields(clazz, UpnpStateVariable.class)) { UpnpStateVariable svAnnotation = field.getAnnotation(UpnpStateVariable.class); StateVariableAccessor accessor = new FieldStateVariableAccessor(field); StateVariable stateVar = new AnnotationStateVariableBinder( svAnnotation, svAnnotation.name().length() == 0 ? toUpnpStateVariableName(field.getName()) : svAnnotation.name(), accessor, stringConvertibleTypes ).createStateVariable(); map.put(stateVar, accessor); } // State variables declared on getters for (Method getter : Reflections.getMethods(clazz, UpnpStateVariable.class)) { String propertyName = Reflections.getMethodPropertyName(getter.getName()); if (propertyName == null) { throw new LocalServiceBindingException( "Annotated method is not a getter method (: " + getter ); } if (getter.getParameterTypes().length > 0) throw new LocalServiceBindingException( "Getter method defined as @UpnpStateVariable can not have parameters: " + getter ); UpnpStateVariable svAnnotation = getter.getAnnotation(UpnpStateVariable.class); StateVariableAccessor accessor = new GetterStateVariableAccessor(getter); StateVariable stateVar = new AnnotationStateVariableBinder( svAnnotation, svAnnotation.name().length() == 0 ? toUpnpStateVariableName(propertyName) : svAnnotation.name(), accessor, stringConvertibleTypes ).createStateVariable(); map.put(stateVar, accessor); } return map; } protected Map<Action, ActionExecutor> readActions(Class<?> clazz, Map<StateVariable, StateVariableAccessor> stateVariables, Set<Class> stringConvertibleTypes) throws LocalServiceBindingException { Map<Action, ActionExecutor> map = new HashMap(); for (Method method : Reflections.getMethods(clazz, UpnpAction.class)) { AnnotationActionBinder actionBinder = new AnnotationActionBinder(method, stateVariables, stringConvertibleTypes); Action action = actionBinder.appendAction(map); if(isActionExcluded(action)) { map.remove(action); } } return map; } /** * Override this method to exclude action/methods after they have been discovered. */ protected boolean isActionExcluded(Action action) { return false; } // TODO: I don't like the exceptions much, user has no idea what to do static String toUpnpStateVariableName(String javaName) { if (javaName.length() < 1) { throw new IllegalArgumentException("Variable name must be at least 1 character long"); } return javaName.substring(0, 1).toUpperCase(Locale.ENGLISH) + javaName.substring(1); } static String toJavaStateVariableName(String upnpName) { if (upnpName.length() < 1) { throw new IllegalArgumentException("Variable name must be at least 1 character long"); } return upnpName.substring(0, 1).toLowerCase(Locale.ENGLISH) + upnpName.substring(1); } static String toUpnpActionName(String javaName) { if (javaName.length() < 1) { throw new IllegalArgumentException("Action name must be at least 1 character long"); } return javaName.substring(0, 1).toUpperCase(Locale.ENGLISH) + javaName.substring(1); } static String toJavaActionName(String upnpName) { if (upnpName.length() < 1) { throw new IllegalArgumentException("Variable name must be at least 1 character long"); } return upnpName.substring(0, 1).toLowerCase(Locale.ENGLISH) + upnpName.substring(1); } }