001/* 002 * Configurate 003 * Copyright (C) zml and Configurate contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ninja.leaping.configurate.loader; 018 019import com.google.common.collect.Collections2; 020import org.checkerframework.checker.nullness.qual.NonNull; 021import org.checkerframework.checker.nullness.qual.Nullable; 022 023import java.io.BufferedReader; 024import java.io.IOException; 025import java.nio.CharBuffer; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Optional; 029import java.util.stream.Collectors; 030 031/** 032 * Defines a number of default {@link CommentHandler}s. 033 */ 034public enum CommentHandlers implements CommentHandler { 035 036 /** 037 * {@link CommentHandler} for comments prefixed by the <code>#</code> character. 038 */ 039 HASH(new AbstractPrefixHandler("#")), 040 041 /** 042 * {@link CommentHandler} for comments prefixed by a <code>//</code> escape. 043 */ 044 DOUBLE_SLASH(new AbstractPrefixHandler("//")), 045 046 /** 047 * {@link CommentHandler} for comments delineated using <code>/* *\</code>. 048 */ 049 SLASH_BLOCK(new AbstractDelineatedHandler("/*", "*/", "*")), 050 051 /** 052 * {@link CommentHandler} for comments delineated using <code><!-- --></code>. 053 */ 054 XML_STYLE(new AbstractDelineatedHandler("<!--", "-->", "~")); 055 056 /** 057 * Limit on the number of characters that may be read by a comment handler while still preserving the mark. 058 */ 059 private static final int READAHEAD_LEN = 4096; 060 061 private final CommentHandler delegate; 062 063 CommentHandlers(CommentHandler delegate) { 064 this.delegate = delegate; 065 } 066 067 @NonNull 068 @Override 069 public Optional<String> extractHeader(@NonNull BufferedReader reader) throws IOException { 070 return delegate.extractHeader(reader); 071 } 072 073 @NonNull 074 @Override 075 public Collection<String> toComment(@NonNull Collection<String> lines) { 076 return delegate.toComment(lines); 077 } 078 079 /** 080 * Uses provided comment handlers to extract a comment from the reader. 081 * 082 * @param reader The reader 083 * @param allowedHeaderTypes The handlers to try 084 * @return The extracted comment, or null if a comment could not be extracted 085 * @throws IOException If an IO error occurs 086 */ 087 @Nullable 088 public static String extractComment(@NonNull BufferedReader reader, @NonNull CommentHandler... allowedHeaderTypes) throws IOException { 089 reader.mark(READAHEAD_LEN); 090 for (CommentHandler handler : allowedHeaderTypes) { 091 Optional<String> comment = handler.extractHeader(reader); 092 if (!comment.isPresent()) { 093 reader.reset(); 094 } else { 095 return comment.get(); 096 } 097 } 098 return null; 099 } 100 101 private static final class AbstractDelineatedHandler implements CommentHandler { 102 private final String startSequence; 103 private final String endSequence; 104 private final String lineIndentSequence; 105 106 private AbstractDelineatedHandler(String startSequence, String endSequence, String lineIndentSequence) { 107 this.startSequence = startSequence; 108 this.endSequence = endSequence; 109 this.lineIndentSequence = lineIndentSequence; 110 } 111 112 @NonNull 113 @Override 114 public Optional<String> extractHeader(@NonNull BufferedReader reader) throws IOException { 115 if (!beginsWithPrefix(this.startSequence, reader)) { 116 return Optional.empty(); 117 } 118 119 final StringBuilder build = new StringBuilder(); 120 String line = reader.readLine(); 121 if (line == null) { 122 return Optional.empty(); 123 } 124 if (handleSingleLine(build, line)) { 125 for (line = reader.readLine(); line != null; line = reader.readLine()) { 126 if (!handleSingleLine(build, line)) { 127 break; 128 } 129 } 130 } 131 line = reader.readLine(); 132 if (!(line == null || line.trim().isEmpty())) { // Require a blank line after a comment to make it a header 133 return Optional.empty(); 134 } 135 136 if (build.length() > 0) { 137 return Optional.of(build.toString()); 138 } else { 139 return Optional.empty(); 140 } 141 } 142 143 private boolean handleSingleLine(StringBuilder builder, String line) { 144 boolean moreLines = true; 145 if (line.trim().endsWith(endSequence)) { 146 line = line.substring(0, line.lastIndexOf(endSequence)); 147 if (line.endsWith(" ")) { 148 line = line.substring(0, line.length() - 1); 149 } 150 151 moreLines = false; 152 if (line.isEmpty()) { 153 return false; 154 } 155 } 156 if (line.trim().startsWith(lineIndentSequence)) { 157 line = line.substring(line.indexOf(lineIndentSequence) + 1); 158 } 159 160 if (line.startsWith(" ")) { 161 line = line.substring(1); 162 } 163 164 if (builder.length() > 0) { 165 builder.append(AbstractConfigurationLoader.CONFIGURATE_LINE_SEPARATOR); 166 } 167 builder.append(line.replace("\r", "").replace("\n", "").replace("\r\n", "")); 168 return moreLines; 169 } 170 171 @NonNull 172 @Override 173 public Collection<String> toComment(@NonNull Collection<String> lines) { 174 if (lines.size() == 1) { 175 return lines.stream().map(i -> startSequence + " " + i + " " + endSequence).collect(Collectors.toList()); 176 } else { 177 Collection<String> ret = new ArrayList<>(); 178 ret.add(startSequence); 179 ret.addAll(lines.stream().map(i -> " " + lineIndentSequence + " " + i).collect(Collectors.toList())); 180 ret.add(" " + endSequence); 181 return ret; 182 } 183 } 184 } 185 186 private static final class AbstractPrefixHandler implements CommentHandler { 187 private final String commentPrefix; 188 189 AbstractPrefixHandler(String commentPrefix) { 190 this.commentPrefix = commentPrefix; 191 } 192 193 @NonNull 194 @Override 195 public Optional<String> extractHeader(@NonNull BufferedReader reader) throws IOException { 196 if (!beginsWithPrefix(commentPrefix, reader)) { 197 return Optional.empty(); 198 } 199 boolean firstLine = true; 200 201 StringBuilder build = new StringBuilder(); 202 for (String line = reader.readLine(); line != null; line = reader.readLine()) { 203 if (firstLine || line.trim().startsWith(commentPrefix)) { 204 line = firstLine ? line : line.substring(line.indexOf(commentPrefix) + 1); 205 firstLine = false; 206 if (line.startsWith(" ")) { 207 line = line.substring(1); 208 } 209 if (build.length() > 0) { 210 build.append(AbstractConfigurationLoader.CONFIGURATE_LINE_SEPARATOR); 211 } 212 build.append(line); 213 } else { 214 if (line.trim().isEmpty()) { 215 break; 216 } else { 217 return Optional.empty(); 218 } 219 } 220 } 221 // We've reached the end of the document? 222 return build.length() > 0 ? Optional.of(build.toString()) : Optional.empty(); 223 } 224 225 @NonNull 226 @Override 227 public Collection<String> toComment(@NonNull Collection<String> lines) { 228 return Collections2.transform(lines, s -> { 229 if (s.startsWith(" ")) { 230 return commentPrefix + s; 231 } else { 232 return commentPrefix + " " + s; 233 } 234 }); 235 } 236 } 237 238 /** 239 * Consumes the length of the comment prefix from the reader and returns whether or not the contents from the reader 240 * matches the expected prefix. 241 */ 242 static boolean beginsWithPrefix(String commentPrefix, BufferedReader reader) throws IOException { 243 final CharBuffer buf = CharBuffer.allocate(commentPrefix.length()); 244 if (reader.read(buf) != buf.limit()) { 245 return false; 246 } 247 buf.flip(); 248 return commentPrefix.contentEquals(buf); 249 } 250 251}