package org.infernus.idea.checkstyle.model; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.infernus.idea.checkstyle.CheckStyleConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; import java.util.Enumeration; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import static org.infernus.idea.checkstyle.util.Strings.isBlank; /** * A configuration file on a mounted file system. */ public class FileConfigurationLocation extends ConfigurationLocation { private static final Log LOG = LogFactory.getLog(FileConfigurationLocation.class); private static final int BUFFER_SIZE = 4096; private final Project project; /** * Create a new file configuration. * * @param project the project. */ FileConfigurationLocation(final Project project) { this(project, ConfigurationType.LOCAL_FILE); } FileConfigurationLocation(final Project project, final ConfigurationType configurationType) { super(configurationType); if (project == null) { throw new IllegalArgumentException("A project is required"); } this.project = project; } @Override public File getBaseDir() { final String location = getLocation(); if (location != null) { final File locationFile = new File(location); if (locationFile.exists()) { return locationFile.getParentFile(); } } return null; } @Override public String getLocation() { return detokenisePath(super.getLocation()); } @Override public void setLocation(final String location) { if (isBlank(location)) { throw new IllegalArgumentException("A non-blank location is required"); } super.setLocation(tokenisePath(location)); } @NotNull protected InputStream resolveFile() throws IOException { final String detokenisedLocation = getLocation(); if (isInJarFile(detokenisedLocation)) { return readLocationFromJar(detokenisedLocation); } final File locationFile = new File(detokenisedLocation); if (!locationFile.exists()) { throw new FileNotFoundException("File does not exist: " + absolutePathOf(locationFile)); } return new FileInputStream(locationFile); } private InputStream readLocationFromJar(final String detokenisedLocation) throws IOException { final String[] fileParts = detokenisedLocation.split("!/"); final InputStream fileStream = readFileFromJar(fileParts[0], fileParts[1]); if (fileStream == null) { throw new FileNotFoundException("File does not exist: " + fileParts[0] + " containing " + fileParts[1]); } return fileStream; } @Nullable @Override public String resolveAssociatedFile(final String filename, final Project project, final Module module) throws IOException { final String associatedFile = super.resolveAssociatedFile(filename, project, module); if (associatedFile != null) { return associatedFile; } final String detokenisedLocation = getLocation(); if (isInJarFile(detokenisedLocation)) { return writeStreamToTemporaryFile( readFileFromJar(detokenisedLocation.split("!/")[0], filename), extensionOf(filename)); } return null; } private String extensionOf(final String filename) { if (filename != null && filename.contains(".")) { return filename.substring(filename.lastIndexOf('.')); } return ".tmp"; } private String writeStreamToTemporaryFile(final InputStream fileStream, final String fileSuffix) throws IOException { if (fileStream == null) { return null; } final File tempFile = File.createTempFile("csidea-", fileSuffix); BufferedOutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(tempFile)); writeTo(fileStream, out); tempFile.deleteOnExit(); return tempFile.getAbsolutePath(); } finally { closeQuietly(out, fileStream); } } private void writeTo(final InputStream fileStream, final BufferedOutputStream out) throws IOException { final byte[] buffer = new byte[BUFFER_SIZE]; int read; while ((read = fileStream.read(buffer)) != -1) { out.write(buffer, 0, read); } } private boolean isInJarFile(final String detokenisedLocation) { return detokenisedLocation != null && detokenisedLocation.toLowerCase().contains(".jar!/"); } public InputStream readFileFromJar(final String jarPath, final String filePath) throws IOException { ZipFile jarFile = null; try { jarFile = new ZipFile(jarPath); for (final Enumeration<? extends ZipEntry> e = jarFile.entries(); e.hasMoreElements(); ) { final ZipEntry entry = e.nextElement(); if (!entry.isDirectory() && entry.getName().equals(filePath)) { BufferedInputStream bis = null; try { bis = new BufferedInputStream(jarFile.getInputStream(entry)); return new ByteArrayInputStream(readFrom(bis)); } finally { closeQuietly(bis); } } } } finally { closeQuietly(jarFile); } return null; } private byte[] readFrom(final BufferedInputStream bis) throws IOException { final ByteArrayOutputStream rulesFile = new ByteArrayOutputStream(); final byte[] readBuffer = new byte[BUFFER_SIZE]; int count; while ((count = bis.read(readBuffer, 0, BUFFER_SIZE)) != -1) { rulesFile.write(readBuffer, 0, count); } return rulesFile.toByteArray(); } private static void closeQuietly(final ZipFile jarFile) { if (jarFile != null) { try { jarFile.close(); } catch (IOException ignored) { } } } private static void closeQuietly(final Closeable... closeables) { for (Closeable closeable : closeables) { if (closeable != null) { try { closeable.close(); } catch (IOException ignored) { } } } } /** * Get the base path of the project. * * @return the base path of the project. */ @Nullable File getProjectPath() { if (project == null) { return null; } try { final VirtualFile baseDir = project.getBaseDir(); if (baseDir == null) { return null; } return new File(baseDir.getPath()); } catch (Exception e) { // IDEA 10.5.2 sometimes throws an AssertionException in project.getBaseDir() LOG.debug("Couldn't retrieve base location", e); return null; } } /** * Process a stored file path for any tokens, and resolve the *nix style path * to the local filesystem path encoding. * * @param path the path to process, in (tokenised) URI syntax. * @return the processed path, in local file path syntax. */ String detokenisePath(final String path) { if (path == null) { return null; } LOG.debug("Processing file: " + path); for (String prefix : new String[]{CheckStyleConfiguration.PROJECT_DIR, CheckStyleConfiguration.LEGACY_PROJECT_DIR}) { if (path.startsWith(prefix)) { // path is relative to project dir final File projectPath = getProjectPath(); if (projectPath != null) { final String projectRelativePath = fromUnixPath(path.substring(prefix.length())); final String completePath = projectPath + File.separator + projectRelativePath; return absolutePathOf(new File(completePath)); } else { LOG.warn("Could not untokenise path as project dir is unset: " + path); } } } return fromUnixPath(path); } /** * Process a path, add tokens as necessary and encode it a *nix-style path. * * @param path the path to process, in local file path syntax. * @return the tokenised path in URI syntax. */ String tokenisePath(final String path) { if (path == null) { return null; } final File projectPath = getProjectPath(); if (projectPath != null && path.startsWith(absolutePathOf(projectPath) + separatorChar())) { return CheckStyleConfiguration.PROJECT_DIR + toUnixPath(path.substring(absolutePathOf(projectPath).length())); } return toUnixPath(path); } String absolutePathOf(final File file) { return file.getAbsolutePath(); } private String toUnixPath(final String path) { if (separatorChar() == '/') { return path; } return path.replace(separatorChar(), '/'); } char separatorChar() { return File.separatorChar; } private String fromUnixPath(final String path) { if (separatorChar() == '/') { return path; } return path.replace('/', separatorChar()); } Project getProject() { return project; } @Override public Object clone() { return cloneCommonPropertiesTo(new FileConfigurationLocation(project)); } }