001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.Utils.getSystemEnv;
007import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
008
009import java.awt.Dimension;
010import java.awt.DisplayMode;
011import java.awt.GraphicsDevice;
012import java.awt.GraphicsEnvironment;
013import java.awt.Toolkit;
014import java.awt.event.ActionEvent;
015import java.awt.event.KeyEvent;
016import java.awt.geom.AffineTransform;
017import java.io.PrintWriter;
018import java.io.StringWriter;
019import java.lang.management.ManagementFactory;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.ListIterator;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Optional;
030import java.util.Set;
031import java.util.stream.Collectors;
032
033import javax.swing.UIManager;
034
035import org.openstreetmap.josm.data.Preferences;
036import org.openstreetmap.josm.data.Version;
037import org.openstreetmap.josm.data.osm.DataSet;
038import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
039import org.openstreetmap.josm.data.preferences.sources.MapPaintPrefHelper;
040import org.openstreetmap.josm.data.preferences.sources.PresetPrefHelper;
041import org.openstreetmap.josm.data.preferences.sources.SourcePrefHelper;
042import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
043import org.openstreetmap.josm.gui.ExtendedDialog;
044import org.openstreetmap.josm.gui.MainApplication;
045import org.openstreetmap.josm.gui.bugreport.DebugTextDisplay;
046import org.openstreetmap.josm.gui.util.GuiHelper;
047import org.openstreetmap.josm.io.OsmApi;
048import org.openstreetmap.josm.plugins.PluginHandler;
049import org.openstreetmap.josm.spi.preferences.Config;
050import org.openstreetmap.josm.tools.Logging;
051import org.openstreetmap.josm.tools.PlatformHookUnixoid;
052import org.openstreetmap.josm.tools.PlatformManager;
053import org.openstreetmap.josm.tools.Shortcut;
054import org.openstreetmap.josm.tools.Utils;
055import org.openstreetmap.josm.tools.bugreport.BugReportSender;
056
057/**
058 * Opens a dialog with useful status information like version numbers for Java, JOSM and plugins
059 * Also includes preferences with stripped username and password.
060 *
061 * @author xeen
062 */
063public final class ShowStatusReportAction extends JosmAction {
064
065    /**
066     * Localized description text for this action
067     */
068    public static final String ACTION_DESCRIPTION = tr("Show status report with useful information that can be attached to bugs");
069
070    /**
071     * Constructs a new {@code ShowStatusReportAction}
072     */
073    public ShowStatusReportAction() {
074        super(
075                tr("Show Status Report"),
076                "misc/statusreport",
077                ACTION_DESCRIPTION,
078                Shortcut.registerShortcut("help:showstatusreport", tr("Help: {0}",
079                        tr("Show Status Report")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE), true, "help/showstatusreport", false);
080
081        setHelpId(ht("/Action/ShowStatusReport"));
082    }
083
084    /**
085     * Replies the report header (software and system info)
086     * @return The report header (software and system info)
087     */
088    public static String getReportHeader() {
089        StringWriter stringWriter = new StringWriter(256);
090        PrintWriter text = new PrintWriter(stringWriter);
091        String runtimeVersion = getSystemProperty("java.runtime.version");
092        text.println(Version.getInstance().getReleaseAttributes());
093        text.format("Identification: %s%n", Version.getInstance().getAgentString());
094        String buildNumber = PlatformManager.getPlatform().getOSBuildNumber();
095        if (!buildNumber.isEmpty()) {
096            text.format("OS Build number: %s%n", buildNumber);
097        }
098        text.format(Locale.ROOT, "Memory Usage: %d MB / %d MB (%d MB allocated, but free)%n",
099                Runtime.getRuntime().totalMemory() / 1024 / 1024,
100                Runtime.getRuntime().maxMemory() / 1024 / 1024,
101                Runtime.getRuntime().freeMemory() / 1024 / 1024);
102        text.format("Java version: %s, %s, %s%n",
103                runtimeVersion != null ? runtimeVersion : getSystemProperty("java.version"),
104                getSystemProperty("java.vendor"),
105                getSystemProperty("java.vm.name"));
106        text.format("Look and Feel: %s%n",
107                Optional.ofNullable(UIManager.getLookAndFeel()).map(laf -> laf.getClass().getName()).orElse("null"));
108        if (!GraphicsEnvironment.isHeadless()) {
109            text.append("Screen:");
110            for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
111                text.append(" ").append(gd.getIDstring());
112                DisplayMode dm = gd.getDisplayMode();
113                if (dm != null) {
114                    AffineTransform transform = gd.getDefaultConfiguration().getDefaultTransform();
115                    // Java 11: use DisplayMode#toString
116                    text.format(Locale.ROOT, " %d\u00D7%d (scaling %.2f\u00D7%.2f)",
117                            dm.getWidth(), dm.getHeight(), transform.getScaleX(), transform.getScaleY());
118                }
119            }
120            text.println();
121        }
122        text.format("Maximum Screen Size: %s%n", toString(GuiHelper.getMaximumScreenSize()));
123        if (!GraphicsEnvironment.isHeadless()) {
124            Dimension bestCursorSize16 = Toolkit.getDefaultToolkit().getBestCursorSize(16, 16);
125            Dimension bestCursorSize32 = Toolkit.getDefaultToolkit().getBestCursorSize(32, 32);
126            text.format("Best cursor sizes: %s→%s, %s→%s%n",
127                    toString(new Dimension(16, 16)), toString(bestCursorSize16),
128                    toString(new Dimension(32, 32)), toString(bestCursorSize32));
129        }
130
131        for (String name : Arrays.asList("LANG", "LC_ALL")) {
132            String value = getSystemEnv(name);
133            if (value != null) {
134                text.format("Environment variable %s: %s%n", name, value);
135            }
136        }
137        for (String name : Arrays.asList("file.encoding", "sun.jnu.encoding")) {
138            String value = getSystemProperty(name);
139            if (value != null) {
140                text.format("System property %s: %s%n", name, value);
141            }
142        }
143        text.format("Locale info: %s%n", Locale.getDefault().toString());
144        text.format("Numbers with default locale: %s -> %d%n", Integer.toString(1_234_567_890), 1_234_567_890);
145
146        if (PlatformManager.isPlatformUnixoid()) {
147            PlatformHookUnixoid platform = (PlatformHookUnixoid) PlatformManager.getPlatform();
148            // Add desktop environment
149            platform.getDesktopEnvironment().ifPresent(desktop -> text.format("Desktop environment: %s%n", desktop));
150            // Add Java package details
151            String packageDetails = platform.getJavaPackageDetails();
152            if (packageDetails != null) {
153                text.format("Java package: %s%n", packageDetails);
154            }
155            // Add WebStart package details if run from JNLP
156            if (Utils.isRunningWebStart()) {
157                String webStartDetails = platform.getWebStartPackageDetails();
158                if (webStartDetails != null) {
159                    text.format("WebStart package: %s%n", webStartDetails);
160                }
161            }
162            // Add Gnome Atk wrapper details if found
163            String atkWrapperDetails = platform.getAtkWrapperPackageDetails();
164            if (atkWrapperDetails != null) {
165                text.format("Java ATK Wrapper package: %s%n", atkWrapperDetails);
166            }
167            // Add dependencies details if found
168            for (String p : new String[] {
169                    "apache-commons-compress", "libcommons-compress-java",
170                    "apache-commons-jcs-core",
171                    "apache-commons-logging", "libcommons-logging-java",
172                    "fonts-noto",
173                    "jsonp",
174                    "metadata-extractor2",
175                    "signpost-core", "liboauth-signpost-java",
176                    "svgsalamander"
177            }) {
178                String details = PlatformHookUnixoid.getPackageDetails(p);
179                if (details != null) {
180                    text.format("%s: %s%n", p, details);
181                }
182            }
183        }
184        try {
185            // Build a new list of VM parameters to modify it below if needed (default implementation returns an UnmodifiableList instance)
186            List<String> vmArguments = new ArrayList<>(ManagementFactory.getRuntimeMXBean().getInputArguments());
187            for (ListIterator<String> it = vmArguments.listIterator(); it.hasNext();) {
188                String value = it.next();
189                if (value.contains("=")) {
190                    String[] param = value.split("=", 2);
191                    // Hide some parameters for privacy concerns
192                    if (param[0].toLowerCase(Locale.ENGLISH).startsWith("-dproxy")) {
193                        it.set(param[0]+"=xxx");
194                    } else if ("-Djnlpx.vmargs".equals(param[0])) {
195                        // Remove jnlpx.vmargs (base64 encoded copy of VM arguments already included in clear)
196                        it.remove();
197                    } else {
198                        // Replace some paths for readability and privacy concerns
199                        String val = paramCleanup(param[1]);
200                        if (!val.equals(param[1])) {
201                            it.set(param[0] + '=' + val);
202                        }
203                    }
204                } else if (value.startsWith("-X")) {
205                    // Remove arguments like -Xbootclasspath/a, -Xverify:remote, that can be very long and unhelpful
206                    it.remove();
207                }
208            }
209            if (!vmArguments.isEmpty()) {
210                text.format("VM arguments: %s%n", vmArguments.toString().replace("\\\\", "\\"));
211            }
212        } catch (SecurityException e) {
213            Logging.trace(e);
214        }
215        List<String> commandLineArgs = MainApplication.getCommandLineArgs();
216        if (!commandLineArgs.isEmpty()) {
217            text.format("Program arguments: %s%n", Arrays.toString(paramCleanup(commandLineArgs).toArray()));
218        }
219        DataSet dataset = MainApplication.getLayerManager().getActiveDataSet();
220        if (dataset != null) {
221            String result = DatasetConsistencyTest.runTests(dataset);
222            if (result.isEmpty()) {
223                text.println("Dataset consistency test: No problems found");
224            } else {
225                text.println();
226                text.println("Dataset consistency test:");
227                text.println(result);
228            }
229        }
230        text.println();
231        appendCollection(text, "Plugins", Utils.transform(PluginHandler.getBugReportInformation(), i -> "+ " + i));
232        appendCollection(text, "Tagging presets", getCustomUrls(PresetPrefHelper.INSTANCE));
233        appendCollection(text, "Map paint styles", getCustomUrls(MapPaintPrefHelper.INSTANCE));
234        appendCollection(text, "Validator rules", getCustomUrls(ValidatorPrefHelper.INSTANCE));
235        appendCollection(text, "Last errors/warnings", Utils.transform(Logging.getLastErrorAndWarnings(), i -> "- " + i));
236
237        String osmApi = OsmApi.getOsmApi().getServerUrl();
238        if (!Config.getUrls().getDefaultOsmApiUrl().equals(osmApi.trim())) {
239            text.format("OSM API: %s%n", osmApi);
240        }
241
242        text.println();
243        return stringWriter.toString();
244    }
245
246    private static String toString(Dimension dimension) {
247        return dimension.width + "\u00D7" + dimension.height;
248    }
249
250    private static Collection<String> getCustomUrls(SourcePrefHelper helper) {
251        final Set<String> defaultUrls = helper.getDefault().stream()
252                .map(i -> i.url)
253                .collect(Collectors.toSet());
254        return helper.get().stream()
255                .filter(i -> !defaultUrls.contains(i.url))
256                .map(i -> (i.active ? "+ " : "- ") + i.url)
257                .collect(Collectors.toList());
258    }
259
260    private static List<String> paramCleanup(Collection<String> params) {
261        return params.stream()
262                .map(ShowStatusReportAction::paramCleanup)
263                .collect(Collectors.toList());
264    }
265
266    /**
267     * Fill map with anonymized name to the actual used path.
268     * @return map that maps shortened name to full directory path
269     */
270    static Map<String, String> getAnonimicDirectorySymbolMap() {
271        /** maps the anonymized name to the actual used path */
272        Map<String, String> map = new LinkedHashMap<>();
273        map.put(PlatformManager.isPlatformWindows() ? "%JAVA_HOME%" : "${JAVA_HOME}", getSystemEnv("JAVA_HOME"));
274        map.put("<java.home>", getSystemProperty("java.home"));
275        map.put("<josm.pref>", Config.getDirs().getPreferencesDirectory(false).toString());
276        map.put("<josm.userdata>", Config.getDirs().getUserDataDirectory(false).toString());
277        map.put("<josm.cache>", Config.getDirs().getCacheDirectory(false).toString());
278        map.put(PlatformManager.isPlatformWindows() ? "%UserProfile%" : "${HOME}", getSystemProperty("user.home"));
279        return map;
280    }
281
282    /**
283     * Shortens and removes private informations from a parameter used for status report.
284     * @param param parameter to cleanup
285     * @return shortened/anonymized parameter
286     */
287    static String paramCleanup(String param) {
288        final String userName = getSystemProperty("user.name");
289        final String userNameAlt = "<user.name>";
290
291        String val = param;
292        for (Entry<String, String> entry : getAnonimicDirectorySymbolMap().entrySet()) {
293            val = paramReplace(val, entry.getValue(), entry.getKey());
294        }
295        if (userName != null && userName.length() >= 3) {
296            val = paramReplace(val, userName, userNameAlt);
297        }
298        return val;
299    }
300
301    private static String paramReplace(String str, String target, String replacement) {
302        return target == null ? str : str.replace(target, replacement);
303    }
304
305    private static void appendCollection(PrintWriter text, String label, Collection<String> col) {
306        if (!col.isEmpty()) {
307            text.append(col.stream().map(o -> paramCleanup(o) + '\n')
308                    .collect(Collectors.joining("", label + ":\n", "\n")));
309        }
310    }
311
312    private static String valueCleanup(Object value) {
313        String valueString = value.toString();
314        if (valueString.length() > 512 && value instanceof Collection<?>) {
315            valueString = ((Collection<?>) value).stream().map(v -> {
316                if (v instanceof Map<?, ?>) {
317                    LinkedHashMap<Object, Object> map = new LinkedHashMap<>(((Map<?, ?>) v));
318                    map.computeIfPresent("icon", (k, icon) -> Utils.shortenString(icon.toString(), 32)); // see #19058
319                    return map.toString();
320                } else {
321                    return String.valueOf(v);
322                }
323            }).collect(Collectors.joining(",\n  ", "[", "\n]"));
324        }
325        return paramCleanup(valueString);
326    }
327
328    @Override
329    public void actionPerformed(ActionEvent e) {
330        StringBuilder text = new StringBuilder();
331        String reportHeader = getReportHeader();
332        text.append(reportHeader);
333
334        Preferences.main().getAllSettings().forEach((key, setting) -> {
335            if ("file-open.history".equals(key)
336                    || "download.overpass.query".equals(key)
337                    || "download.overpass.queries".equals(key)
338                    || key.contains("username")
339                    || key.contains("password")
340                    || key.contains("access-token")) {
341                // Remove sensitive information from status report
342                return;
343            }
344            text.append(paramCleanup(key))
345                    .append('=')
346                    .append(valueCleanup(setting.getValue()))
347                    .append('\n');
348        });
349
350        DebugTextDisplay ta = new DebugTextDisplay(text.toString());
351
352        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
353                tr("Status Report"),
354                tr("Copy to clipboard and close"), tr("Report bug"), tr("Close"));
355        ed.setButtonIcons("copy", "bug", "cancel");
356        ed.configureContextsensitiveHelp("/Action/ShowStatusReport", true);
357        ed.setContent(ta, false);
358        ed.setMinimumSize(new Dimension(380, 200));
359        ed.setPreferredSize(new Dimension(700, MainApplication.getMainFrame().getHeight()-50));
360
361        switch (ed.showDialog().getValue()) {
362            case 1: ta.copyToClipboard(); break;
363            case 2: BugReportSender.reportBug(reportHeader); break;
364            default: // do nothing
365        }
366        GuiHelper.destroyComponents(ed, false);
367    }
368}