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        System.out.println("Schema path: "+schemaAddress);
116        File schemaDirectory = new File(schemaAddress);
117        if (schemaDirectory.exists() && schemaDirectory.isDirectory())
118        {
119            Path schemaSourcePath = Paths.get(schemaAddress);
120            try
121            {
122                DirectoryStream<Path>
123                        schemaDirectoryStream = Files.newDirectoryStream(
124                        schemaSourcePath, "??-*.ldif");
125                for (Path schemaFile : schemaDirectoryStream)
126                {
127                    try
128                    {
129                        System.out.println("Copying schema file " + schemaFile.toString());
130                        Files.copy(schemaFile, Paths.get(instanceRoot + "/config/schema/" + schemaFile.getFileName())
131                                , REPLACE_EXISTING);
132                    } catch (IOException e)
133                    {
134                        System.out.println(e.getMessage());
135                    }
136                }
137            } catch (IOException e)
138            {
139                System.out.println(e.getMessage());
140            }
141        }
142        System.out.println("Schema preparation: done.");
143    }
144
145    /**
146     * This method performs the necessary processing to attempt opening a browser window for the user at
147     * installation time and pop the documentation up.
148     *
149     * @param bundle the extension bundle to get the information from
150     */
151    private void openBrowser(final ExtensionBundle bundle)
152    {
153        System.out.println();
154        System.out.println("Documentation convenience");
155        String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html";
156        System.out.println("You may find the documentation for this extension here:");
157        System.out.println(docUrl);
158        String OS = System.getProperty("os.name").toLowerCase();
159        try
160        {
161            if (OS.contains("mac"))
162            {
163                Runtime.getRuntime().exec("open " + docUrl);
164            } else if (OS.contains("win"))
165            {
166                Runtime.getRuntime().exec("cmd /c start " + docUrl);
167            } else if (OS.contains("nux"))
168            {
169                Runtime.getRuntime().exec("xdg-open " + docUrl);
170            }
171        } catch (IOException e)
172        {
173        }
174        System.out.println("Documentation convenience: done.");
175    }
176
177
178    /**
179     * This method performs the necessary processing to attempt copying the documentation bundled with the extension
180     * into a folder that can be reached by the docs servlet
181     *
182     * @param instanceRoot     the instance installation root file
183     * @param extensionDetails the extension detail information
184     */
185    private void copyDocumentation(final File instanceRoot,
186                                   InstallExtensionDetails extensionDetails)
187    {
188        System.out.println();
189        System.out.println("Documentation preparation");
190
191        String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle()
192                .getBundleId();
193
194        File newDocDir = new File(extensionNewDocRoot);
195        newDocDir.mkdirs();
196
197        Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath());
198        Path dstDocPath = Paths.get(extensionNewDocRoot);
199
200        TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath);
201        try
202        {
203            Files.walkFileTree(srcDocPath, tc);
204        } catch (IOException e)
205        {
206            System.out.println(e);
207        }
208        System.out.println("Documentation preparation: done.");
209    }
210
211    /**
212     * This method performs the necessary processing to attempt updating the default documentation html page to
213     * add a
214     * direct link to the freshly copied extension documentation.
215     * Avoids duplicating links when updating the same extension version, for example during development
216     *
217     * @param extensionDetails the bundle details
218     */
219    private void updateIndex(final File syncRoot, InstallExtensionDetails extensionDetails)
220    {
221        System.out.println();
222        System.out.println("Documentation index update");
223
224        Path path = Paths.get(syncRoot.getPath() + "/docs/index.html");
225        Charset charset = StandardCharsets.UTF_8;
226        String content;
227        try
228        {
229            content = new String(Files.readAllBytes(path), charset);
230            String bundleId = extensionDetails.getExtensionBundle().getBundleId();
231            if (!content.contains("extensions/" + bundleId))
232            {
233                content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId +
234                        "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails
235                        .getExtensionBundle().getVersion
236                                () + "</A>");
237                Files.write(path, content.getBytes(charset));
238            }
239        } catch (IOException e)
240        {
241            System.out.println(e);
242        }
243        System.out.println("Documentation index update: done.");
244    }
245
246    /**
247     * Copy source file to target location. If {@code prompt} is true then
248     * prompt user to overwrite target if it exists. The {@code preserve}
249     * parameter determines if file attributes should be copied/preserved.
250     */
251    static void copyFile(Path source, Path target)
252    {
253        CopyOption[] options = new CopyOption[]{REPLACE_EXISTING};
254        if (Files.notExists(target))
255        {
256            try
257            {
258                Files.copy(source, target, options);
259            } catch (IOException x)
260            {
261                System.err.format("Unable to copy: %s: %s%n", source, x);
262            }
263        }
264    }
265
266    /**
267     * A {@code FileVisitor} that copies a file-tree ("cp -r")
268     */
269    static class TreeCopier implements FileVisitor<Path>
270    {
271        private final Path source;
272        private final Path target;
273
274        TreeCopier(Path source, Path target)
275        {
276            this.source = source;
277            this.target = target;
278        }
279
280        @Override
281        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
282        {
283            // before visiting entries in a directory we copy the directory
284            // (okay if directory already exists).
285            CopyOption[] options = new CopyOption[0];
286
287            Path newdir = target.resolve(source.relativize(dir));
288            try
289            {
290                Files.copy(dir, newdir, options);
291            } catch (FileAlreadyExistsException x)
292            {
293                // ignore
294            } catch (IOException x)
295            {
296                System.err.format("Unable to create: %s: %s%n", newdir, x);
297                return SKIP_SUBTREE;
298            }
299            return CONTINUE;
300        }
301
302        @Override
303        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
304        {
305            copyFile(file, target.resolve(source.relativize(file)));
306            return CONTINUE;
307        }
308
309        @Override
310        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
311        {
312            // fix up modification time of directory when done
313            if (exc == null)
314            {
315                Path newdir = target.resolve(source.relativize(dir));
316                try
317                {
318                    FileTime time = Files.getLastModifiedTime(dir);
319                    Files.setLastModifiedTime(newdir, time);
320                } catch (IOException x)
321                {
322                    System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x);
323                }
324            }
325            return CONTINUE;
326        }
327
328        @Override
329        public FileVisitResult visitFileFailed(Path file, IOException exc)
330        {
331            if (exc instanceof FileSystemLoopException)
332            {
333                System.err.println("cycle detected: " + file);
334            } else
335            {
336                System.err.format("Unable to copy: %s: %s%n", file, exc);
337            }
338            return CONTINUE;
339        }
340    }
341}