/* * Copyright 2009 Google Inc. * * 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 com.google.jstestdriver; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.jstestdriver.config.UnreadableFile; import com.google.jstestdriver.config.UnreadableFilesException; import com.google.jstestdriver.hooks.FileParsePostProcessor; import com.google.jstestdriver.model.BasePaths; import com.google.jstestdriver.util.DisplayPathSanitizer; import org.apache.oro.io.GlobFilenameFilter; import org.apache.oro.text.GlobCompiler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; /** * Handles the resolution of glob paths (*.js) and relative paths. * @author jeremiele@google.com (Jeremie Lenfant-Engelmann) */ public class PathResolver { private static final Logger logger = LoggerFactory.getLogger(PathResolver.class); private final Set<FileParsePostProcessor> processors; private final BasePaths basePaths; private DisplayPathSanitizer sanitizer; @Inject public PathResolver(BasePaths basePaths, Set<FileParsePostProcessor> processors, DisplayPathSanitizer sanitizer) { this.basePaths = basePaths; this.processors = processors; this.sanitizer = sanitizer; } private Set<FileInfo> consolidatePatches(Set<FileInfo> resolvedFilesLoad) { Set<FileInfo> consolidated = new LinkedHashSet<FileInfo>(resolvedFilesLoad.size()); FileInfo currentNonPatch = null; for (FileInfo fileInfo : resolvedFilesLoad) { if (fileInfo.isPatch()) { if (currentNonPatch == null) { throw new IllegalStateException("Patch " + fileInfo + " without a core file to patch"); } currentNonPatch.addPatch(fileInfo); } else { consolidated.add(fileInfo); currentNonPatch = fileInfo; } } return consolidated; } /** * Resolves files for a set of FileInfos: * - Expands glob paths (e.g. "*.js") into distinct FileInfos * - Sets last modified timestamp for each FileInfo * * @param unresolvedFiles the FileInfos to resolved * @return the resolved FileInfos */ public Set<FileInfo> resolve(Set<FileInfo> unresolvedFiles) { Set<FileInfo> resolvedFiles = new LinkedHashSet<FileInfo>(); List<UnreadableFile> unreadable = Lists.newLinkedList(); for (FileInfo fileInfo : unresolvedFiles) { String filePath = fileInfo.getFilePath(); if (fileInfo.isWebAddress()) { resolvedFiles.add(fileInfo.fromResolvedPath(filePath, filePath, -1)); } else { expandFileInfosFromFileInfo(resolvedFiles, unreadable, fileInfo, filePath); } } if (!unreadable.isEmpty()) { throw new UnreadableFilesException(unreadable); } resolvedFiles = postProcessFiles(resolvedFiles); return consolidatePatches(resolvedFiles); } private void expandFileInfosFromFileInfo(Set<FileInfo> resolvedFiles, List<UnreadableFile> unreadable, FileInfo fileInfo, String filePath) { List<String> unresolvedPaths = Lists.newArrayListWithCapacity(basePaths.size()); for (File basePath : basePaths) { File file = new File(basePath, filePath); File absoluteDir = file.getParentFile().getAbsoluteFile(); if (absoluteDir.getName().equals("**")) { absoluteDir = absoluteDir.getParentFile(); } // Get all files for the current FileInfo. This will return one file // if the FileInfo doesn't represent a glob String[] expandedFileNames = expandGlob(absoluteDir.getAbsolutePath(), file.getName(), absoluteDir); if (expandedFileNames == null) { continue; } for (String fileName : expandedFileNames) { File sourceFile = new File(absoluteDir, fileName); createFileInfo(resolvedFiles, unreadable, fileInfo, sourceFile, basePath); } return; } unreadable.add(new UnreadableFile(fileInfo.getFilePath(), basePaths.toErrorString(fileInfo.getFilePath()))); } private void createFileInfo(Set<FileInfo> resolvedFiles, List<UnreadableFile> unreadable, FileInfo fileInfo, File sourceFile, File basePath) { if (!sourceFile.canRead()) { unreadable.add( new UnreadableFile(fileInfo.getFilePath(), sourceFile.getAbsolutePath())); } else { String absolutePath = resolveRelativePathReferences(sourceFile.getAbsolutePath()); String displayPath = sanitizer.sanitize(absolutePath, basePath); File resolvedFile = new File(absolutePath); long timestamp = resolvedFile.lastModified(); FileInfo newFileInfo = fileInfo.fromResolvedPath(absolutePath, displayPath, timestamp); resolvedFiles.add(newFileInfo); } } /** * Creates a full resolved path to a resource without following the sym links. */ public File resolvePath(String filePath) { return resolvePathToFileInfo(filePath, false, false).toFile(); } /** * Resolves a path to a {@link FileInfo}. * @param path The path to the file. * @param isPatch Indicates if this file is intended to patch the file it loads before. * @param serveOnly Indicates that this * @return A FileInfo generated from the path. * @throws {@link UnreadableFilesException} If the file can't be read. */ public FileInfo resolvePathToFileInfo(String path, boolean isPatch, boolean serveOnly) { for (File basePath : basePaths) { File resolved = resolvePath(path, basePath); if (resolved == null) { continue; } return new FileInfo(resolved.getAbsolutePath(), resolved.lastModified(), resolved.length(), isPatch, serveOnly, null, sanitizer.sanitize(resolved.getAbsolutePath(), basePath)); } throw new UnreadableFilesException(Lists.newArrayList(new UnreadableFile(path, basePaths .toErrorString(path)))); } private File resolvePath(String filePath, File basePath) { File absolute = new File(filePath); if (!absolute.isAbsolute()) { absolute = new File(basePath, filePath); } File resolved = new File(resolveRelativePathReferences(absolute.getAbsolutePath())); if (resolved.canRead()) { return resolved; } return null; } /** * This function is needed to deal with removing ".." from a path. * On a linux/unix based system, using the canonical file name can cause * some strange issues, as well as confusing debugging, as the file name * may not match the users expectations. */ private String resolveRelativePathReferences(String path) { Pattern pattern = Pattern.compile(Pattern.quote(File.separator)); String[] elements = pattern.split(path); List<String> resolved = Lists.newArrayListWithExpectedSize(elements.length); for (String element : elements) { if ("..".equals(element)) { resolved.remove(resolved.size() - 1); } else { resolved.add(element); } } return Joiner.on(File.separator).join(resolved); } private String[] expandGlob(String filePath, String fileNamePattern, File dir) { FilenameFilter fileFilter = new GlobFilenameFilter(fileNamePattern, GlobCompiler.DEFAULT_MASK | GlobCompiler.CASE_INSENSITIVE_MASK); String[] filteredFiles = expandDeepDirectoryGlobPaths(dir, fileFilter, "").toArray(new String[0]); if (filteredFiles == null || filteredFiles.length == 0) { return null; } Arrays.sort(filteredFiles, String.CASE_INSENSITIVE_ORDER); return filteredFiles; } private Set<String> expandDeepDirectoryGlobPaths( File rootDir, FilenameFilter fileFilter, String basePath ) { Set<String> foundFiles = new LinkedHashSet<String>(); if (!rootDir.isDirectory()) return foundFiles; File[] children = rootDir.listFiles(); for (File child : children) { foundFiles.addAll( expandDeepDirectoryGlobPaths(child, fileFilter, basePath + "/" + child.getName()) ); } String[] childFiles = rootDir.list(fileFilter); for (String childFilename : childFiles) { foundFiles.add(basePath + "/" + childFilename); } return foundFiles; } public List<Plugin> resolve(List<Plugin> plugins) { List<UnreadableFile> unreadable = Lists.newLinkedList(); List<Plugin> resolved = Lists.newLinkedList(); for (Plugin plugin : plugins) { File resolvedFile = resolvePath(plugin.getPathToJar()); /*if (!resolvedFile.exists()) { unreadable.add(new UnreadableFile(plugin.getPathToJar(), basePaths.toErrorString(plugin.getPathToJar()))); continue; }*/ resolved.add(plugin.getPluginFromPath(resolvedFile.getAbsolutePath())); } if (!unreadable.isEmpty()) { throw new UnreadableFilesException(unreadable); } return resolved; } private Set<FileInfo> postProcessFiles(Set<FileInfo> resolvedFiles) { Set<FileInfo> processedFiles = resolvedFiles; for (FileParsePostProcessor processor : processors) { processedFiles = processor.process(resolvedFiles); } return processedFiles; } }