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 File schemaDirectory = new File(schemaAddress); 116 if (schemaDirectory.exists() && schemaDirectory.isDirectory()) 117 { 118 Path schemaSourcePath = Paths.get(schemaAddress); 119 try 120 { 121 DirectoryStream<Path> 122 schemaDirectoryStream = Files.newDirectoryStream( 123 schemaSourcePath, "??-*.ldif"); 124 for (Path schemaFile : schemaDirectoryStream) 125 { 126 try 127 { 128 System.out.println("Copying schema file " + schemaFile.toString()); 129 Files.copy(schemaFile, Paths.get(instanceRoot + "/config/schema/" + schemaFile.getFileName()) 130 , REPLACE_EXISTING); 131 } catch (IOException e) 132 { 133 System.out.println(e.getMessage()); 134 } 135 } 136 } catch (IOException e) 137 { 138 System.out.println(e.getMessage()); 139 } 140 } 141 System.out.println("Schema preparation: done."); 142 } 143 144 /** 145 * This method performs the necessary processing to attempt opening a browser window for the user at 146 * installation time and pop the documentation up. 147 * 148 * @param bundle the extension bundle to get the information from 149 */ 150 private void openBrowser(final ExtensionBundle bundle) 151 { 152 System.out.println(); 153 System.out.println("Documentation convenience"); 154 String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html"; 155 System.out.println("You may find the documentation for this extension here:"); 156 System.out.println(docUrl); 157 String OS = System.getProperty("os.name").toLowerCase(); 158 try 159 { 160 if (OS.contains("mac")) 161 { 162 Runtime.getRuntime().exec("open " + docUrl); 163 } else if (OS.contains("win")) 164 { 165 Runtime.getRuntime().exec("cmd /c start " + docUrl); 166 } else if (OS.contains("nux")) 167 { 168 Runtime.getRuntime().exec("xdg-open " + docUrl); 169 } 170 } catch (IOException e) 171 { 172 } 173 System.out.println("Documentation convenience: done."); 174 } 175 176 177 /** 178 * This method performs the necessary processing to attempt copying the documentation bundled with the extension 179 * into a folder that can be reached by the docs servlet 180 * 181 * @param instanceRoot the instance installation root file 182 * @param extensionDetails the extension detail information 183 */ 184 private void copyDocumentation(final File instanceRoot, 185 InstallExtensionDetails extensionDetails) 186 { 187 System.out.println(); 188 System.out.println("Documentation preparation"); 189 190 String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle() 191 .getBundleId(); 192 193 File newDocDir = new File(extensionNewDocRoot); 194 newDocDir.mkdirs(); 195 196 Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath()); 197 Path dstDocPath = Paths.get(extensionNewDocRoot); 198 199 TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath); 200 try 201 { 202 Files.walkFileTree(srcDocPath, tc); 203 } catch (IOException e) 204 { 205 System.out.println(e); 206 } 207 System.out.println("Documentation preparation: done."); 208 } 209 210 /** 211 * This method performs the necessary processing to attempt updating the default documentation html page to 212 * add a 213 * direct link to the freshly copied extension documentation. 214 * Avoids duplicating links when updating the same extension version, for example during development 215 * 216 * @param extensionDetails the bundle details 217 */ 218 private void updateIndex(final File syncRoot, InstallExtensionDetails extensionDetails) 219 { 220 System.out.println(); 221 System.out.println("Documentation index update"); 222 223 Path path = Paths.get(syncRoot.getPath() + "/docs/index.html"); 224 225 if (path.toFile().exists() ) { 226 Charset charset = StandardCharsets.UTF_8; 227 String content; 228 try { 229 content = new String(Files.readAllBytes(path), charset); 230 String bundleId = extensionDetails.getExtensionBundle().getBundleId(); 231 if (!content.contains("extensions/" + bundleId)) { 232 content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId + 233 "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails 234 .getExtensionBundle().getVersion 235 () + "</A>"); 236 Files.write(path, content.getBytes(charset)); 237 } 238 } catch (IOException e) { 239 System.out.println(e); 240 } 241 System.out.println("Documentation index update: done."); 242 } else { 243 System.out.println("Documentation index update: skipped."); 244 } 245 } 246 247 /** 248 * Copy source file to target location. If {@code prompt} is true then 249 * prompt user to overwrite target if it exists. The {@code preserve} 250 * parameter determines if file attributes should be copied/preserved. 251 */ 252 static void copyFile(Path source, Path target) 253 { 254 CopyOption[] options = new CopyOption[]{REPLACE_EXISTING}; 255 if (Files.notExists(target)) 256 { 257 try 258 { 259 Files.copy(source, target, options); 260 } catch (IOException x) 261 { 262 System.err.format("Unable to copy: %s: %s%n", source, x); 263 } 264 } 265 } 266 267 /** 268 * A {@code FileVisitor} that copies a file-tree ("cp -r") 269 */ 270 static class TreeCopier implements FileVisitor<Path> 271 { 272 private final Path source; 273 private final Path target; 274 275 TreeCopier(Path source, Path target) 276 { 277 this.source = source; 278 this.target = target; 279 } 280 281 @Override 282 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 283 { 284 // before visiting entries in a directory we copy the directory 285 // (okay if directory already exists). 286 CopyOption[] options = new CopyOption[0]; 287 288 Path newdir = target.resolve(source.relativize(dir)); 289 try 290 { 291 Files.copy(dir, newdir, options); 292 } catch (FileAlreadyExistsException x) 293 { 294 // ignore 295 } catch (IOException x) 296 { 297 System.err.format("Unable to create: %s: %s%n", newdir, x); 298 return SKIP_SUBTREE; 299 } 300 return CONTINUE; 301 } 302 303 @Override 304 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 305 { 306 copyFile(file, target.resolve(source.relativize(file))); 307 return CONTINUE; 308 } 309 310 @Override 311 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 312 { 313 // fix up modification time of directory when done 314 if (exc == null) 315 { 316 Path newdir = target.resolve(source.relativize(dir)); 317 try 318 { 319 FileTime time = Files.getLastModifiedTime(dir); 320 Files.setLastModifiedTime(newdir, time); 321 } catch (IOException x) 322 { 323 System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x); 324 } 325 } 326 return CONTINUE; 327 } 328 329 @Override 330 public FileVisitResult visitFileFailed(Path file, IOException exc) 331 { 332 if (exc instanceof FileSystemLoopException) 333 { 334 System.err.println("cycle detected: " + file); 335 } else 336 { 337 System.err.format("Unable to copy: %s: %s%n", file, exc); 338 } 339 return CONTINUE; 340 } 341 } 342}