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 Charset charset = StandardCharsets.UTF_8; 225 String content; 226 try 227 { 228 content = new String(Files.readAllBytes(path), charset); 229 String bundleId = extensionDetails.getExtensionBundle().getBundleId(); 230 if (!content.contains("extensions/" + bundleId)) 231 { 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 { 240 System.out.println(e); 241 } 242 System.out.println("Documentation index update: done."); 243 } 244 245 /** 246 * Copy source file to target location. If {@code prompt} is true then 247 * prompt user to overwrite target if it exists. The {@code preserve} 248 * parameter determines if file attributes should be copied/preserved. 249 */ 250 static void copyFile(Path source, Path target) 251 { 252 CopyOption[] options = new CopyOption[]{REPLACE_EXISTING}; 253 if (Files.notExists(target)) 254 { 255 try 256 { 257 Files.copy(source, target, options); 258 } catch (IOException x) 259 { 260 System.err.format("Unable to copy: %s: %s%n", source, x); 261 } 262 } 263 } 264 265 /** 266 * A {@code FileVisitor} that copies a file-tree ("cp -r") 267 */ 268 static class TreeCopier implements FileVisitor<Path> 269 { 270 private final Path source; 271 private final Path target; 272 273 TreeCopier(Path source, Path target) 274 { 275 this.source = source; 276 this.target = target; 277 } 278 279 @Override 280 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 281 { 282 // before visiting entries in a directory we copy the directory 283 // (okay if directory already exists). 284 CopyOption[] options = new CopyOption[0]; 285 286 Path newdir = target.resolve(source.relativize(dir)); 287 try 288 { 289 Files.copy(dir, newdir, options); 290 } catch (FileAlreadyExistsException x) 291 { 292 // ignore 293 } catch (IOException x) 294 { 295 System.err.format("Unable to create: %s: %s%n", newdir, x); 296 return SKIP_SUBTREE; 297 } 298 return CONTINUE; 299 } 300 301 @Override 302 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 303 { 304 copyFile(file, target.resolve(source.relativize(file))); 305 return CONTINUE; 306 } 307 308 @Override 309 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 310 { 311 // fix up modification time of directory when done 312 if (exc == null) 313 { 314 Path newdir = target.resolve(source.relativize(dir)); 315 try 316 { 317 FileTime time = Files.getLastModifiedTime(dir); 318 Files.setLastModifiedTime(newdir, time); 319 } catch (IOException x) 320 { 321 System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x); 322 } 323 } 324 return CONTINUE; 325 } 326 327 @Override 328 public FileVisitResult visitFileFailed(Path file, IOException exc) 329 { 330 if (exc instanceof FileSystemLoopException) 331 { 332 System.err.println("cycle detected: " + file); 333 } else 334 { 335 System.err.format("Unable to copy: %s: %s%n", file, exc); 336 } 337 return CONTINUE; 338 } 339 } 340}