001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.configuration.file; 020 021import com.plotsquared.core.configuration.Configuration; 022import com.plotsquared.core.configuration.ConfigurationSection; 023import com.plotsquared.core.configuration.InvalidConfigurationException; 024import org.apache.logging.log4j.LogManager; 025import org.apache.logging.log4j.Logger; 026import org.yaml.snakeyaml.DumperOptions; 027import org.yaml.snakeyaml.Yaml; 028import org.yaml.snakeyaml.error.YAMLException; 029import org.yaml.snakeyaml.representer.Representer; 030 031import java.io.File; 032import java.io.IOException; 033import java.nio.file.Files; 034import java.nio.file.StandardCopyOption; 035import java.util.Map; 036 037/** 038 * An implementation of {@link Configuration} which saves all files in Yaml. 039 * Note that this implementation is not synchronized. 040 */ 041public class YamlConfiguration extends FileConfiguration { 042 043 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + YamlConfiguration.class.getSimpleName()); 044 045 private static final String COMMENT_PREFIX = "# "; 046 private static final String BLANK_CONFIG = "{}\n"; 047 private final DumperOptions yamlOptions = new DumperOptions(); 048 private final Representer yamlRepresenter = new YamlRepresenter(); 049 private final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions); 050 051 /** 052 * Creates a new {@link YamlConfiguration}, loading from the given file. 053 * 054 * <p>Any errors loading the Configuration will be logged and then ignored. 055 * If the specified input is not a valid config, a blank config will be 056 * returned. 057 * 058 * <p>The encoding used may follow the system dependent default. 059 * 060 * @param file Input file 061 * @return Resulting configuration 062 */ 063 public static YamlConfiguration loadConfiguration(File file) { 064 YamlConfiguration config = new YamlConfiguration(); 065 066 try { 067 config.load(file); 068 } catch (InvalidConfigurationException | IOException ex) { 069 try { 070 File dest = new File(file.getAbsolutePath() + "_broken"); 071 int i = 0; 072 while (dest.exists()) { 073 dest = new File(file.getAbsolutePath() + "_broken_" + i++); 074 } 075 Files.copy(file.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); 076 LOGGER.error("Could not read: {}", file); 077 LOGGER.error("Renamed to: {}", file); 078 LOGGER.error("============ Full stacktrace ============"); 079 ex.printStackTrace(); 080 LOGGER.error("========================================="); 081 } catch (IOException e) { 082 e.printStackTrace(); 083 } 084 } 085 086 return config; 087 } 088 089 @Override 090 public String saveToString() { 091 yamlOptions.setIndent(options().indent()); 092 yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 093 yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 094 095 String header = buildHeader(); 096 String dump = yaml.dump(getValues(false)); 097 098 if (dump.equals(BLANK_CONFIG)) { 099 dump = ""; 100 } 101 102 return header + dump; 103 } 104 105 @Override 106 public void loadFromString(String contents) throws InvalidConfigurationException { 107 108 Map<?, ?> input; 109 try { 110 input = yaml.load(contents); 111 } catch (YAMLException e) { 112 throw new InvalidConfigurationException(e); 113 } catch (ClassCastException ignored) { 114 throw new InvalidConfigurationException("Top level is not a Map."); 115 } 116 117 String header = parseHeader(contents); 118 if (!header.isEmpty()) { 119 options().header(header); 120 } 121 122 if (input != null) { 123 convertMapsToSections(input, this); 124 } 125 } 126 127 protected void convertMapsToSections(Map<?, ?> input, ConfigurationSection section) { 128 for (Map.Entry<?, ?> entry : input.entrySet()) { 129 String key = entry.getKey().toString(); 130 Object value = entry.getValue(); 131 132 if (value instanceof Map) { 133 convertMapsToSections((Map<?, ?>) value, section.createSection(key)); 134 } else { 135 section.set(key, value); 136 } 137 } 138 } 139 140 protected String parseHeader(String input) { 141 String[] lines = input.split("\r?\n", -1); 142 StringBuilder result = new StringBuilder(); 143 boolean readingHeader = true; 144 boolean foundHeader = false; 145 146 for (int i = 0; (i < lines.length) && readingHeader; i++) { 147 String line = lines[i]; 148 149 if (line.startsWith(COMMENT_PREFIX)) { 150 if (i > 0) { 151 result.append('\n'); 152 } 153 154 if (line.length() > COMMENT_PREFIX.length()) { 155 result.append(line.substring(COMMENT_PREFIX.length())); 156 } 157 158 foundHeader = true; 159 } else if (foundHeader && line.isEmpty()) { 160 result.append('\n'); 161 } else if (foundHeader) { 162 readingHeader = false; 163 } 164 } 165 166 return result.toString(); 167 } 168 169 @Override 170 protected String buildHeader() { 171 String header = options().header(); 172 173 if (options().copyHeader()) { 174 Configuration def = getDefaults(); 175 176 if (def instanceof FileConfiguration fileDefaults) { 177 String defaultsHeader = fileDefaults.buildHeader(); 178 179 if ((defaultsHeader != null) && !defaultsHeader.isEmpty()) { 180 return defaultsHeader; 181 } 182 } 183 } 184 185 if (header == null) { 186 return ""; 187 } 188 189 StringBuilder builder = new StringBuilder(); 190 String[] lines = header.split("\r?\n", -1); 191 boolean startedHeader = false; 192 193 for (int i = lines.length - 1; i >= 0; i--) { 194 builder.insert(0, '\n'); 195 196 if (startedHeader || !lines[i].isEmpty()) { 197 builder.insert(0, lines[i]); 198 builder.insert(0, COMMENT_PREFIX); 199 startedHeader = true; 200 } 201 } 202 203 return builder.toString(); 204 } 205 206 @Override 207 public YamlConfigurationOptions options() { 208 if (options == null) { 209 options = new YamlConfigurationOptions(this); 210 } 211 212 return (YamlConfigurationOptions) options; 213 } 214 215}