/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * 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: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.view.internal.filter.ext; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.text.StrSubstitutor; import org.eclipse.skalli.model.Member; import org.eclipse.skalli.model.Project; import org.eclipse.skalli.model.User; import org.eclipse.skalli.model.ext.commons.PeopleExtension; import org.eclipse.skalli.model.ext.devinf.DevInfProjectExt; import org.eclipse.skalli.services.Services; import org.eclipse.skalli.services.configuration.ConfigurationService; import org.eclipse.skalli.services.entity.EntityServices; import org.eclipse.skalli.services.extension.PropertyLookup; import org.eclipse.skalli.services.gerrit.CommandException; import org.eclipse.skalli.services.gerrit.ConnectionException; import org.eclipse.skalli.services.gerrit.GerritClient; import org.eclipse.skalli.services.gerrit.GerritClientException; import org.eclipse.skalli.services.gerrit.GerritServerConfig; import org.eclipse.skalli.services.gerrit.GerritServersConfig; import org.eclipse.skalli.services.gerrit.GerritService; import org.eclipse.skalli.services.gerrit.InheritableBoolean; import org.eclipse.skalli.services.gerrit.ProjectOptions; import org.eclipse.skalli.services.project.ProjectService; import org.eclipse.skalli.view.Consts; import org.eclipse.skalli.view.internal.filter.FilterException; import org.eclipse.skalli.view.internal.filter.FilterUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class GitGerritFilter implements Filter { private static final String URL_GITGERRITERROR = "gitgerriterror"; //$NON-NLS-1$ public static final String ATTRIBUTE_INVALID_GROUP = "invalidGroup"; //$NON-NLS-1$ public static final String ATTRIBUTE_INVALID_GROUP_MSG = "invalidGroupMsg"; //$NON-NLS-1$ public static final String ATTRIBUTE_GROUP_EXISTS = "groupExists"; //$NON-NLS-1$ public static final String ATTRIBUTE_INVALID_REPO = "invalidRepo"; //$NON-NLS-1$ public static final String ATTRIBUTE_INVALID_REPO_MSG = "invalidRepoMsg"; //$NON-NLS-1$ public static final String ATTRIBUTE_REPO_EXISTS = "repoExists"; //$NON-NLS-1$ public static final String ATTRIBUTE_INVALID_PARENT = "invalidParent"; //$NON-NLS-1$ public static final String ATTRIBUTE_KNOWN_ACCOUNTS = "knownAccounts"; //$NON-NLS-1$ public static final String ATTRIBUTE_DATA_SAVED = "dataSaved"; //$NON-NLS-1$ public static final String ATTRIBUTE_NO_GERRIT_CLIENT = "noGerritClient"; //$NON-NLS-1$ public static final String ATTRIBUTE_NO_GERRIT_USER = "noGerritUser"; //$NON-NLS-1$ public static final String ATTRIBUTE_NO_PROJECT_MEMBER = "noProjectMember"; //$NON-NLS-1$ public static final String ATTRIBUTE_EXCEPTION = "exception"; //$NON-NLS-1$ public static final String ATTRIBUTE_ERROR_MESSAGE = "errormessage"; //$NON-NLS-1$ public static final String ERROR_NO_PARENT = "noParent"; //$NON-NLS-1$ public static final String ERROR_NO_CONFIGURATION = "noConfiguration"; //$NON-NLS-1$ private static final String GIT_PREFIX = "scm:git:"; //$NON-NLS-1$ private static final String GIT_EXT = ".git"; //$NON-NLS-1$ static final String DEFAULT_SCM_TEMPLATE = GIT_PREFIX +"${protocol}://${host}:${port}/${repository}" + GIT_EXT; //$NON-NLS-1$ static final String DEFAULT_DESCRIPTION = "Created by ${user.displayName}. More details: ${link}"; //$NON-NLS-1$ private static final Logger LOG = LoggerFactory.getLogger(GitGerritFilter.class); @Override public void init(FilterConfig arg0) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)resp; // get some attributes provided by previous filters in the chain final String projectId = (String) request.getAttribute(Consts.ATTRIBUTE_PROJECTID); final Project project = (Project) request.getAttribute(Consts.ATTRIBUTE_PROJECT); final User user = (User) request.getAttribute(Consts.ATTRIBUTE_USER); FilterException fe = null; if (StringUtils.isBlank(projectId)) { fe = new FilterException("Missing required request parameter 'projectId'"); } else if (project == null) { fe = new FilterException("Missing required request parameter 'project'"); } else if (user == null) { fe = new FilterException("Missing required request parameter 'user'"); } if (fe != null) { FilterUtil.handleException(request, response, fe); return; } // no configuration service: cancel further processing since we will not // be able to create a Gerrit client ConfigurationService configService = Services.getService(ConfigurationService.class); if (configService == null) { request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, ERROR_NO_CONFIGURATION); FilterUtil.forward(request, response, URL_GITGERRITERROR); return; } // no Gerrit server configurations: cancel further processing since we will not // be able to create a Gerrit client GerritServersConfig gerritServersConfig = configService.readConfiguration(GerritServersConfig.class); if (gerritServersConfig == null || gerritServersConfig.isEmpty()) { request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, ERROR_NO_CONFIGURATION); FilterUtil.forward(request, response, URL_GITGERRITERROR); return; } // no Gerrit service available: cancel further processing since we will not // be able to create a Gerrit client GerritService gerritService = Services.getService(GerritService.class); if (gerritService == null) { request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, ERROR_NO_CONFIGURATION); FilterUtil.forward(request, response, URL_GITGERRITERROR); return; } // retrieve form parameters final String action = request.getParameter("action"); //$NON-NLS-1$ final String gerritId = request.getParameter("gerritId"); //$NON-NLS-1$ final String groupMode = request.getParameter("groupMode"); //$NON-NLS-1$ final String group = request.getParameter("group"); //$NON-NLS-1$ final String repository = request.getParameter("repository"); //$NON-NLS-1$ final String parentMode = request.getParameter("parentMode"); //$NON-NLS-1$ final String parent = request.getParameter("parent"); //$NON-NLS-1$ final boolean permitsOnly = "permitsOnly".equals(request.getParameter("permitsOnly")); final boolean emptyCommit = request.getParameter("emptyCommit") != null? "emptyCommit".equals(request.getParameter("emptyCommit")) : StringUtils.isBlank(action); // Determine which Gerrit server to use: if a serverId is known, take that // dedicated server; otherwise search for a server with the "preferred" flag; // if no server is marked as preferred, take the first List<GerritServerConfig> gerritServers = gerritServersConfig.getServers(); request.setAttribute("gerritServers", gerritServers); //$NON-NLS-1$ GerritServerConfig gerritServer = StringUtils.isNotBlank(gerritId)? gerritServersConfig.getServer(gerritId) : gerritServersConfig.getPreferredServer(); request.setAttribute("gerritServer", gerritServer); //$NON-NLS-1$ GerritClient client = null; try { // no Gerrit client available: cancel further processing since we will not // be able to communicate with Gerrit client = gerritService.getClient(gerritServer.getId(), user.getUserId()); if (client == null) { request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, ERROR_NO_CONFIGURATION); FilterUtil.forward(request, response, URL_GITGERRITERROR); return; } // if "subprojectsOnly" flag is set in configuration, // ensure that project has at least one parent if (gerritServer.isSubprojectsOnly() && project.getParentEntityId() == null) { request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, ERROR_NO_PARENT); FilterUtil.forward(request, response, URL_GITGERRITERROR); return; } // render a contact address, if available if (StringUtils.isNotBlank(gerritServer.getContact())) { request.setAttribute("gerritContact", gerritServer.getContact()); } // ***** HERE starts the action evaluation/handling **** // action=undefined: create initial proposals for all form elements // action=refresh: create initial proposals except for the changed form elements if (StringUtils.isBlank(action) || "refresh".equals(action)) { request.setAttribute("proposedRepo", StringUtils.isNotBlank(repository) ? repository : generateName(project, "/", StringUtils.EMPTY)); request.setAttribute("proposedPermitsOnly", permitsOnly); request.setAttribute("proposedEmptyCommit", emptyCommit); request.setAttribute("proposedGroup", StringUtils.isNotBlank(group) ? group : generateName(project, "_", "_committers")); if ("related".equals(groupMode)) { request.setAttribute("proposedGroups", getRelatedGroups(gerritServer, client, project, user)); } else if ("all".equals(groupMode)) { request.setAttribute("proposedGroups", getAllGroups(client, project)); } request.setAttribute("proposedParent", StringUtils.isNotBlank(parent) ? parent : gerritServer.getParent()); if ("related".equals(parentMode)) { request.setAttribute("proposedProjects", getRelatedProjects(gerritServer, client, project, user)); } else if ("permissions".equals(parentMode)) { request.setAttribute("proposedProjects", getProjects(client, "permissions")); } else if ("all".equals(parentMode)) { request.setAttribute("proposedProjects", getProjects(client, "all")); } } // action=check: validate the input // action=save: validate the input and do the needful on Gerrit else if ("check".equals(action) || "save".equals(action)) { client.connect(); // is the group name valid? String invalidGroupMsg = client.checkGroupName(group); boolean invalidGroup = invalidGroupMsg != null; request.setAttribute(ATTRIBUTE_INVALID_GROUP, invalidGroup); if (invalidGroup) { request.setAttribute(ATTRIBUTE_INVALID_GROUP_MSG, invalidGroupMsg); } // does the group already exist? boolean groupExists = !invalidGroup && client.groupExists(group); request.setAttribute(ATTRIBUTE_GROUP_EXISTS, groupExists); // is the repository name valid? String invalidRepoMsg = client.checkProjectName(repository); boolean invalidRepo = invalidRepoMsg != null; request.setAttribute(ATTRIBUTE_INVALID_REPO, invalidRepo); if (invalidRepo) { request.setAttribute(ATTRIBUTE_INVALID_REPO_MSG, invalidRepoMsg); } // does the selected repository already exist? boolean repoExists = !invalidRepo && client.projectExists(repository); request.setAttribute(ATTRIBUTE_REPO_EXISTS, repoExists); // does the selected parent project already exist? if (StringUtils.isNotBlank(parent)) { request.setAttribute(ATTRIBUTE_INVALID_PARENT, !client.projectExists(parent)); } // proposed committers: do they have accounts? will the acting user be a committer? Set<String> knownAccounts = Collections.emptySet(); boolean actingUserHasAccount = false; boolean actingUserIsProjectMember = false; boolean createGroup = !invalidGroup && !groupExists; if (createGroup) { Set<String> projectMembers = getProposedProjectMembers(project); actingUserIsProjectMember = projectMembers.contains(user.getUserId()); knownAccounts = client.getKnownAccounts(projectMembers); request.setAttribute(ATTRIBUTE_KNOWN_ACCOUNTS, knownAccounts); actingUserHasAccount = knownAccounts.contains(user.getUserId()); } request.setAttribute(ATTRIBUTE_NO_PROJECT_MEMBER, !actingUserIsProjectMember); request.setAttribute(ATTRIBUTE_NO_GERRIT_USER, actingUserIsProjectMember && !actingUserHasAccount); // (3) SAVE (action: create group/repo, set SCM location) if ("save".equals(action)) { // Only proceed if ... boolean proceed = groupExists && repoExists; // ... both entities exist boolean createRepo = !invalidGroup && !invalidRepo && !repoExists; proceed = proceed || groupExists && createRepo; // ... or repo will be created proceed = proceed || createGroup && createRepo; // ... or repo and group will be created if (proceed) { // perform operations on Gerrit if (createGroup || createRepo) { String baseUrl = (String)request.getAttribute(Consts.ATTRIBUTE_BASE_URL); if (createGroup) { String groupDescription = getDescription(gerritServer.getGroupDescription(), baseUrl, project, user, Collections.singletonMap("group", group)); //$NON-NLS-1$ client.createGroup(group, StringUtils.EMPTY, groupDescription, knownAccounts); } if (createRepo) { String projectDescription = getDescription(gerritServer.getProjectDescription(), baseUrl, project, user, Collections.singletonMap("repository", repository)); //$NON-NLS-1$ ProjectOptions options = new ProjectOptions(); options.setName(repository); options.setBranch(gerritServer.getBranch()); options.setOwner(group); options.setParent(StringUtils.isNotBlank(parent)? parent : gerritServer.getParent()); options.setPermissionsOnly(permitsOnly); options.setDescription(projectDescription); options.setSubmitType(gerritServer.getSubmitType()); options.setUseContributorAgreements( InheritableBoolean.valueOf(gerritServer.isUseContributorAgreement())); options.setUseSignedOffBy( InheritableBoolean.valueOf(gerritServer.isUseSignedOffBy())); options.setRequiredChangeId(InheritableBoolean.TRUE); options.setUseContentMerge(InheritableBoolean.TRUE); options.setCreateEmptyCommit(emptyCommit); client.createProject(options); } } // persist new SCM location @ project (if it doesn't exist already) DevInfProjectExt devInf = project.getExtension(DevInfProjectExt.class); if (devInf == null) { devInf = new DevInfProjectExt(); project.addExtension(devInf); } String scmLocation = getScmLocation(gerritServer, repository, project, user); if (!devInf.hasScmLocation(scmLocation)) { devInf.addScmLocation(scmLocation); ProjectService projectService = ((ProjectService)EntityServices.getByEntityClass(Project.class)); projectService.persist(project, user.getUserId()); } } request.setAttribute(ATTRIBUTE_DATA_SAVED, proceed); } } // (!) CANCEL (redirect to project landing page) else if ("cancel".equals(action)) { response.sendRedirect(Consts.URL_PROJECTS + "/" + projectId); //$NON-NLS-1$ } } catch (GerritClientException e) { handleException(request, response, e); } catch (Exception e) { handleException(request, response, e); } finally { if (client != null) { client.disconnect(); } } // proceed along the chain chain.doFilter(request, response); } /** * Dispatch this request to error page */ private void handleException(ServletRequest request, ServletResponse response, Exception e) throws ServletException, IOException { RequestDispatcher rd = request.getRequestDispatcher(Consts.URL_ERROR); request.setAttribute(ATTRIBUTE_EXCEPTION, e); rd.forward(request, response); } /** * Generates a name based on the project hierarchy (short names) and the project ID using a delimiter and a possible suffix. */ String generateName(Project project, String delimiter, String suffix) { StringBuffer sb = new StringBuffer(); ProjectService projectService = ((ProjectService)EntityServices.getByEntityClass(Project.class)); for (Project parent : projectService.getParentChain(project.getUuid())) { if (parent.getProjectId() == project.getProjectId()) { sb.insert(0, parent.getProjectId()); } else { sb.insert(0, delimiter); sb.insert(0, parent.getOrConstructShortName()); } } sb.append(suffix); return sb.toString(); } Set<String> getProposedProjectMembers(Project project) { PeopleExtension peopleExt = project.getExtension(PeopleExtension.class); if (peopleExt == null) { return Collections.emptySet(); } Set<Member> projectMembers = new HashSet<Member>(); projectMembers.addAll(peopleExt.getLeads()); projectMembers.addAll(peopleExt.getMembers()); Set<String> userIds = new HashSet<String>(); for (Member projectMember : projectMembers) { userIds.add(projectMember.getUserID()); } return userIds; } Set<String> getRelatedProjects(GerritServerConfig gerritConfig, GerritClient client, Project project, User user) throws GerritClientException { // pattern to match with SCM location strings of related projects: // we replace ${repository} with the regular expression "(.*)", which allows us to extract // the repository name from a matching SCM location with Matcher.group(int). Pattern scmPattern = Pattern.compile(getScmLocation(gerritConfig, "(.*)", project, user)); //$NON-NLS-1$ ProjectService projectService = ((ProjectService)EntityServices.getByEntityClass(Project.class)); // first, add all my parents: inheriting a group from a parent is // likely the most common use case List<Project> relatedProjects = projectService.getParentChain(project.getUuid()); // second, add my siblings unless I'm a top-level project Project parent = project.getParentProject(); if (parent != null) { relatedProjects.addAll(parent.getSubProjects()); } // finally, add my own subprojects relatedProjects.addAll(project.getSubProjects()); return getRepositoryNames(relatedProjects, scmPattern); } /** * Returns groups that are related to the given project based on its position in * the project hierarchy: Groups used by parents, groups used by siblings, * groups used by subprojects. */ List<String> getRelatedGroups(GerritServerConfig gerritConfig, GerritClient client, Project project, User user) throws GerritClientException { Set<String> repositoryNames = getRelatedProjects(gerritConfig, client, project, user); return client.getGroups(repositoryNames.toArray(new String[repositoryNames.size()])); } Set<String> getAllGroups(final GerritClient client, final Project project) { Set<String> result = new TreeSet<String>(); try { result.addAll(client.getGroups()); return result; } catch (ConnectionException e) { LOG.warn("Can't connect to Gerrit:", e); return Collections.emptySet(); } catch (CommandException e) { LOG.warn("Can't connect to Gerrit:", e); return Collections.emptySet(); } } Set<String> getProjects(final GerritClient client, String type) { Set<String> result = new TreeSet<String>(); try { result.addAll(client.getProjects(type)); return result; } catch (ConnectionException e) { LOG.warn("Can't connect to Gerrit:", e); return Collections.emptySet(); } catch (CommandException e) { LOG.warn("Can't connect to Gerrit:", e); return Collections.emptySet(); } } Set<String> getRepositoryNames(List<Project> projects, Pattern scmPattern) { LinkedHashSet<String> repositoryNames = new LinkedHashSet<String>(); for (Project project : projects) { DevInfProjectExt devInf = project.getExtension(DevInfProjectExt.class); if (devInf == null || project.isInherited(DevInfProjectExt.class)) { continue; } for (String scmLocation : devInf.getScmLocations()) { Matcher matcher = scmPattern.matcher(scmLocation); if (matcher.matches() && matcher.groupCount() > 0) { repositoryNames.add(matcher.group(1)); } } } return repositoryNames; } @SuppressWarnings("nls") String getScmLocation(GerritServerConfig gerritConfig, String repository, Project project, User user) { Map<String, String> parameters = new HashMap<String, String>(); if (StringUtils.isNotBlank(gerritConfig.getProtocol())) { parameters.put("protocol", gerritConfig.getProtocol()); } parameters.put("host", gerritConfig.getHost()); if (StringUtils.isNotBlank(gerritConfig.getPort())) { parameters.put("port", gerritConfig.getPort()); } parameters.put("repository", repository); if (StringUtils.isNotBlank(gerritConfig.getParent())) { parameters.put("parent", gerritConfig.getParent()); } if (StringUtils.isNotBlank(gerritConfig.getBranch())) { parameters.put("branch", gerritConfig.getBranch()); } parameters.put("user", user.getDisplayName()); parameters.put("userId", user.getUserId()); String scmTemplate = gerritConfig.getScmTemplate(); if (StringUtils.isBlank(scmTemplate)) { scmTemplate = DEFAULT_SCM_TEMPLATE; } PropertyLookup propertyLookup = new PropertyLookup(parameters); propertyLookup.putAllProperties(project, ""); propertyLookup.putAllProperties(user, "user"); StrSubstitutor substitutor = new StrSubstitutor(propertyLookup); return substitutor.replace(scmTemplate); } @SuppressWarnings("nls") String getDescription(String descriptionTemplate, String baseUrl, Project project, User user, Map<String, String> properties) { Map<String, String> parameters = new HashMap<String, String>(); parameters.putAll(properties); parameters.put("user", user.getDisplayName()); parameters.put("userId", user.getUserId()); parameters.put("link", baseUrl + Consts.URL_PROJECTS + "/" + project.getProjectId()); if (StringUtils.isBlank(descriptionTemplate)) { descriptionTemplate = DEFAULT_DESCRIPTION; } PropertyLookup propertyLookup = new PropertyLookup(parameters); propertyLookup.putAllProperties(project, ""); propertyLookup.putAllProperties(user, "user"); StrSubstitutor substitutor = new StrSubstitutor(propertyLookup); return substitutor.replace(descriptionTemplate); } }