1/*
2 * Copyright (c) 2007 Mockito contributors
3 * This program is made available under the terms of the MIT License.
4 */
5package org.mockito.internal.configuration;
6
7import org.mockito.configuration.IMockitoConfiguration;
8import org.mockito.exceptions.base.MockitoException;
9import org.mockito.exceptions.misusing.MockitoConfigurationException;
10import org.mockito.plugins.MockMaker;
11import org.mockito.plugins.StackTraceCleanerProvider;
12
13import java.io.*;
14import java.net.URL;
15import java.util.ArrayList;
16import java.util.Collections;
17import java.util.Enumeration;
18import java.util.List;
19
20/**
21 * Loads configuration or extension points available in the classpath.
22 *
23 * <p>
24 * <ul>
25 *     <li>
26 *         Can load the mockito configuration. The user who want to provide his own mockito configuration
27 *         should write the class <code>org.mockito.configuration.MockitoConfiguration</code> that implements
28 *         {@link IMockitoConfiguration}. For example :
29 *         <pre class="code"><code class="java">
30 * package org.mockito.configuration;
31 *
32 * //...
33 *
34 * public class MockitoConfiguration implements IMockitoConfiguration {
35 *     boolean enableClassCache() { return false; }
36 *
37 *     // ...
38 * }
39 *     </code></pre>
40 *     </li>
41 *     <li>
42 *         Can load available mockito extensions. Currently Mockito only have one extension point the
43 *         {@link MockMaker}. This extension point allows a user to provide his own bytecode engine to build mocks.
44 *         <br>Suppose you wrote an extension to create mocks with some <em>Awesome</em> library, in order to tell
45 *         Mockito to use it you need to put in your classpath
46 *         <ol style="list-style-type: lower-alpha">
47 *             <li>The implementation itself, for example <code>org.awesome.mockito.AwesomeMockMaker</code>.</li>
48 *             <li>A file named <code>org.mockito.plugins.MockMaker</code> in a folder named
49 *             <code>mockito-extensions</code>, the content of this file need to have <strong>one</strong> line with
50 *             the qualified name <code>org.awesome.mockito.AwesomeMockMaker</code>.</li>
51 *         </ol>
52 *     </li>
53 * </ul>
54 * </p>
55 */
56public class ClassPathLoader {
57    private static final String DEFAULT_MOCK_MAKER_CLASS =
58            "org.mockito.internal.creation.CglibMockMaker";
59    private static final String DEFAULT_STACK_TRACE_CLEANER_PROVIDER_CLASS =
60            "org.mockito.internal.exceptions.stacktrace.DefaultStackTraceCleanerProvider";
61    public static final String MOCKITO_CONFIGURATION_CLASS_NAME = "org.mockito.configuration.MockitoConfiguration";
62
63    private static final MockMaker mockMaker = findPlatformMockMaker();
64    private static final StackTraceCleanerProvider stackTraceCleanerProvider =
65            findPlatformStackTraceCleanerProvider();
66
67    /**
68     * @return configuration loaded from classpath or null
69     */
70    @SuppressWarnings({"unchecked"})
71    public IMockitoConfiguration loadConfiguration() {
72        //Trying to get config from classpath
73        Class configClass;
74        try {
75            configClass = (Class) Class.forName(MOCKITO_CONFIGURATION_CLASS_NAME);
76        } catch (ClassNotFoundException e) {
77            //that's ok, it means there is no global config, using default one.
78            return null;
79        }
80
81        try {
82            return (IMockitoConfiguration) configClass.newInstance();
83        } catch (ClassCastException e) {
84            throw new MockitoConfigurationException("MockitoConfiguration class must implement " + IMockitoConfiguration.class.getName() + " interface.", e);
85        } catch (Exception e) {
86            throw new MockitoConfigurationException("Unable to instantiate " + MOCKITO_CONFIGURATION_CLASS_NAME +" class. Does it have a safe, no-arg constructor?", e);
87        }
88    }
89
90    /**
91     * Returns the implementation of the mock maker available for the current runtime.
92     *
93     * <p>Returns {@link org.mockito.internal.creation.CglibMockMaker} if no
94     * {@link MockMaker} extension exists or is visible in the current classpath.</p>
95     */
96    public static MockMaker getMockMaker() {
97        return mockMaker;
98    }
99
100    public static StackTraceCleanerProvider getStackTraceCleanerProvider() {
101        //TODO we should throw some sensible exception if this is null.
102        return stackTraceCleanerProvider;
103    }
104
105    /**
106     * Scans the classpath to find a mock maker plugin if one is available,
107     * allowing mockito to run on alternative platforms like Android.
108     */
109    static MockMaker findPlatformMockMaker() {
110        return findPluginImplementation(MockMaker.class, DEFAULT_MOCK_MAKER_CLASS);
111    }
112
113    static StackTraceCleanerProvider findPlatformStackTraceCleanerProvider() {
114        return findPluginImplementation(
115                StackTraceCleanerProvider.class, DEFAULT_STACK_TRACE_CLEANER_PROVIDER_CLASS);
116    }
117
118    static <T> T findPluginImplementation(Class<T> pluginType, String defaultPluginClassName) {
119        for (T plugin : loadImplementations(pluginType)) {
120            return plugin; // return the first one service loader finds (if any)
121        }
122
123        try {
124            // Default implementation. Use our own ClassLoader instead of the context
125            // ClassLoader, as the default implementation is assumed to be part of
126            // Mockito and may not be available via the context ClassLoader.
127            return pluginType.cast(Class.forName(defaultPluginClassName).newInstance());
128        } catch (Exception e) {
129            throw new MockitoException("Internal problem occurred, please report it. " +
130                    "Mockito is unable to load the default implementation of class that is a part of Mockito distribution. " +
131                    "Failed to load " + pluginType, e);
132        }
133    }
134
135    /**
136     * Equivalent to {@link java.util.ServiceLoader#load} but without requiring
137     * Java 6 / Android 2.3 (Gingerbread).
138     */
139    static <T> List<T> loadImplementations(Class<T> service) {
140        ClassLoader loader = Thread.currentThread().getContextClassLoader();
141        if (loader == null) {
142            loader = ClassLoader.getSystemClassLoader();
143        }
144        Enumeration<URL> resources;
145        try {
146            resources = loader.getResources("mockito-extensions/" + service.getName());
147        } catch (IOException e) {
148            throw new MockitoException("Failed to load " + service, e);
149        }
150
151        List<T> result = new ArrayList<T>();
152        for (URL resource : Collections.list(resources)) {
153            InputStream in = null;
154            try {
155                in = resource.openStream();
156                for (String line : readerToLines(new InputStreamReader(in, "UTF-8"))) {
157                    String name = stripCommentAndWhitespace(line);
158                    if (name.length() != 0) {
159                        result.add(service.cast(loader.loadClass(name).newInstance()));
160                    }
161                }
162            } catch (Exception e) {
163                throw new MockitoConfigurationException(
164                        "Failed to load " + service + " using " + resource, e);
165            } finally {
166                closeQuietly(in);
167            }
168        }
169        return result;
170    }
171
172    static List<String> readerToLines(Reader reader) throws IOException {
173        List<String> result = new ArrayList<String>();
174        BufferedReader lineReader = new BufferedReader(reader);
175        String line;
176        while ((line = lineReader.readLine()) != null) {
177            result.add(line);
178        }
179        return result;
180    }
181
182    static String stripCommentAndWhitespace(String line) {
183        int hash = line.indexOf('#');
184        if (hash != -1) {
185            line = line.substring(0, hash);
186        }
187        return line.trim();
188    }
189
190    private static void closeQuietly(InputStream in) {
191        if (in != null) {
192            try {
193                in.close();
194            } catch (IOException ignored) {
195            }
196        }
197    }
198}