Skip to content

Commit

Permalink
Added sqm file upload to generate slotlist
Browse files Browse the repository at this point in the history
Improved RegEx compilation in StringUtils
Renamed FileWebController
  • Loading branch information
Alf-Melmac committed Jun 6, 2021
1 parent 49d72c4 commit 58003cd
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 19 deletions.
24 changes: 24 additions & 0 deletions src/main/java/de/webalf/slotbot/controller/FileController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.webalf.slotbot.controller;

import de.webalf.slotbot.model.Squad;
import de.webalf.slotbot.util.SqmParser;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
* @author Alf
* @since 06.06.2021
*/
@RestController
@RequestMapping("/files")
public class FileController {
@PostMapping("/uploadSqm")
public List<Squad> postSqmFile(@RequestParam(name = "file") MultipartFile file) {
return SqmParser.createSlotListFromFile(file);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.webalf.slotbot.assembler.website.EventDetailsAssembler;
import de.webalf.slotbot.controller.EventController;
import de.webalf.slotbot.controller.FileController;
import de.webalf.slotbot.model.Event;
import de.webalf.slotbot.model.dtos.website.EventDetailsDto;
import de.webalf.slotbot.service.EventService;
Expand Down Expand Up @@ -67,6 +68,7 @@ public ModelAndView getWizardHtml(@RequestParam(required = false) String date) {
mav.addObject("date", date);
mav.addObject("eventTypes", eventTypeService.findAll());
mav.addObject("eventFieldDefaultsUrl", linkTo(methodOn(EventController.class).getEventFieldDefaults(null)).toUri().toString());
mav.addObject("uploadSqmFileUrl", linkTo(methodOn(FileController.class).postSqmFile(null)).toUri().toString());
mav.addObject("postEventUrl", linkTo(methodOn(EventController.class).postEvent(null)).toUri().toString());
mav.addObject("eventDetailsUrl", linkTo(methodOn(EventWebController.class)
.getEventDetailsHtml(Long.MIN_VALUE))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
@Controller
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class FileController {
public class FileWebController {
private final FileService fileService;

@GetMapping("/download/{filename:.+}")
Expand Down
195 changes: 195 additions & 0 deletions src/main/java/de/webalf/slotbot/util/SqmParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package de.webalf.slotbot.util;

import de.webalf.slotbot.exception.BusinessRuntimeException;
import de.webalf.slotbot.model.Slot;
import de.webalf.slotbot.model.Squad;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static de.webalf.slotbot.util.SqmParser.ReadStep.NONE;
import static de.webalf.slotbot.util.StringUtils.removeNonDigitCharacters;

/**
* @author Alf
* @see <a href="https://community.bistudio.com/wiki/Mission.sqm">Mission.sqm</a>
* @since 05.06.2021
*/
@UtilityClass
@Slf4j
public final class SqmParser {

public static List<Squad> createSlotListFromFile(MultipartFile file) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
final List<Squad> read = read(reader);
for (Squad squad : read) {
squad.setSlotList(squad.getSlotList().stream().sorted(Comparator.comparingInt(Slot::getNumber)).collect(Collectors.toUnmodifiableList()));
}
return read;
} catch (IOException e) {
log.error("Failed to get, read or buffer file {}", file.getName(), e);
throw BusinessRuntimeException.builder()
.title("Die Datei " + file.getName() + " konnte nicht gelesen werden.")
.description(e.getMessage())
.cause(e)
.build();
}
}

private static final String DATA_TYPE_GROUP = "dataType=\"Group\";";
private static final String CURLY_BRACE_OPEN = "{";
private static final String CURLY_BRACE_CLOSE = "}";
private static final String DESCRIPTION = "description=\"";
private static final String QUOTE_MARK = "\"";

private static List<Squad> read(BufferedReader reader) throws IOException {
int lineNumber = 0;

ReadStep step = NONE;
int braces = 0;
Squad nextSquad = null;

List<Squad> slotList = new ArrayList<>();

while (true) {
final String line = reader.readLine();
if (line == null) break; //EOF
lineNumber++;

switch (step) {
case NONE: //Loop until dataType=Group is found
if (line.contains(DATA_TYPE_GROUP)) {
log.trace("Group definition start in line {}", lineNumber);
step = step.next();
}
break;
case GROUP_SEARCH: //Await opening curly brace for group definition class
if (line.contains(CURLY_BRACE_OPEN)) {
log.trace("Group found beginning in line {}", lineNumber);
step = step.next();
}
break;
case GROUP_FOUND:
braces = calculateBraces(braces, line);
if (line.contains(DESCRIPTION)) { //First slot description contains squad/group name
final String descriptionText = getDescriptionText(line);

if (!descriptionText.contains("@")) {
throw BusinessRuntimeException.builder().title("In Zeile " + lineNumber + " fehlt die erwartete Gruppendefinition.").build();
}

final String[] squadNameSplit = descriptionText.split("@", 2);
nextSquad = Squad.builder().name(squadNameSplit[1]).slotList(new ArrayList<>()).build();
log.trace("Created new Squad '{}'", nextSquad.getName());
readSlot(squadNameSplit[0], nextSquad);
step = step.next();
}
break;
case GROUP_FILL:
braces = calculateBraces(braces, line);
if (braces == -1) {
log.trace("Stopped filling squad {} in line {}", nextSquad.getName(), lineNumber);
braces = 0;
step = step.next();
}
if (line.contains(DESCRIPTION)) {
assert nextSquad != null; //This step is only reached, if the squad has been initialized
readSlot(getDescriptionText(line), nextSquad);
}
break;
case GROUP_END:
braces = calculateBraces(braces, line);
if (braces == -1) {
slotList.add(nextSquad);
log.trace("Group definition end in line {}", lineNumber);

//Reset all values
braces = 0;
nextSquad = null;
step = step.next();
}
break;
}
}

return slotList;
}

/**
* Adds one if open curly brace was found, subtracts one if a closing curly brace was found
*
* @param braces current brace count
* @param line to read
* @return new brace count
*/
private static int calculateBraces(int braces, @NonNull String line) {
if (line.contains(CURLY_BRACE_OPEN)) {
braces++;
}
if (line.contains(CURLY_BRACE_CLOSE)) {
braces--;
}
return braces;
}

/**
* @param line complete line
* @return text inside outer quotation marks
*/
private static String getDescriptionText(String line) {
return line.substring(line.indexOf(QUOTE_MARK) + 1, line.lastIndexOf(QUOTE_MARK));
}

private static final Pattern DIGIT = Pattern.compile("^#?\\d+");
private static final Pattern END_OF_STRING_AND_LINE = Pattern.compile("\";$");

/**
* Parses the {@link Slot} definition from the given string and adds it to the squad
*
* @param s to parse slot from. Include number and name
* @param squad to add slot to
*/
private void readSlot(String s, Squad squad) {
final Matcher matcher = DIGIT.matcher(s);

Slot slot;
if (matcher.find()) {
final String slotNumber = matcher.group();
final int end = END_OF_STRING_AND_LINE.matcher(s).find() ? s.indexOf("\";") : s.length();
final String slotName = s.substring(s.indexOf(slotNumber) + slotNumber.length(), end).trim();

slot = Slot.builder().number(Integer.parseInt(removeNonDigitCharacters(slotNumber))).name(slotName).squad(squad).build();
} else {
slot = Slot.builder().name(s.trim()).squad(squad).build();
}
log.trace("Added slot '{}'", slot.getName());
squad.getSlotList().add(slot);
}

enum ReadStep {
NONE,
GROUP_SEARCH,
GROUP_FOUND,
GROUP_FILL,
GROUP_END;

private static final ReadStep[] values = values();

public ReadStep next() {
final ReadStep nextStep = values[(ordinal() + 1) % values.length];
log.trace("Next step {}", nextStep.name());
return nextStep;
}
}
}
10 changes: 8 additions & 2 deletions src/main/java/de/webalf/slotbot/util/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public static boolean isNotEmpty(String term) {
return org.springframework.util.StringUtils.hasText(term);
}

private static final String REGEX = "(\"[^\"]*\"|'[^']*'|[^\"' ]+)";
private static final Pattern QUOTES = Pattern.compile("(\"[^\"]*\"|'[^']*'|[^\"' ]+)");

public static List<String> splitOnSpacesExceptQuotes(String str) {
final Matcher m = Pattern.compile(REGEX).matcher(str);
final Matcher m = QUOTES.matcher(str);
List<String> s = new ArrayList<>();
while (m.find()) {
String group = m.group();
Expand Down Expand Up @@ -53,4 +53,10 @@ public static boolean onlyNumbers(String str) {
}
return true;
}

private static final String NON_DIGIT_REGEX = "\\D";

public static String removeNonDigitCharacters(String str) {
return str.replaceAll(NON_DIGIT_REGEX, "");
}
}
4 changes: 3 additions & 1 deletion src/main/java/de/webalf/slotbot/util/bot/MentionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import java.util.regex.Pattern;

import static de.webalf.slotbot.util.StringUtils.removeNonDigitCharacters;

/**
* @author Alf
* @since 11.01.2021
Expand All @@ -22,6 +24,6 @@ public static boolean isChannelMention(String arg) {
}

public static String getId(String mention) {
return mention.replaceAll("\\D", "");
return removeNonDigitCharacters(mention);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.webalf.slotbot.util.eventfield;

import de.webalf.slotbot.controller.website.FileController;
import de.webalf.slotbot.controller.website.FileWebController;
import de.webalf.slotbot.model.annotations.EventFieldDefault;
import de.webalf.slotbot.model.dtos.EventFieldDefaultDto;
import lombok.experimental.UtilityClass;
Expand Down Expand Up @@ -58,23 +58,23 @@ public static String getModPackUrl(String modPack) {
}
switch (modPack) {
case "2008_ArmaMachtBock":
return linkTo(methodOn(FileController.class).getFile("Arma_3_Preset_2008_ArmaMachtBock.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Arma_3_Preset_2008_ArmaMachtBock.html")).toUri().toString();
case "2012_ArmaMachtBock":
return linkTo(methodOn(FileController.class).getFile("Arma_3_Preset_2012_ArmaMachtBock_Full.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Arma_3_Preset_2012_ArmaMachtBock_Full.html")).toUri().toString();
case "2101_ArmaMachtBock":
return linkTo(methodOn(FileController.class).getFile("Arma_3_Preset_2101_ArmaMachtBock_Full_v2.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Arma_3_Preset_2101_ArmaMachtBock_Full_v2.html")).toUri().toString();
case "2103_ArmaMachtBock":
return linkTo(methodOn(FileController.class).getFile("Arma_3_Preset_2103_ArmaMachtBock_Full.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Arma_3_Preset_2103_ArmaMachtBock_Full.html")).toUri().toString();
case "2102_Event":
return linkTo(methodOn(FileController.class).getFile("Arma_3_Preset_2102_Event.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Arma_3_Preset_2102_Event.html")).toUri().toString();
case "2104_ArmaMachtBock_GM":
return linkTo(methodOn(FileController.class).getFile("Arma_3_Preset_2104_ArmaMachtBock_GM.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Arma_3_Preset_2104_ArmaMachtBock_GM.html")).toUri().toString();
case "2105_ArmaMachtBock_VN":
return linkTo(methodOn(FileController.class).getFile("2105_ArmaMachtBock_VN.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("2105_ArmaMachtBock_VN.html")).toUri().toString();
case "Joined_Operations_2020":
return linkTo(methodOn(FileController.class).getFile("Joined_Operations_2020v2.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Joined_Operations_2020v2.html")).toUri().toString();
case "Alliance_2021v1":
return linkTo(methodOn(FileController.class).getFile("Alliance_2021v1.html")).toUri().toString();
return linkTo(methodOn(FileWebController.class).getFile("Alliance_2021v1.html")).toUri().toString();
default:
return null;
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/resources/static/assets/js/slotList.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,30 @@ function checkUniqueSlotNumbers() {
const slotNumbers = getSlotNumbers();
return slotNumbers.length === new Set(slotNumbers).size;
}

function addSlotList(slotList) {
$('#uploadSlotlistModal').modal('toggle'); //Close upload modal
$('.js-trash').trigger('click'); //Remove all existing squads and slots

for (const squad of slotList) {
$('#addSquad').trigger('click');
fillSquad($('.js-complete-squad').last(), squad);
}
}

function fillSquad($squad, squad) {
$squad.find('.js-squad-name').val(squad.name);
let first = true;
for (const slot of squad.slotList) {
if (!first) {
$squad.find('.js-add-slot').trigger('click');
} else {
first = false;
}
const $slot = $squad.find('.js-slot').last();
if (slot.number !== 0) {
$slot.find('.js-slot-number').val(slot.number);
}
$slot.find('.js-slot-name').val(slot.name);
}
}
36 changes: 36 additions & 0 deletions src/main/resources/static/assets/js/uploadSqmFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
$(function () {
"use strict";
$('progress').hide();
$('input[type="file"]').on('change', function () {
$('progress').show()
const formData = new FormData();
formData.append('file', this.files[0]);
$.ajax(uploadSqmFileUrl, {
method: 'POST',
data: formData,
cache: false,
contentType: false,
processData: false,

// Custom XMLHttpRequest
xhr: function () {
var myXhr = $.ajaxSettings.xhr();
if (myXhr.upload) {
// For handling the progress of the upload
myXhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
$('progress').attr({
value: e.loaded,
max: e.total,
});
}
}, false);
}
return myXhr;
}
})
.done(slotList => {
addSlotList(slotList);
});
});
});

0 comments on commit 58003cd

Please sign in to comment.