/*
* dCache - http://www.dcache.org/
*
* Copyright (C) 2016 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.dcache.gplazma.util;
import com.google.common.base.Splitter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.dcache.gplazma.util.IGTFInfo.ParserException;
import org.dcache.gplazma.util.IGTFInfo.Type;
import org.dcache.util.Glob;
/**
* A class that represents an IGTF info file. The file
* format is described here:
*
* https://wiki.eugridpma.org/Main/IGTFInfoFile
*/
public class IGTFInfoFile
{
private static final Logger LOG = LoggerFactory.getLogger(IGTFInfoFile.class);
/**
* All IGTF Policy files have filenames that match the following glob.
*/
public static final Glob POLICY_FILENAME_GLOB = new Glob("policy-*.info");
/** Minimum duration, in ms, to stat file. */
private static final long CHECK_THRESHOLD = 1_000;
private final Path file;
private final Type type;
private IGTFInfo policy;
private long lastChecked;
private FileTime lastModified;
public IGTFInfoFile(String filename)
{
this(FileSystems.getDefault().getPath(filename));
}
public IGTFInfoFile(Path path)
{
file = path;
String filename = path.getFileName().toString();
type = POLICY_FILENAME_GLOB.matches(filename) ? Type.POLICY : Type.TRUST_ANCHOR;
}
public Path getPath()
{
return file;
}
public Optional<IGTFInfo> get()
{
if (System.currentTimeMillis() - lastChecked > CHECK_THRESHOLD) {
lastChecked = System.currentTimeMillis();
policy = null;
try {
FileTime fileLastModified = Files.getLastModifiedTime(file);
if (lastModified == null || fileLastModified.compareTo(lastModified) > 0) {
policy = read(file, type);
lastModified = fileLastModified;
}
} catch (IOException | ParserException e) {
LOG.warn("{}: {}", file.getFileName(), e.getMessage());
}
}
return Optional.ofNullable(policy);
}
private static Collection<LogicalLine> readLines(Path path) throws IOException
{
List<LogicalLine> lines = new ArrayList<>();
StringBuilder sb = null;
int lineIndex = 0;
int startOfLine = -1;
for (String line : Files.readAllLines(path)) {
lineIndex++;
if (line.endsWith("\\")) {
if (sb == null) {
sb = new StringBuilder();
startOfLine = lineIndex;
}
sb.append(line.substring(0, line.length()-1));
} else {
String logicalLine = trim(sb == null ? line : sb.append(line).toString());
if (!logicalLine.isEmpty()) {
lines.add(new LogicalLine(logicalLine, sb == null ? lineIndex : startOfLine));
}
sb = null;
}
}
if (sb != null) {
String logicalLine = trim(sb.toString());
if (!logicalLine.isEmpty()) {
lines.add(new LogicalLine(logicalLine, startOfLine));
}
}
return lines;
}
private static String trim(String line)
{
int hash = line.indexOf('#');
if (hash > -1) {
line = line.substring(0, hash);
}
return line.trim();
}
private static IGTFInfo read(Path path, Type type) throws IOException, ParserException
{
IGTFInfo.Builder builder = IGTFInfo.builder(type);
builder.setFilename(path.getFileName().toString());
for (LogicalLine line : readLines(path)) {
try {
List<String> items = Splitter.on('=').limit(2).trimResults().splitToList(line.getValue());
if (items.size() != 2) {
throw new ParserException("missing '='");
}
String key = items.get(0);
String value = items.get(1);
try {
switch (key) {
case "alias":
builder.setAlias(value);
break;
case "version":
builder.setVersion(value);
break;
case "ca_url":
builder.setCAUrl(value);
break;
case "crl_url":
builder.setCRLUrl(value);
break;
case "policy_url":
builder.setPolicyUrl(value);
break;
case "email":
builder.setEmail(value);
break;
case "status":
builder.setStatus(value);
break;
case "url":
builder.setUrl(value);
break;
case "sha1fp.0":
builder.setSHA1FP0(value);
break;
case "subjectdn":
builder.setSubjectDN(value);
break;
case "requires":
builder.setRequires(value);
break;
case "obsoletes":
builder.setObsoletes(value);
break;
default:
throw new ParserException("unknown key");
}
} catch (ParserException e) {
throw new ParserException("Problem with '" + key + "' line: " + e.getMessage());
}
} catch (ParserException e) {
LOG.warn("{}:{} {}", path.getFileName(), line.getLineNumber(), e.getMessage());
}
}
return builder.build();
}
/**
* Represents a logical line: a line that may span multiple (physical)
* lines in the file.
*/
static class LogicalLine
{
private final String value;
private final int lineNumber;
LogicalLine(String value, int lineNumber)
{
this.value = value.trim();
this.lineNumber = lineNumber;
}
public String getValue()
{
return value;
}
/**
* Provide the (physical) line number where this logical line
* started.
*/
public int getLineNumber()
{
return lineNumber;
}
public boolean isEmpty()
{
return value.isEmpty();
}
}
}