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        // On android, behaviour around clearing "java.io.tmpdir" is inconsistent.
78        // Some release set it to "null" and others set it to "/tmp" both values are
79        // wrong for normal apps (mode=activity), where the value that the framework
80        // sets must be used. We unconditionally restore that value here. This code
81        // should be correct on the host and on the jvm too, since tmpdir is assumed
82        // to be immutable.
83        System.setProperty("java.io.tmpdir", tmpDir);
84
85        String userHome = System.getProperty("user.home");
86        String userDir = System.getProperty("user.dir");
87        if (userHome == null || userDir == null) {
88            throw new NullPointerException("user.home=" + userHome + ", user.dir=" + userDir);
89        }
90
91        defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
92        defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
93        defaultDateFormatIs24Hour = hasDateFormatIs24Hour() ? getDateFormatIs24Hour() : null;
94
95        disableSecurity();
96    }
97
98    private String createTempDirectory(String subDirName) {
99        String dirName = tmpDir + "/" + subDirName;
100        IoUtils.safeMkdirs(new File(dirName));
101        return dirName;
102    }
103
104    public void reset() {
105        // Reset system properties.
106        System.setProperties(null);
107
108        // On android, behaviour around clearing "java.io.tmpdir" is inconsistent.
109        // Some release set it to "null" and others set it to "/tmp" both values are
110        // wrong for normal apps (mode=activity), where the value that the framework
111        // sets must be used. We unconditionally restore that value here. This code
112        // should be correct on the host and on the jvm too, since tmpdir is assumed
113        // to be immutable.
114        System.setProperty("java.io.tmpdir", tmpDir);
115
116        // Require writable java.home and user.dir directories for preferences
117        if ("Dalvik".equals(System.getProperty("java.vm.name"))) {
118            setPropertyIfNull("java.home", createTempDirectory("java.home"));
119            setPropertyIfNull("dexmaker.dexcache", createTempDirectory("dexmaker.dexcache"));
120        } else {
121            // The mode --jvm has these properties writable.
122            if (JAVA_RUNTIME_VERSION != null) {
123                System.setProperty("java.runtime.version", JAVA_RUNTIME_VERSION);
124            }
125            if (JAVA_VM_INFO != null) {
126                System.setProperty("java.vm.info", JAVA_VM_INFO);
127            }
128            if (JAVA_VM_VERSION != null) {
129                System.setProperty("java.vm.version", JAVA_VM_VERSION);
130            }
131            if (JAVA_VM_VENDOR != null) {
132                System.setProperty("java.vm.vendor", JAVA_VM_VENDOR);
133            }
134            if (JAVA_VM_NAME != null) {
135                System.setProperty("java.vm.name", JAVA_VM_NAME);
136            }
137        }
138        String userHome = System.getProperty("user.home");
139        if (userHome.length() == 0) {
140            userHome = tmpDir + "/user.home";
141            IoUtils.safeMkdirs(new File(userHome));
142            System.setProperty("user.home", userHome);
143        }
144
145        // Localization
146        Locale.setDefault(Locale.US);
147        TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
148        if (hasDateFormatIs24Hour()) {
149            setDateFormatIs24Hour(defaultDateFormatIs24Hour);
150        }
151
152        // Preferences
153        // Temporarily silence the java.util.prefs logger, which otherwise emits
154        // an unactionable warning. See RI bug 4751540.
155        Logger loggerToMute = Logger.getLogger("java.util.prefs");
156        boolean usedParentHandlers = loggerToMute.getUseParentHandlers();
157        loggerToMute.setUseParentHandlers(false);
158        try {
159            // resetPreferences(Preferences.systemRoot());
160            resetPreferences(Preferences.userRoot());
161        } finally {
162            loggerToMute.setUseParentHandlers(usedParentHandlers);
163        }
164
165        // HttpURLConnection
166        Authenticator.setDefault(null);
167        CookieHandler.setDefault(null);
168        ResponseCache.setDefault(null);
169        HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
170        HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
171
172        // Logging
173        LogManager.getLogManager().reset();
174        Logger.getLogger("").addHandler(new ConsoleHandler());
175
176        // Cleanup to force CloseGuard warnings etc
177        System.gc();
178        System.runFinalization();
179    }
180
181    private static void resetPreferences(Preferences root) {
182        try {
183            root.sync();
184        } catch (BackingStoreException e) {
185            // Indicates that Preferences is probably not working. It's not really supported on
186            // Android so ignore.
187            return;
188        }
189
190        try {
191            for (String child : root.childrenNames()) {
192                root.node(child).removeNode();
193            }
194            root.clear();
195            root.flush();
196        } catch (BackingStoreException e) {
197            throw new RuntimeException(e);
198        }
199    }
200
201    /** A class that always returns TRUE. */
202    @SuppressWarnings("serial")
203    public static class LyingMap extends HashMap<Object, Boolean> {
204        @Override
205        public Boolean get(Object key) {
206            return Boolean.TRUE;
207        }
208    }
209
210    /**
211     * Does what is necessary to disable security checks for testing security-related classes.
212     */
213    @SuppressWarnings("unchecked")
214    private static void disableSecurity() {
215        try {
216            Class<?> securityBrokerClass = Class.forName("javax.crypto.JceSecurity");
217
218            Field modifiersField = Field.class.getDeclaredField("modifiers");
219            modifiersField.setAccessible(true);
220
221            Field verifyMapField = securityBrokerClass.getDeclaredField("verificationResults");
222            modifiersField.setInt(verifyMapField, verifyMapField.getModifiers() & ~Modifier.FINAL);
223            verifyMapField.setAccessible(true);
224            verifyMapField.set(null, new LyingMap());
225
226            Field restrictedField = securityBrokerClass.getDeclaredField("isRestricted");
227            restrictedField.setAccessible(true);
228            restrictedField.set(null, Boolean.FALSE);
229        } catch (Exception ignored) {
230        }
231    }
232
233    private static boolean hasDateFormatIs24Hour() {
234        return dateFormatIs24HourField != null;
235    }
236
237    private static Boolean getDateFormatIs24Hour() {
238        try {
239            return (Boolean) dateFormatIs24HourField.get(null);
240        } catch (IllegalAccessException e) {
241            Error e2 = new AssertionError("Unable to get java.text.DateFormat.is24Hour");
242            e2.initCause(e);
243            throw e2;
244        }
245    }
246
247    private static void setDateFormatIs24Hour(Boolean value) {
248        try {
249            dateFormatIs24HourField.set(null, value);
250        } catch (IllegalAccessException e) {
251            Error e2 = new AssertionError("Unable to set java.text.DateFormat.is24Hour");
252            e2.initCause(e);
253            throw e2;
254        }
255    }
256
257    private static void setPropertyIfNull(String property, String value) {
258        if (System.getProperty(property) == null) {
259           System.setProperty(property, value);
260        }
261    }
262}
263