LangReader.java
/*
* Copyright 2020-2022 Foreseeti AB <https://foreseeti.com>
*
* 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 org.mal_lang.langspec.io;
import static java.util.Objects.requireNonNull;
import jakarta.json.JsonObject;
import jakarta.json.stream.JsonParsingException;
import java.io.Closeable;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipInputStream;
import org.leadpony.justify.api.JsonValidatingException;
import org.leadpony.justify.api.JsonValidationService;
import org.leadpony.justify.api.ProblemHandler;
import org.mal_lang.langspec.Lang;
import org.mal_lang.langspec.Utils;
import org.mal_lang.langspec.builders.LangBuilder;
/**
* Reads {@code .mar} files into {@link org.mal_lang.langspec.Lang} objects.
*
* @since 1.0.0
*/
public final class LangReader implements Closeable {
private final InputStream in;
private boolean isRead = false;
private boolean isClosed = false;
private JsonObject langSpec;
private final Map<String, byte[]> svgIcons = new LinkedHashMap<>();
private final Map<String, byte[]> pngIcons = new LinkedHashMap<>();
private String license;
private String notice;
/**
* Constructs a new {@code LangReader} object.
*
* @param in an input stream from which a {@code .mar} file is to be read
* @throws java.lang.NullPointerException if {@code in} is {@code null}
* @since 1.0.0
*/
public LangReader(InputStream in) {
this.in = requireNonNull(in);
}
private static JsonObject readLangSpec(ZipInputStream zipIn) throws IOException {
try (var schemaIn = Utils.getLangSpecSchema()) {
var service = JsonValidationService.newInstance();
var schema = service.readSchema(schemaIn, StandardCharsets.UTF_8);
var wrappedIn =
new FilterInputStream(zipIn) {
@Override
public void close() throws IOException {}
};
try (var reader =
service.createReader(
wrappedIn, StandardCharsets.UTF_8, schema, ProblemHandler.throwing())) {
return reader.readObject();
} catch (JsonValidatingException e) {
throw new IOException("Failed to validate \"langspec.json\"", e);
} catch (JsonParsingException e) {
throw new IOException("Failed to parse \"langspec.json\"", e);
}
}
}
/**
* Returns a {@link org.mal_lang.langspec.Lang} object that is represented in the input source.
* This method needs to be called only once for a reader instance.
*
* @return a {@link org.mal_lang.langspec.Lang} object
* @throws java.io.IOException if an I/O error occurs
* @throws java.lang.IllegalStateException if {@code read} or {@code close} method is already
* called
* @since 1.0.0
*/
public Lang read() throws IOException {
return this.read(true, true);
}
/**
* Returns a {@link org.mal_lang.langspec.Lang} object that is represented in the input source.
* This method needs to be called only once for a reader instance.
*
* @param readIcons whether icons should be read
* @param readLicense whether license and notice should be read
* @return a {@link org.mal_lang.langspec.Lang} object
* @throws java.io.IOException if an I/O error occurs
* @throws java.lang.IllegalStateException if {@code read} or {@code close} method is already
* called
* @since 1.0.0
*/
public Lang read(boolean readIcons, boolean readLicense) throws IOException {
if (this.isRead) {
throw new IllegalStateException("read method is already called");
}
if (this.isClosed) {
throw new IllegalStateException("close method is already called");
}
try (var zipIn = new ZipInputStream(this.in, StandardCharsets.UTF_8)) {
for (var zipEntry = zipIn.getNextEntry(); zipEntry != null; zipEntry = zipIn.getNextEntry()) {
if (!zipEntry.isDirectory()) {
var name = zipEntry.getName();
if (name.equals("langspec.json")) {
this.langSpec = LangReader.readLangSpec(zipIn);
} else if (readIcons && name.startsWith("icons/")) {
if (name.endsWith(".svg")) {
var assetName = name.substring("icons/".length(), name.length() - ".svg".length());
if (Utils.isIdentifier(assetName)) {
this.svgIcons.put(assetName, zipIn.readAllBytes());
}
} else if (name.endsWith(".png")) {
var assetName = name.substring("icons/".length(), name.length() - ".png".length());
if (Utils.isIdentifier(assetName)) {
this.pngIcons.put(assetName, zipIn.readAllBytes());
}
}
} else if (readLicense && name.equals("LICENSE")) {
this.license = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
} else if (readLicense && name.equals("NOTICE")) {
this.notice = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
} finally {
this.isRead = true;
}
if (this.langSpec == null) {
throw new IOException("File \"langspec.json\" not found");
}
return Lang.fromBuilder(
LangBuilder.fromJson(
this.langSpec, this.svgIcons, this.pngIcons, this.license, this.notice));
}
/**
* Closes this stream and releases any system resources associated with it. If the stream is
* already closed then invoking this method has no effect.
*
* @throws java.io.IOException if an I/O error occurs
* @since 1.0.0
*/
@Override
public void close() throws IOException {
if (this.isClosed) {
return;
}
try {
this.in.close();
} finally {
this.isClosed = true;
}
}
}