FlatFileDataProcessor.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. package com.gmail.nossr50.database;
  2. import com.gmail.nossr50.database.flatfile.FlatFileDataBuilder;
  3. import com.gmail.nossr50.database.flatfile.FlatFileDataContainer;
  4. import com.gmail.nossr50.database.flatfile.FlatFileDataUtil;
  5. import org.jetbrains.annotations.NotNull;
  6. import org.jetbrains.annotations.Nullable;
  7. import java.util.*;
  8. import java.util.logging.Logger;
  9. import static com.gmail.nossr50.database.FlatFileDatabaseManager.*;
  10. public class FlatFileDataProcessor {
  11. private final @NotNull List<FlatFileDataContainer> flatFileDataContainers;
  12. private final @NotNull List<FlatFileDataFlag> flatFileDataFlags;
  13. private final @NotNull Logger logger;
  14. private final HashSet<String> names;
  15. private final HashSet<UUID> uuids;
  16. private int uniqueProcessingID; //TODO: Not being used, should we use it?
  17. boolean corruptDataFound;
  18. public FlatFileDataProcessor(@NotNull Logger logger) {
  19. this.logger = logger;
  20. flatFileDataContainers = new ArrayList<>();
  21. flatFileDataFlags = new ArrayList<>();
  22. names = new HashSet<>();
  23. uuids = new HashSet<>();
  24. uniqueProcessingID = 0;
  25. }
  26. public void processData(@NotNull String lineData) {
  27. assert !lineData.isEmpty();
  28. //Make sure the data line is "correct"
  29. if(lineData.charAt(lineData.length() - 1) != ':') {
  30. // Length checks depend on last rawSplitData being ':'
  31. // We add it here if it is missing
  32. lineData = lineData.concat(":");
  33. }
  34. //Split the data into an array
  35. String[] splitDataLine = lineData.split(":");
  36. FlatFileDataBuilder builder = new FlatFileDataBuilder(splitDataLine, uniqueProcessingID);
  37. uniqueProcessingID++;
  38. boolean[] badDataValues = new boolean[DATA_ENTRY_COUNT];
  39. boolean anyBadData = false;
  40. //This is the minimum size of the split array needed to be considered proper data
  41. if(splitDataLine.length < getMinimumSplitDataLength()) {
  42. //Data is considered junk
  43. if(!corruptDataFound) {
  44. logger.severe("Some corrupt data was found in mcmmo.users and has been repaired, it is possible that some player data has been lost in this process.");
  45. corruptDataFound = true;
  46. }
  47. //Flag as junk (corrupt)
  48. builder.appendFlag(FlatFileDataFlag.CORRUPTED_OR_UNRECOGNIZABLE);
  49. //TODO: This block here is probably pointless
  50. if(splitDataLine.length >= 10 //The value here is kind of arbitrary, it shouldn't be too low to avoid false positives, but also we aren't really going to correctly identify when player data has been corrupted or not with 100% accuracy ever
  51. && splitDataLine[0] != null && !splitDataLine[0].isEmpty()) {
  52. if(splitDataLine[0].length() <= 16 && splitDataLine[0].length() >= 3) {
  53. logger.severe("Not enough data found to recover corrupted player data for user: "+splitDataLine[0]);
  54. registerData(builder.appendFlag(FlatFileDataFlag.TOO_INCOMPLETE));
  55. return;
  56. }
  57. }
  58. registerData(builder.appendFlag(FlatFileDataFlag.CORRUPTED_OR_UNRECOGNIZABLE));
  59. return;
  60. }
  61. /*
  62. * Check for duplicate names
  63. */
  64. boolean invalidUUID = false;
  65. String name = splitDataLine[USERNAME_INDEX];
  66. String strOfUUID = splitDataLine[UUID_INDEX];
  67. if(name.isEmpty()) {
  68. reportBadDataLine("No name found for data", "[MISSING NAME]", lineData);
  69. builder.appendFlag(FlatFileDataFlag.MISSING_NAME);
  70. anyBadData = true;
  71. badDataValues[USERNAME_INDEX] = true;
  72. }
  73. if(strOfUUID.isEmpty() || strOfUUID.equalsIgnoreCase("NULL")) {
  74. invalidUUID = true;
  75. badDataValues[UUID_INDEX] = true;
  76. reportBadDataLine("Empty/null UUID for user", "Empty/null", lineData);
  77. builder.appendFlag(FlatFileDataFlag.BAD_UUID_DATA);
  78. anyBadData = true;
  79. }
  80. UUID uuid = null;
  81. try {
  82. uuid = UUID.fromString(strOfUUID);
  83. } catch (IllegalArgumentException e) {
  84. //UUID does not conform
  85. invalidUUID = true;
  86. badDataValues[UUID_INDEX] = true;
  87. reportBadDataLine("Invalid UUID data found for user", strOfUUID, lineData);
  88. builder.appendFlag(FlatFileDataFlag.BAD_UUID_DATA);
  89. }
  90. //Duplicate UUID is no good, reject them
  91. if(!invalidUUID && uuid != null && uuids.contains(uuid)) {
  92. registerData(builder.appendFlag(FlatFileDataFlag.DUPLICATE_UUID));
  93. return;
  94. }
  95. uuids.add(uuid);
  96. if(names.contains(name)) {
  97. builder.appendFlag(FlatFileDataFlag.DUPLICATE_NAME);
  98. anyBadData = true;
  99. badDataValues[USERNAME_INDEX] = true;
  100. }
  101. if(!name.isEmpty())
  102. names.add(name);
  103. //Make sure the data is up to date schema wise, if it isn't we adjust it to the correct size and flag it for repair
  104. splitDataLine = isDataSchemaUpToDate(splitDataLine, builder, badDataValues);
  105. /*
  106. * After establishing this data has at least an identity we check for bad data
  107. * Bad Value checks
  108. */
  109. //Check each data for bad values
  110. for(int i = 0; i < DATA_ENTRY_COUNT; i++) {
  111. if(shouldNotBeEmpty(splitDataLine[i], i)) {
  112. if(i == OVERHAUL_LAST_LOGIN) {
  113. builder.appendFlag(FlatFileDataFlag.LAST_LOGIN_SCHEMA_UPGRADE);
  114. }
  115. badDataValues[i] = true;
  116. anyBadData = true;
  117. continue;
  118. }
  119. boolean isCorrectType = isOfExpectedType(splitDataLine[i], getExpectedValueType(i));
  120. if(!isCorrectType) {
  121. anyBadData = true;
  122. badDataValues[i] = true;
  123. }
  124. }
  125. if(anyBadData) {
  126. builder.appendFlag(FlatFileDataFlag.BAD_VALUES);
  127. builder.appendBadDataValues(badDataValues);
  128. }
  129. registerData(builder);
  130. }
  131. public @NotNull String[] isDataSchemaUpToDate(@NotNull String[] splitDataLine, @NotNull FlatFileDataBuilder builder, boolean[] badDataValues) {
  132. assert splitDataLine.length <= DATA_ENTRY_COUNT; //should NEVER be higher
  133. if(splitDataLine.length < DATA_ENTRY_COUNT) {
  134. int oldLength = splitDataLine.length;
  135. splitDataLine = Arrays.copyOf(splitDataLine, DATA_ENTRY_COUNT);
  136. int newLength = splitDataLine.length;
  137. //TODO: Test this
  138. for(int i = oldLength; i < (newLength - 1); i++){
  139. badDataValues[i] = true;
  140. }
  141. builder.appendFlag(FlatFileDataFlag.INCOMPLETE);
  142. builder.setSplitStringData(splitDataLine);
  143. }
  144. return splitDataLine;
  145. }
  146. public boolean shouldNotBeEmpty(@Nullable String data, int index) {
  147. if(getExpectedValueType(index) == ExpectedType.IGNORED) {
  148. return false;
  149. } else {
  150. return data == null || data.isEmpty();
  151. }
  152. }
  153. public boolean isOfExpectedType(@NotNull String data, @NotNull ExpectedType expectedType) {
  154. switch(expectedType) {
  155. case STRING:
  156. return true;
  157. case INTEGER:
  158. try {
  159. Integer.valueOf(data);
  160. return true;
  161. } catch (Exception e) {
  162. return false;
  163. }
  164. case BOOLEAN:
  165. return data.equalsIgnoreCase("true") || data.equalsIgnoreCase("false");
  166. case FLOAT:
  167. try {
  168. Float.valueOf(data);
  169. return true;
  170. } catch (NumberFormatException e) {
  171. return false;
  172. }
  173. case DOUBLE:
  174. try {
  175. Double.valueOf(data);
  176. return true;
  177. } catch (NumberFormatException e) {
  178. return false;
  179. }
  180. case UUID:
  181. try {
  182. UUID.fromString(data);
  183. return true;
  184. } catch (IllegalArgumentException e) {
  185. return false;
  186. }
  187. case OUT_OF_RANGE:
  188. throw new ArrayIndexOutOfBoundsException("Value matched type OUT_OF_RANGE, this should never happen.");
  189. case IGNORED:
  190. default:
  191. return true;
  192. }
  193. }
  194. private void reportBadDataLine(String warning, String context, String dataLine) {
  195. logger.warning("FlatFileDatabaseBuilder Warning: " + warning + " - " + context);
  196. logger.warning("FlatFileDatabaseBuilder: (Line Data) - " + dataLine);
  197. logger.warning("mcMMO will repair this data if automatically (if it is possible).");
  198. }
  199. private int getMinimumSplitDataLength() {
  200. return UUID_INDEX + 1;
  201. }
  202. private void registerData(@NotNull FlatFileDataBuilder builder) {
  203. FlatFileDataContainer flatFileDataContainer = builder.build();
  204. flatFileDataContainers.add(flatFileDataContainer);
  205. if(flatFileDataContainer.getDataFlags() != null)
  206. flatFileDataFlags.addAll(flatFileDataContainer.getDataFlags());
  207. }
  208. public static @NotNull ExpectedType getExpectedValueType(int dataIndex) throws IndexOutOfBoundsException {
  209. return switch (dataIndex) {
  210. case USERNAME_INDEX -> ExpectedType.STRING; //Assumption: Used to be for something, no longer used
  211. //Assumption: Used to be for something, no longer used
  212. //Assumption: Used to be used for something, no longer used
  213. //Assumption: Used to be used for something, no longer used
  214. case 2, 3, 23, 33, HEALTHBAR, LEGACY_LAST_LOGIN -> ExpectedType.IGNORED;
  215. case SKILLS_MINING, SKILLS_REPAIR, SKILLS_UNARMED, SKILLS_HERBALISM, SKILLS_EXCAVATION, SKILLS_ARCHERY,
  216. SKILLS_SWORDS, SKILLS_AXES, SKILLS_WOODCUTTING, SKILLS_ACROBATICS, SKILLS_TAMING, SKILLS_FISHING,
  217. SKILLS_ALCHEMY, SKILLS_CROSSBOWS, SKILLS_TRIDENTS, SKILLS_MACES, COOLDOWN_BERSERK,
  218. COOLDOWN_GIGA_DRILL_BREAKER, COOLDOWN_TREE_FELLER, COOLDOWN_GREEN_TERRA, COOLDOWN_SERRATED_STRIKES,
  219. COOLDOWN_SKULL_SPLITTER, COOLDOWN_SUPER_BREAKER, COOLDOWN_BLAST_MINING, SCOREBOARD_TIPS,
  220. COOLDOWN_CHIMAERA_WING, COOLDOWN_SUPER_SHOTGUN, COOLDOWN_TRIDENTS, COOLDOWN_ARCHERY, COOLDOWN_MACES ->
  221. ExpectedType.INTEGER;
  222. case EXP_MINING, EXP_WOODCUTTING, EXP_REPAIR, EXP_UNARMED, EXP_HERBALISM, EXP_EXCAVATION, EXP_ARCHERY,
  223. EXP_SWORDS, EXP_AXES, EXP_ACROBATICS, EXP_TAMING, EXP_FISHING, EXP_ALCHEMY, EXP_CROSSBOWS,
  224. EXP_TRIDENTS, EXP_MACES -> ExpectedType.FLOAT;
  225. case UUID_INDEX -> ExpectedType.UUID;
  226. case OVERHAUL_LAST_LOGIN -> ExpectedType.LONG;
  227. default -> throw new IndexOutOfBoundsException();
  228. };
  229. }
  230. public @NotNull List<FlatFileDataContainer> getFlatFileDataContainers() {
  231. return flatFileDataContainers;
  232. }
  233. public @NotNull List<FlatFileDataFlag> getFlatFileDataFlags() {
  234. return flatFileDataFlags;
  235. }
  236. public int getDataFlagCount() {
  237. return flatFileDataFlags.size();
  238. }
  239. public @NotNull StringBuilder processDataForSave() {
  240. StringBuilder stringBuilder = new StringBuilder();
  241. //Fix our data if needed and prepare it to be saved
  242. for(FlatFileDataContainer dataContainer : flatFileDataContainers) {
  243. String[] splitData = FlatFileDataUtil.getPreparedSaveDataLine(dataContainer);
  244. if(splitData == null)
  245. continue;
  246. //We add a trailing : as it is needed for some reason (is it?)
  247. //TODO: Is the trailing ":" actually necessary?
  248. String fromSplit = org.apache.commons.lang.StringUtils.join(splitData, ":") + ":";
  249. stringBuilder.append(fromSplit).append("\r\n");
  250. }
  251. return stringBuilder;
  252. }
  253. }