jetbrains.buildServer.server.rest.APIController.java Source code

Java tutorial

Introduction

Here is the source code for jetbrains.buildServer.server.rest.APIController.java

Source

/*
 * Copyright 2000-2011 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jetbrains.buildServer.server.rest;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.Function;
import jetbrains.buildServer.ExtensionHolder;
import jetbrains.buildServer.controllers.BaseController;
import jetbrains.buildServer.plugins.bean.ServerPluginInfo;
import jetbrains.buildServer.server.rest.jersey.JerseyWebComponent;
import jetbrains.buildServer.server.rest.request.Constants;
import jetbrains.buildServer.serverSide.SBuildServer;
import jetbrains.buildServer.serverSide.SecurityContextEx;
import jetbrains.buildServer.serverSide.TeamCityProperties;
import jetbrains.buildServer.util.FuncThrow;
import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.web.openapi.WebControllerManager;
import jetbrains.buildServer.web.util.WebUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;

/**
 * @author Yegor.Yarko
 *         Date: 23.03.2009
 */
public class APIController extends BaseController implements ServletContextAware {
    final Logger LOG = Logger.getInstance(APIController.class.getName());
    private JerseyWebComponent myWebComponent;
    private final ConfigurableApplicationContext myConfigurableApplicationContext;
    private final SecurityContextEx mySecurityContext;
    private final ExtensionHolder myExtensionHolder;

    private final ClassLoader myClassloader;
    private String myAuthToken;
    private RequestPathTransformInfo myRequestPathTransformInfo;

    public APIController(final SBuildServer server, WebControllerManager webControllerManager,
            final ConfigurableApplicationContext configurableApplicationContext,
            final SecurityContextEx securityContext, final RequestPathTransformInfo requestPathTransformInfo,
            final ServerPluginInfo pluginDescriptor, final ExtensionHolder extensionHolder)
            throws ServletException {
        super(server);
        myExtensionHolder = extensionHolder;
        setSupportedMethods(new String[] { METHOD_GET, METHOD_HEAD, METHOD_POST, "PUT", "OPTIONS", "DELETE" });

        myConfigurableApplicationContext = configurableApplicationContext;
        mySecurityContext = securityContext;
        myRequestPathTransformInfo = requestPathTransformInfo;

        final List<String> originalBindPaths = getBindPaths(pluginDescriptor);
        List<String> bindPaths = new ArrayList<String>(originalBindPaths);
        bindPaths.addAll(addPrefix(originalBindPaths, StringUtil.removeTailingSlash(WebUtil.HTTP_AUTH_PREFIX)));
        bindPaths.addAll(addPrefix(originalBindPaths, StringUtil.removeTailingSlash(WebUtil.GUEST_AUTH_PREFIX)));

        Map<String, String> transformBindPaths = new HashMap<String, String>();
        addEntries(transformBindPaths, bindPaths, Constants.API_URL);
        addEntries(transformBindPaths, addSuffix(bindPaths, Constants.EXTERNAL_APPLICATION_WADL_NAME),
                Constants.JERSEY_APPLICATION_WADL_NAME);

        myRequestPathTransformInfo.setPathMapping(transformBindPaths);
        LOG.debug("Will use request mapping: " + myRequestPathTransformInfo);

        registerController(webControllerManager, originalBindPaths);

        myClassloader = getClass().getClassLoader();

        if (TeamCityProperties.getBoolean("rest.use.authToken")) {
            try {
                myAuthToken = URLEncoder.encode(UUID.randomUUID().toString() + (new Date()).toString().hashCode(),
                        "UTF-8");
                LOG.info("Authentication token for superuser generated: '" + myAuthToken + "'.");
            } catch (UnsupportedEncodingException e) {
                LOG.warn(e);
            }
        }
    }

    private static void addEntries(final Map<String, String> map, final List<String> keys, final String value) {
        for (String key : keys) {
            map.put(key, value);
        }
    }

    private List<String> addPrefix(final List<String> paths, final String prefix) {
        List<String> result = new ArrayList<String>(paths.size());
        for (String path : paths) {
            result.add(prefix + path);
        }
        return result;
    }

    private List<String> addSuffix(final List<String> paths, final String suffix) {
        List<String> result = new ArrayList<String>(paths.size());
        for (String path : paths) {
            result.add(path + suffix);
        }
        return result;
    }

    private void registerController(final WebControllerManager webControllerManager, final List<String> bindPaths) {
        try {
            for (String controllerBindPath : bindPaths) {
                LOG.debug("Binding REST API to path '" + controllerBindPath + "'");
                webControllerManager.registerController(controllerBindPath + "/**", this);
            }
        } catch (Exception e) {
            LOG.error("Error registering controller", e);
        }
    }

    private List<String> getBindPaths(final ServerPluginInfo pluginDescriptor) {
        String bindPath = pluginDescriptor.getParameterValue(Constants.BIND_PATH_PROPERTY_NAME);
        if (bindPath == null) {
            return Collections.singletonList(Constants.API_URL);
        }

        final String[] bindPaths = bindPath.split(",");

        if (bindPath.length() == 0) {
            LOG.error("Invalid REST API bind path in plugin descriptor: '" + bindPath + "', using defaults");
            return Collections.singletonList(Constants.API_URL);
        }

        return Arrays.asList(bindPaths);
    }

    private void init() throws ServletException {
        myWebComponent = new JerseyWebComponent();
        myWebComponent.setExtensionHolder(myExtensionHolder);
        myWebComponent.setWebApplicationContext(myConfigurableApplicationContext);
        myWebComponent.init(createJerseyConfig());
    }

    private FilterConfig createJerseyConfig() {
        return new FilterConfig() {
            Map<String, String> initParameters = new HashMap<String, String>();

            {
                //        initParameters.put("com.sun.jersey.config.property.WadlGeneratorConfig", "jetbrains.buildServer.server.rest.WadlGenerator");
                initParameters.put("com.sun.jersey.config.property.packages",
                        "jetbrains.buildServer.server.rest.request;" + getPackagesFromExtensions());
            }

            private String getPackagesFromExtensions() {
                return StringUtil.join(myServer.getExtensions(RESTControllerExtension.class),
                        new Function<RESTControllerExtension, String>() {
                            public String fun(final RESTControllerExtension restControllerExtension) {
                                return restControllerExtension.getPackage();
                            }
                        }, ";");
            }

            public String getFilterName() {
                return "jerseyFilter";
            }

            public ServletContext getServletContext() {
                //return APIController.this.getServletContext();
                // workaround for http://jetbrains.net/tracker/issue2/TW-7656
                for (ApplicationContext ctx = getApplicationContext(); ctx != null; ctx = ctx.getParent()) {
                    if (ctx instanceof WebApplicationContext) {
                        return ((WebApplicationContext) ctx).getServletContext();
                    }
                }
                throw new RuntimeException("WebApplication context was not found.");
            }

            public String getInitParameter(final String s) {
                return initParameters.get(s);
            }

            public Enumeration getInitParameterNames() {
                return new Vector<String>(initParameters.keySet()).elements();
            }
        };
    }

    static final boolean ENABLE_DISABLING_CHECK = TeamCityProperties.getBoolean("rest.enable.disabling.check");

    protected ModelAndView doHandle(final HttpServletRequest request, final HttpServletResponse response)
            throws Exception {
        if (ENABLE_DISABLING_CHECK) { //necessary until TW-16750 is fixed
            if (TeamCityProperties.getBoolean("rest.disable")) {
                response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED,
                        "REST API is disabled on TeamCity server with 'rest.disable' internal property.");
                return null;
            }
        }
        final long requestStartProcessing = System.nanoTime();
        if (LOG.isDebugEnabled()) {
            LOG.debug("REST API request received: " + WebUtil.getRequestDump(request));
        }
        ensureInitialized();

        boolean runAsSystem = false;
        if (TeamCityProperties.getBoolean("rest.use.authToken")) {
            String authToken = request.getParameter("authToken");
            if (StringUtil.isNotEmpty(authToken) && StringUtil.isNotEmpty(getAuthToken())) {
                if (authToken.equals(getAuthToken())) {
                    runAsSystem = true;
                } else {
                    synchronized (this) {
                        Thread.sleep(10000); //to prevent bruteforcing
                    }
                    response.sendError(403, "Wrong authToken specified");
                    return null;
                }
            }
        }

        final boolean runAsSystemActual = runAsSystem;
        // workaround for http://jetbrains.net/tracker/issue2/TW-7656
        jetbrains.buildServer.util.Util.doUnderContextClassLoader(getClass().getClassLoader(),
                new FuncThrow<Void, Exception>() {
                    public Void apply() throws Exception {
                        // patching request
                        final HttpServletRequest actualRequest = new RequestWrapper(
                                patchRequest(request, "Accept", "overrideAccept"), myRequestPathTransformInfo);

                        if (runAsSystemActual) {
                            try {
                                LOG.debug("Executing request with system security level");
                                mySecurityContext.runAsSystem(new SecurityContextEx.RunAsAction() {
                                    public void run() throws Throwable {
                                        myWebComponent.doFilter(actualRequest, response, null);
                                    }
                                });
                            } catch (Throwable throwable) {
                                LOG.debug(throwable.getMessage(), throwable);
                                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                        throwable.getMessage());
                            }
                        } else {
                            myWebComponent.doFilter(actualRequest, response, null);
                        }
                        return null;
                    }
                });

        if (LOG.isDebugEnabled()) {
            final long requestFinishProcessing = System.nanoTime();
            LOG.debug("REST API request processing finished in "
                    + (requestFinishProcessing - requestStartProcessing) / 1000000 + " ms");
        }
        return null;
    }

    //todo: move to RequestWrapper

    private HttpServletRequest patchRequest(final HttpServletRequest request, final String headerName,
            final String parameterName) {
        final String newValue = request.getParameter(parameterName);
        if (!StringUtil.isEmpty(newValue)) {
            return modifyRequestHeader(request, headerName, newValue);
        }
        return request;
    }

    private HttpServletRequest modifyRequestHeader(final HttpServletRequest request, final String headerName,
            final String newValue) {
        return new HttpServletRequestWrapper(request) {
            @Override
            public String getHeader(final String name) {
                if (headerName.equalsIgnoreCase(name)) {
                    return newValue;
                }
                return super.getHeader(name);
            }

            @Override
            public Enumeration getHeaders(final String name) {
                if (headerName.equalsIgnoreCase(name)) {
                    return Collections.enumeration(Collections.singletonList(newValue));
                }
                return super.getHeaders(name);
            }
        };
    }

    private void ensureInitialized() throws ServletException {
        //todo: check synchronization
        synchronized (this) {
            // workaround for http://jetbrains.net/tracker/issue2/TW-7656
            if (myWebComponent == null) {
                final ClassLoader cl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(myClassloader);
                try {
                    init();
                } catch (RuntimeException e) {
                    //otherwise exception here is swallowed and logged nowhere
                    LOG.error("Error initializing REST API: ", e);
                    myWebComponent = null;
                    throw e;
                } catch (Error e) {
                    LOG.error("Error initializing REST API: ", e);
                    myWebComponent = null;
                    throw e;
                } catch (ServletException e) {
                    LOG.error("Error initializing REST API: ", e);
                    myWebComponent = null;
                    throw e;
                } finally {
                    Thread.currentThread().setContextClassLoader(cl);
                }
            }
        }
    }

    private String getAuthToken() {
        return myAuthToken;
    }

}