001package com.pingidentity.util; 002 003import com.sun.istack.NotNull; 004import com.unboundid.directory.sdk.common.api.ManageExtensionPlugin; 005import com.unboundid.directory.sdk.common.types.ExtensionBundle; 006import com.unboundid.directory.sdk.common.types.InstallExtensionDetails; 007import com.unboundid.directory.sdk.common.types.PostManageExtensionPluginResult; 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 @Override 047 public String getExtensionName() 048 { 049 return "Generic Extension Installer"; 050 } 051 052 @Override 053 public String[] getExtensionDescription() 054 { 055 return new String[]{"Installs the extension this installer is bundled with"}; 056 } 057 058 @Override 059 public PostManageExtensionPluginResult postInstall( 060 InstallExtensionDetails details) 061 { 062 File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile(); 063 064 /* copy the documentation so it can be readily available for users */ 065 copyDocumentation(instanceRoot, details); 066 067 /* Update the docs index to add a link for convenience */ 068 updateIndex(instanceRoot, details); 069 070 /* if there is a browser to be opened, attempt to open the documentation directly */ 071 openBrowser(details.getExtensionBundle()); 072 073 String confBatch = details.getExtensionBundle().getConfigDir() + "/config/install.dsconfig"; 074 System.out.println("You can get started more quickly by running this command: "); 075 System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch); 076 System.out.println("Executing this command will create basic configuration objects."); 077 078 return PostManageExtensionPluginResult.SUCCESS; 079 } 080 081 /** 082 * This method performs the necessary processing to attempt opening a browser window for the user at 083 * installation time and pop the documentation up. 084 * 085 * @param bundle the extension bundle to get the information from 086 */ 087 private void openBrowser(final ExtensionBundle bundle) 088 { 089 String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html"; 090 String OS = System.getProperty("os.name").toLowerCase(); 091 try 092 { 093 if (OS.contains("mac")) 094 { 095 Runtime.getRuntime().exec("open " + docUrl); 096 } else if (OS.contains("win")) 097 { 098 Runtime.getRuntime().exec("cmd /c start " + docUrl); 099 } else if (OS.contains("nux")) 100 { 101 Runtime.getRuntime().exec("xdg-open " + docUrl); 102 } 103 } catch (IOException e) 104 { 105 } 106 } 107 108 /** 109 * This method performs the necessary processing to attempt copying the documentation bundled with the extension 110 * into a folder that can be reached by the docs servlet 111 * 112 * @param instanceRoot the instance installation root file 113 * @param extensionDetails the extension detail information 114 */ 115 private void copyDocumentation(@NotNull final File instanceRoot, @NotNull InstallExtensionDetails extensionDetails) 116 { 117 System.out.println("Deploying documentation set"); 118 119 String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle() 120 .getBundleId(); 121 122 File newDocDir = new File(extensionNewDocRoot); 123 newDocDir.mkdirs(); 124 125 Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath()); 126 Path dstDocPath = Paths.get(extensionNewDocRoot); 127 128 TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath); 129 try 130 { 131 Files.walkFileTree(srcDocPath, tc); 132 } catch (IOException e) 133 { 134 System.out.println(e); 135 } 136 } 137 138 /** 139 * This method performs the necessary processing to attempt updating the default documentation html page to add a 140 * direct link to the freshly copied extension documentation. 141 * Avoids duplicating links when updating the same extension version, for example during development 142 * 143 * @param extensionDetails the bundle details 144 */ 145 private void updateIndex(@NotNull final File syncRoot, @NotNull InstallExtensionDetails extensionDetails) 146 { 147 Path path = Paths.get(syncRoot.getPath() + "/docs/index.html"); 148 Charset charset = StandardCharsets.UTF_8; 149 150 String content; 151 try 152 { 153 content = new String(Files.readAllBytes(path), charset); 154 String bundleId = extensionDetails.getExtensionBundle().getBundleId(); 155 if (!content.contains("extensions/" + bundleId)) 156 { 157 content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId + 158 "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails 159 .getExtensionBundle().getVersion 160 () + "</A>"); 161 Files.write(path, content.getBytes(charset)); 162 } 163 } catch (IOException e) 164 { 165 System.out.println(e); 166 } 167 } 168 169 /** 170 * A {@code FileVisitor} that copies a file-tree ("cp -r") 171 */ 172 static class TreeCopier implements FileVisitor<Path> 173 { 174 private final Path source; 175 private final Path target; 176 177 TreeCopier(Path source, Path target) 178 { 179 this.source = source; 180 this.target = target; 181 } 182 183 @Override 184 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 185 { 186 // before visiting entries in a directory we copy the directory 187 // (okay if directory already exists). 188 CopyOption[] options = new CopyOption[0]; 189 190 Path newdir = target.resolve(source.relativize(dir)); 191 try 192 { 193 Files.copy(dir, newdir, options); 194 } catch (FileAlreadyExistsException x) 195 { 196 // ignore 197 } catch (IOException x) 198 { 199 System.err.format("Unable to create: %s: %s%n", newdir, x); 200 return SKIP_SUBTREE; 201 } 202 return CONTINUE; 203 } 204 205 @Override 206 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 207 { 208 copyFile(file, target.resolve(source.relativize(file))); 209 return CONTINUE; 210 } 211 212 @Override 213 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 214 { 215 // fix up modification time of directory when done 216 if (exc == null) 217 { 218 Path newdir = target.resolve(source.relativize(dir)); 219 try 220 { 221 FileTime time = Files.getLastModifiedTime(dir); 222 Files.setLastModifiedTime(newdir, time); 223 } catch (IOException x) 224 { 225 System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x); 226 } 227 } 228 return CONTINUE; 229 } 230 231 @Override 232 public FileVisitResult visitFileFailed(Path file, IOException exc) 233 { 234 if (exc instanceof FileSystemLoopException) 235 { 236 System.err.println("cycle detected: " + file); 237 } else 238 { 239 System.err.format("Unable to copy: %s: %s%n", file, exc); 240 } 241 return CONTINUE; 242 } 243 } 244}