1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package vogar.target;
18
19import java.io.File;
20import java.lang.reflect.Field;
21import java.lang.reflect.Modifier;
22import java.net.Authenticator;
23import java.net.CookieHandler;
24import java.net.ResponseCache;
25import java.text.DateFormat;
26import java.util.Locale;
27import java.util.HashMap;
28import java.util.TimeZone;
29import java.util.logging.ConsoleHandler;
30import java.util.logging.LogManager;
31import java.util.logging.Logger;
32import java.util.prefs.BackingStoreException;
33import java.util.prefs.Preferences;
34import javax.net.ssl.HostnameVerifier;
35import javax.net.ssl.HttpsURLConnection;
36import javax.net.ssl.SSLSocketFactory;
37import vogar.util.IoUtils;
38
39/**
40 * This class resets the VM to a relatively pristine state. Useful to defend
41 * against tests that muck with system properties and other global state.
42 */
43public final class TestEnvironment {
44
45    private final HostnameVerifier defaultHostnameVerifier;
46    private final SSLSocketFactory defaultSSLSocketFactory;
47
48    /** The DateFormat.is24Hour field. Not present on older versions of Android or the RI. */
49    private static final Field dateFormatIs24HourField;
50    static {
51        Field f;
52        try {
53            Class<?> dateFormatClass = Class.forName("java.text.DateFormat");
54            f = dateFormatClass.getDeclaredField("is24Hour");
55        } catch (ClassNotFoundException | NoSuchFieldException e) {
56            f = null;
57        }
58        dateFormatIs24HourField = f;
59    }
60    private final Boolean defaultDateFormatIs24Hour;
61
62    private static final String JAVA_RUNTIME_VERSION = System.getProperty("java.runtime.version");
63    private static final String JAVA_VM_INFO = System.getProperty("java.vm.info");
64    private static final String JAVA_VM_VERSION = System.getProperty("java.vm.version");
65    private static final String JAVA_VM_VENDOR = System.getProperty("java.vm.vendor");
66    private static final String JAVA_VM_NAME = System.getProperty("java.vm.name");
67
68    private final String tmpDir;
69
70    public TestEnvironment() {
71        this.tmpDir = System.getProperty("java.io.tmpdir");
72        if (tmpDir == null || tmpDir.length() == 0) {
73            throw new AssertionError("tmpDir is null or empty: " + tmpDir);
74        }
75        System.setProperties(null); // Reset.
76
77        // From "L" release onwards, calling System.setProperties(null) clears the java.io.tmpdir,
78        // so we set it again. No-op on earlier releases.
79        System.setProperty("java.io.tmpdir", tmpDir);
80
81        String userHome = System.getProperty("user.home");
82        String userDir = System.getProperty("user.dir");
83        if (userHome == null || userDir == null) {
84            throw new NullPointerException("user.home=" + userHome + ", user.dir=" + userDir);
85        }
86
87        defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
88        defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
89        defaultDateFormatIs24Hour = hasDateFormatIs24Hour() ? getDateFormatIs24Hour() : null;
90
91        disableSecurity();
92    }
93
94    public void reset() {
95        // Reset system properties.
96        System.setProperties(null);
97
98        // From "L" release onwards, calling System.setProperties(null) clears the java.io.tmpdir,
99        // so we set it again. No-op on earlier releases.
100        System.setProperty("java.io.tmpdir", tmpDir);
101
102        if (JAVA_RUNTIME_VERSION != null) {
103            System.setProperty("java.runtime.version", JAVA_RUNTIME_VERSION);
104        }
105        if (JAVA_VM_INFO != null) {
106            System.setProperty("java.vm.info", JAVA_VM_INFO);
107        }
108        if (JAVA_VM_VERSION != null) {
109            System.setProperty("java.vm.version", JAVA_VM_VERSION);
110        }
111        if (JAVA_VM_VENDOR != null) {
112            System.setProperty("java.vm.vendor", JAVA_VM_VENDOR);
113        }
114        if (JAVA_VM_NAME != null) {
115            System.setProperty("java.vm.name", JAVA_VM_NAME);
116        }
117
118        // Require writable java.home and user.dir directories for preferences
119        if ("Dalvik".equals(System.getProperty("java.vm.name"))) {
120            String javaHome = tmpDir + "/java.home";
121            IoUtils.safeMkdirs(new File(javaHome));
122            System.setProperty("java.home", javaHome);
123        }
124        String userHome = System.getProperty("user.home");
125        if (userHome.length() == 0) {
126            userHome = tmpDir + "/user.home";
127            IoUtils.safeMkdirs(new File(userHome));
128            System.setProperty("user.home", userHome);
129        }
130
131        // Localization
132        Locale.setDefault(Locale.US);
133        TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
134        if (hasDateFormatIs24Hour()) {
135            setDateFormatIs24Hour(defaultDateFormatIs24Hour);
136        }
137
138        // Preferences
139        // Temporarily silence the java.util.prefs logger, which otherwise emits
140        // an unactionable warning. See RI bug 4751540.
141        Logger loggerToMute = Logger.getLogger("java.util.prefs");
142        boolean usedParentHandlers = loggerToMute.getUseParentHandlers();
143        loggerToMute.setUseParentHandlers(false);
144        try {
145            // resetPreferences(Preferences.systemRoot());
146            resetPreferences(Preferences.userRoot());
147        } finally {
148            loggerToMute.setUseParentHandlers(usedParentHandlers);
149        }
150
151        // HttpURLConnection
152        Authenticator.setDefault(null);
153        CookieHandler.setDefault(null);
154        ResponseCache.setDefault(null);
155        HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
156        HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
157
158        // Logging
159        LogManager.getLogManager().reset();
160        Logger.getLogger("").addHandler(new ConsoleHandler());
161
162        // Cleanup to force CloseGuard warnings etc
163        System.gc();
164        System.runFinalization();
165    }
166
167    private static void resetPreferences(Preferences root) {
168        try {
169            root.sync();
170        } catch (BackingStoreException e) {
171            // Indicates that Preferences is probably not working. It's not really supported on
172            // Android so ignore.
173            return;
174        }
175
176        try {
177            for (String child : root.childrenNames()) {
178                root.node(child).removeNode();
179            }
180            root.clear();
181            root.flush();
182        } catch (BackingStoreException e) {
183            throw new RuntimeException(e);
184        }
185    }
186
187    /** A class that always returns TRUE. */
188    @SuppressWarnings("serial")
189    public static class LyingMap extends HashMap<Object, Boolean> {
190        @Override
191        public Boolean get(Object key) {
192            return Boolean.TRUE;
193        }
194    }
195
196    /**
197     * Does what is necessary to disable security checks for testing security-related classes.
198     */
199    @SuppressWarnings("unchecked")
200    private static void disableSecurity() {
201        try {
202            Class<?> securityBrokerClass = Class.forName("javax.crypto.JceSecurity");
203
204            Field modifiersField = Field.class.getDeclaredField("modifiers");
205            modifiersField.setAccessible(true);
206
207            Field verifyMapField = securityBrokerClass.getDeclaredField("verificationResults");
208            modifiersField.setInt(verifyMapField, verifyMapField.getModifiers() & ~Modifier.FINAL);
209            verifyMapField.setAccessible(true);
210            verifyMapField.set(null, new LyingMap());
211
212            Field restrictedField = securityBrokerClass.getDeclaredField("isRestricted");
213            restrictedField.setAccessible(true);
214            restrictedField.set(null, Boolean.FALSE);
215        } catch (Exception ignored) {
216        }
217    }
218
219    private static boolean hasDateFormatIs24Hour() {
220        return dateFormatIs24HourField != null;
221    }
222
223    private static Boolean getDateFormatIs24Hour() {
224        try {
225            return (Boolean) dateFormatIs24HourField.get(null);
226        } catch (IllegalAccessException e) {
227            Error e2 = new AssertionError("Unable to get java.text.DateFormat.is24Hour");
228            e2.initCause(e);
229            throw e2;
230        }
231    }
232
233    private static void setDateFormatIs24Hour(Boolean value) {
234        try {
235            dateFormatIs24HourField.set(null, value);
236        } catch (IllegalAccessException e) {
237            Error e2 = new AssertionError("Unable to set java.text.DateFormat.is24Hour");
238            e2.initCause(e);
239            throw e2;
240        }
241    }
242}
243