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 org.spongepowered.configurate.loader; 018 019import com.google.errorprone.annotations.Immutable; 020import org.checkerframework.checker.nullness.qual.Nullable; 021import org.spongepowered.configurate.util.Strings; 022 023import java.io.BufferedReader; 024import java.io.IOException; 025import java.nio.CharBuffer; 026import java.util.Iterator; 027import java.util.stream.Stream; 028 029/** 030 * Defines a number of default {@link CommentHandler}s. 031 * 032 * @since 4.0.0 033 */ 034@Immutable 035public enum CommentHandlers implements CommentHandler { 036 037 /** 038 * {@link CommentHandler} for comments prefixed by the <code>#</code> character. 039 */ 040 HASH(new AbstractPrefixHandler("#")), 041 042 /** 043 * {@link CommentHandler} for comments prefixed by a <code>//</code> escape. 044 */ 045 DOUBLE_SLASH(new AbstractPrefixHandler("//")), 046 047 /** 048 * {@link CommentHandler} for comments delineated using <code>/* *\</code>. 049 */ 050 SLASH_BLOCK(new AbstractDelineatedHandler("/*", "*/", "*")), 051 052 /** 053 * {@link CommentHandler} for comments delineated using <code><!-- --></code>. 054 */ 055 XML_STYLE(new AbstractDelineatedHandler("<!--", "-->", "~")); 056 057 /** 058 * Limit on the number of characters that may be read by a comment handler 059 * while still preserving the mark. 060 */ 061 private static final int READAHEAD_LEN = 4096; 062 063 private final CommentHandler delegate; 064 065 CommentHandlers(final CommentHandler delegate) { 066 this.delegate = delegate; 067 } 068 069 @Override 070 public @Nullable String extractHeader(final BufferedReader reader) throws IOException { 071 return this.delegate.extractHeader(reader); 072 } 073 074 @Override 075 public Stream<String> toComment(final Stream<String> lines) { 076 return this.delegate.toComment(lines); 077 } 078 079 /** 080 * Uses provided comment handlers to extract a comment from the reader. 081 * 082 * @param reader to extract a comment from 083 * @param allowedHeaderTypes handlers to try 084 * @return extracted comment, or null if a comment could not be extracted 085 * @throws IOException if an IO error occurs 086 * @since 4.0.0 087 */ 088 public static @Nullable String extractComment(final BufferedReader reader, 089 final Iterable<CommentHandler> allowedHeaderTypes) throws IOException { 090 reader.mark(READAHEAD_LEN); 091 for (CommentHandler handler : allowedHeaderTypes) { 092 final @Nullable String comment = handler.extractHeader(reader); 093 if (comment == null) { 094 reader.reset(); 095 } else { 096 return comment; 097 } 098 } 099 return null; 100 } 101 102 @Immutable 103 private static final class AbstractDelineatedHandler implements CommentHandler { 104 private final String startSequence; 105 private final String endSequence; 106 private final String lineIndentSequence; 107 108 private AbstractDelineatedHandler(final String startSequence, final String endSequence, final String lineIndentSequence) { 109 this.startSequence = startSequence; 110 this.endSequence = endSequence; 111 this.lineIndentSequence = lineIndentSequence; 112 } 113 114 @Override 115 public @Nullable String extractHeader(final BufferedReader reader) throws IOException { 116 if (!beginsWithPrefix(this.startSequence, reader)) { 117 return null; 118 } 119 120 final StringBuilder build = new StringBuilder(); 121 String line = reader.readLine(); 122 if (line == null) { 123 return null; 124 } 125 if (handleSingleLine(build, line)) { 126 for (line = reader.readLine(); line != null; line = reader.readLine()) { 127 if (!handleSingleLine(build, line)) { 128 break; 129 } 130 } 131 } 132 line = reader.readLine(); 133 if (!(line == null || line.trim().isEmpty())) { // Require a blank line after a comment to make it a header 134 return null; 135 } 136 137 if (build.length() > 0) { 138 return build.toString(); 139 } else { 140 return null; 141 } 142 } 143 144 private boolean handleSingleLine(final StringBuilder builder, String line) { 145 boolean moreLines = true; 146 if (line.trim().endsWith(this.endSequence)) { 147 line = line.substring(0, line.lastIndexOf(this.endSequence)); 148 if (line.endsWith(" ")) { 149 line = line.substring(0, line.length() - 1); 150 } 151 152 moreLines = false; 153 if (line.isEmpty()) { 154 return false; 155 } 156 } 157 if (line.trim().startsWith(this.lineIndentSequence)) { 158 line = line.substring(line.indexOf(this.lineIndentSequence) + 1); 159 } 160 161 if (line.length() > 0 && line.charAt(0) == ' ') { 162 line = line.substring(1); 163 } 164 165 if (builder.length() > 0) { 166 builder.append(AbstractConfigurationLoader.CONFIGURATE_LINE_SEPARATOR); 167 } 168 builder.append(line.replace("\r", "").replace("\n", "").replace("\r\n", "")); 169 return moreLines; 170 } 171 172 @Override 173 public Stream<String> toComment(final Stream<String> lines) { 174 final Stream.Builder<String> build = Stream.builder(); 175 boolean first = true; 176 for (Iterator<String> it = lines.iterator(); it.hasNext();) { 177 final String next = it.next(); 178 if (first) { 179 if (!it.hasNext()) { 180 build.add(this.startSequence + " " + next + " " + this.endSequence); 181 return build.build(); 182 } else { 183 build.add(this.startSequence); 184 } 185 first = false; 186 } 187 build.add(" " + this.lineIndentSequence + " " + next); 188 } 189 build.add(" " + this.endSequence); 190 return build.build(); 191 } 192 } 193 194 @Immutable 195 private static final class AbstractPrefixHandler implements CommentHandler { 196 private final String commentPrefix; 197 198 AbstractPrefixHandler(final String commentPrefix) { 199 this.commentPrefix = commentPrefix; 200 } 201 202 @Override 203 public @Nullable String extractHeader(final BufferedReader reader) throws IOException { 204 if (!beginsWithPrefix(this.commentPrefix, reader)) { 205 return null; 206 } 207 boolean firstLine = true; 208 209 final StringBuilder build = new StringBuilder(); 210 for (String line = reader.readLine(); line != null; line = reader.readLine()) { 211 if (firstLine) { 212 if (line.length() > 0 && line.charAt(0) == ' ') { 213 line = line.substring(1); 214 } 215 build.append(line); 216 firstLine = false; 217 } else if (line.trim().startsWith(this.commentPrefix)) { 218 line = line.substring(line.indexOf(this.commentPrefix) + 1); 219 if (line.length() > 0 && line.charAt(0) == ' ') { 220 line = line.substring(1); 221 } 222 if (build.length() > 0) { 223 build.append(AbstractConfigurationLoader.CONFIGURATE_LINE_SEPARATOR); 224 } 225 build.append(line); 226 } else if (Strings.isBlank(line)) { 227 break; 228 } else { 229 return null; 230 } 231 } 232 // We've reached the end of the document? 233 return build.length() > 0 ? build.toString() : null; 234 } 235 236 @Override 237 public Stream<String> toComment(final Stream<String> lines) { 238 return lines 239 .map(s -> { 240 if (s.length() > 0 && s.charAt(0) == ' ') { 241 return this.commentPrefix + s; 242 } else { 243 return this.commentPrefix + " " + s; 244 } 245 }); 246 } 247 } 248 249 /** 250 * Consumes the length of the comment prefix from the reader and returns 251 * whether or not the contents from the reader matches the expected prefix. 252 */ 253 static boolean beginsWithPrefix(final String commentPrefix, final BufferedReader reader) throws IOException { 254 final CharBuffer buf = CharBuffer.allocate(commentPrefix.length()); 255 if (reader.read(buf) != buf.limit()) { 256 return false; 257 } 258 buf.flip(); 259 return commentPrefix.contentEquals(buf); 260 } 261 262}