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