/** * Copyright © 2014 Instituto Superior Técnico * * This file is part of FenixEdu CMS. * * FenixEdu CMS is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * FenixEdu CMS 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FenixEdu CMS. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.cms.domain.component; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import org.fenixedu.cms.domain.Page; import org.fenixedu.cms.domain.Site; import org.fenixedu.cms.domain.component.ComponentContextProvider.EmptyProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.ClassUtils; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import pt.ist.fenixframework.DomainObject; import pt.ist.fenixframework.FenixFramework; /** * Describes a {@link Component}, containing all the necessary information to * dynamically instantiate them. * * @author João Carvalho (joao.pedro.carvalho@tecnico.ulisboa.pt) */ public class ComponentDescriptor { private static final Logger logger = LoggerFactory.getLogger(ComponentDescriptor.class); private final Class<?> type; private final String name; private final boolean stateless; private final Map<String, ComponentParameterDescriptor> parameters = new HashMap<>(); private final Constructor<?> ctor; private final Constructor<?> jsonCtor; private final Method filter; ComponentDescriptor(Class<?> type) { this.type = type; ComponentType ann = type.getAnnotation(ComponentType.class); this.name = ann.name(); this.stateless = CMSComponent.class.isAssignableFrom(type); this.filter = ClassUtils.getMethodIfAvailable(type, "supportsSite", Site.class); if (!this.stateless) { this.ctor = getCustomCtor(type); this.jsonCtor = getJsonCtor(type); for (Parameter param : ctor.getParameters()) { parameters.put(param.getName(), new ComponentParameterDescriptor(param)); } } else { this.ctor = null; this.jsonCtor = null; } } private Constructor<?> getCustomCtor(Class<?> type) { for (Constructor<?> ctor : type.getDeclaredConstructors()) { if (ctor.isAnnotationPresent(DynamicComponent.class) && !isJsonConstructor(ctor)) { return ctor; } } return ClassUtils.getConstructorIfAvailable(type); } private Constructor<?> getJsonCtor(Class<?> type) { return Stream.of(type.getDeclaredConstructors()).filter(this::isJsonConstructor).findFirst().orElse(null); } private boolean isJsonConstructor(Constructor<?> constructor) { return constructor.isAnnotationPresent(DynamicComponent.class) && Stream.of(constructor.getParameterTypes()) .filter(parameterType -> JsonObject.class.isAssignableFrom(parameterType)).findAny().isPresent(); } public Class<?> getType() { return type; } public String getName() { return name; } public boolean isStateless() { return stateless; } public boolean isForSite(Site site) { try { return filter == null ? true : (boolean) filter.invoke(null, site); } catch (Exception e) { logger.warn("Exception when running component site filter, returning true!", e); return true; } } private class ComponentParameterDescriptor { private final ComponentParameter ann; private final ParameterType type; private final Parameter parameter; private final ComponentContextProvider<Object> provider; public ComponentParameterDescriptor(Parameter parameter) { this.parameter = parameter; this.ann = parameter.getAnnotation(ComponentParameter.class); this.type = getType(parameter.getType()); this.provider = findProviderMethod(); } @SuppressWarnings("unchecked") private ComponentContextProvider<Object> findProviderMethod() { try { return (ComponentContextProvider<Object>) ann.provider().newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException("Exception while creating provider", e); } } private ParameterType getType(Class<?> parameterClass) { if (String.class == parameterClass) { return ParameterType.STRING; } if (Number.class.isAssignableFrom(parameterClass)) { return ParameterType.NUMBER; } if (Enum.class.isAssignableFrom(parameterClass)) { return ParameterType.ENUM; } if (DomainObject.class.isAssignableFrom(parameterClass)) { return ParameterType.DOMAIN_OBJECT; } if (Boolean.class.isAssignableFrom(parameterClass)) { return ParameterType.BOOLEAN; } throw new IllegalArgumentException("ComponentParameter parameter is not supported!"); } public JsonObject toJson(Page page) { JsonObject json = new JsonObject(); json.addProperty("key", parameter.getName()); json.addProperty("title", ann.value()); json.addProperty("type", type.name()); json.addProperty("required", ann.required()); JsonArray values = new JsonArray(); Iterable<?> possibleValues = getPossibleValues(page); for (Object value : possibleValues) { JsonObject obj = new JsonObject(); obj.addProperty("value", type.stringify(value)); obj.addProperty("label", provider.present(value)); values.add(obj); } json.add("values", values); return json; } private Iterable<?> getPossibleValues(Page page) { if (parameter.getType().isEnum() && provider instanceof EmptyProvider) { return Arrays.asList(parameter.getType().getEnumConstants()); } else { return provider.provide(page); } } public boolean isRequired() { return ann.required(); } } private static enum ParameterType { STRING, NUMBER { @Override public Object coerce(Class<?> type, String value) throws Exception { return ClassUtils.getMethodIfAvailable(type, "valueOf", String.class).invoke(null, value); } }, BOOLEAN { @Override public Object coerce(Class<?> type, String value) { return Boolean.valueOf(value); } }, ENUM { @Override public String stringify(Object object) { return ((Enum<?>) object).name(); } @Override @SuppressWarnings({"unchecked", "rawtypes"}) public Object coerce(Class<?> type, String value) { return Enum.valueOf((Class) type, value); } }, DOMAIN_OBJECT { @Override public String stringify(Object object) { return ((DomainObject) object).getExternalId(); } @Override public Object coerce(Class<?> type, String value) { return FenixFramework.getDomainObject(value); } }; public String stringify(Object object) { return String.valueOf(object); } ; public Object coerce(Class<?> type, String value) throws Exception { return value; } } public JsonObject toJson() { JsonObject json = new JsonObject(); json.addProperty("type", type.getName()); json.addProperty("name", name); json.addProperty("stateless", stateless); return json; } public JsonArray getParameterDescription(Page page) { JsonArray array = new JsonArray(); parameters.values().stream().forEach(param -> array.add(param.toJson(page))); return array; } public Component instantiate(JsonObject params) throws Exception { Object[] arguments = new Object[ctor.getParameterCount()]; for (int i = 0; i < ctor.getParameterCount(); i++) { Parameter parameter = ctor.getParameters()[i]; ComponentParameterDescriptor descriptor = parameters.get(parameter.getName()); String value = params.has(parameter.getName()) ? params.get(parameter.getName()).getAsString() : null; Class<?> type = parameter.getType(); Object coercedValue = descriptor.type.coerce(type, value); if (coercedValue == null && descriptor.isRequired()) { throw new IllegalArgumentException("Required parameter " + parameter.getName() + " was not found!"); } arguments[i] = coercedValue; } return (Component) ctor.newInstance(arguments); } public Component fromJson(JsonObject jsonObject) throws Exception { Optional.ofNullable(jsonCtor) .orElseThrow(() -> new RuntimeException("Components of type '" + getType() + "' don't have a JSON constructor")); jsonCtor.setAccessible(true); return (Component) jsonCtor.newInstance(jsonObject); } }