blob: b898317712b09d7ea1b65740495b7e1fa00758bb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sling.graphql.schema.aggregator.impl;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedReader;
import org.jetbrains.annotations.NotNull;
/** Reader for the partials format, which parses a partial file and
* provides access to its sections.
* See the example.partial.txt and the tests for a description of
* the format.
*/
public class PartialReader implements Partial {
private static final Pattern SECTION_LINE = Pattern.compile("([A-Z]+) *:(.*)");
private static final int EOL = '\n';
private final Map<SectionName, Section> sections = new EnumMap<>(SectionName.class);
private final PartialInfo partialInfo;
private final Set<PartialInfo> requiredPartialNames;
private final String digest;
/** The PARTIAL section is the only required one */
public static final String PARTIAL_SECTION = "PARTIAL";
static class SyntaxException extends IOException {
SyntaxException(String reason) {
super(reason);
}
}
static class ParsedSection implements Partial.Section {
private final Supplier<Reader> sectionSource;
private final SectionName name;
private final String description;
private final int startCharIndex;
private final int endCharIndex;
ParsedSection(Supplier<Reader> sectionSource, SectionName name, String description, int start, int end) {
this.sectionSource = sectionSource;
this.name = name;
this.description = description;
this.startCharIndex = start;
this.endCharIndex = end;
}
@Override
public SectionName getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public Reader getContent() throws IOException {
final Reader r = sectionSource.get();
r.skip(startCharIndex);
return new BoundedReader(r, endCharIndex - startCharIndex);
}
}
public PartialReader(@NotNull PartialInfo partialInfo, @NotNull Supplier<Reader> source) throws IOException {
this.partialInfo = partialInfo;
parse(source);
this.digest = "SHA-256: " + Hex.encodeHexString(
DigestUtils.updateDigest(DigestUtils.getSha256Digest(), IOUtils.toByteArray(source.get(), StandardCharsets.UTF_8)).digest());
final Partial.Section requirements = sections.get(SectionName.REQUIRES);
if(requirements == null) {
requiredPartialNames = Collections.emptySet();
} else {
requiredPartialNames = PartialInfo.fromRequiresSection(requirements.getDescription());
}
}
/* Detect lines that start with a <SECTION>: name
* in the input, and save them as sections
*/
private void parse(Supplier<Reader> source) throws IOException {
final Reader input = source.get();
StringBuilder line = new StringBuilder();
int c;
int charCount = 0;
int lastSectionStart = 0;
String sectionName = null;
String sectionDescription = "";
while((c = input.read()) != -1) {
if(c == EOL) {
final Matcher m = SECTION_LINE.matcher(line);
if(m.matches()) {
// Add previous section
addSectionIfNameIsSet(source, toSectionName(sectionName), sectionDescription, lastSectionStart, charCount - line.length());
// And setup for the new section
sectionName = m.group(1).trim();
sectionDescription = m.group(2).trim();
lastSectionStart = charCount + 1;
}
line = new StringBuilder();
} else {
line.append((char)c);
}
charCount++;
}
// Add last section
addSectionIfNameIsSet(source, toSectionName(sectionName), sectionDescription, lastSectionStart, Integer.MAX_VALUE);
// And validate
if(!sections.containsKey(SectionName.PARTIAL)) {
throw new SyntaxException(String.format("Missing required %s section", PARTIAL_SECTION));
}
}
private void addSectionIfNameIsSet(Supplier<Reader> sectionSource, SectionName name, String description, int start, int end) throws SyntaxException {
if(name == null) {
return;
}
if(sections.containsKey(name)) {
throw new SyntaxException(String.format("Duplicate section '%s'", name));
}
sections.put(name, new ParsedSection(sectionSource, name, description, start, end));
}
private SectionName toSectionName(String str) throws SyntaxException {
if(str == null) {
return null;
}
try {
return SectionName.valueOf(str);
} catch(Exception e) {
throw new SyntaxException(String.format("Invalid section name '%s'", str));
}
}
@Override
public @NotNull PartialInfo getPartialInfo() {
return partialInfo;
}
@Override
public @NotNull Optional<Section> getSection(Partial.SectionName name) {
final Section s = sections.get(name);
return Optional.ofNullable(s);
}
@Override
public @NotNull Set<PartialInfo> getRequiredPartialNames() {
return Collections.unmodifiableSet(requiredPartialNames);
}
@Override
public @NotNull String getDigest() {
return digest;
}
}