All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.loader.sourcecontext.DefaultSourceLoader Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.model.loader.sourcecontext;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
 * This class loads model files into memory, one at a time, and either shows leading lines up to a source location or
 * shows lines that are relevant to a source location use context from a {@link Model}.
 *
 * 

Sort sequences of {@link SourceLocation}s before calling {@link #loadContext(FromSourceLocation)} to avoid * needing to open and parse the same file more than once. * * @see SourceContextLoader#createLineBasedLoader * @see SourceContextLoader#createModelAwareLoader */ final class DefaultSourceLoader implements SourceContextLoader { private static final Logger LOGGER = Logger.getLogger(DefaultSourceLoader.class.getName()); private final List lines = new ArrayList<>(); private final Model model; private final int defaultCodeLines; private SourceLocation lastLoadedLocation; private Collection lastLoadedLocationLines; DefaultSourceLoader(int defaultCodeLines, Model model) { if (defaultCodeLines < 1) { throw new IllegalArgumentException("Must allow at least one code hint line: " + defaultCodeLines); } this.defaultCodeLines = defaultCodeLines; this.model = model; } @Override public Collection loadContext(FromSourceLocation source) { SourceLocation location = source.getSourceLocation(); // Ignore the components that were generated and have no location. if (location == SourceLocation.NONE) { return Collections.emptyList(); } // Use the cache if possible (e.g., multiple validation events for the same shape). if (location.equals(lastLoadedLocation)) { return lastLoadedLocationLines; } // Open a new file if no file is open, or it differs from the source location file. if (lastLoadedLocation == null || !lastLoadedLocation.getFilename().equals(location.getFilename())) { loadNextFile(location); } int line = location.getLine(); if (!isValidLine(line)) { LOGGER.finer(() -> "Attempted to load context for an invalid source location: " + location); lastLoadedLocationLines = Collections.emptyList(); } else if (model == null) { int start = Math.max(0, line - defaultCodeLines); lastLoadedLocationLines = lines.subList(start, line); } else { lastLoadedLocationLines = parseLines(source); } return lastLoadedLocationLines; } private void loadNextFile(SourceLocation source) { lines.clear(); lastLoadedLocation = source; LOGGER.finer(() -> "Opening source location file for " + source); try (LineNumberReader reader = openSourceLocation(source)) { String lineString; while ((lineString = reader.readLine()) != null) { lines.add(new Line(reader.getLineNumber(), lineString)); } } catch (IOException e) { throw new UncheckedIOException(e); } } private LineNumberReader openSourceLocation(FromSourceLocation source) { try { // Ensure that there's a scheme. SourceLocation location = source.getSourceLocation(); String normalizedFile = location.getFilename(); // Refuse to open URLs that are not files or JARs by forcing the file protocol. if (!location.getFilename().startsWith("file:") && !location.getFilename().startsWith("jar:")) { normalizedFile = "file:" + normalizedFile; } // Loading from a JAR needs special treatment, but this can // all actually be handled in a uniform way using URLs. URL url = new URL(normalizedFile); URLConnection connection = url.openConnection(); connection.setUseCaches(false); return new LineNumberReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); } catch (IOException e) { throw new UncheckedIOException("Unable to load source location context for " + source, e); } } private boolean isValidLine(int line) { line--; return line >= 0 && line < lines.size(); } private void addLineIfValid(int line, List mutate) { if (isValidLine(line)) { mutate.add(lines.get(line - 1)); } } private List parseLines(FromSourceLocation source) { SourceLocation location = source.getSourceLocation(); int line = location.getLine(); if (source instanceof ValidationEvent) { ValidationEvent event = (ValidationEvent) source; Shape targetShape = event.getShapeId().flatMap(model::getShape).orElse(null); if (targetShape != null) { if (targetShape.getSourceLocation().equals(location)) { // The validation event is on a shape since the location is equal to the shape location. source = targetShape; } else if (targetShape.getSourceLocation().getLine() > location.getLine()) { // Assume it's a trait in the Smithy IDL. Show the trait definition followed by shape definition. return parseTraitBeforeShape(location, targetShape); } else if (location.getFilename().endsWith(".json")) { // Assume it's a trait defined in JSON. Show the trait definition followed by shape definition. return parseTraitAfterShape(location, targetShape); } else { // It's a trait that uses "apply" or the location is invalid, so just show the trait line. return Collections.singletonList(lines.get(line - 1)); } } } if (source instanceof MemberShape) { return parseMemberShape(location, (MemberShape) source); } return parseOtherComponents(location); } private List parseTraitBeforeShape(SourceLocation location, Shape targetShape) { List result = new ArrayList<>(2); addLineIfValid(location.getLine(), result); addLineIfValid(targetShape.getSourceLocation().getLine(), result); return result; } private List parseTraitAfterShape(SourceLocation location, Shape targetShape) { List result = new ArrayList<>(2); addLineIfValid(targetShape.getSourceLocation().getLine(), result); addLineIfValid(location.getLine(), result); return result; } private List parseMemberShape(SourceLocation location, MemberShape member) { // Members should always crawl up to the defining shape in both the IDL and JSON. Shape container = model.getShape(member.getContainer()).orElse(null); // This should never be null, but guard here just in case. if (container == null) { LOGGER.warning(() -> "Member container not found: " + member.getId() + " -> " + member.getTarget()); } else { SourceLocation containerLocation = container.getSourceLocation(); // Some basic checking to ensure the member after the container in the same file. if (containerLocation.getFilename().equals(location.getFilename()) && containerLocation.getLine() < location.getLine()) { List result = new ArrayList<>(2); addLineIfValid(containerLocation.getLine(), result); addLineIfValid(location.getLine(), result); return result; } } return Collections.emptyList(); } private List parseOtherComponents(SourceLocation location) { // Show context lines up from the shape until maxCode or until an empty line is encountered. int line = location.getLine(); int lowestPossibleLine = Math.max(0, line - 1 - defaultCodeLines); int foundStart = line - 1; for (int i = line - 1; i > lowestPossibleLine; i--) { Line foundLine = lines.get(i); if (foundLine.getContent().length() > 0) { foundStart = i; } else { break; } } return lines.subList(foundStart, line); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy