Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ca13504
Backend Prüfung klappt, aber Html Tags etc werden noch mitgezählt
lschulen Jul 1, 2026
2fb3e8f
Backend Prüfung klappt, aber Html Tags etc werden noch mitgezählt
lschulen Jul 7, 2026
afcbb64
Backend Prüfung klappt, aber Html Tags etc werden noch mitgezählt
lschulen Jul 7, 2026
b4c9fe5
aktueller Stand 26-07-15
lschulen Jul 15, 2026
aea6bcc
Merge remote-tracking branch 'origin/L_8-question-texts-can-be-too-lo…
lschulen Jul 15, 2026
8acfdbc
errormessages
lschulen Jul 15, 2026
2a54c74
Added a helper function to remove all HTML tags when counting the cha…
lschulen Jul 15, 2026
ba12df4
Added Counting functionality for the amount of characters in Descript…
lschulen Jul 21, 2026
122905c
corrected the calculation of the number of characters
lschulen Jul 21, 2026
086b742
Merge branch 'v3.4.0' into L_8-question-texts-can-be-too-long-restored
ywarnecke Aug 18, 2026
07ca4e7
Refactor visible text extraction into HtmlUtilities
lschulen Aug 18, 2026
5a6f8cc
Add live question text length validation
lschulen Aug 18, 2026
b182d43
Made live WYSIWYG length validation reusable and applied it in questi…
lschulen Aug 19, 2026
6d9927f
Added message keys and adjusted the frontend
lschulen Aug 19, 2026
30db67d
adjusted the backend as well
lschulen Aug 19, 2026
3e83d87
implemented frontend check for bundles and enures that info disappear…
lschulen Aug 19, 2026
0fc62f2
implemented backend check for bundles
lschulen Aug 19, 2026
d69f783
Added new message keys if welcometext or final text is too long
lschulen Aug 25, 2026
3fb2314
Added backend check if any welcome text in bundle is too long
lschulen Aug 25, 2026
e651dfa
Added backend check if any final text in bundle is too long
lschulen Aug 25, 2026
dba9c6e
Added frontend check if any welcome or final text in bundle is too long
lschulen Aug 25, 2026
fe0eed7
Added backend check if any welcome or final text in questionnaire is …
lschulen Aug 25, 2026
350d406
Added frontend check if any welcome or final text in questionnaire is…
lschulen Aug 25, 2026
5d52c45
correced a variable name
lschulen Aug 25, 2026
df160aa
Added frontend check if description text in clinic is too long
lschulen Aug 25, 2026
4faae27
Added backend check if description text in clinic is too long
lschulen Aug 25, 2026
8fdda9b
Added backend check if description text in clinic is empty
lschulen Aug 25, 2026
065e298
Optimized isEmpty check for description in bundle
lschulen Aug 25, 2026
506a5cb
Optimized isEmpty check for description in bundle
lschulen Aug 25, 2026
7ba9138
Added backend check if message in invitation is too long. added those…
lschulen Aug 26, 2026
ab9467e
Added frontend check if message in invitation is too long. needed a d…
lschulen Aug 26, 2026
13ee45c
comment
lschulen Aug 26, 2026
89e9667
deleted sysout
lschulen Aug 26, 2026
372caee
Show only one error message for completely empty text fields in Bundl…
lschulen Sep 1, 2026
57e16f1
Fixed JUnit errors
lschulen Sep 1, 2026
dee334f
Started JUnit Tests in Bundle. Still producing error
lschulen Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/main/java/de/imi/mopat/helper/controller/HtmlUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,34 @@ public static String getStringWithoutHtml(final String htmlString) {
}
return htmlString.replaceAll("\\<[^>]*>", "");
}

/*
* Converts HTML content to countable visible text for validation.
* Unlike getStringWithoutHtml(...), this also preserves visible line breaks
* from tags like <br>, </p> or </div> and decodes common HTML entities
* such as &nbsp.
*/
public static String getVisibleText(String html) {
if (html == null || html.isEmpty()) {
return "";
}

String text = html
.replace("\r\n", "\n")
.replace("\r", "\n");

text = text.replaceAll("(?i)<br\\s*/?>", "\n");
text = text.replaceAll("(?i)</(?:div|p|li|h[1-6])\\s*>", "\n");
text = text.replaceAll("(?s)<[^>]+>", "");

text = text.replace("&nbsp;", " ");
text = text.replace("&amp;", "&");
text = text.replace("&lt;", "<");
text = text.replace("&gt;", ">");
text = text.replace("&quot;", "\"");
text = text.replace("&#39;", "'");

return text.replaceAll("\n{3,}", "\n\n").trim();
}

}
43 changes: 38 additions & 5 deletions src/main/java/de/imi/mopat/validator/BundleDTOValidator.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.imi.mopat.validator;

import de.imi.mopat.dao.BundleDao;
import de.imi.mopat.helper.controller.HtmlUtilities;
import de.imi.mopat.helper.controller.HtmlUtils;
import de.imi.mopat.model.Bundle;
import de.imi.mopat.model.dto.BundleDTO;
Expand All @@ -21,6 +22,11 @@
@Component
public class BundleDTOValidator implements Validator {

// Needs to be public because it's used in BundleDto
public static final int MAX_DESCRIPTION_TEXT_LENGTH = 2_000;
public static final int MAX_WELCOME_TEXT_LENGTH = 5_000;
public static final int MAX_FINAL_TEXT_LENGTH = 5_000;

@Autowired
private MessageSource messageSource;

Expand Down Expand Up @@ -56,12 +62,17 @@ public void validate(final Object o, final Errors errors) {
LocaleContextHolder.getLocale()));
}

String bundleDescription = HtmlUtils.removeHtmlTags(bundleDTO.getDescription());

if (bundleDescription != null && bundleDescription.isEmpty()) {
// Check if description text is too long or empty
int visibleDescpriptionLength = HtmlUtilities.getVisibleText(bundleDTO.getDescription()).length();
if(visibleDescpriptionLength > MAX_DESCRIPTION_TEXT_LENGTH){
errors.rejectValue("description",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.descriptionTooLong",
new Object[]{visibleDescpriptionLength, MAX_DESCRIPTION_TEXT_LENGTH}, LocaleContextHolder.getLocale()));
} else if (bundleDTO.getDescription() != null && !bundleDTO.getDescription().isEmpty() && visibleDescpriptionLength == 0) {
errors.rejectValue("description", "errormessage",
messageSource.getMessage("bundle.description.notNull", new Object[]{},
LocaleContextHolder.getLocale()));
messageSource.getMessage("bundle.description.notNull", new Object[]{},
LocaleContextHolder.getLocale()));
}

// Check if at least the first questionnaire is enabled in this bundle
Expand Down Expand Up @@ -104,6 +115,17 @@ public void validate(final Object o, final Errors errors) {
messageSource.getMessage("bundle.validator" + ".welcomeText" + ".notNull",
new Object[]{}, LocaleContextHolder.getLocale()));
}
// Check if current welcome text is too long
else {
int visibleWelcomeTextLength = HtmlUtilities.getVisibleText(entry.getValue()).length();
if (visibleWelcomeTextLength > MAX_WELCOME_TEXT_LENGTH){
errors.rejectValue("localizedWelcomeText[" + entry.getKey() + "]",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.welcomeTextTooLong",
new Object[]{visibleWelcomeTextLength, MAX_WELCOME_TEXT_LENGTH}, LocaleContextHolder.getLocale()));

}
}
}
}

Expand All @@ -130,6 +152,17 @@ public void validate(final Object o, final Errors errors) {
messageSource.getMessage("bundle.validator" + ".finalText" + ".notNull",
new Object[]{}, LocaleContextHolder.getLocale()));
}
// Check if current final text is too long
else {
int visibleFinalTextLength = HtmlUtilities.getVisibleText(entry.getValue()).length();
if (visibleFinalTextLength > MAX_FINAL_TEXT_LENGTH){
errors.rejectValue("localizedFinalText[" + entry.getKey() + "]",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.finalTextTooLong",
new Object[]{visibleFinalTextLength, MAX_FINAL_TEXT_LENGTH}, LocaleContextHolder.getLocale()));

}
}
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/de/imi/mopat/validator/ClinicDTOValidator.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.imi.mopat.validator;

import de.imi.mopat.dao.ClinicDao;
import de.imi.mopat.helper.controller.HtmlUtilities;
import de.imi.mopat.model.dto.ClinicConfigurationMappingDTO;
import de.imi.mopat.model.dto.ClinicDTO;

Expand All @@ -18,6 +19,8 @@
@Component
public class ClinicDTOValidator implements Validator {

private static final int MAX_DESCRIPTION_TEXT_LENGTH = 2_000;

@Autowired
private MessageSource messageSource;
@Autowired
Expand Down Expand Up @@ -53,6 +56,22 @@ public void validate(final Object o, final Errors errors) {
messageSource.getMessage("clinic.error.nameInUse", new Object[]{},
LocaleContextHolder.getLocale()));
}

// Check if description text is too long or empty
int visibleDescpriptionLength = HtmlUtilities.getVisibleText(clinicDTO.getDescription()).length();
if(visibleDescpriptionLength > MAX_DESCRIPTION_TEXT_LENGTH){
errors.rejectValue("description",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.descriptionTooLong",
new Object[]{visibleDescpriptionLength, MAX_DESCRIPTION_TEXT_LENGTH}, LocaleContextHolder.getLocale()));
} else if (visibleDescpriptionLength == 0) {
errors.rejectValue("description", "errormessage",
messageSource.getMessage("clinic.description.notNull", new Object[]{},
LocaleContextHolder.getLocale()));
}



if (!checkIfAnyOnePatientRetrieverIsEnabled(clinicDTO)) {
errors.rejectValue("clinicConfigurationMappingDTOS[0].value", MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("clinic.error.noConfiguration",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.imi.mopat.validator;

import de.imi.mopat.helper.controller.HtmlUtilities;
import de.imi.mopat.model.dto.InvitationDTO;
import de.imi.mopat.model.dto.InvitationUserDTO;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -15,6 +16,8 @@
@Component
public class InvitationDTOValidator implements Validator {

private static final int MAX_PERSONAL_TEXT_LENGTH = 2000;

@Autowired
private MessageSource messageSource;

Expand Down Expand Up @@ -65,5 +68,15 @@ public void validate(final Object o, final Errors errors) {
}
}

// Checks if message is too long (message is named personalText in DTO)
int personalTextLength = HtmlUtilities.getVisibleText(invitationDTO.getPersonalText()).length();
if (personalTextLength > MAX_PERSONAL_TEXT_LENGTH){
errors.rejectValue("personalText",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("invitation.message.tooLong",
new Object[]{personalTextLength, MAX_PERSONAL_TEXT_LENGTH},
LocaleContextHolder.getLocale()));
}

}
}
14 changes: 10 additions & 4 deletions src/main/java/de/imi/mopat/validator/QuestionDTOValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import de.imi.mopat.model.enumeration.QuestionType;
import de.imi.mopat.model.dto.AnswerDTO;
import de.imi.mopat.model.dto.QuestionDTO;
import de.imi.mopat.helper.controller.HtmlUtilities;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
Expand All @@ -25,6 +25,7 @@ public class QuestionDTOValidator implements Validator {

private static final String MIN_NUMBER_ANSWERS = "minNumberAnswers";
private static final String MAX_NUMBER_ANSWERS = "maxNumberAnswers";
private static final int MAX_QUESTION_TEXT_LENGTH = 2_000;
@Autowired
private SelectAnswerDTOValidator selectAnswerDTOValidator;
@Autowired
Expand Down Expand Up @@ -56,10 +57,10 @@ public void validate(final Object target, final Errors errors) {
// [bt] now it's my time to validate the more complex stuff
QuestionDTO questionDTO = (QuestionDTO) target;

// [sw] Check if any added language contains an empty questionText
// [sw] Check if any added language contains an empty or too long questionText
for (Map.Entry<String, String> entry : questionDTO.getLocalizedQuestionText().entrySet()) {
if (entry.getValue() == null || entry.getValue().trim().isEmpty() || Pattern.matches(
"<p>(<p>|</p>|\\s|&nbsp;|<br>)+<\\/p>", entry.getValue())) {
String visibleQuestionText = HtmlUtilities.getVisibleText(entry.getValue());
if (visibleQuestionText.isEmpty()) {
questionDTO.getLocalizedQuestionText().put(entry.getKey(), "");
if (questionDTO.getQuestionType() == QuestionType.INFO_TEXT) {
errors.rejectValue("localizedQuestionText[" + entry.getKey() + "]",
Expand All @@ -72,6 +73,11 @@ public void validate(final Object target, final Errors errors) {
messageSource.getMessage("question.error" + ".questionTextIsNull",
new Object[]{}, LocaleContextHolder.getLocale()));
}
} else if(visibleQuestionText.length() > MAX_QUESTION_TEXT_LENGTH){
errors.rejectValue("localizedQuestionText[" + entry.getKey() + "]",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("question.error.questionTextTooLong",
new Object[]{visibleQuestionText.length(),MAX_QUESTION_TEXT_LENGTH}, LocaleContextHolder.getLocale()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.model.Questionnaire;
import de.imi.mopat.model.dto.QuestionnaireDTO;
import de.imi.mopat.helper.controller.HtmlUtilities;

import java.util.Map;
import java.util.regex.Pattern;
Expand All @@ -20,12 +21,18 @@
@Component
public class QuestionnaireDTOValidator implements Validator {

private static final int MAX_DESCRIPTION_TEXT_LENGTH = 2_000;
private static final int MAX_WELCOME_TEXT_LENGTH = 5_000;
private static final int MAX_FINAL_TEXT_LENGTH = 5_000;


@Autowired
private MessageSource messageSource;

@Autowired
private QuestionnaireDao questionnaireDao;


@Override
public boolean supports(final Class<?> type) {
return QuestionnaireDTO.class.isAssignableFrom(type);
Expand Down Expand Up @@ -56,6 +63,19 @@ public void validate(final Object o, final Errors errors) {
LocaleContextHolder.getLocale()));
}

// Check if the description text is too long or empty
int visibleDescpriptionLength = HtmlUtilities.getVisibleText(questionnaireDTO.getDescription()).length();
if(visibleDescpriptionLength > MAX_DESCRIPTION_TEXT_LENGTH){
errors.rejectValue("description",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.descriptionTooLong",
new Object[]{visibleDescpriptionLength, MAX_DESCRIPTION_TEXT_LENGTH}, LocaleContextHolder.getLocale()));
} else if (questionnaireDTO.getDescription() != null && !questionnaireDTO.getDescription().isEmpty() && visibleDescpriptionLength == 0) {
errors.rejectValue("description", "errormessage",
messageSource.getMessage("questionnaire.description.notNull", new Object[]{},
LocaleContextHolder.getLocale()));
}

// [sw] Check if any added language contains an empty questionText
for (Map.Entry<String, String> entry : questionnaireDTO.getLocalizedDisplayName()
.entrySet()) {
Expand Down Expand Up @@ -93,6 +113,17 @@ public void validate(final Object o, final Errors errors) {
"questionnaire.validator" + ".welcomeText" + ".notNull", new Object[]{},
LocaleContextHolder.getLocale()));
}
// Check if current welcome text is too long
else {
int visibleWelcomeTextLength = HtmlUtilities.getVisibleText(entry.getValue()).length();
if (visibleWelcomeTextLength > MAX_WELCOME_TEXT_LENGTH){
errors.rejectValue("localizedWelcomeText[" + entry.getKey() + "]",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.welcomeTextTooLong",
new Object[]{visibleWelcomeTextLength, MAX_WELCOME_TEXT_LENGTH}, LocaleContextHolder.getLocale()));

}
}
}
}

Expand Down Expand Up @@ -121,6 +152,17 @@ public void validate(final Object o, final Errors errors) {
"questionnaire.validator" + ".finalText" + ".notNull", new Object[]{},
LocaleContextHolder.getLocale()));
}
// Check if current final text is too long
else {
int visibleFinalTextLength = HtmlUtilities.getVisibleText(entry.getValue()).length();
if (visibleFinalTextLength > MAX_FINAL_TEXT_LENGTH){
errors.rejectValue("localizedFinalText[" + entry.getKey() + "]",
MoPatValidator.ERRORCODE_ERRORMESSAGE,
messageSource.getMessage("questionnaire.error.finalTextTooLong",
new Object[]{visibleFinalTextLength, MAX_FINAL_TEXT_LENGTH}, LocaleContextHolder.getLocale()));

}
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/message/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ invitation.label.fileInfo=The personal data, first name, surname and e-mail addr
invitation.label.firstname=First name
invitation.label.lastname=Last name
invitation.label.user=User
invitation.message.tooLong=The message contains {0} characters and exceeds the maximum of {1} characters.
layout.button.back=Go Back
layout.button.close=Cancel and exit
layout.footer.copyright=2026 Institute of Medical Informatics,<br> University of M\u00fcnster
Expand Down Expand Up @@ -734,6 +735,7 @@ question.error.noValidScoreMinMax=It is not possible to give exactly one answer.
question.error.noValidScoreQuestionType=A question type has been selected that does not support score calculation. If you save the question, all scores that contain this question will be deleted. Do you really want to save the question?
question.error.notModifiable=This question is not editable due to already existing responses from surveys
question.error.questionTextIsNull=The localized question text is required
question.error.questionTextTooLong=The question text contains {0} characters and exceeds the maximum of {1} characters.
question.error.sliderDifferenceMaxMinNotDivisibleByStepsize=The difference between max and min values is not divisible without remainder by the step size
question.error.sliderStepsizeLessOrEqualToZero=The stepsize should not be less or equal to 0
question.heading.editQuestion=Edit question
Expand Down Expand Up @@ -815,11 +817,14 @@ questionnaire.button.showExportTemplates=Edit export templates
questionnaire.displayName.notNull=The localized display name is required
questionnaire.error.deleteQuestionnaireNotPossible=Questionnaire {0} cannot be deleted because the questionnaire is already answered in a survey.
questionnaire.error.deleteQuestionnairePossible=The questionnaire {0} was deleted.
questionnaire.error.descriptionTooLong=The description contains {0} characters and exceeds the maximum of {1} characters.
questionnaire.error.editLastCondition=This is the last question of this questionnaire. Thus, conditions for this question cannot be specified.
questionnaire.error.finalTextTooLong=The final text contains {0} characters and exceeds the maximum of {1} characters.
questionnaire.error.import=An error occured during the import of a questionnaire file: {0}
questionnaire.error.nameContainsSpecialCharacters=The name you entered contains invalid characters. Only letters, numbers and the special characters !?+-_.:()[] are allowed.
questionnaire.error.nameInUse=The chosen name is already in use. Please choose a different one.
questionnaire.error.nameIsEmpty=The name shouldn't only consist of space characters.
questionnaire.error.welcomeTextTooLong=The welcome text contains {0} characters and exceeds the maximum of {1} characters.
questionnaire.heading.editQuestionnaire=Edit questionnaire
questionnaire.heading.title=Questionnaires
questionnaire.import.button.import=Upload &amp; Import
Expand Down Expand Up @@ -1049,6 +1054,7 @@ survey.title.fontSize=Select a font size
survey.title.searchCaseNumber=Search for case number
survey.title.selectBundle=Select bundle
survey.title.selectClinic=Select clinic
textLength.error.exceeded=Character limit exceeded ({0} / {1})
typeMismatch.answers.maxValue=The maximum's value of the slider must be an integer value
typeMismatch.answers.minValue=The minimum's value of the slider must be an integer value
typeMismatch.answers.value=The score must be an integer value
Expand Down
Loading
Loading