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}