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