package org.jenkinsci.plugins.activiti_explorer; import com.cisco.step.jenkins.plugins.jenkow.JenkowBuilder.DescriptorImpl; import com.cisco.step.jenkins.plugins.jenkow.JenkowEngine; import com.cloudbees.vietnam4j.ProxiedWebApplication; import hudson.Extension; import hudson.Util; import hudson.model.UnprotectedRootAction; import hudson.model.User; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.activiti.engine.ProcessEngine; import org.apache.commons.io.IOUtils; import org.dom4j.DocumentException; import org.jenkinsci.plugins.activiti_explorer.dto.UserDTO; import org.jenkinsci.plugins.jenkow.activiti.override.JenkinsProcessEngineFactory; import org.jenkinsci.plugins.jenkow.activiti.override.ServletContextDataSource; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; /** * Activiti Explorer web application embedded inside Jenkins. * * @author Kohsuke Kawaguchi */ @Extension public class ActivitiExplorer implements UnprotectedRootAction { private ProxiedWebApplication webApp; @Inject DescriptorImpl descriptor; public String getIconFileName() { return "/plugin/jenkow-activiti-explorer/images/24x24/activiti.png"; } public String getDisplayName() { return "Activiti Explorer"; } public String getUrlName() { return "activiti-explorer"; } public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { ProxiedWebApplication webApp = getProxyWebApplication(req); HttpSession session = req.getSession(); HttpSession ps = webApp.getProxiedSession(session); UserDTO oldUser = (UserDTO)ps.getAttribute("jenkins.user"); UserDTO newUser = createUserInfo(); if (!mapToId(oldUser).equals(mapToId(newUser))) { // force a new session. AE isn't designed to anticipate the user change without invalidating a session, // but Jenkins does that. So when we see that the user has changed in Jenkins, force a new session // (but only in AE.) webApp.clearProxiedSession(session); ps = webApp.getProxiedSession(session); } ps.setAttribute("jenkins.user", newUser); webApp.handleRequest(req, rsp); } private String mapToId(UserDTO o) { return o==null ? "\u0000" : o.id; } /** * Creates {@link UserDTO} that represents the currently logged-in user. */ private UserDTO createUserInfo() { Authentication a = Jenkins.getAuthentication(); User u = User.current(); UserDTO user = new UserDTO(); user.id = a.getName(); user.firstName = u != null ? u.getFullName() : a.getName(); user.lastName = ""; user.fullName = u != null ? u.getFullName() : a.getName(); user.isAdmin = Jenkins.getInstance().getACL().hasPermission(a,Jenkins.ADMINISTER); user.isUser = true; return user; } /** * Extracts a war file into the specified directory. */ private void extract(URL war, File dir) throws IOException { if (dir.exists()) Util.deleteRecursive(dir); JarInputStream jar = new JarInputStream(war.openStream()); try { JarEntry e; while ((e=jar.getNextJarEntry())!=null) { File dst = new File(dir,e.getName()); if (e.isDirectory()) dst.mkdirs(); else { dst.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(dst); try { IOUtils.copy(jar, out); } finally { out.close(); } if (e.getTime()>=0) dst.setLastModified(e.getTime()); } } } finally { jar.close(); } } /** * Patch activiti-explorer war file so that we can inject our stuff into it. */ private void patch(File war) throws DocumentException, IOException { new XmlPatcher(new File(war,"WEB-INF/activiti-standalone-context.xml")) { public void patch() { // patch data source in overrideBeanTo("dataSource", ServletContextDataSource.class.getName()); // single sign-on // can't load the class yet, so string overrideBeanTo("activitiLoginHandler", "org.jenkinsci.plugins.jenkow.activiti.override.JenkinsLoginHandler"); // inject our own fully configured ProcessEngine overrideBeanTo("processEngine", JenkinsProcessEngineFactory.class.getName()); // no more demo data generation removeBean("demoDataGenerator"); } }; new XmlPatcher(new File(war,"WEB-INF/activiti-ui-context.xml")) { public void patch() { // tweak the navigation bar overrideBeanTo("componentFactories", "org.jenkinsci.plugins.jenkow.activiti.override.JenkinsComponentFactories"); swapClass("explorerApp","org.jenkinsci.plugins.jenkow.activiti.override.ExplorerApp2"); } }; } private synchronized ProxiedWebApplication getProxyWebApplication(StaplerRequest req) throws ServletException { if (webApp==null) { try { final ClassLoader ourLoader = getClass().getClassLoader(); File war = new File(Jenkins.getInstance().getRootDir(), "cache/activiti-explorer"); extract(ourLoader.getResource("activiti-explorer.war"),war); patch(war); webApp = new ProxiedWebApplication( war, req.getContextPath()+'/'+getUrlName()); webApp.setParentLoaderHasPriority(true); webApp.addClassPath(ourLoader.getResource("activiti-explorer-override.jar")); // inject DataSource webApp.getProxiedServletContext().setAttribute(ServletContextDataSource.class.getName(), descriptor.getDatabase().getDataSource()); webApp.getProxiedServletContext().setAttribute(ProcessEngine.class.getName(), JenkowEngine.getEngine()); // pass through to the servlet container so that Activiti won't get confused // by what Jenkins loads (e.g., log4j version inconsistency) // but we do expose DTO classes to share them between two apps. webApp.setParentClassLoader(new URLClassLoader(new URL[0],HttpServletRequest.class.getClassLoader()) { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { if (name.startsWith("org.jenkinsci.plugins.activiti_explorer.dto.") || name.startsWith("org.activiti.engine.")) { return getClass().getClassLoader().loadClass(name); } throw new ClassNotFoundException(name); } }); webApp.start(); } catch (Exception e) { throw new ServletException(e); } } return webApp; } }