1 // Copyright (C) 2009 Red Hat, Inc.
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Lesser General Public License for more details.
12 //
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16 
17 package net.sourceforge.jnlp.util;
18 
19 import net.sourceforge.jnlp.util.logging.OutputController;
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.io.OutputStreamWriter;
27 import java.io.Reader;
28 import java.io.StringReader;
29 import java.net.URL;
30 import java.nio.charset.Charset;
31 import java.nio.file.Files;
32 import java.nio.file.StandardCopyOption;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.Comparator;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Map.Entry;
40 
41 import net.sourceforge.jnlp.IconDesc;
42 import net.sourceforge.jnlp.JNLPFile;
43 import net.sourceforge.jnlp.OptionsDefinitions;
44 import net.sourceforge.jnlp.PluginBridge;
45 import net.sourceforge.jnlp.StreamEater;
46 import net.sourceforge.jnlp.cache.CacheUtil;
47 import net.sourceforge.jnlp.cache.UpdatePolicy;
48 import net.sourceforge.jnlp.config.PathsAndFiles;
49 import net.sourceforge.jnlp.runtime.JNLPRuntime;
50 import net.sourceforge.jnlp.security.dialogs.AccessWarningPaneComplexReturn;
51 
52 /**
53  * This class builds a (freedesktop.org) desktop entry out of a {@link JNLPFile}
54  * . This entry can be used to install desktop shortcuts. See xdg-desktop-icon
55  * (1) and http://standards.freedesktop.org/desktop-entry-spec/latest/ for more
56  * information
57  *
58  * @author Omair Majid
59  *
60  *
61  * This class builds also (freedesktop.org) menu entry out of a {@link JNLPFile}
62  * Few notes valid November 2014:
63  *    Mate/gnome 2/xfce - no meter of exec or icon put icon to defined/"others" Category
64  *                      - name is as expected Name's value
65  *                      - if removed, xfce kept icon until login/logout
66  *    kde 4 - unknown Cathegory is sorted to Lost & Found -thats bad
67  *          - if icon is not found, nothing shows
68  *          - name is GENERIC NAME and then little name
69  *    Gnome 3 shell - exec must be valid program!
70  *                  - also had issues with icon
71  *
72  * conclusion:
73  *  - backup icon to .config
74  *  - use "Network" category
75  *  - force valid launcher
76 
77  * @author (not so proudly) Jiri Vanek
78  */
79 public class XDesktopEntry {
80 
81     public static final String JAVA_ICON_NAME = "itweb-javaws";
82 
83     private JNLPFile file = null;
84     private int iconSize = -1;
85     private String iconLocation = null;
86 
87     //in pixels
88     private static final int[] VALID_ICON_SIZES = new int[] { 16, 22, 32, 48, 64, 128 };
89     //browsers we try to find  on path for html shortcut
90     public static final String[] BROWSERS = new String[]{"firefox", "midori", "epiphany", "opera", "chromium", "chrome", "konqueror"};
91     public static final String FAVICON = "favicon.ico";
92 
93     /**
94      * Create a XDesktopEntry for the given JNLP file
95      *
96      * @param file a {@link JNLPFile} that indicates the application to launch
97      */
XDesktopEntry(JNLPFile file)98     public XDesktopEntry(JNLPFile file) {
99         this.file = file;
100 
101         /* looks like a good initial value */
102         iconSize = VALID_ICON_SIZES[2];
103     }
104 
105     /**
106      * Returns the contents of the {@link XDesktopEntry} through the
107      * {@link Reader} interface.
108      * @param menu whether to create this icon to menu
109      * @param info result of user's interference
110      * @param isSigned whether the app is signed
111      * @return reader with desktop shortcut specification
112      */
getContentsAsReader(boolean menu, AccessWarningPaneComplexReturn.ShortcutResult info, boolean isSigned)113     public Reader getContentsAsReader(boolean menu, AccessWarningPaneComplexReturn.ShortcutResult info, boolean isSigned) {
114 
115         File generatedJnlp = null;
116         if (file instanceof PluginBridge && (info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.GENERATED_JNLP || info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.JNLP_HREF)) {
117             try {
118                 String content = ((PluginBridge) file).toJnlp(isSigned, info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.JNLP_HREF, info.isFixHref());
119                 generatedJnlp = getGeneratedJnlpFileName();
120                 FileUtils.saveFile(content, generatedJnlp);
121             } catch (Exception ex) {
122                 OutputController.getLogger().log(ex);
123             }
124         }
125 
126         String fileContents = "[Desktop Entry]\n";
127         fileContents += "Version=1.0\n";
128         fileContents += "Name=" + getDesktopIconName() + "\n";
129         fileContents += "GenericName=Java Web Start Application\n";
130         fileContents += "Comment=" + sanitize(file.getInformation().getDescription()) + "\n";
131         if (menu) {
132             //keeping the default category because of KDE
133             String menuString = "Categories=Network;";
134             if (file.getInformation().getShortcut() != null
135                     && file.getInformation().getShortcut().getMenu() != null
136                     && file.getInformation().getShortcut().getMenu().getSubMenu() != null
137                     && !file.getInformation().getShortcut().getMenu().getSubMenu().trim().isEmpty()) {
138                 menuString += file.getInformation().getShortcut().getMenu().getSubMenu().trim() + ";";
139             }
140             menuString += "Java;Javaws;";
141             fileContents += menuString + "\n";
142         }
143         fileContents += "Type=Application\n";
144         if (iconLocation != null) {
145             fileContents += "Icon=" + iconLocation + "\n";
146         } else {
147             fileContents += "Icon=" + JAVA_ICON_NAME + "\n";
148 
149         }
150         if (file.getInformation().getVendor() != null) {
151             fileContents += "X-Vendor=" + sanitize(file.getInformation().getVendor()) + "\n";
152         }
153 
154         if (JNLPRuntime.isWebstartApplication()) {
155             String htmlSwitch = "";
156             if (JNLPRuntime.isHtml()){
157                 htmlSwitch = " "+OptionsDefinitions.OPTIONS.HTML.option;
158             }
159             fileContents += "Exec="
160                     + getJavaWsBin() + htmlSwitch + " \"" + file.getSourceLocation() + "\"\n";
161             OutputController.getLogger().log("Using " + getJavaWsBin()  + htmlSwitch + " as binary for " + file.getSourceLocation());
162         } else {
163             if (info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.BROWSER) {
164                 String browser = info.getBrowser();
165                 if (browser == null) {
166                     browser = getBrowserBin();
167                 }
168                 fileContents += "Exec="
169                         + browser + " \"" + file.getSourceLocation() + "\"\n";
170                 OutputController.getLogger().log("Using " + browser + " as binary for " + file.getSourceLocation());
171             } else if ((info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.GENERATED_JNLP
172                     || info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.JNLP_HREF) && generatedJnlp != null) {
173                 fileContents += "Exec="
174                         + getJavaWsBin() + " \"" + generatedJnlp.getAbsolutePath() + "\"\n";
175                 OutputController.getLogger().log("Using " + getJavaWsBin() + " (generated) as binary for " + file.getSourceLocation() + " to " + generatedJnlp.getAbsolutePath());
176             } else if (info.getShortcutType() == AccessWarningPaneComplexReturn.ShortcutResult.Shortcut.JAVAWS_HTML) {
177                 fileContents += "Exec="
178                         + getJavaWsBin() + " -html  \"" + file.getSourceLocation() + "\"\n";
179                 OutputController.getLogger().log("Using " + getJavaWsBin() + " -html as binary for " + file.getSourceLocation());
180             } else {
181                 fileContents += "Exec="
182                         + getBrowserBin() + " \"" + file.getSourceLocation() + "\"\n";
183                 OutputController.getLogger().log("Using " + getBrowserBin() + " as binary for " + file.getSourceLocation());
184             }
185         }
186 
187         return new StringReader(fileContents);
188 
189     }
190 
getBrowserBin()191     public static String getBrowserBin() {
192         String pathResult = findOnPath(BROWSERS);
193         if (pathResult != null) {
194             return pathResult;
195         } else {
196             return "browser_not_found";
197         }
198 
199     }
200 
getJavaWsBin()201     private String getJavaWsBin() {
202         //Shortcut executes the jnlp as it was with system preferred java. It should work fine offline
203         //absolute - works in case of self built
204         String exec = System.getProperty("icedtea-web.bin.location");
205         String pathResult = findOnPath(new String[]{"itweb-javaws", System.getProperty("icedtea-web.bin.name")});
206         if (pathResult != null) {
207             return pathResult;
208         }
209         if (exec != null) {
210             return exec;
211         }
212         return "itweb-javaws";
213     }
214 
215 
findOnPath(String[] bins)216     private static String findOnPath(String[] bins) {
217         String exec = null;
218         //find if one of binaries is on path
219         String path = System.getenv().get("PATH");
220         if (path == null || path.trim().isEmpty()) {
221             path = System.getenv().get("path");
222         }
223         if (path == null || path.trim().isEmpty()) {
224             path = System.getenv().get("Path");
225         }
226         if (path != null && !path.trim().isEmpty()) {
227             //relative - works with alternatives
228             String[] paths = path.split(File.pathSeparator);
229             outerloop:
230             for (String bin : bins) {
231                 //when property is not set
232                 if (bin == null) {
233                     continue;
234                 }
235                 for (String p : paths) {
236                     if (new File(p, bin).exists()) {
237                         exec = bin;
238                         break outerloop;
239                     }
240                 }
241 
242             }
243         }
244         return exec;
245     }
246 
247     /**
248      * Sanitizes a string so that it can be used safely in a key=value pair in a
249      * desktop entry file.
250      *
251      * @param input a String to sanitize
252      * @return a string safe to use as either the key or the value in the
253      * key=value pair in a desktop entry file
254      */
sanitize(String input)255     private static String sanitize(String input) {
256         if (input == null) {
257             return "";
258         }
259         /* key=value pairs must be a single line */
260         input = FileUtils.sanitizeFileName(input, '-');
261         //return first line or replace new lines by space?
262         return input.split("\n")[0];
263     }
264 
265     /**
266      * @return the size of the icon (in pixels) for the desktop shortcut
267      */
getIconSize()268     public int getIconSize() {
269         return iconSize;
270     }
271 
getShortcutTmpFile()272     public File getShortcutTmpFile() {
273         String userTmp = PathsAndFiles.TMP_DIR.getFullPath();
274         File shortcutFile = new File(userTmp + File.separator + getDesktopIconFileName());
275         return shortcutFile;
276     }
277 
278     /**
279      * Set the icon size to use for the desktop shortcut
280      *
281      * @param size the size (in pixels) of the icon to use. Commonly used sizes
282      *        are of 16, 22, 32, 48, 64 and 128
283      */
setIconSize(int size)284     public void setIconSize(int size) {
285         iconSize = size;
286     }
287 
288     /**
289      * Create a desktop shortcut for this desktop entry
290      * @param menu how to create in menu
291      * @param desktop how to create on desktop
292      * @param isSigned if it is signed
293      */
createDesktopShortcuts(AccessWarningPaneComplexReturn.ShortcutResult menu, AccessWarningPaneComplexReturn.ShortcutResult desktop, boolean isSigned)294     public void createDesktopShortcuts(AccessWarningPaneComplexReturn.ShortcutResult menu, AccessWarningPaneComplexReturn.ShortcutResult desktop, boolean isSigned) {
295         boolean isDesktop = false;
296         if (desktop != null && desktop.isCreate()) {
297             isDesktop = true;
298         }
299         boolean isMenu = false;
300         if (menu != null && menu.isCreate()) {
301             isMenu = true;
302         }
303         try {
304             if (isMenu || isDesktop) {
305                 try {
306                     cacheIcon();
307                 } catch (NonFileProtocolException ex) {
308                     OutputController.getLogger().log(ex);
309                     //default icon will be used later
310                 }
311             }
312             if (isDesktop) {
313                 installDesktopLauncher(desktop, isSigned);
314             }
315             if (isMenu) {
316                 installMenuLauncher(menu, isSigned);
317             }
318         } catch (Exception e) {
319             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
320         }
321     }
322 
323     /**
324      * Install this XDesktopEntry into the user's menu.
325      */
installMenuLauncher(AccessWarningPaneComplexReturn.ShortcutResult info, boolean isSigned)326     private void installMenuLauncher(AccessWarningPaneComplexReturn.ShortcutResult info, boolean isSigned) {
327         //TODO add itweb-settings tab which alows to remove inidividual items/icons
328         try {
329             File f = getLinuxMenuIconFile();
330             try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(f),
331                     Charset.forName("UTF-8")); Reader reader = getContentsAsReader(true, info, isSigned)) {
332 
333                 char[] buffer = new char[1024];
334                 int ret = 0;
335                 while (-1 != (ret = reader.read(buffer))) {
336                     writer.write(buffer, 0, ret);
337                 }
338 
339             }
340             OutputController.getLogger().log("Menu item created: " + f.getAbsolutePath());
341         } catch (FileNotFoundException e) {
342             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
343         } catch (IOException e) {
344             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
345         }
346     }
347 
348     /**
349      * Install this XDesktopEntry into the user's desktop as a launcher.
350      */
installDesktopLauncher(AccessWarningPaneComplexReturn.ShortcutResult info, boolean isSigned)351     private void installDesktopLauncher(AccessWarningPaneComplexReturn.ShortcutResult info, boolean isSigned) {
352         File shortcutFile = getShortcutTmpFile();
353         try {
354 
355             if (!shortcutFile.getParentFile().isDirectory() && !shortcutFile.getParentFile().mkdirs()) {
356                 throw new IOException(shortcutFile.getParentFile().toString());
357             }
358 
359             FileUtils.createRestrictedFile(shortcutFile, true);
360 
361             try ( /*
362              * Write out a Java String (UTF-16) as a UTF-8 file
363              */ OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(shortcutFile),
364                      Charset.forName("UTF-8")); Reader reader = getContentsAsReader(false, info, isSigned)) {
365 
366                 char[] buffer = new char[1024];
367                 int ret = 0;
368                 while (-1 != (ret = reader.read(buffer))) {
369                     writer.write(buffer, 0, ret);
370                 }
371 
372             }
373 
374             /*
375              * Install the desktop entry
376              */
377 
378             String[] execString = new String[] { "xdg-desktop-icon", "install", "--novendor",
379                     shortcutFile.getCanonicalPath() };
380             OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "Execing: " + Arrays.toString(execString));
381             Process installer = Runtime.getRuntime().exec(execString);
382             new StreamEater(installer.getInputStream()).start();
383             new StreamEater(installer.getErrorStream()).start();
384 
385             try {
386                 installer.waitFor();
387             } catch (InterruptedException e) {
388                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
389             }
390 
391             if (!shortcutFile.delete()) {
392                 throw new IOException("Unable to delete temporary file:" + shortcutFile);
393             }
394 
395         } catch (FileNotFoundException e) {
396             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
397         } catch (IOException e) {
398             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
399         }
400     }
401 
402 
refreshExistingShortcuts(boolean desktop, boolean menu)403     public void refreshExistingShortcuts(boolean desktop, boolean menu) {
404         //TODO TODO TODO TODO TODO TODO TODO TODO
405         //check existing jnlp files
406         //check luncher
407         //get where it poiints
408         //try all supported  shortcuts methods
409         //choose the one which have most similar result to exisitng ones
410 
411     }
412 
getGeneratedJnlpFileName()413     public File getGeneratedJnlpFileName() {
414         String name = FileUtils.sanitizeFileName(file.createJnlpTitle());
415         while (name.endsWith(".jnlp")) {
416             name = name.substring(0, name.length() - 5);
417         }
418         name += ".jnlp";
419         return new File(findAndVerifyGeneratedJnlpDir(), name);
420     }
421 
422     private class NonFileProtocolException extends Exception {
423 
NonFileProtocolException(String unable_to_cache_icon)424         private NonFileProtocolException(String unable_to_cache_icon) {
425             super(unable_to_cache_icon);
426         }
427 
428     }
429 
430     /**
431      * Cache the icon for the desktop entry
432      */
cacheIcon()433     private void cacheIcon() throws IOException, NonFileProtocolException {
434 
435         URL uiconLocation = file.getInformation().getIconLocation(IconDesc.SHORTCUT, iconSize,
436                 iconSize);
437 
438         if (uiconLocation == null) {
439             uiconLocation = file.getInformation().getIconLocation(IconDesc.DEFAULT, iconSize,
440                     iconSize);
441         }
442 
443         String location = null;
444         if (uiconLocation != null) {
445             //this throws npe, if url (specified in jnlp) points to 404
446             URL urlLocation = CacheUtil.getCachedResourceURL(uiconLocation, null, UpdatePolicy.SESSION);
447             if (urlLocation == null) {
448                 cantCache();
449             }
450             location = urlLocation.toString();
451             if (!location.startsWith("file:")) {
452                 cantCache();
453             }
454         } else {
455             //try favicon.ico
456             try {
457                 URL favico = new URL(
458                         file.getCodeBase().getProtocol(),
459                         file.getCodeBase().getHost(),
460                         file.getCodeBase().getPort(),
461                         "/" + FAVICON);
462                 JNLPFile.openURL(favico, null, UpdatePolicy.ALWAYS);
463                 //this MAY throw npe, if url (specified in jnlp) points to 404
464                 URL urlLocation = CacheUtil.getCachedResourceURL(favico, null, UpdatePolicy.SESSION);
465                 if (urlLocation == null) {
466                     cantCache();
467                 }
468                 location = urlLocation.toString();
469                 if (!location.startsWith("file:")) {
470                     cantCache();
471                 }
472             } catch (IOException ex) {
473                 //favicon 404 or similar
474                 OutputController.getLogger().log(ex);
475             }
476         }
477         if (location != null) {
478             String origLocation = location.substring("file:".length());
479             this.iconLocation = origLocation;
480             // icons are never unisntalled by itw. however, system clears them on its own.. soemtimes.
481             // once the -Xcelarcache is run, system MAY clean it later, and so image wil be lost.
482             // copy icon somewhere where -Xclearcache can not
483             PathsAndFiles.ICONS_DIR.getFile().mkdirs();
484             File source = new File(origLocation);
485             String targetName = source.getName();
486             if (targetName.equals(FAVICON)) {
487                 targetName = file.getCodeBase().getHost() + ".ico";
488             }
489             File target = new File(PathsAndFiles.ICONS_DIR.getFile(), targetName);
490             Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
491             this.iconLocation = target.getAbsolutePath();
492             OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "Cached desktop shortcut icon: " + target + " ,  With source from: " + origLocation);
493         }
494     }
495 
cantCache()496     private void cantCache() throws NonFileProtocolException {
497         throw new NonFileProtocolException("Unable to cache icon");
498     }
499 
getDesktopIconName()500     private String getDesktopIconName() {
501         return sanitize(file.createJnlpTitle());
502     }
503 
getLinuxDesktopIconFile()504     public File getLinuxDesktopIconFile() {
505         return new File(findFreedesktopOrgDesktopPathCatch() + "/" + getDesktopIconFileName());
506     }
507 
getLinuxMenuIconFile()508     public File getLinuxMenuIconFile() {
509         return new File(findAndVerifyJavawsMenuDir() + "/" + getDesktopIconFileName());
510     }
511 
getDesktopIconFileName()512     private String getDesktopIconFileName() {
513         return getDesktopIconName() + ".desktop";
514     }
515 
findAndVerifyGeneratedJnlpDir()516     private static String findAndVerifyGeneratedJnlpDir() {
517         return findAndVerifyBasicDir(PathsAndFiles.GEN_JNLPS_DIR.getFile(), " directroy for stroing generated jnlps cannot be created. You may expect failure");
518     }
519 
findAndVerifyJavawsMenuDir()520     private static String findAndVerifyJavawsMenuDir() {
521         return findAndVerifyBasicDir(PathsAndFiles.MENUS_DIR.getFile(), " directroy for stroing menu entry cannot be created. You may expect failure");
522     }
523 
findAndVerifyBasicDir(File f, String message)524     private static String findAndVerifyBasicDir(File f, String message) {
525         String fPath = f.getAbsolutePath();
526         if (!f.exists()) {
527             if (!f.mkdirs()) {
528                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, fPath + message);
529             }
530         }
531         return fPath;
532     }
533 
findFreedesktopOrgDesktopPathCatch()534     public static String findFreedesktopOrgDesktopPathCatch() {
535         try {
536             return findFreedesktopOrgDesktopPath();
537         } catch (Exception ex) {
538             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, ex);
539             return System.getProperty("user.home") + "/Desktop";
540         }
541     }
542 
543     /**
544      * Instead of having all this parsing of user-dirs.dirs and replacing
545      * variables we can execute `echo $(xdg-user-dir DESKTOP)` and it will do
546      * all the job in case approaches below become failing
547      *
548      * @return variables (if declared) and quotation marks (unless escaped) free
549      * path
550      * @throws IOException if no file do not exists or key with desktop do not
551      * exists
552      */
findFreedesktopOrgDesktopPath()553     private static String findFreedesktopOrgDesktopPath() throws IOException {
554         File userDirs = new File(System.getProperty("user.home") + "/.config/user-dirs.dirs");
555         if (!userDirs.exists()) {
556             return System.getProperty("user.home") + "/Desktop/";
557         }
558         return getFreedesktopOrgDesktopPathFrom(userDirs);
559     }
560 
getFreedesktopOrgDesktopPathFrom(File userDirs)561     private static String getFreedesktopOrgDesktopPathFrom(File userDirs) throws IOException {
562         try (BufferedReader r = new BufferedReader(new FileReader(userDirs))) {
563             return getFreedesktopOrgDesktopPathFrom(r);
564         }
565 
566     }
567     static final String XDG_DESKTOP_DIR = "XDG_DESKTOP_DIR";
568 
getFreedesktopOrgDesktopPathFrom(BufferedReader r)569     static String getFreedesktopOrgDesktopPathFrom(BufferedReader r) throws IOException {
570         while (true) {
571             String s = r.readLine();
572             if (s == null) {
573                 throw new IOException("End of user-dirs found, but no " + XDG_DESKTOP_DIR + " key found");
574             }
575             s = s.trim();
576             if (s.startsWith(XDG_DESKTOP_DIR)) {
577                 if (!s.contains("=")) {
578                     throw new IOException(XDG_DESKTOP_DIR + " has no value");
579                 }
580                 String[] keyAndValue = s.split("=");
581                 keyAndValue[1] = keyAndValue[1].trim();
582                 String filteredQuotes = filterQuotes(keyAndValue[1]);
583                 return evaluateLinuxVariables(filteredQuotes);
584             }
585         }
586     }
587     private static final String MIC = "MAGIC_QUOTIN_ITW_CONSTANT_FOR_DUMMIES";
588 
filterQuotes(String string)589     private static String filterQuotes(String string) {
590         //get rid of " but not of
591         String s = string.replaceAll("\\\\\"", MIC);
592         s = s.replaceAll("\"", "");
593         s = s.replaceAll(MIC, "\\\"");
594         return s;
595     }
596 
evaluateLinuxVariables(String orig)597     private static String evaluateLinuxVariables(String orig) {
598         return evaluateLinuxVariables(orig, System.getenv());
599     }
600 
evaluateLinuxVariables(String orig, Map<String, String> variables)601     private static String evaluateLinuxVariables(String orig, Map<String, String> variables) {
602         List<Entry<String, String>> envVariables = new ArrayList<>(variables.entrySet());
603         Collections.sort(envVariables, new Comparator<Entry<String, String>>() {
604             @Override
605             public int compare(Entry<String, String> o1, Entry<String, String> o2) {
606                 return o2.getKey().length() - o1.getKey().length();
607             }
608         });
609         while (true) {
610             String before = orig;
611             for (Entry<String, String> entry : envVariables) {
612                 orig = orig.replaceAll("\\$" + entry.getKey(), entry.getValue());
613             }
614             if (before.equals(orig)) {
615                 return orig;
616             }
617         }
618 
619     }
620 }
621