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