/* * Copyright (C) 2011 Everit Kft. (http://everit.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.everit.osgi.dev.maven.util; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.StringWriter; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermission; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.zip.ZipException; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.MojoExecutionException; import org.everit.expression.ExpressionCompiler; import org.everit.expression.ParserConfiguration; import org.everit.expression.jexl.JexlExpressionCompiler; import org.everit.osgi.dev.dist.util.configuration.schema.TemplateEnginesType; import org.everit.templating.CompiledTemplate; import org.everit.templating.TemplateCompiler; import org.everit.templating.html.HTMLTemplateCompiler; import org.everit.templating.text.TextTemplateCompiler; import com.greenbird.xml.prettyprinter.PrettyPrinter; import com.greenbird.xml.prettyprinter.PrettyPrinterBuilder; /** * This class is not thread-safe. It should be used within one thread only. */ public class FileManager { private static final int GROUP_EXECUTE_BITMASK; private static final int GROUP_READ_BITMASK; private static final int GROUP_WRITE_BITMASK; private static final int OTHERS_EXECUTE_BITMASK; private static final int OTHERS_READ_BITMASK; private static final int OTHERS_WRITE_BITMASK; private static final int OWNER_EXECUTE_BITMASK; private static final int OWNER_READ_BITMASK; private static final int OWNER_WRITE_BITMASK; private static final TemplateCompiler TEMPLATE_COMPILER_HTML; private static final TemplateCompiler TEMPLATE_COMPILER_TEXT; static { final int octalDigitNum = 3; final int executeOctal = 1; OTHERS_EXECUTE_BITMASK = executeOctal; GROUP_EXECUTE_BITMASK = OTHERS_EXECUTE_BITMASK << octalDigitNum; OWNER_EXECUTE_BITMASK = GROUP_EXECUTE_BITMASK << octalDigitNum; final int writeOctal = 2; OTHERS_WRITE_BITMASK = writeOctal; GROUP_WRITE_BITMASK = OTHERS_WRITE_BITMASK << octalDigitNum; OWNER_WRITE_BITMASK = GROUP_WRITE_BITMASK << octalDigitNum; final int readOctal = 4; OTHERS_READ_BITMASK = readOctal; GROUP_READ_BITMASK = OTHERS_READ_BITMASK << octalDigitNum; OWNER_READ_BITMASK = GROUP_READ_BITMASK << octalDigitNum; ExpressionCompiler expressionCompiler = new JexlExpressionCompiler(); TEMPLATE_COMPILER_TEXT = new TextTemplateCompiler(expressionCompiler); Map<String, TemplateCompiler> inlineCompilers = new HashMap<>(); inlineCompilers.put("text", TEMPLATE_COMPILER_TEXT); TEMPLATE_COMPILER_HTML = new HTMLTemplateCompiler(expressionCompiler, inlineCompilers); } private static Set<PosixFilePermission> getGroupPermissions(final int unixPermissions) { Set<PosixFilePermission> perms = new HashSet<>(); if ((unixPermissions & GROUP_EXECUTE_BITMASK) > 0) { perms.add(PosixFilePermission.GROUP_EXECUTE); } if ((unixPermissions & GROUP_READ_BITMASK) > 0) { perms.add(PosixFilePermission.GROUP_READ); } if ((unixPermissions & GROUP_WRITE_BITMASK) > 0) { perms.add(PosixFilePermission.GROUP_WRITE); } return perms; } private static Set<PosixFilePermission> getOthersPermission(final int unixPermissions) { Set<PosixFilePermission> perms = new HashSet<>(); if ((unixPermissions & OTHERS_EXECUTE_BITMASK) > 0) { perms.add(PosixFilePermission.OTHERS_EXECUTE); } if ((unixPermissions & OTHERS_READ_BITMASK) > 0) { perms.add(PosixFilePermission.OTHERS_READ); } if ((unixPermissions & OTHERS_WRITE_BITMASK) > 0) { perms.add(PosixFilePermission.OTHERS_WRITE); } return perms; } private static Set<PosixFilePermission> getOwnerPerssions(final int unixPermissions) { Set<PosixFilePermission> perms = new HashSet<>(); if ((unixPermissions & OWNER_EXECUTE_BITMASK) > 0) { perms.add(PosixFilePermission.OWNER_EXECUTE); } if ((unixPermissions & OWNER_READ_BITMASK) > 0) { perms.add(PosixFilePermission.OWNER_READ); } if ((unixPermissions & OWNER_WRITE_BITMASK) > 0) { perms.add(PosixFilePermission.OWNER_WRITE); } return perms; } private static void setPermissionsOnFile(final File file, final ZipArchiveEntry entry) throws IOException { if (entry.getPlatform() == ZipArchiveEntry.PLATFORM_FAT) { return; } int unixPermissions = entry.getUnixMode(); Set<PosixFilePermission> perms = new HashSet<>(); perms.addAll(FileManager.getOwnerPerssions(unixPermissions)); perms.addAll(FileManager.getGroupPermissions(unixPermissions)); perms.addAll(FileManager.getOthersPermission(unixPermissions)); Path path = file.toPath(); if (path.getFileSystem().supportedFileAttributeViews().contains("posix")) { Files.setPosixFilePermissions(path, perms); } else { FileManager.setPermissionsOnFileInNonPosixSystem(file, perms); } } private static void setPermissionsOnFileInNonPosixSystem(final File file, final Set<PosixFilePermission> perms) { if (perms.contains(PosixFilePermission.OWNER_EXECUTE)) { file.setExecutable(true, !perms.contains(PosixFilePermission.OTHERS_EXECUTE)); } if (perms.contains(PosixFilePermission.OWNER_READ)) { file.setReadable(true, !perms.contains(PosixFilePermission.OTHERS_READ)); } if (perms.contains(PosixFilePermission.OWNER_WRITE)) { file.setWritable(true, !perms.contains(PosixFilePermission.OTHERS_WRITE)); } } private final PrettyPrinter prettyPrinter = new PrettyPrinterBuilder().indentate(' ', 2).ignoreWhitespace().keepXMLDeclaration().build(); private final HashSet<File> touchedFiles = new HashSet<>(); /** * Copies a directory recursively or a file from the source to the target. */ public void copyDirectory(final File sourceLocation, final File targetLocation) throws MojoExecutionException { if (sourceLocation.isDirectory()) { touchedFiles.add(targetLocation); if (!targetLocation.exists()) { targetLocation.mkdir(); } String[] children = sourceLocation.list(); for (String element : children) { copyDirectory(new File(sourceLocation, element), new File(targetLocation, element)); } } else { overCopyFile(sourceLocation, targetLocation); } } /** * Returns the files that were created, modified or just touched (as their content did not change) * but they were not deleted afterwards. * * @return Set of touched files. */ public Set<File> getTouchedFiles() { @SuppressWarnings("unchecked") Set<File> result = (Set<File>) touchedFiles.clone(); return result; } private boolean isSameFile(final File destFile, final long sourceLength, final long sourceLastModified) { return destFile.exists() && destFile.length() == sourceLength && destFile.lastModified() == sourceLastModified; } /** * Copies the source file into a target file. In case the file already exists, only those bytes * are overwritten in the target file that are changed. */ public boolean overCopyFile(final File source, final File target) throws MojoExecutionException { if (target.exists() && source.lastModified() == target.lastModified() && source.length() == target.length()) { touchedFiles.add(target); return false; } try (FileChannel sourceChannel = FileChannel.open(source.toPath(), StandardOpenOption.READ)) { return overCopyFile(sourceChannel, source.length(), source.lastModified(), target); } catch (IOException e) { throw new MojoExecutionException("Cannot copy file " + source.getAbsolutePath() + " to " + target.getAbsolutePath(), e); } } /** * Copies an {@link InputStream} into a file. In case the file already exists, only those bytes * are overwritten in the target file that are changed. * * @param sourceSize * the size of the source file. * @param sourceLastModified * The timestamp of the source file or entry when it was modified. * @param targetFile * The file that will be overridden if it is necessary. * @param is * The {@link InputStream} of the source. * * @return true if the target file had to be changed, false if the target file was not changed. * @throws IOException * if there is an error during copying the file. */ private boolean overCopyFile(final ReadableByteChannel sourceChannel, final long sourceSize, final long sourceLastModified, final File targetFile) throws IOException { targetFile.getParentFile().mkdirs(); touchedFiles.add(targetFile); try (FileChannel fileChannel = FileChannel.open(targetFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { long position = 0; while (position < sourceSize) { position += fileChannel.transferFrom(sourceChannel, position, sourceSize - position); } if (fileChannel.size() > sourceSize) { fileChannel.truncate(sourceSize); } } targetFile.setLastModified(sourceLastModified); return true; } /** * Replaces the original template file with parsed and processed file. */ public void replaceFileWithParsed(final File parseableFile, final Map<String, Object> vars, final String encoding, final TemplateEnginesType templateEngine, final boolean prettify) throws IOException, MojoExecutionException { File tmpFile = File.createTempFile("eosgi-dist-parse", "tmp"); ClassLoader cl = this.getClass().getClassLoader(); ParserConfiguration configuration = new ParserConfiguration(cl); configuration.setName(parseableFile.getPath()); try (FileInputStream fin = new FileInputStream(parseableFile); OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(tmpFile), encoding)) { String templateText = IOUtils.toString(fin, encoding); CompiledTemplate compiledTemplate; if (TemplateEnginesType.TEXT.equals(templateEngine)) { compiledTemplate = TEMPLATE_COMPILER_TEXT.compile(templateText, configuration); } else { compiledTemplate = TEMPLATE_COMPILER_HTML.compile(templateText, configuration); } StringWriter sw = new StringWriter(); compiledTemplate.render(sw, vars); String parsedContent = sw.toString(); if (prettify) { StringBuilder sb = new StringBuilder(); prettyPrinter.process(parsedContent, sb); parsedContent = sb.toString(); if (parsedContent.length() > 1 && parsedContent.startsWith("\n")) { // Avoiding pretty printer bug that it takes a new line in the beginning of the file. parsedContent = parsedContent.substring(1); } } fw.write(parsedContent); } overCopyFile(tmpFile, parseableFile); tmpFile.delete(); } /** * Reads the given amount of bytes from the the {@link RandomAccessFile}. */ public byte[] tryReadingAmount(final FileChannel is, final int amount) throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocate(amount); ByteBuffer[] bbArray = new ByteBuffer[] { byteBuffer }; int rSum = 0; int left = amount; for (long r = is.read(bbArray, rSum, left); rSum < amount && r >= 0; r = is.read(bbArray, rSum, left)) { rSum = rSum + (int) r; left = left - (int) r; } byte[] result; if (rSum == amount) { result = byteBuffer.array(); } else { result = new byte[rSum]; byteBuffer.get(result); } return result; } private void unpackEntry(final File destFile, final ZipFile zipFile, final ZipArchiveEntry entry) throws IOException, ZipException { if (entry.isDirectory()) { touchedFiles.add(destFile); destFile.mkdirs(); } else if (!isSameFile(destFile, entry.getSize(), entry.getLastModifiedDate().getTime())) { File parentFolder = destFile.getParentFile(); parentFolder.mkdirs(); InputStream inputStream = zipFile.getInputStream(entry); overCopyFile(Channels.newChannel(inputStream), entry.getSize(), entry.getLastModifiedDate().getTime(), destFile); FileManager.setPermissionsOnFile(destFile, entry); } else { touchedFiles.add(destFile); } } /** * Unpacks one entry from the zip file. * * @param zipFile * The zip file. * @param destinationFile * The destination file where the file should be copied to. * @param entry * The entry that should be unpacked from the zip file. */ public void unpackZipEntry(final File zipFile, final File destinationFile, final String entry) { try (ZipFile zipFileObj = new ZipFile(zipFile)) { ZipArchiveEntry zipEntry = zipFileObj.getEntry(entry); unpackEntry(destinationFile, zipFileObj, zipEntry); } catch (IOException e) { throw new UncheckedIOException("Could not uncompress distribution package file entry " + zipFile.getAbsolutePath() + " to target folder " + destinationFile.getAbsolutePath(), e); } } /** * Unpacks a ZIP file to the destination directory. * * @throws MojoExecutionException * if something goes wrong during unpacking the files. */ public void unpackZipFile(final File file, final File destinationDirectory, final String... exclusions) { Set<String> exclusionSet = new HashSet<>(Arrays.asList(exclusions)); try (ZipFile zipFile = new ZipFile(file)) { Enumeration<? extends ZipArchiveEntry> entries = zipFile.getEntries(); while (entries.hasMoreElements()) { ZipArchiveEntry entry = entries.nextElement(); String name = entry.getName(); if (!exclusionSet.contains(name)) { File destFile = new File(destinationDirectory, entry.getName()); unpackEntry(destFile, zipFile, entry); } } } catch (IOException e) { throw new UncheckedIOException("Could not uncompress distribution package file " + file.getAbsolutePath() + " to target folder " + destinationDirectory.getAbsolutePath(), e); } } }