org.xwiki.test.ui.TestUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.test.ui.TestUtils.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.test.ui;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
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.concurrent.TimeUnit;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.Wait;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.xwiki.rest.model.jaxb.ObjectFactory;
import org.xwiki.rest.model.jaxb.Xwiki;
import org.xwiki.test.integration.XWikiExecutor;
import org.xwiki.test.ui.po.ViewPage;
import org.xwiki.test.ui.po.editor.ClassEditPage;
import org.xwiki.test.ui.po.editor.ObjectEditPage;

/**
 * Helper methods for testing, not related to a specific Page Object. Also made available to tests classes.
 *
 * @version $Id: d3c1db1466f7b1dc0a82e320c187f1b11b58a6c2 $
 * @since 3.2M3
 */
public class TestUtils {
    /**
     * @since 5.0M2
     */
    public static final UsernamePasswordCredentials ADMIN_CREDENTIALS = new UsernamePasswordCredentials("Admin",
            "admin");

    /**
     * @since 5.1M1
     */
    public static final UsernamePasswordCredentials SUPER_ADMIN_CREDENTIALS = new UsernamePasswordCredentials(
            "superadmin", "pass");

    /**
     * @since 5.0M2
     */
    public static final String BASE_URL = XWikiExecutor.URL + ":" + XWikiExecutor.DEFAULT_PORT + "/xwiki/";

    /**
     * @since 5.0M2
     */
    public static final String BASE_BIN_URL = BASE_URL + "bin/";

    /**
     * @since 5.0M2
     */
    public static final String BASE_REST_URL = BASE_URL + "rest/";

    private static PersistentTestContext context;

    /**
     * Used to convert Java object into its REST XML representation.
     */
    private static Marshaller marshaller;

    /**
     * Used to convert REST request XML result into its Java representation.
     */
    private static Unmarshaller unmarshaller;

    /**
     * Used to create REST Java resources.
     */
    private static ObjectFactory objectFactory;

    {
        {
            try {
                // Initialize REST related tools
                JAXBContext context = JAXBContext.newInstance(
                        "org.xwiki.rest.model.jaxb" + ":org.xwiki.extension.repository.xwiki.model.jaxb");
                marshaller = context.createMarshaller();
                unmarshaller = context.createUnmarshaller();
                objectFactory = new ObjectFactory();
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * How long to wait before failing a test because an element cannot be found. Can be overridden with setTimeout.
     */
    private int timeout = 10;

    /** Cached secret token. TODO cache for each user. */
    private String secretToken = null;

    private HttpClient adminHTTPClient;

    public TestUtils() {
        this.adminHTTPClient = new HttpClient();
        this.adminHTTPClient.getState().setCredentials(AuthScope.ANY, ADMIN_CREDENTIALS);
        this.adminHTTPClient.getParams().setAuthenticationPreemptive(true);
    }

    /** Used so that AllTests can set the persistent test context. */
    public static void setContext(PersistentTestContext context) {
        TestUtils.context = context;
    }

    protected WebDriver getDriver() {
        return context.getDriver();
    }

    public Session getSession() {
        return this.new Session(getDriver().manage().getCookies(), getSecretToken());
    }

    public void setSession(Session session) {
        WebDriver.Options options = getDriver().manage();
        options.deleteAllCookies();
        if (session != null) {
            for (Cookie cookie : session.getCookies()) {
                options.addCookie(cookie);
            }
        }
        if (session != null && !StringUtils.isEmpty(session.getSecretToken())) {
            this.secretToken = session.getSecretToken();
        } else {
            recacheSecretToken();
        }
    }

    /**
     * Consider using setSession(null) because it will drop the cookies which is faster than invoking a logout action.
     */
    public String getURLToLogout() {
        return getURL("XWiki", "XWikiLogin", "logout");
    }

    public String getURLToLoginAsAdmin() {
        return getURLToLoginAs(ADMIN_CREDENTIALS.getUserName(), ADMIN_CREDENTIALS.getPassword());
    }

    public String getURLToLoginAsSuperAdmin() {
        return getURLToLoginAs(SUPER_ADMIN_CREDENTIALS.getUserName(), SUPER_ADMIN_CREDENTIALS.getPassword());
    }

    public String getURLToLoginAs(final String username, final String password) {
        return getURLToLoginAndGotoPage(username, password, null);
    }

    /**
     * @param pageURL the URL of the page to go to after logging in.
     * @return URL to accomplish login and goto.
     */
    public String getURLToLoginAsAdminAndGotoPage(final String pageURL) {
        return getURLToLoginAndGotoPage(ADMIN_CREDENTIALS.getUserName(), ADMIN_CREDENTIALS.getPassword(), pageURL);
    }

    /**
     * @param pageURL the URL of the page to go to after logging in.
     * @return URL to accomplish login and goto.
     */
    public String getURLToLoginAsSuperAdminAndGotoPage(final String pageURL) {
        return getURLToLoginAndGotoPage(SUPER_ADMIN_CREDENTIALS.getUserName(),
                SUPER_ADMIN_CREDENTIALS.getPassword(), pageURL);
    }

    /**
     * @param username the name of the user to log in as.
     * @param password the password for the user to log in.
     * @param pageURL the URL of the page to go to after logging in.
     * @return URL to accomplish login and goto.
     */
    public String getURLToLoginAndGotoPage(final String username, final String password, final String pageURL) {
        Map<String, String> parameters = new HashMap<String, String>() {
            {
                put("j_username", username);
                put("j_password", password);
                if (pageURL != null && pageURL.length() > 0) {
                    put("xredirect", pageURL);
                }
            }
        };
        return getURL("XWiki", "XWikiLogin", "loginsubmit", parameters);
    }

    /**
     * @return URL to a non existent page that loads very fast (we are using plain mode so that we don't even have to
     *         display the skin ;))
     */
    public String getURLToNonExistentPage() {
        return getURL("NonExistentSpace", "NonExistentPage", "view", "xpage=plain");
    }

    /**
     * After successful completion of this function, you are guaranteed to be logged in as the given user and on the
     * page passed in pageURL.
     *
     * @param pageURL
     */
    public void assertOnPage(final String pageURL) {
        final String pageURI = pageURL.replaceAll("\\?.*", "");
        waitUntilCondition(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver driver) {
                return getDriver().getCurrentUrl().contains(pageURI);
            }
        });
    }

    public <T> void waitUntilCondition(ExpectedCondition<T> condition) {
        // Temporarily remove the implicit wait on the driver since we're doing our own waits...
        getDriver().manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);

        Wait<WebDriver> wait = new WebDriverWait(getDriver(), getTimeout());
        try {
            wait.until(condition);
        } finally {
            // Reset timeout
            setDriverImplicitWait(getDriver());
        }
    }

    public String getLoggedInUserName() {
        String loggedInUserName = null;
        List<WebElement> elements = findElementsWithoutWaiting(getDriver(), By.xpath("//div[@id='tmUser']/span/a"));
        if (!elements.isEmpty()) {
            String href = elements.get(0).getAttribute("href");
            loggedInUserName = href.substring(href.lastIndexOf("/") + 1);
        }
        return loggedInUserName;
    }

    public void createUserAndLogin(final String username, final String password, Object... properties) {
        createUserAndLoginWithRedirect(username, password, getURLToNonExistentPage(), properties);
    }

    public void createUserAndLoginWithRedirect(final String username, final String password, String url,
            Object... properties) {
        createUser(username, password, getURLToLoginAndGotoPage(username, password, url), properties);
    }

    public void createUser(final String username, final String password, String redirectURL, Object... properties) {
        Map<String, String> parameters = new HashMap<String, String>();
        parameters.put("register", "1");
        parameters.put("xwikiname", username);
        parameters.put("register_password", password);
        parameters.put("register2_password", password);
        parameters.put("register_email", "");
        parameters.put("xredirect", redirectURL);
        parameters.put("form_token", getSecretToken());
        getDriver().get(getURL("XWiki", "Register", "register", parameters));
        recacheSecretToken();
        if (properties.length > 0) {
            updateObject("XWiki", username, "XWiki.XWikiUsers", 0, properties);
        }
    }

    /**
     * @deprecated starting with 5.0M2 use {@link #createUserAndLogin(String, String, Object...)} instead
     */
    @Deprecated
    public void registerLoginAndGotoPage(final String username, final String password, final String pageURL) {
        createUserAndLogin(username, password);
        getDriver().get(pageURL);
    }

    public ViewPage gotoPage(String space, String page) {
        gotoPage(space, page, "view");
        return new ViewPage();
    }

    public void gotoPage(String space, String page, String action) {
        gotoPage(space, page, action, "");
    }

    /**
     * @since 3.5M1
     */
    public void gotoPage(String space, String page, String action, Object... queryParameters) {
        gotoPage(space, page, action, toQueryString(queryParameters));
    }

    public void gotoPage(String space, String page, String action, Map<String, ?> queryParameters) {
        gotoPage(space, page, action, toQueryString(queryParameters));
    }

    public void gotoPage(String space, String page, String action, String queryString) {
        // Only navigate if the current URL is different from the one to go to, in order to improve performances.
        gotoPage(getURL(space, page, action, queryString));
    }

    public void gotoPage(String url) {
        // Only navigate if the current URL is different from the one to go to, in order to improve performances.
        if (!getDriver().getCurrentUrl().equals(url)) {
            getDriver().get(url);
        }
    }

    public String getURLToDeletePage(String space, String page) {
        return getURL(space, page, "delete", "confirm=1");
    }

    /**
     * @param space the name of the space to delete
     * @return the URL that can be used to delete the specified pace
     * @since 4.5
     */
    public String getURLToDeleteSpace(String space) {
        return getURL(space, "WebHome", "deletespace", "confirm=1");
    }

    public ViewPage createPage(String space, String page, String content, String title) {
        return createPage(space, page, content, title, null);
    }

    public ViewPage createPage(String space, String page, String content, String title, String syntaxId) {
        return createPage(space, page, content, title, syntaxId, null);
    }

    public ViewPage createPage(String space, String page, String content, String title, String syntaxId,
            String parentFullPageName) {
        Map<String, String> queryMap = new HashMap<String, String>();
        if (content != null) {
            queryMap.put("content", content);
        }
        if (title != null) {
            queryMap.put("title", title);
        }
        if (syntaxId != null) {
            queryMap.put("syntaxId", syntaxId);
        }
        if (parentFullPageName != null) {
            queryMap.put("parent", parentFullPageName);
        }
        gotoPage(space, page, "save", queryMap);
        return new ViewPage();
    }

    /**
     * @since 5.1M2
     */
    public ViewPage createPageWithAttachment(String space, String page, String content, String title,
            String syntaxId, String parentFullPageName, String attachmentName, InputStream attachmentData)
            throws Exception {
        return createPageWithAttachment(space, page, content, title, syntaxId, parentFullPageName, attachmentName,
                attachmentData, null);
    }

    /**
     * @since 5.1M2
     */
    public ViewPage createPageWithAttachment(String space, String page, String content, String title,
            String syntaxId, String parentFullPageName, String attachmentName, InputStream attachmentData,
            UsernamePasswordCredentials credentials) throws Exception {
        ViewPage vp = createPage(space, page, content, title, syntaxId, parentFullPageName);
        attachFile(space, page, attachmentName, attachmentData, false, credentials);
        return vp;
    }

    /**
     * @since 5.1M2
     */
    public ViewPage createPageWithAttachment(String space, String page, String content, String title,
            String attachmentName, InputStream attachmentData) throws Exception {
        return createPageWithAttachment(space, page, content, title, null, null, attachmentName, attachmentData);
    }

    /**
     * @since 5.1M2
     */
    public ViewPage createPageWithAttachment(String space, String page, String content, String title,
            String attachmentName, InputStream attachmentData, UsernamePasswordCredentials credentials)
            throws Exception {
        ViewPage vp = createPage(space, page, content, title);
        attachFile(space, page, attachmentName, attachmentData, false, credentials);
        return vp;
    }

    public void deletePage(String space, String page) {
        getDriver().get(getURLToDeletePage(space, page));
    }

    /**
     * Accesses the URL to delete the specified space.
     *
     * @param space the name of the space to delete
     * @since 4.5
     */
    public void deleteSpace(String space) {
        getDriver().get(getURLToDeleteSpace(space));
    }

    public boolean pageExists(String space, String page) {
        boolean exists;
        try {
            executeGet(getURL(space, page), Status.OK.getStatusCode());
            exists = true;
        } catch (Exception e) {
            exists = false;
        }

        return exists;
    }

    /**
     * Get the URL to view a page.
     *
     * @param space the space in which the page resides.
     * @param page the name of the page.
     */
    public String getURL(String space, String page) {
        return getURL(space, page, "view");
    }

    /**
     * Get the URL of an action on a page.
     *
     * @param space the space in which the page resides.
     * @param page the name of the page.
     * @param action the action to do on the page.
     */
    public String getURL(String space, String page, String action) {
        return getURL(space, page, action, "");
    }

    /**
     * Get the URL of an action on a page with a specified query string.
     *
     * @param space the space in which the page resides.
     * @param page the name of the page.
     * @param action the action to do on the page.
     * @param queryString the query string to pass in the URL.
     */
    public String getURL(String space, String page, String action, String queryString) {
        return getURL(new String[] { space, page }, action, queryString);
    }

    private String getURL(String[] path, String action, String queryString) {
        StringBuilder builder = new StringBuilder(TestUtils.BASE_BIN_URL);

        builder.append(action);
        for (String element : path) {
            builder.append('/').append(escapeURL(element));
        }

        boolean needToAddSecretToken = !Arrays.asList("view", "register", "download").contains(action);
        if (needToAddSecretToken || !StringUtils.isEmpty(queryString)) {
            builder.append('?');
        }
        if (needToAddSecretToken) {
            addQueryStringEntry(builder, "form_token", getSecretToken());
            builder.append('&');
        }
        if (!StringUtils.isEmpty(queryString)) {
            builder.append(queryString);
        }

        return builder.toString();
    }

    /**
     * Get the URL of an action on a page with specified parameters. If you need to pass multiple parameters with the
     * same key, this function will not work.
     *
     * @param space the space in which the page resides.
     * @param page the name of the page.
     * @param action the action to do on the page.
     * @param queryParameters the parameters to pass in the URL, these will be automatically URL encoded.
     */
    public String getURL(String space, String page, String action, Map<String, ?> queryParameters) {
        return getURL(space, page, action, toQueryString(queryParameters));
    }

    /**
     * @param space the name of the space that contains the page with the specified attachment
     * @param page the name of the page that holds the attachment
     * @param attachment the attachment name
     * @param action the action to perform on the attachment
     * @param queryString the URL query string
     * @return the URL that performs the specified action on the specified attachment
     */
    public String getAttachmentURL(String space, String page, String attachment, String action,
            String queryString) {
        return getURL(new String[] { space, page, attachment }, action, queryString);
    }

    /**
     * @param space the name of the space that contains the page with the specified attachment
     * @param page the name of the page that holds the attachment
     * @param attachment the attachment name
     * @param action the action to perform on the attachment
     * @return the URL that performs the specified action on the specified attachment
     */
    public String getAttachmentURL(String space, String page, String attachment, String action) {
        return getAttachmentURL(space, page, attachment, action, "");
    }

    /**
     * @param space the name of the space that contains the page with the specified attachment
     * @param page the name of the page that holds the attachment
     * @param attachment the attachment name
     * @return the URL to download the specified attachment
     */
    public String getAttachmentURL(String space, String page, String attachment) {
        return getAttachmentURL(space, page, attachment, "download");
    }

    /**
     * (Re)-cache the secret token used for CSRF protection. A user with edit rights on Main.WebHome must be logged in.
     * This method must be called before {@link #getSecretToken()} is called and after each re-login.
     *
     * @see #getSecretToken()
     */
    public void recacheSecretToken() {
        // Save the current URL to be able to get back after we cache the secret token. We're not using the browser's
        // Back button because if the current page is the result of a POST request then by going back we are re-sending
        // the POST data which can have unexpected results. Moreover, some browsers pop up a modal confirmation box
        // which blocks the test.
        String previousURL = getDriver().getCurrentUrl();
        // Go to the registration page because the registration form uses secret token.
        gotoPage("XWiki", "Register", "register");
        try {
            WebElement tokenInput = getDriver().findElement(By.xpath("//input[@name='form_token']"));
            this.secretToken = tokenInput.getAttribute("value");
        } catch (NoSuchElementException exception) {
            // Something is really wrong if this happens.
            System.out.println("Warning: Failed to cache anti-CSRF secret token, some tests might fail!");
            exception.printStackTrace();
        }
        // Return to the previous page.
        getDriver().get(previousURL);
    }

    /**
     * Get the secret token used for CSRF protection. Remember to call {@link #recacheSecretToken()} first.
     *
     * @return anti-CSRF secret token, or empty string if the token was not cached
     * @see #recacheSecretToken()
     */
    public String getSecretToken() {
        if (this.secretToken == null) {
            System.out.println("Warning: No cached anti-CSRF token found. "
                    + "Make sure to call recacheSecretToken() before getSecretToken(), otherwise this test might fail.");
            return "";
        }
        return this.secretToken;
    }

    /**
     * Encodes a given string so that it may be used as a URL component. Compatable with javascript decodeURIComponent,
     * though more strict than encodeURIComponent: all characters except [a-zA-Z0-9], '.', '-', '*', '_' are converted
     * to hexadecimal, and spaces are substituted by '+'.
     *
     * @param s
     */
    public String escapeURL(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // should not happen
            throw new RuntimeException(e);
        }
    }

    /**
     * This class represents all cookies stored in the browser. Use with getSession() and setSession()
     */
    public class Session {
        private final Set<Cookie> cookies;

        private final String secretToken;

        private Session(final Set<Cookie> cookies, final String secretToken) {
            this.cookies = Collections.unmodifiableSet(new HashSet<Cookie>() {
                {
                    addAll(cookies);
                }
            });
            this.secretToken = secretToken;
        }

        private Set<Cookie> getCookies() {
            return this.cookies;
        }

        private String getSecretToken() {
            return this.secretToken;
        }
    }

    public int getTimeout() {
        return this.timeout;
    }

    /**
     * @param timeout the number of seconds after which we consider the action to have failed
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * Forces the passed driver to wait for a {@link #getTimeout()} number of seconds when looking up page elements
     * before declaring that it cannot find them.
     */
    public void setDriverImplicitWait(WebDriver driver) {
        driver.manage().timeouts().implicitlyWait(getTimeout(), TimeUnit.SECONDS);
    }

    public boolean isInWYSIWYGEditMode() {
        return getDriver().findElements(By.xpath("//div[@id='editcolumn' and contains(@class, 'editor-wysiwyg')]"))
                .size() > 0;
    }

    public boolean isInWikiEditMode() {
        return getDriver().findElements(By.xpath("//div[@id='editcolumn' and contains(@class, 'editor-wiki')]"))
                .size() > 0;
    }

    public boolean isInViewMode() {
        return getDriver().findElements(By.id("tmEdit")).size() > 0;
    }

    public boolean isInSourceViewMode() {
        return getDriver().findElements(By.xpath("//textarea[@class = 'wiki-code']")).size() > 0;
    }

    public boolean isInInlineEditMode() {
        String currentURL = getDriver().getCurrentUrl();
        // Keep checking the deprecated inline action for backward compatibility.
        return currentURL.contains("editor=inline") || currentURL.contains("/inline/");
    }

    public boolean isInRightsEditMode() {
        return getDriver().getCurrentUrl().contains("editor=rights");
    }

    public boolean isInObjectEditMode() {
        return getDriver().getCurrentUrl().contains("editor=object");
    }

    public boolean isInClassEditMode() {
        return getDriver().getCurrentUrl().contains("editor=class");
    }

    public boolean isInDeleteMode() {
        return getDriver().getCurrentUrl().contains("/delete/");
    }

    public boolean isInRenameMode() {
        return getDriver().getCurrentUrl().contains("xpage=rename");
    }

    public boolean isInCreateMode() {
        return getDriver().getCurrentUrl().contains("/create/");
    }

    /**
     * Forces the current user to be the Guest user by clearing all coookies.
     */
    public void forceGuestUser() {
        setSession(null);
    }

    public void addObject(String space, String page, String className, Object... properties) {
        gotoPage(space, page, "objectadd", toQueryParameters(className, null, properties));
    }

    public void addObject(String space, String page, String className, Map<String, ?> properties) {
        gotoPage(space, page, "objectadd", toQueryParameters(className, null, properties));
    }

    public void deleteObject(String space, String page, String className, int objectNumber) {
        StringBuilder queryString = new StringBuilder();

        queryString.append("classname=");
        queryString.append(escapeURL(className));
        queryString.append('&');
        queryString.append("classid=");
        queryString.append(objectNumber);

        gotoPage(space, page, "objectremove", queryString.toString());
    }

    public void updateObject(String space, String page, String className, int objectNumber,
            Map<String, ?> properties) {
        gotoPage(space, page, "save", toQueryParameters(className, objectNumber, properties));
    }

    public void updateObject(String space, String page, String className, int objectNumber, Object... properties) {
        // TODO: would be even quicker using REST
        gotoPage(space, page, "save", toQueryParameters(className, objectNumber, properties));
    }

    public ClassEditPage addClassProperty(String space, String page, String propertyName, String propertyType) {
        gotoPage(space, page, "propadd", "propname", propertyName, "proptype", propertyType);
        return new ClassEditPage();
    }

    /**
     * @since 3.5M1
     */
    public String toQueryString(Object... queryParameters) {
        return toQueryString(toQueryParameters(queryParameters));
    }

    /**
     * @since 3.5M1
     */
    public String toQueryString(Map<String, ?> queryParameters) {
        StringBuilder builder = new StringBuilder();

        for (Map.Entry<String, ?> entry : queryParameters.entrySet()) {
            addQueryStringEntry(builder, entry.getKey(), entry.getValue());
            builder.append('&');
        }

        return builder.toString();
    }

    /**
     * @sice 3.2M1
     */
    public void addQueryStringEntry(StringBuilder builder, String key, Object value) {
        if (value != null) {
            if (value instanceof Iterable) {
                for (Object element : (Iterable<?>) value) {
                    addQueryStringEntry(builder, key, element.toString());
                    builder.append('&');
                }
            } else {
                addQueryStringEntry(builder, key, value.toString());
            }
        } else {
            addQueryStringEntry(builder, key, (String) null);
        }
    }

    /**
     * @sice 3.2M1
     */
    public void addQueryStringEntry(StringBuilder builder, String key, String value) {
        builder.append(escapeURL(key));
        if (value != null) {
            builder.append('=');
            builder.append(escapeURL(value));
        }
    }

    /**
     * @since 3.5M1
     */
    public Map<String, ?> toQueryParameters(Object... properties) {
        return toQueryParameters(null, null, properties);
    }

    public Map<String, ?> toQueryParameters(String className, Integer objectNumber, Object... properties) {
        Map<String, Object> queryParameters = new HashMap<String, Object>();

        queryParameters.put("classname", className);

        for (int i = 0; i < properties.length; i += 2) {
            int nextIndex = i + 1;
            queryParameters.put(toQueryParameterKey(className, objectNumber, (String) properties[i]),
                    nextIndex < properties.length ? properties[nextIndex] : null);
        }

        return queryParameters;
    }

    public Map<String, ?> toQueryParameters(String className, Integer objectNumber, Map<String, ?> properties) {
        Map<String, Object> queryParameters = new HashMap<String, Object>();

        if (className != null) {
            queryParameters.put("classname", className);
        }

        for (Map.Entry<String, ?> entry : properties.entrySet()) {
            queryParameters.put(toQueryParameterKey(className, objectNumber, entry.getKey()), entry.getValue());
        }

        return queryParameters;
    }

    public String toQueryParameterKey(String className, Integer objectNumber, String key) {
        if (className == null) {
            return key;
        } else {
            StringBuilder keyBuilder = new StringBuilder(className);

            keyBuilder.append('_');

            if (objectNumber != null) {
                keyBuilder.append(objectNumber);
                keyBuilder.append('_');
            }

            keyBuilder.append(key);

            return keyBuilder.toString();
        }
    }

    public WebElement findElementWithoutWaiting(WebDriver driver, By by) {
        // Temporarily remove the implicit wait on the driver since we're doing our own waits...
        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        try {
            return driver.findElement(by);
        } finally {
            setDriverImplicitWait(driver);
        }
    }

    public List<WebElement> findElementsWithoutWaiting(WebDriver driver, By by) {
        // Temporarily remove the implicit wait on the driver since we're doing our own waits...
        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        try {
            return driver.findElements(by);
        } finally {
            setDriverImplicitWait(driver);
        }
    }

    public WebElement findElementWithoutWaiting(WebDriver driver, WebElement element, By by) {
        // Temporarily remove the implicit wait on the driver since we're doing our own waits...
        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        try {
            return element.findElement(by);
        } finally {
            setDriverImplicitWait(driver);
        }
    }

    public List<WebElement> findElementsWithoutWaiting(WebDriver driver, WebElement element, By by) {
        // Temporarily remove the implicit wait on the driver since we're doing our own waits...
        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        try {
            return element.findElements(by);
        } finally {
            setDriverImplicitWait(driver);
        }
    }

    /**
     * Should be used when the result is supposed to be true (otherwise you'll incur the timeout).
     */
    public boolean hasElement(By by) {
        try {
            getDriver().findElement(by);
            return true;
        } catch (NoSuchElementException e) {
            return false;
        }
    }

    public boolean hasElementWithoutWaiting(By by) {
        try {
            findElementWithoutWaiting(getDriver(), by);
            return true;
        } catch (NoSuchElementException e) {
            return false;
        }
    }

    /**
     * Should be used when the result is supposed to be true (otherwise you'll incur the timeout).
     */
    public boolean hasElement(WebElement element, By by) {
        try {
            element.findElement(by);
            return true;
        } catch (NoSuchElementException e) {
            return false;
        }
    }

    public ObjectEditPage editObjects(String space, String page) {
        gotoPage(space, page, "edit", "editor=object");
        return new ObjectEditPage();
    }

    public ClassEditPage editClass(String space, String page) {
        gotoPage(space, page, "edit", "editor=class");
        return new ClassEditPage();
    }

    public String getVersion() throws Exception {
        Xwiki xwiki = getRESTResource("", null);

        return xwiki.getVersion();
    }

    public String getMavenVersion() throws Exception {
        String version = getVersion();

        int index = version.indexOf('-');
        if (index > 0) {
            version = version.substring(0, index) + "-SNAPSHOT";
        }

        return version;
    }

    public void attachFile(String space, String page, String name, File file, boolean failIfExists)
            throws Exception {
        InputStream is = new FileInputStream(file);
        try {
            attachFile(space, page, name, is, failIfExists);
        } finally {
            is.close();
        }
    }

    /**
     * @since 5.1M2
     */
    public void attachFile(String space, String page, String name, InputStream is, boolean failIfExists,
            UsernamePasswordCredentials credentials) throws Exception {
        if (credentials != null) {
            this.adminHTTPClient.getState().setCredentials(AuthScope.ANY, credentials);
        }
        attachFile(space, page, name, is, failIfExists);
    }

    public void attachFile(String space, String page, String name, InputStream is, boolean failIfExists)
            throws Exception {
        // make sure xwiki.Import exists
        if (!pageExists(space, page)) {
            createPage(space, page, null, null);
        }

        StringBuilder url = new StringBuilder(BASE_REST_URL);

        url.append("wikis/xwiki/spaces/");
        url.append(escapeURL(space));
        url.append("/pages/");
        url.append(escapeURL(page));
        url.append("/attachments/");
        url.append(escapeURL(name));

        if (failIfExists) {
            executePut(url.toString(), is, MediaType.APPLICATION_OCTET_STREAM, Status.CREATED.getStatusCode());
        } else {
            executePut(url.toString(), is, MediaType.APPLICATION_OCTET_STREAM, Status.CREATED.getStatusCode(),
                    Status.ACCEPTED.getStatusCode());
        }
    }

    // FIXME: improve that with a REST API to directly import a XAR
    public void importXar(File file) throws Exception {
        // attach file
        attachFile("XWiki", "Import", file.getName(), file, false);

        // import file
        executeGet(BASE_BIN_URL
                + "import/XWiki/Import?historyStrategy=add&importAsBackup=true&ajax&action=import&name="
                + escapeURL(file.getName()), Status.OK.getStatusCode());
    }

    public InputStream getRESTInputStream(String resourceUri, Map<String, Object[]> queryParams, Object... elements)
            throws Exception {
        UriBuilder builder = UriBuilder.fromUri(BASE_REST_URL.substring(0, BASE_REST_URL.length() - 1)).path(
                !resourceUri.isEmpty() && resourceUri.charAt(0) == '/' ? resourceUri.substring(1) : resourceUri);

        if (queryParams != null) {
            for (Map.Entry<String, Object[]> entry : queryParams.entrySet()) {
                builder.queryParam(entry.getKey(), entry.getValue());
            }
        }

        String url = builder.build(elements).toString();

        return executeGet(url, Status.OK.getStatusCode()).getResponseBodyAsStream();
    }

    public byte[] getRESTBuffer(String resourceUri, Map<String, Object[]> queryParams, Object... elements)
            throws Exception {
        InputStream is = getRESTInputStream(resourceUri, queryParams, elements);

        byte[] buffer;
        try {
            buffer = IOUtils.toByteArray(is);
        } finally {
            is.close();
        }

        return buffer;
    }

    public <T> T getRESTResource(String resourceUri, Map<String, Object[]> queryParams, Object... elements)
            throws Exception {
        T resource;
        try (InputStream is = getRESTInputStream(resourceUri, queryParams, elements)) {
            resource = (T) unmarshaller.unmarshal(is);
        }

        return resource;
    }

    protected GetMethod executeGet(String uri, int expectedCode) throws Exception {
        GetMethod getMethod = new GetMethod(uri);

        int code = this.adminHTTPClient.executeMethod(getMethod);
        if (code != expectedCode) {
            throw new Exception("Failed to execute get [" + uri + "] with code [" + code + "]");
        }

        return getMethod;
    }

    protected PutMethod executePut(String uri, InputStream content, String mediaType, int... expectedCodes)
            throws Exception {
        PutMethod putMethod = new PutMethod(uri);
        RequestEntity entity = new InputStreamRequestEntity(content, mediaType);
        putMethod.setRequestEntity(entity);

        int code = this.adminHTTPClient.executeMethod(putMethod);
        if (!ArrayUtils.contains(expectedCodes, code)) {
            throw new Exception("Failed to execute put [" + uri + "] with code [" + code + "]");
        }

        return putMethod;
    }
}