001package com.pingidentity.util; 002 003import com.sun.istack.NotNull; 004import com.unboundid.directory.sdk.common.api.ManageExtensionPlugin; 005import com.unboundid.directory.sdk.common.types.ExtensionBundle; 006import com.unboundid.directory.sdk.common.types.InstallExtensionDetails; 007import com.unboundid.directory.sdk.common.types.PostManageExtensionPluginResult; 008 009import java.io.File; 010import java.io.IOException; 011import java.nio.charset.Charset; 012import java.nio.charset.StandardCharsets; 013import java.nio.file.*; 014import java.nio.file.attribute.BasicFileAttributes; 015import java.nio.file.attribute.FileTime; 016 017import static java.nio.file.FileVisitResult.CONTINUE; 018import static java.nio.file.FileVisitResult.SKIP_SUBTREE; 019import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 020 021/** 022 * Created by arnaudlacour on 5/17/17. 023 */ 024public class Installer extends ManageExtensionPlugin 025{ 026 @Override 027 public String getExtensionName() 028 { 029 return "Generic Extension Installer"; 030 } 031 032 @Override 033 public String[] getExtensionDescription() 034 { 035 return new String[]{"Installs the extension this installer is bundled with"}; 036 } 037 038 @Override 039 public PostManageExtensionPluginResult postInstall( 040 InstallExtensionDetails details) 041 { 042 File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile(); 043 044 /* copy the documentation so it can be readily available for users */ 045 copyDocumentation(instanceRoot, details); 046 047 /* Update the docs index to add a link for convenience */ 048 updateIndex(instanceRoot, details); 049 050 /* if there is a browser to be opened, attempt to open the documentation directly */ 051 openBrowser(details.getExtensionBundle()); 052 053 String confBatch = details.getExtensionBundle().getConfigDir() + "/config/install.dsconfig"; 054 System.out.println("You can get started more quickly by running this command: "); 055 System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch); 056 System.out.println("Executing this command will create basic configuration objects."); 057 058 return PostManageExtensionPluginResult.SUCCESS; 059 } 060 061 062 /** 063 * This method performs the necessary processing to attempt opening a browser window for the user at 064 * installation time and pop the documentation up. 065 * 066 * @param bundle the extension bundle to get the information from 067 */ 068 private void openBrowser(final ExtensionBundle bundle) 069 { 070 String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html"; 071 String OS = System.getProperty("os.name").toLowerCase(); 072 try 073 { 074 if (OS.contains("mac")) 075 { 076 Runtime.getRuntime().exec("open " + docUrl); 077 } else if (OS.contains("win")) 078 { 079 Runtime.getRuntime().exec("cmd /c start " + docUrl); 080 } else if (OS.contains("nux")) 081 { 082 Runtime.getRuntime().exec("xdg-open " + docUrl); 083 } 084 } catch (IOException e) 085 { 086 } 087 } 088 089 /** 090 * This method performs the necessary processing to attempt copying the documentation bundled with the extension 091 * into a folder that can be reached by the docs servlet 092 * 093 * @param instanceRoot the instance installation root file 094 * @param extensionDetails the extension detail information 095 */ 096 private void copyDocumentation(@NotNull final File instanceRoot, @NotNull InstallExtensionDetails extensionDetails) 097 { 098 System.out.println("Deploying documentation set"); 099 100 String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle() 101 .getBundleId(); 102 103 File newDocDir = new File(extensionNewDocRoot); 104 newDocDir.mkdirs(); 105 106 Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath()); 107 Path dstDocPath = Paths.get(extensionNewDocRoot); 108 109 TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath); 110 try 111 { 112 Files.walkFileTree(srcDocPath, tc); 113 } catch (IOException e) 114 { 115 System.out.println(e); 116 } 117 } 118 119 /** 120 * This method performs the necessary processing to attempt updating the default documentation html page to add a 121 * direct link to the freshly copied extension documentation. 122 * Avoids duplicating links when updating the same extension version, for example during development 123 * 124 * @param extensionDetails the bundle details 125 */ 126 private void updateIndex(@NotNull final File syncRoot, @NotNull InstallExtensionDetails extensionDetails) 127 { 128 Path path = Paths.get(syncRoot.getPath() + "/docs/index.html"); 129 Charset charset = StandardCharsets.UTF_8; 130 131 String content; 132 try 133 { 134 content = new String(Files.readAllBytes(path), charset); 135 String bundleId = extensionDetails.getExtensionBundle().getBundleId(); 136 if (!content.contains("extensions/"+bundleId)) 137 { 138 content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId + 139 "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails 140 .getExtensionBundle().getVersion 141 () + "</A>"); 142 Files.write(path, content.getBytes(charset)); 143 } 144 } catch (IOException e) 145 { 146 System.out.println(e); 147 } 148 } 149 150 /** 151 * Copy source file to target location. If {@code prompt} is true then 152 * prompt user to overwrite target if it exists. The {@code preserve} 153 * parameter determines if file attributes should be copied/preserved. 154 */ 155 static void copyFile(Path source, Path target) 156 { 157 CopyOption[] options = new CopyOption[]{REPLACE_EXISTING}; 158 if (Files.notExists(target)) 159 { 160 try 161 { 162 Files.copy(source, target, options); 163 } catch (IOException x) 164 { 165 System.err.format("Unable to copy: %s: %s%n", source, x); 166 } 167 } 168 } 169 170 /** 171 * A {@code FileVisitor} that copies a file-tree ("cp -r") 172 */ 173 static class TreeCopier implements FileVisitor<Path> 174 { 175 private final Path source; 176 private final Path target; 177 178 TreeCopier(Path source, Path target) 179 { 180 this.source = source; 181 this.target = target; 182 } 183 184 @Override 185 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 186 { 187 // before visiting entries in a directory we copy the directory 188 // (okay if directory already exists). 189 CopyOption[] options = new CopyOption[0]; 190 191 Path newdir = target.resolve(source.relativize(dir)); 192 try 193 { 194 Files.copy(dir, newdir, options); 195 } catch (FileAlreadyExistsException x) 196 { 197 // ignore 198 } catch (IOException x) 199 { 200 System.err.format("Unable to create: %s: %s%n", newdir, x); 201 return SKIP_SUBTREE; 202 } 203 return CONTINUE; 204 } 205 206 @Override 207 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 208 { 209 copyFile(file, target.resolve(source.relativize(file))); 210 return CONTINUE; 211 } 212 213 @Override 214 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 215 { 216 // fix up modification time of directory when done 217 if (exc == null) 218 { 219 Path newdir = target.resolve(source.relativize(dir)); 220 try 221 { 222 FileTime time = Files.getLastModifiedTime(dir); 223 Files.setLastModifiedTime(newdir, time); 224 } catch (IOException x) 225 { 226 System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x); 227 } 228 } 229 return CONTINUE; 230 } 231 232 @Override 233 public FileVisitResult visitFileFailed(Path file, IOException exc) 234 { 235 if (exc instanceof FileSystemLoopException) 236 { 237 System.err.println("cycle detected: " + file); 238 } else 239 { 240 System.err.format("Unable to copy: %s: %s%n", file, exc); 241 } 242 return CONTINUE; 243 } 244 } 245}