/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * 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.dart.engine.source; import com.google.common.annotations.VisibleForTesting; import com.google.dart.engine.AnalysisEngine; import com.google.dart.engine.internal.context.PerformanceStatistics; import com.google.dart.engine.sdk.DirectoryBasedDartSdk; import com.google.dart.engine.utilities.general.TimeCounter.TimeCounterHandle; import com.google.dart.engine.utilities.io.ProcessRunner; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * An explicit package: resolver. This UriResolver shells out to pub, calling it's list-package-dirs * command. It parses the resulting json map, which maps symbolic package references to their * concrete locations on disk. * * <pre> *{ *"packages": { *"foo": "path/to/foo", *"bar": "path/to/bar" *}, *"input_files": [ *... *] *}, *</pre> */ public class ExplicitPackageUriResolver extends UriResolver { /** * The name of the {@code package} scheme. */ public static final String PACKAGE_SCHEME = "package"; protected static final String PUB_LIST_COMMAND = "list-package-dirs"; /** * Return {@code true} if the given URI is a {@code package} URI. * * @param uri the URI being tested * @return {@code true} if the given URI is a {@code package} URI */ public static boolean isPackageUri(URI uri) { return PACKAGE_SCHEME.equals(uri.getScheme()); } private File rootDir; private DirectoryBasedDartSdk sdk; @VisibleForTesting protected Map<String, List<File>> packageMap; // TODO: For now, this takes a DirectoryBasedDartSdk. We may want to abstract this out into // something that can return a package map. /** * Create a new ExplicitPackageUriResolver. * * @param sdk the sdk; this is used to locate the pub command to run * @param rootDir the directory for which we'll be resolving package information */ public ExplicitPackageUriResolver(DirectoryBasedDartSdk sdk, File rootDir) { if (rootDir == null) { throw new IllegalArgumentException("the root dir must not be null"); } this.sdk = sdk; this.rootDir = rootDir; } public String[] getCommand() { return new String[] {sdk.getPubExecutable().getAbsolutePath(), PUB_LIST_COMMAND}; } public File getRootDir() { return rootDir; } @Override public Source resolveAbsolute(URI uri) { if (!isPackageUri(uri)) { return null; } String path = uri.getPath(); if (path == null) { path = uri.getSchemeSpecificPart(); if (path == null) { return null; } } String pkgName; String relPath; int index = path.indexOf('/'); if (index == -1) { // No slash pkgName = path; relPath = ""; } else if (index == 0) { // Leading slash is invalid return null; } else { // <pkgName>/<relPath> pkgName = path.substring(0, index); relPath = path.substring(index + 1); } if (packageMap == null) { TimeCounterHandle handle = PerformanceStatistics.pubList.start(); try { packageMap = calculatePackageMap(); } finally { handle.stop(); } } List<File> dirs = packageMap.get(pkgName); if (dirs != null) { for (File packageDir : dirs) { if (packageDir.exists()) { File resolvedFile = new File(packageDir, relPath.replace('/', File.separatorChar)); if (resolvedFile.exists()) { return new FileBasedSource(uri, resolvedFile); } } } } // // Return a FileBasedSource that doesn't exist. This helps provide more meaningful error // messages to users (a missing file error, as opposed to an invalid uri error). // String fullPackagePath = pkgName + "/" + relPath; return new FileBasedSource(uri, new File(getRootDir(), fullPackagePath.replace( '/', File.separatorChar))); } public String resolvePathToPackage(String path) { if (packageMap == null) { return null; } for (String key : packageMap.keySet()) { List<File> files = packageMap.get(key); for (File file : files) { try { if (file.getCanonicalPath().endsWith(path)) { return key; } } catch (IOException e) { } } } return null; } @Override public URI restoreAbsolute(Source source) { if (packageMap == null) { return null; } if (source instanceof FileBasedSource) { String sourcePath = ((FileBasedSource) source).getFile().getPath(); for (Entry<String, List<File>> entry : packageMap.entrySet()) { for (File pkgFolder : entry.getValue()) { String pkgCanonicalPath = pkgFolder.getAbsolutePath(); if (sourcePath.startsWith(pkgCanonicalPath)) { String packageName = entry.getKey(); String relPath = sourcePath.substring(pkgCanonicalPath.length()); return URI.create(PACKAGE_SCHEME + ":" + packageName + relPath); } } } } return null; } protected Map<String, List<File>> calculatePackageMap() { ProcessBuilder builder = new ProcessBuilder(getCommand()); builder.directory(getRootDir()); ProcessRunner runner = new ProcessRunner(builder); try { if (runProcess(runner) == 0) { return parsePackageMap(runner.getStdOut()); } else { AnalysisEngine.getInstance().getLogger().logInformation( "pub " + PUB_LIST_COMMAND + " failed: exit code " + runner.getExitCode()); } } catch (IOException ioe) { AnalysisEngine.getInstance().getLogger().logInformation( "error running pub " + PUB_LIST_COMMAND, ioe); } catch (JSONException e) { AnalysisEngine.getInstance().getLogger().logError( "malformed json from pub " + PUB_LIST_COMMAND, e); } return new HashMap<String, List<File>>(); } protected Map<String, List<File>> parsePackageMap(String jsonText) throws JSONException { Map<String, List<File>> map = new HashMap<String, List<File>>(); // Json format: // { // "packages": { // "foo": "path/to/foo", // "bar": "path/to/bar", // "myapp": "path/to/myapp" // <- self link // }, // "input_files": [ // "path/to/myapp/pubspec.lock" // ] // } JSONObject obj = new JSONObject(jsonText); JSONObject packages = obj.optJSONObject("packages"); // TODO: also parse 'input_files'; use that information to check file timestamps if (packages != null) { Iterator<?> keys = packages.keys(); while (keys.hasNext()) { Object key = keys.next(); if (key instanceof String) { String strKey = (String) key; List<File> files = new ArrayList<File>(); map.put(strKey, files); Object val = packages.get(strKey); if (val instanceof String) { String path = (String) val; files.add(new File(path)); } else if (val instanceof JSONArray) { JSONArray arr = (JSONArray) val; for (int i = 0; i < arr.length(); i++) { files.add(new File(arr.getString(i))); } } } } } return map; } /** * Run the external process and return the exit value once the external process has completed. * * @param runner the external process runner * @return the external process exit code */ protected int runProcess(ProcessRunner runner) throws IOException { return runner.runSync(0); } }