/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.factory.server.builder; import com.google.common.base.CaseFormat; import org.eclipse.che.api.core.ApiException; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.factory.FactoryParameter; import org.eclipse.che.api.core.model.project.ProjectConfig; import org.eclipse.che.api.core.model.project.SourceStorage; import org.eclipse.che.api.factory.server.FactoryConstants; import org.eclipse.che.api.factory.server.LegacyConverter; import org.eclipse.che.api.factory.server.ValueHelper; import org.eclipse.che.api.factory.server.impl.SourceStorageParametersValidator; import org.eclipse.che.api.factory.shared.dto.FactoryDto; import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto; import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.dto.shared.DTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import static org.eclipse.che.api.core.factory.FactoryParameter.Obligation; import static org.eclipse.che.api.core.factory.FactoryParameter.Version; /** * Tool to easy convert Factory object to json and vise versa. * Also it provides factory parameters compatibility. * * @author Sergii Kabashniuk * @author Alexander Garagatyi */ @Singleton public class FactoryBuilder { private static final Logger LOG = LoggerFactory.getLogger(FactoryBuilder.class); /** List contains all possible implementation of factory legacy converters. */ static final List<LegacyConverter> LEGACY_CONVERTERS; static { List<LegacyConverter> l = new ArrayList<>(1); l.add(factory -> {}); LEGACY_CONVERTERS = Collections.unmodifiableList(l); } private final SourceStorageParametersValidator sourceStorageParametersValidator; @Inject public FactoryBuilder(SourceStorageParametersValidator sourceStorageParametersValidator) { this.sourceStorageParametersValidator = sourceStorageParametersValidator; } /** * Build factory from json and validate its compatibility. * * @param json * - json Reader from encoded factory. * @return - Factory object represented by given factory json. */ public FactoryDto build(Reader json) throws IOException, ApiException { FactoryDto factory = DtoFactory.getInstance() .createDtoFromJson(json, FactoryDto.class); checkValid(factory); return factory; } /** * Build factory from json and validate its compatibility. * * @param json * - json string from encoded factory. * @return - Factory object represented by given factory json. */ public FactoryDto build(String json) throws ApiException { FactoryDto factory = DtoFactory.getInstance() .createDtoFromJson(json, FactoryDto.class); checkValid(factory); return factory; } /** * Build factory from json and validate its compatibility. * * @param json * - json InputStream from encoded factory. * @return - Factory object represented by given factory json. */ public FactoryDto build(InputStream json) throws IOException, ConflictException { FactoryDto factory = DtoFactory.getInstance() .createDtoFromJson(json, FactoryDto.class); checkValid(factory); return factory; } /** * Validate factory compatibility at creation time. * * @param factory * - factory object to validate * @throws ConflictException */ public void checkValid(FactoryDto factory) throws ConflictException { checkValid(factory, false); } /** * Validate factory compatibility. * * @param factory * - factory object to validate * @param isUpdate * - indicates is validation performed on update time. * Set-by-server variables are allowed during update. * @throws ConflictException */ public void checkValid(FactoryDto factory, boolean isUpdate) throws ConflictException { if (null == factory) { throw new ConflictException(FactoryConstants.UNPARSABLE_FACTORY_MESSAGE); } if (factory.getV() == null) { throw new ConflictException(FactoryConstants.INVALID_VERSION_MESSAGE); } Version v; try { v = Version.fromString(factory.getV()); } catch (IllegalArgumentException e) { throw new ConflictException(FactoryConstants.INVALID_VERSION_MESSAGE); } Class usedFactoryVersionMethodProvider; switch (v) { case V4_0: usedFactoryVersionMethodProvider = FactoryDto.class; break; default: throw new ConflictException(FactoryConstants.INVALID_VERSION_MESSAGE); } validateCompatibility(factory, null, FactoryDto.class, usedFactoryVersionMethodProvider, v, "", isUpdate); } /** * Convert factory of given version to the latest factory format. * * @param factory * - given factory. * @return - factory in latest format. * @throws org.eclipse.che.api.core.ApiException */ public FactoryDto convertToLatest(FactoryDto factory) throws ApiException { FactoryDto resultFactory = DtoFactory.getInstance().clone(factory).withV("4.0"); for (LegacyConverter converter : LEGACY_CONVERTERS) { converter.convert(resultFactory); } return resultFactory; } /** * Validate compatibility of factory parameters. * * @param object * - object to validate factory parameters * @param parent * - parent object * @param methodsProvider * - class that provides methods with {@link org.eclipse.che.api.core.factory.FactoryParameter} * annotations * @param allowedMethodsProvider * - class that provides allowed methods * @param version * - version of factory * @param parentName * - parent parameter queryParameterName * @throws org.eclipse.che.api.core.ConflictException */ void validateCompatibility(Object object, Object parent, Class methodsProvider, Class allowedMethodsProvider, Version version, String parentName, boolean isUpdate) throws ConflictException { // validate source if (SourceStorageDto.class.equals(methodsProvider) && !hasSubprojectInPath(parent)) { sourceStorageParametersValidator.validate((SourceStorage)object, version); } // get all methods recursively for (Method method : methodsProvider.getMethods()) { FactoryParameter factoryParameter = method.getAnnotation(FactoryParameter.class); // is it factory parameter if (factoryParameter == null) { continue; } String fullName = (parentName.isEmpty() ? "" : (parentName + ".")) + CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, method.getName().startsWith("is") ? method.getName() .substring(2) : method.getName() .substring(3) .toLowerCase()); // check that field is set Object parameterValue; try { parameterValue = method.invoke(object); } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) { // should never happen LOG.error(e.getLocalizedMessage(), e); throw new ConflictException(FactoryConstants.INVALID_PARAMETER_MESSAGE); } // if value is null or empty collection or default value for primitives if (ValueHelper.isEmpty(parameterValue)) { // field must not be a mandatory, unless it's ignored or deprecated or doesn't suit to the version if (Obligation.MANDATORY.equals(factoryParameter.obligation()) && factoryParameter.deprecatedSince() .compareTo(version) > 0 && factoryParameter.ignoredSince() .compareTo(version) > 0 && method.getDeclaringClass() .isAssignableFrom(allowedMethodsProvider)) { throw new ConflictException(String.format(FactoryConstants.MISSING_MANDATORY_MESSAGE, method.getName())); } } else if (!method.getDeclaringClass() .isAssignableFrom(allowedMethodsProvider)) { throw new ConflictException(String.format(FactoryConstants.PARAMETRIZED_INVALID_PARAMETER_MESSAGE, fullName, version)); } else { // is parameter deprecated if (factoryParameter.deprecatedSince().compareTo(version) <= 0 || (!isUpdate && factoryParameter.setByServer())) { throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_INVALID_PARAMETER_MESSAGE, fullName, version)); } // use recursion if parameter is DTO object if (method.getReturnType().isAnnotationPresent(DTO.class)) { // validate inner objects such Git ot ProjectAttributes validateCompatibility(parameterValue, object, method.getReturnType(), method.getReturnType(), version, fullName, isUpdate); } else if (Map.class.isAssignableFrom(method.getReturnType())) { Type tp = ((ParameterizedType)method.getGenericReturnType()).getActualTypeArguments()[1]; Class secMapParamClass = (tp instanceof ParameterizedType) ? (Class)((ParameterizedType)tp).getRawType() : (Class)tp; if (!String.class.equals(secMapParamClass) && !List.class.equals(secMapParamClass)) { if (secMapParamClass.isAnnotationPresent(DTO.class)) { Map<Object, Object> map = (Map)parameterValue; for (Map.Entry<Object, Object> entry : map.entrySet()) { validateCompatibility(entry.getValue(), object, secMapParamClass, secMapParamClass, version, fullName + "." + entry.getKey(), isUpdate); } } else { throw new RuntimeException("This type of fields is not supported by factory."); } } } else if (List.class.isAssignableFrom(method.getReturnType())) { Type tp = ((ParameterizedType)method.getGenericReturnType()).getActualTypeArguments()[0]; Class secListParamClass = (tp instanceof ParameterizedType) ? (Class)((ParameterizedType)tp).getRawType() : (Class)tp; if (!String.class.equals(secListParamClass) && !List.class.equals(secListParamClass)) { if (secListParamClass.isAnnotationPresent(DTO.class)) { List<Object> list = (List)parameterValue; for (Object entry : list) { validateCompatibility(entry, object, secListParamClass, secListParamClass, version, fullName, isUpdate); } } else { throw new RuntimeException("This type of fields is not supported by factory."); } } } } } } private boolean hasSubprojectInPath(Object parent) { return parent != null && ProjectConfig.class.isAssignableFrom(parent.getClass()) && ((ProjectConfig)parent).getPath().indexOf('/', 1) != -1; } }