com.bendb.thrifty.schema.Loader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of thrifty-schema Show documentation
Show all versions of thrifty-schema Show documentation
A simple Thrift IDL parser and validator
/*
* Copyright (C) 2015-2016 Benjamin Bader
*
* 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.bendb.thrifty.schema;
import com.bendb.thrifty.schema.parser.IncludeElement;
import com.bendb.thrifty.schema.parser.ThriftFileElement;
import com.bendb.thrifty.schema.parser.ThriftParser;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import okio.Okio;
import okio.Source;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
public final class Loader {
/**
* Attempts to identify strings that represent absolute filesystem paths.
* Does not attempt to support more unusual paths like UNC ("\\c\path") or
* filesystem URIs ("file:///c/path").
*/
private static final Pattern ABSOLUTE_PATH_PATTERN = Pattern.compile("^(/|\\w:\\\\).*");
private static final Predicate IS_THRIFT = new Predicate() {
@Override
public boolean apply(@Nullable File input) {
return input != null && input.getName().endsWith(".thrift");
}
};
/**
* A list of thrift files to be loaded. If empty, all .thrift files within
* {@link #includePaths} will be loaded.
*/
private final List thriftFiles = new ArrayList<>();
/**
* The search path for imported thrift files. If {@link #thriftFiles} is
* empty, then all .thrift files located on the search path will be loaded.
*/
private final Deque includePaths = new ArrayDeque<>();
private final LinkEnvironment environment = new LinkEnvironment();
private volatile ImmutableList linkedPrograms;
private Map loadedPrograms;
private FieldNamingPolicy fieldNamingPolicy;
public Loader() {
this(FieldNamingPolicy.DEFAULT);
}
public Loader(FieldNamingPolicy policy) {
this.fieldNamingPolicy = policy;
}
public Loader addThriftFile(String file) {
Preconditions.checkNotNull(file, "file");
thriftFiles.add(file);
return this;
}
public Loader addIncludePath(File path) {
Preconditions.checkNotNull(path, "path");
Preconditions.checkArgument(path.isDirectory(), "path must be a directory");
includePaths.add(path.getAbsoluteFile());
return this;
}
public Schema load() throws IOException {
loadFromDisk();
linkPrograms();
return new Schema(loadedPrograms.values());
}
private void loadFromDisk() throws IOException {
final List filesToLoad = new ArrayList<>(thriftFiles);
if (filesToLoad.isEmpty()) {
for (File file : includePaths) {
FluentIterable iterable = Files.fileTreeTraverser()
.breadthFirstTraversal(file)
.filter(IS_THRIFT);
for (File thriftFile : iterable) {
filesToLoad.add(thriftFile.getAbsolutePath());
}
}
}
Map loadedFiles = new LinkedHashMap<>();
for (String path : filesToLoad) {
loadFileRecursively(path, loadedFiles);
}
// Convert to Programs
loadedPrograms = new LinkedHashMap<>();
for (ThriftFileElement fileElement : loadedFiles.values()) {
File file = new File(fileElement.location().base(), fileElement.location().path());
if (!file.exists()) throw new AssertionError(
"WTF, we have a parsed ThriftFileElement with a non-existing location");
if (!file.isAbsolute()) throw new AssertionError("WTF, we have a non-canonical path");
Program program = new Program(fileElement, fieldNamingPolicy);
loadedPrograms.put(file.getCanonicalPath(), program);
}
// Link included programs together
Set visited = new HashSet<>(loadedPrograms.size());
for (Program program : loadedPrograms.values()) {
program.loadIncludedPrograms(this, visited);
}
}
/**
* Loads and parses a Thrift file and all files included (both directly and
* transitively) by it.
*
* @param path A relative or absolute path to a Thrift file.
* @param loadedFiles A mapping of absolute paths to parsed Thrift files.
*/
private void loadFileRecursively(String path, Map loadedFiles) throws IOException {
ThriftFileElement element = null;
File dir = null;
File file = findFirstExisting(path, null);
if (file != null) {
// Resolve symlinks, redundant '.' and '..' segments.
file = file.getCanonicalFile();
if (loadedFiles.containsKey(file.getAbsolutePath())) {
return;
}
dir = file.getParentFile();
element = loadSingleFile(file.getParentFile(), file.getName());
}
if (element == null) {
throw new FileNotFoundException(
"Failed to locate " + path + " in " + includePaths);
}
loadedFiles.put(file.getAbsolutePath(), element);
ImmutableList includes = element.includes();
if (includes.size() > 0) {
includePaths.addFirst(dir);
for (IncludeElement include : includes) {
if (!include.isCpp()) {
loadFileRecursively(include.path(), loadedFiles);
}
}
includePaths.removeFirst();
}
}
private void linkPrograms() {
synchronized (environment) {
for (Program program : loadedPrograms.values()) {
Linker linker = environment.getLinker(program);
linker.link();
}
if (environment.hasErrors()) {
String report = Joiner.on('\n').join(environment.getErrors());
throw new IllegalStateException(report);
}
linkedPrograms = ImmutableList.copyOf(loadedPrograms.values());
}
}
private ThriftFileElement loadSingleFile(File base, String path) throws IOException {
File file = new File(base, path).getAbsoluteFile();
if (!file.exists()) {
return null;
}
Source source = Okio.source(file);
try {
Location location = Location.get(base.toString(), path);
String data = Okio.buffer(source).readUtf8();
return ThriftParser.parse(location, data);
} catch (IOException e) {
throw new IOException("Failed to load " + path + " from " + base, e);
} finally {
Closeables.close(source, true);
}
}
Program resolveIncludedProgram(Location currentPath, String importPath) {
File resolved = findFirstExisting(importPath, currentPath);
if (resolved == null) {
throw new AssertionError("Included thrift file not found: " + importPath);
}
return getAndCheck(resolved.getAbsolutePath());
}
/**
* Resolves a relative path to the first existing match.
*
* Resolution rules favor, in order:
* 1. Absolute paths
* 2. The current working location, if given
* 3. The include path, in the order given.
*
* @param path a relative or absolute path to the file being sought.
* @param currentLocation the current working directory.
* @return the first matching file on the search path, or {@code null}.
*/
private File findFirstExisting(String path, @Nullable Location currentLocation) {
if (isAbsolutePath(path)) {
// absolute path, should be loaded as-is
File f = new File(path);
return f.exists() ? f : null;
}
if (currentLocation != null) {
File maybeFile = new File(currentLocation.base(), path).getAbsoluteFile();
if (maybeFile.exists()) {
return maybeFile;
}
}
for (File includePath : includePaths) {
File maybeFile = new File(includePath, path).getAbsoluteFile();
if (maybeFile.exists()) {
return maybeFile;
}
}
return null;
}
private Program getAndCheck(String absolutePath) {
Program p = loadedPrograms.get(absolutePath);
if (p == null) {
throw new AssertionError("All includes should have been resolved by now: " + absolutePath);
}
return p;
}
/**
* Checks if the path is absolute in an attempted cross-platform manner.
*/
private static boolean isAbsolutePath(String path) {
return ABSOLUTE_PATH_PATTERN.matcher(path).matches();
}
}