/******************************************************************************* * Copyright (c) 2012-2015 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; 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.factory.converter.ActionsConverter; import org.eclipse.che.api.factory.converter.LegacyConverter; import org.eclipse.che.api.factory.dto.Factory; import org.eclipse.che.api.factory.dto.FactoryV2_0; import org.eclipse.che.api.factory.dto.FactoryV2_1; import org.eclipse.che.api.project.shared.dto.ImportSourceDescriptor; import org.eclipse.che.api.vfs.shared.dto.ReplacementSet; import org.eclipse.che.commons.lang.URLEncodedUtils; 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.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URI; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import static com.google.common.base.Strings.emptyToNull; import static org.eclipse.che.api.core.factory.FactoryParameter.FactoryFormat; import static org.eclipse.che.api.core.factory.FactoryParameter.FactoryFormat.ENCODED; import static org.eclipse.che.api.core.factory.FactoryParameter.FactoryFormat.NONENCODED; 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 nonencoded version or * to json version and vise versa. * Also it provides factory parameters compatibility. * * @author Sergii Kabashniuk * @author Alexander Garagatyi */ @Singleton public class FactoryBuilder extends NonEncodedFactoryBuilder { private static final Logger LOG = LoggerFactory.getLogger(FactoryService.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(new ActionsConverter()); LEGACY_CONVERTERS = Collections.unmodifiableList(l); } private final SourceProjectParametersValidator sourceProjectParametersValidator; @Inject public FactoryBuilder(SourceProjectParametersValidator sourceProjectParametersValidator) { this.sourceProjectParametersValidator = sourceProjectParametersValidator; } /** * Build factory from query string of non-encoded factory URI and validate compatibility. * * @param uri * - uri with non-encoded factory parameters. * @return - Factory object represented by given factory URI. */ public Factory buildEncoded(URI uri) throws ApiException { if (uri == null) { throw new ConflictException("Passed in invalid query parameters."); } Map<String, Set<String>> queryParams = URLEncodedUtils.parse(uri, "UTF-8"); Factory factory = buildDtoObject(queryParams, "", Factory.class); // there is unsupported parameters in query if (!queryParams.isEmpty()) { String nameInvalidParams = queryParams.keySet().iterator().next(); throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_INVALID_PARAMETER_MESSAGE, nameInvalidParams, factory.getV())); } else if (null == factory) { throw new ConflictException(FactoryConstants.MISSING_MANDATORY_MESSAGE); } checkValid(factory, NONENCODED); return factory; } /** * Build factory from json of encoded factory and validate compatibility. * * @param json * - json Reader from encoded factory. * @return - Factory object represented by given factory json. */ public Factory buildEncoded(Reader json) throws IOException, ApiException { Factory factory = DtoFactory.getInstance().createDtoFromJson(json, Factory.class); checkValid(factory, ENCODED); return factory; } /** * Build factory from json of encoded factory and validate compatibility. * * @param json * - json string from encoded factory. * @return - Factory object represented by given factory json. */ public Factory buildEncoded(String json) throws ApiException { Factory factory = DtoFactory.getInstance().createDtoFromJson(json, Factory.class); checkValid(factory, ENCODED); return factory; } /** * Build factory from json of encoded factory and validate compatibility. * * @param json * - json InputStream from encoded factory. * @return - Factory object represented by given factory json. */ public Factory buildEncoded(InputStream json) throws IOException, ApiException { Factory factory = DtoFactory.getInstance().createDtoFromJson(json, Factory.class); checkValid(factory, ENCODED); return factory; } /** * Validate factory compatibility. * * @param factory * - factory object to validate * @param sourceFormat * - is it encoded factory or not * @throws ApiException */ public void checkValid(Factory factory, FactoryFormat sourceFormat) throws ApiException { 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); } String accountId; Class usedFactoryVersionMethodProvider; switch (v) { case V2_0: usedFactoryVersionMethodProvider = FactoryV2_0.class; accountId = factory.getCreator() != null ? factory.getCreator().getAccountId() : null; break; case V2_1: usedFactoryVersionMethodProvider = FactoryV2_1.class; accountId = factory.getCreator() != null ? factory.getCreator().getAccountId() : null; break; default: throw new ConflictException(FactoryConstants.INVALID_VERSION_MESSAGE); } accountId = emptyToNull(accountId); validateCompatibility(factory, Factory.class, usedFactoryVersionMethodProvider, v, sourceFormat, accountId, ""); } /** * 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 Factory convertToLatest(Factory factory) throws ApiException { Factory resultFactory = DtoFactory.getInstance().clone(factory); resultFactory.setV("2.1"); for (LegacyConverter converter : LEGACY_CONVERTERS) { converter.convert(resultFactory); } return resultFactory; } /** * Validate compatibility of factory parameters. * * @param object * - object to validate factory parameters * @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 sourceFormat * - factory format * @param accountId * - account id of a factory * @param parentName * - parent parameter queryParameterName * @throws org.eclipse.che.api.core.ApiException */ void validateCompatibility(Object object, Class methodsProvider, Class allowedMethodsProvider, Version version, FactoryFormat sourceFormat, String accountId, String parentName) throws ApiException { // get all methods recursively for (Method method : methodsProvider.getMethods()) { FactoryParameter factoryParameter = method.getAnnotation(FactoryParameter.class); // is it factory parameter if (factoryParameter != null) { String fullName = (parentName.isEmpty() ? "" : (parentName + ".")) + factoryParameter.queryParameterName(); // 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(FactoryConstants.MISSING_MANDATORY_MESSAGE); } } 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) { throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_INVALID_PARAMETER_MESSAGE, fullName, version)); } if (factoryParameter.setByServer()) { throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_INVALID_PARAMETER_MESSAGE, fullName, version)); } // check that field satisfies format rules if (!FactoryFormat.BOTH.equals(factoryParameter.format()) && !factoryParameter.format().equals(sourceFormat)) { throw new ConflictException(String.format(FactoryConstants.PARAMETRIZED_ENCODED_ONLY_PARAMETER_MESSAGE, fullName)); } // use recursion if parameter is DTO object if (method.getReturnType().isAnnotationPresent(DTO.class)) { // validate inner objects such Git ot ProjectAttributes validateCompatibility(parameterValue, method.getReturnType(), method.getReturnType(), version, sourceFormat, accountId, fullName); } else if (Map.class.isAssignableFrom(method.getReturnType())) { Type tp = ((ParameterizedType)method.getGenericReturnType()).getActualTypeArguments()[1]; Class secMapParamClass; if (tp instanceof ParameterizedType) { secMapParamClass = (Class)((ParameterizedType)tp).getRawType(); } else { secMapParamClass = (Class)tp; } if (String.class.equals(secMapParamClass)) { if (ImportSourceDescriptor.class.equals(methodsProvider)) { sourceProjectParametersValidator.validate((ImportSourceDescriptor)object, version); } } else if (List.class.equals(secMapParamClass)) { // do nothing } else { if (secMapParamClass.isAnnotationPresent(DTO.class)) { Map<Object, Object> map = (Map)parameterValue; for (Map.Entry<Object, Object> entry : map.entrySet()) { validateCompatibility(entry.getValue(), secMapParamClass, secMapParamClass, version, sourceFormat, accountId, fullName + "." + (String)entry.getKey()); } } else { throw new RuntimeException("This type of fields is not supported by factory."); } } } } } } } /** * Build dto object with {@link org.eclipse.che.api.core.factory.FactoryParameter} annotations on its methods. * * @param queryParams * - source of parameters to parse * @param parentName * - queryParameterName of parent object. Allow provide support of nested parameters such as * projectattributes.pname * @param cl * - class of the object to build. * @return - built object * @throws org.eclipse.che.api.core.ApiException */ private <T> T buildDtoObject(Map<String, Set<String>> queryParams, String parentName, Class<T> cl) throws ApiException { T result = DtoFactory.getInstance().createDto(cl); boolean returnNull = true; // get all methods of object recursively for (Method method : cl.getMethods()) { FactoryParameter factoryParameter = method.getAnnotation(FactoryParameter.class); try { if (factoryParameter != null) { final String queryParameterName = factoryParameter.queryParameterName(); // define full queryParameterName of parameter to be able retrieving nested parameters String fullName = (parentName.isEmpty() ? "" : parentName + ".") + queryParameterName; Class<?> returnClass = method.getReturnType(); if (factoryParameter.format() == FactoryFormat.ENCODED) { if (queryParams.containsKey(fullName)) { throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_ENCODED_ONLY_PARAMETER_MESSAGE, fullName)); } else { continue; } } //PrimitiveTypeProducer Object param = null; if (queryParams.containsKey(fullName)) { Set<String> values; if (null == (values = queryParams.remove(fullName)) || values.size() != 1) { throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_ILLEGAL_PARAMETER_VALUE_MESSAGE, fullName, null != values ? values.toString() : "null")); } param = ValueHelper.createValue(returnClass, values); if (null == param) { if ("variables".equals(fullName) || "actions.findReplace".equals(fullName)) { try { param = DtoFactory.getInstance().createListDtoFromJson(values.iterator().next(), ReplacementSet.class); } catch (Exception e) { throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_ILLEGAL_PARAMETER_VALUE_MESSAGE, fullName, values.toString())); } } else { // should never happen throw new ConflictException( String.format(FactoryConstants.PARAMETRIZED_ILLEGAL_PARAMETER_VALUE_MESSAGE, fullName, values.toString())); } } } else if (returnClass.isAnnotationPresent(DTO.class)) { // use recursion if parameter is DTO object param = buildDtoObject(queryParams, fullName, returnClass); } else if (List.class.isAssignableFrom(returnClass)) { Type tp = ((ParameterizedType)method.getGenericReturnType()).getActualTypeArguments()[0]; Class listClass; if (tp instanceof ParameterizedType) { listClass = (Class)((ParameterizedType)tp).getRawType(); } else { listClass = (Class)tp; } Set<String> keys = new TreeSet<>(); for (String key : queryParams.keySet()) { if (key.startsWith(fullName)) { keys.add(key.substring(fullName.length() + 1, key.indexOf(".", fullName.length() + 1))); } } if (!keys.isEmpty()) { param = new ArrayList<>(keys.size()); for (String key : keys) { Map<String, Set<String>> listQueryParams = new HashMap<>(); Set<String> removeKeys = new HashSet<>(); for (Map.Entry<String, Set<String>> queryParam : queryParams.entrySet()) { String queryParamKey = queryParam.getKey(); if (queryParamKey.startsWith(fullName + "." + key + ".")) { removeKeys.add(queryParamKey); listQueryParams .put(queryParamKey.substring(fullName.length() + key.length() + 2), queryParam.getValue()); } } ((List)param).add(buildDtoObject(listQueryParams, "", listClass)); //cleanup in list of query params. for (String removeKey : removeKeys) { queryParams.remove(removeKey); } } } } else if (Map.class.isAssignableFrom(returnClass)) { Type tp = ((ParameterizedType)method.getGenericReturnType()).getActualTypeArguments()[1]; Class secMapParamClass; if (tp instanceof ParameterizedType) { secMapParamClass = (Class)((ParameterizedType)tp).getRawType(); } else { secMapParamClass = (Class)tp; } String mapEntryPrefix = fullName + "."; Map<String, Object> map; if (Map.class == returnClass) { map = new HashMap<>(); } else { map = (Map)returnClass.newInstance(); } if (String.class.equals(secMapParamClass)) { for (Map.Entry<String, Set<String>> parameterEntry : queryParams.entrySet()) { if (parameterEntry.getKey().startsWith(mapEntryPrefix)) { map.put(parameterEntry.getKey().substring(mapEntryPrefix.length()), parameterEntry.getValue().iterator().next()); } } for (String key : map.keySet()) { queryParams.remove(mapEntryPrefix + key); } if (!map.isEmpty()) { param = map; } } else if (List.class.equals(secMapParamClass)) { for (Map.Entry<String, Set<String>> parameterEntry : queryParams.entrySet()) { if (parameterEntry.getKey().startsWith(mapEntryPrefix)) { map.put(parameterEntry.getKey().substring(mapEntryPrefix.length()), new ArrayList<>(parameterEntry.getValue())); } } for (String key : map.keySet()) { queryParams.remove(mapEntryPrefix + key); } if (!map.isEmpty()) { param = map; } } else { if (secMapParamClass.isAnnotationPresent(DTO.class)) { final Map<String, Map<String, Set<String>>> dtosQueries = new HashMap<>(); for (Map.Entry<String, Set<String>> parameterEntry : queryParams.entrySet()) { if (parameterEntry.getKey().startsWith(mapEntryPrefix) && parameterEntry.getKey().length() > mapEntryPrefix.length()) { final String currentKey = parameterEntry.getKey().substring(mapEntryPrefix.length()); final int i = currentKey.indexOf('.'); if (i != -1) { String dtoKey = currentKey.substring(0, i); Map<String, Set<String>> dtoMap; if ((dtoMap = dtosQueries.get(dtoKey)) == null) { dtosQueries.put(dtoKey, dtoMap = new HashMap<>()); } dtoMap.put(parameterEntry.getKey(), parameterEntry.getValue()); } } } for (Map.Entry<String, Map<String, Set<String>>> dtoEntry : dtosQueries.entrySet()) { Object dto = buildDtoObject(queryParams, mapEntryPrefix + dtoEntry.getKey(), secMapParamClass); map.put(dtoEntry.getKey(), dto); } if (!map.isEmpty()) { param = map; } } } } if (param != null) { // call appropriate setter to set current parameter String setterMethodName = "set" + Character.toUpperCase(method.getName().substring(3).charAt(0)) + method.getName().substring(4); Method setterMethod = cl.getMethod(setterMethodName, returnClass); setterMethod.invoke(result, param); returnNull = false; } } } catch (ApiException e) { throw e; } catch (Exception e) { LOG.error(e.getLocalizedMessage(), e); throw new ConflictException( "Can't validate '" + (parentName.isEmpty() ? "" : parentName + ".") + factoryParameter.queryParameterName() + "' parameter." ); } } return returnNull ? null : result; } @Override protected String encode(String value) { try { return URLEncoder.encode(value, "UTF-8"); } catch (UnsupportedEncodingException e) { return value; } } @Override protected String toJson(List<ReplacementSet> dto) { return DtoFactory.getInstance().toJson(dto); } }