001package com.pingidentity.util; 002 003import com.unboundid.directory.sdk.common.api.ManageExtensionPlugin; 004import com.unboundid.directory.sdk.common.types.ExtensionBundle; 005import com.unboundid.directory.sdk.common.types.InstallExtensionDetails; 006import com.unboundid.directory.sdk.common.types.PostManageExtensionPluginResult; 007import com.unboundid.directory.sdk.common.types.UpdateExtensionDetails; 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 /** 027 * Returns the extension name 028 * @return the extension name 029 */ 030 @Override 031 public String getExtensionName() 032 { 033 return "Generic Extension Installer"; 034 } 035 036 /** 037 * Provides descriptions for the extension 038 * @return an array of descriptive paragraphs 039 */ 040 @Override 041 public String[] getExtensionDescription() 042 { 043 return new String[]{"Installs the extension this installer is bundled with"}; 044 } 045 046 /** 047 * Performs the necessary processing after the extension has been updated 048 * @param details the extension bundle details 049 * @return SUCCESS if the update was successful 050 */ 051 @Override 052 public PostManageExtensionPluginResult postUpdate(UpdateExtensionDetails details) 053 { 054 File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile(); 055 String confBatch = details.getExtensionBundle().getConfigDir() + "/update.dsconfig"; 056 File configurationBatchFile = new File(confBatch); 057 if (configurationBatchFile.exists() && configurationBatchFile.isFile()) 058 { 059 System.out.println(); 060 System.out.println("You can get started more quickly by running this command: "); 061 System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch); 062 } 063 return PostManageExtensionPluginResult.SUCCESS; 064 } 065 066 /** 067 * Performs the necessary processing after the extension has been installed 068 * This currently includes things like laying down schema files, updating documentation, attempting to open 069 * a browser to the extension documentation page and displaying the appropriate commands to run 070 * @param details the extension bundle details 071 * @return SUCCESS if the install was successful 072 */ 073 @Override 074 public PostManageExtensionPluginResult postInstall( 075 InstallExtensionDetails details) 076 { 077 File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile(); 078 079 /* copy schema files included in the extension if any */ 080 copySchema(details, instanceRoot); 081 082 /* copy the documentation so it can be readily available for users */ 083 copyDocumentation(instanceRoot, details); 084 085 /* Update the docs index to add a link for convenience */ 086 updateIndex(instanceRoot, details); 087 088 /* if there is a browser to be opened, attempt to open the documentation directly */ 089 openBrowser(details.getExtensionBundle()); 090 091 092 String confBatch = details.getExtensionBundle().getConfigDir() + "/install.dsconfig"; 093 File configurationBatchFile = new File(confBatch); 094 if (configurationBatchFile.exists() && configurationBatchFile.isFile()) 095 { 096 System.out.println(); 097 System.out.println("You can get started more quickly by running this command: "); 098 System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch); 099 System.out.println("Executing this command will create basic configuration objects."); 100 } 101 return PostManageExtensionPluginResult.SUCCESS; 102 } 103 104 /** 105 * This method performs the necessary processing to copy schema to the instance 106 * 107 * @param details the extension details 108 * @param instanceRoot the instance root 109 */ 110 private void copySchema(final InstallExtensionDetails details, File instanceRoot) 111 { 112 System.out.println(); 113 System.out.println("Schema preparation"); 114 String schemaAddress = details.getExtensionBundle().getConfigDir() + "/schema"; 115 System.out.println("Schema path: "+schemaAddress); 116 File schemaDirectory = new File(schemaAddress); 117 if (schemaDirectory.exists() && schemaDirectory.isDirectory()) 118 { 119 Path schemaSourcePath = Paths.get(schemaAddress); 120 try 121 { 122 DirectoryStream<Path> 123 schemaDirectoryStream = Files.newDirectoryStream( 124 schemaSourcePath, "??-*.ldif"); 125 for (Path schemaFile : schemaDirectoryStream) 126 { 127 try 128 { 129 System.out.println("Copying schema file " + schemaFile.toString()); 130 Files.copy(schemaFile, Paths.get(instanceRoot + "/config/schema/" + schemaFile.getFileName()) 131 , REPLACE_EXISTING); 132 } catch (IOException e) 133 { 134 System.out.println(e.getMessage()); 135 } 136 } 137 } catch (IOException e) 138 { 139 System.out.println(e.getMessage()); 140 } 141 } 142 System.out.println("Schema preparation: done."); 143 } 144 145 /** 146 * This method performs the necessary processing to attempt opening a browser window for the user at 147 * installation time and pop the documentation up. 148 * 149 * @param bundle the extension bundle to get the information from 150 */ 151 private void openBrowser(final ExtensionBundle bundle) 152 { 153 System.out.println(); 154 System.out.println("Documentation convenience"); 155 String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html"; 156 System.out.println("You may find the documentation for this extension here:"); 157 System.out.println(docUrl); 158 String OS = System.getProperty("os.name").toLowerCase(); 159 try 160 { 161 if (OS.contains("mac")) 162 { 163 Runtime.getRuntime().exec("open " + docUrl); 164 } else if (OS.contains("win")) 165 { 166 Runtime.getRuntime().exec("cmd /c start " + docUrl); 167 } else if (OS.contains("nux")) 168 { 169 Runtime.getRuntime().exec("xdg-open " + docUrl); 170 } 171 } catch (IOException e) 172 { 173 } 174 System.out.println("Documentation convenience: done."); 175 } 176 177 178 /** 179 * This method performs the necessary processing to attempt copying the documentation bundled with the extension 180 * into a folder that can be reached by the docs servlet 181 * 182 * @param instanceRoot the instance installation root file 183 * @param extensionDetails the extension detail information 184 */ 185 private void copyDocumentation(final File instanceRoot, 186 InstallExtensionDetails extensionDetails) 187 { 188 System.out.println(); 189 System.out.println("Documentation preparation"); 190 191 String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle() 192 .getBundleId(); 193 194 File newDocDir = new File(extensionNewDocRoot); 195 newDocDir.mkdirs(); 196 197 Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath()); 198 Path dstDocPath = Paths.get(extensionNewDocRoot); 199 200 TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath); 201 try 202 { 203 Files.walkFileTree(srcDocPath, tc); 204 } catch (IOException e) 205 { 206 System.out.println(e); 207 } 208 System.out.println("Documentation preparation: done."); 209 } 210 211 /** 212 * This method performs the necessary processing to attempt updating the default documentation html page to 213 * add a 214 * direct link to the freshly copied extension documentation. 215 * Avoids duplicating links when updating the same extension version, for example during development 216 * 217 * @param extensionDetails the bundle details 218 */ 219 private void updateIndex(final File syncRoot, InstallExtensionDetails extensionDetails) 220 { 221 System.out.println(); 222 System.out.println("Documentation index update"); 223 224 Path path = Paths.get(syncRoot.getPath() + "/docs/index.html"); 225 Charset charset = StandardCharsets.UTF_8; 226 String content; 227 try 228 { 229 content = new String(Files.readAllBytes(path), charset); 230 String bundleId = extensionDetails.getExtensionBundle().getBundleId(); 231 if (!content.contains("extensions/" + bundleId)) 232 { 233 content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId + 234 "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails 235 .getExtensionBundle().getVersion 236 () + "</A>"); 237 Files.write(path, content.getBytes(charset)); 238 } 239 } catch (IOException e) 240 { 241 System.out.println(e); 242 } 243 System.out.println("Documentation index update: done."); 244 } 245 246 /** 247 * Copy source file to target location. If {@code prompt} is true then 248 * prompt user to overwrite target if it exists. The {@code preserve} 249 * parameter determines if file attributes should be copied/preserved. 250 */ 251 static void copyFile(Path source, Path target) 252 { 253 CopyOption[] options = new CopyOption[]{REPLACE_EXISTING}; 254 if (Files.notExists(target)) 255 { 256 try 257 { 258 Files.copy(source, target, options); 259 } catch (IOException x) 260 { 261 System.err.format("Unable to copy: %s: %s%n", source, x); 262 } 263 } 264 } 265 266 /** 267 * A {@code FileVisitor} that copies a file-tree ("cp -r") 268 */ 269 static class TreeCopier implements FileVisitor<Path> 270 { 271 private final Path source; 272 private final Path target; 273 274 TreeCopier(Path source, Path target) 275 { 276 this.source = source; 277 this.target = target; 278 } 279 280 @Override 281 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 282 { 283 // before visiting entries in a directory we copy the directory 284 // (okay if directory already exists). 285 CopyOption[] options = new CopyOption[0]; 286 287 Path newdir = target.resolve(source.relativize(dir)); 288 try 289 { 290 Files.copy(dir, newdir, options); 291 } catch (FileAlreadyExistsException x) 292 { 293 // ignore 294 } catch (IOException x) 295 { 296 System.err.format("Unable to create: %s: %s%n", newdir, x); 297 return SKIP_SUBTREE; 298 } 299 return CONTINUE; 300 } 301 302 @Override 303 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 304 { 305 copyFile(file, target.resolve(source.relativize(file))); 306 return CONTINUE; 307 } 308 309 @Override 310 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 311 { 312 // fix up modification time of directory when done 313 if (exc == null) 314 { 315 Path newdir = target.resolve(source.relativize(dir)); 316 try 317 { 318 FileTime time = Files.getLastModifiedTime(dir); 319 Files.setLastModifiedTime(newdir, time); 320 } catch (IOException x) 321 { 322 System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x); 323 } 324 } 325 return CONTINUE; 326 } 327 328 @Override 329 public FileVisitResult visitFileFailed(Path file, IOException exc) 330 { 331 if (exc instanceof FileSystemLoopException) 332 { 333 System.err.println("cycle detected: " + file); 334 } else 335 { 336 System.err.format("Unable to copy: %s: %s%n", file, exc); 337 } 338 return CONTINUE; 339 } 340 } 341}