diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java index 066ea39fafc0..59aa3506ff65 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java @@ -174,6 +174,11 @@ public class DCInput { */ private String typeBindField = null; + /** + * validation group for group validation (e.g., "evyuka-codes") + */ + private String validationGroup = null; + /** * the dropdown input type could have defined a default value */ @@ -267,6 +272,8 @@ public DCInput(Map fieldMap, Map> listMap, typeBindField = fieldMap.get(DCInputsReader.TYPE_BIND_FIELD_ATTRIBUTE); this.insertToTypeBind(typeBindField); + validationGroup = fieldMap.get("validation-group"); + style = fieldMap.get("style"); isRelationshipField = fieldMap.containsKey("relationship-type"); @@ -754,6 +761,14 @@ public void setTypeBindField(String typeBindField) { this.typeBindField = typeBindField; } + public String getValidationGroup() { + return validationGroup; + } + + public void setValidationGroup(String validationGroup) { + this.validationGroup = validationGroup; + } + /** * Class representing a Map of the ComplexDefinition object * Class is copied from UFAL/CLARIN-DSPACE (https://github.com/ufal/clarin-dspace) and modified by diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaCodesValidation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaCodesValidation.java new file mode 100644 index 000000000000..f0e0de45a15e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaCodesValidation.java @@ -0,0 +1,415 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.submit.step.validation; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.app.rest.model.ErrorRest; +import org.dspace.app.rest.repository.WorkspaceItemRestRepository; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.util.DCInput; +import org.dspace.app.util.DCInputSet; +import org.dspace.app.util.DCInputsReader; +import org.dspace.app.util.DCInputsReaderException; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; + +/** + * Master validator for E-výuka codes with intelligent form detection and multi-level error generation. + * + * This validator automatically detects the form type based on available evyuka fields and validates + * that at least one required code is filled. Supports validation for: + * - evyuka.subject.version (Kód verze předmětu) + * - evyuka.subject (Kód předmětu) + * - evyuka.discipline (Kód studijního oboru) + * - evyuka.programme (Kód studijního programu) + * + * Form Type Detection: + * - FULL_FORM: All 4 codes available - requires at least one from any group + * - SUBJECT_ONLY: Only subject codes (2) - requires at least one subject field + * - DISCIPLINE_ONLY: Only discipline codes (2) - requires at least one discipline field + * - NO_EVYUKA_FIELDS: No evyuka fields - validation skipped + * + * Error Generation Strategy: + * - L4 Level: Individual field errors for each empty field (4 types max) + * - L2 Level: Master step errors - double generation per form type (6 types total) + * * Primary L2: Context-aware message based on form type + * * Additional L2: Form-specific HTML description + * + * Used by collections: 10084/139351 (subject-only), 10084/138949-138955 (full forms) + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public class EvyukaCodesValidation extends AbstractValidation { + + // Error message constants for different form types + private static final String ERROR_VALIDATION_EVYUKA_CODES_ALL_REQUIRED = + "error.validation.evyuka.codes.all.required"; + private static final String ERROR_VALIDATION_EVYUKA_CODES_SUBJECT_ONLY_REQUIRED = + "error.validation.evyuka.codes.subject.only.required"; + private static final String ERROR_VALIDATION_EVYUKA_CODES_DISCIPLINE_ONLY_REQUIRED = + "error.validation.evyuka.codes.discipline.only.required"; + + // The metadata fields for evyuka codes + private static final String EVYUKA_SUBJECT_VERSION = "evyuka.subject.version"; + private static final String EVYUKA_SUBJECT = "evyuka.subject"; + private static final String EVYUKA_DISCIPLINE = "evyuka.discipline"; + private static final String EVYUKA_PROGRAMME = "evyuka.programme"; + + // Form type enumeration + private enum FormType { + FULL_FORM, // Both subject and discipline fields present + SUBJECT_ONLY, // Only subject fields present + DISCIPLINE_ONLY, // Only discipline fields present + NO_EVYUKA_FIELDS // No evyuka fields present + } + + private DCInputsReader inputReader; + private ItemService itemService; + + @Override + public List validate(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) throws DCInputsReaderException, SQLException { + + List errors = new ArrayList<>(); + + DCInputSet inputConfig = getInputReader().getInputsByFormName(config.getId()); + if (inputConfig == null) { + return errors; + } + + // Detect available evyuka fields in the form + FieldAvailability fieldAvailability = detectEvyukaFields(inputConfig); + FormType formType = determineFormType(fieldAvailability); + + // Skip validation if no evyuka fields are present + if (formType == FormType.NO_EVYUKA_FIELDS) { + return errors; + } + + // Check which field groups have any values (for general validation) + boolean hasAnySubjectValues = hasValuesInFields(obj, fieldAvailability.subjectFields); + boolean hasAnyDisciplineValues = hasValuesInFields(obj, fieldAvailability.disciplineFields); + + // Check which field groups have ALL values filled (for complete validation) + boolean hasAllSubjectValues = hasAllValuesInFields(obj, fieldAvailability.subjectFields); + boolean hasAllDisciplineValues = hasAllValuesInFields(obj, fieldAvailability.disciplineFields); + + // For compatibility with existing code + boolean hasSubjectCodes = hasAnySubjectValues; + boolean hasDisciplineCodes = hasAnyDisciplineValues; + + // Validate based on form type and field values + if (!isValidationPassed(formType, hasAnySubjectValues, hasAnyDisciplineValues, + hasAllSubjectValues, hasAllDisciplineValues)) { + + // Add individual field error messages + addIndividualFieldErrors(errors, fieldAvailability, config, obj); + + // Add master validation errors at the beginning of the step (context-aware) + String masterErrorCode = getErrorMessageCode(formType, hasSubjectCodes, hasDisciplineCodes); + addError(errors, masterErrorCode, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId()); + + // Add additional L2 error messages based on form type + addAdditionalL2Errors(errors, config, formType, hasSubjectCodes, hasDisciplineCodes); + } + + return errors; + } + + /** + * Detects which evyuka fields are available in the form configuration. + */ + private FieldAvailability detectEvyukaFields(DCInputSet inputConfig) { + FieldAvailability availability = new FieldAvailability(); + + for (DCInput[] row : inputConfig.getFields()) { + for (DCInput input : row) { + String fieldName = input.getFieldName(); + + // Check for subject fields (both validation groups) + if (fieldName.equals(EVYUKA_SUBJECT_VERSION) || fieldName.equals(EVYUKA_SUBJECT)) { + availability.subjectFields.add(input); + } + + // Check for discipline fields (both validation groups) + if (fieldName.equals(EVYUKA_DISCIPLINE) || fieldName.equals(EVYUKA_PROGRAMME)) { + availability.disciplineFields.add(input); + } + } + } + + return availability; + } + + /** + * Determines the form type based on available fields. + */ + private FormType determineFormType(FieldAvailability availability) { + boolean hasSubjectFields = !availability.subjectFields.isEmpty(); + boolean hasDisciplineFields = !availability.disciplineFields.isEmpty(); + + if (hasSubjectFields && hasDisciplineFields) { + return FormType.FULL_FORM; + } else if (hasSubjectFields) { + return FormType.SUBJECT_ONLY; + } else if (hasDisciplineFields) { + return FormType.DISCIPLINE_ONLY; + } else { + return FormType.NO_EVYUKA_FIELDS; + } + } + + /** + * Checks if any field in the given list has a non-empty value. + */ + private boolean hasValuesInFields(InProgressSubmission obj, List fields) { + for (DCInput field : fields) { + List values = itemService.getMetadataByMetadataString(obj.getItem(), field.getFieldName()); + + if (CollectionUtils.isNotEmpty(values)) { + for (MetadataValue value : values) { + if (value != null && value.getValue() != null && !value.getValue().trim().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** + * Checks if all fields in the given list have non-empty values. + */ + private boolean hasAllValuesInFields(InProgressSubmission obj, List fields) { + for (DCInput field : fields) { + if (!hasValueInField(obj, field)) { + return false; + } + } + return true; + } + + /** + * Determines if validation passes based on form type and filled fields. + */ + private boolean isValidationPassed(FormType formType, boolean hasAnySubjectValues, boolean hasAnyDisciplineValues, + boolean hasAllSubjectValues, boolean hasAllDisciplineValues) { + switch (formType) { + case FULL_FORM: + // For full forms, at least one group must have any values + return hasAnySubjectValues || hasAnyDisciplineValues; + case SUBJECT_ONLY: + // Subject-only forms: require at least one field filled + return hasAnySubjectValues; + case DISCIPLINE_ONLY: + // Discipline-only forms: require at least one field filled + return hasAnyDisciplineValues; + default: + return true; // No validation needed + } + } + + /** + * Gets the appropriate error message code based on form type and field states. + */ + private String getErrorMessageCode(FormType formType, boolean hasSubjectCodes, boolean hasDisciplineCodes) { + switch (formType) { + case FULL_FORM: + return ERROR_VALIDATION_EVYUKA_CODES_ALL_REQUIRED; + case SUBJECT_ONLY: + return ERROR_VALIDATION_EVYUKA_CODES_SUBJECT_ONLY_REQUIRED; + case DISCIPLINE_ONLY: + return ERROR_VALIDATION_EVYUKA_CODES_DISCIPLINE_ONLY_REQUIRED; + default: + return ERROR_VALIDATION_EVYUKA_CODES_ALL_REQUIRED; + } + } + + /** + * Gets all evyuka fields from the availability object. + */ + private List getAllEvyukaFields(FieldAvailability availability) { + List allFields = new ArrayList<>(); + allFields.addAll(availability.subjectFields); + allFields.addAll(availability.disciplineFields); + return allFields; + } + + /** + * Adds individual error messages for each empty field based on form type. + */ + private void addIndividualFieldErrors(List errors, FieldAvailability fieldAvailability, + SubmissionStepConfig config, InProgressSubmission obj) { + + // Add errors for empty subject fields + for (DCInput field : fieldAvailability.subjectFields) { + if (!hasValueInField(obj, field)) { + String fieldErrorCode = getIndividualFieldErrorCode(field.getFieldName()); + addError(errors, fieldErrorCode, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + field.getFieldName()); + } + } + + // Add errors for empty discipline fields + for (DCInput field : fieldAvailability.disciplineFields) { + if (!hasValueInField(obj, field)) { + String fieldErrorCode = getIndividualFieldErrorCode(field.getFieldName()); + addError(errors, fieldErrorCode, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + field.getFieldName()); + } + } + } + + /** + * Adds group-specific error messages (HTML formatted for frontend). + */ + private void addGroupSpecificErrors(List errors, FieldAvailability fieldAvailability, + SubmissionStepConfig config, InProgressSubmission obj, FormType formType, + boolean hasSubjectCodes, boolean hasDisciplineCodes) { + + if (formType == FormType.FULL_FORM) { + // For full forms with both groups available, add combined error message + if (!hasSubjectCodes && !hasDisciplineCodes) { + addError(errors, "error.validation.evyuka.combined.groups.required", + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + + "/evyuka.combined.groups"); + } + } else { + // For forms with only one group, add individual group errors + + // Add subject group error if subject fields exist but are empty + if (!fieldAvailability.subjectFields.isEmpty() && !hasSubjectCodes) { + addError(errors, ERROR_VALIDATION_EVYUKA_CODES_SUBJECT_ONLY_REQUIRED, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + + "/evyuka.subject.group"); + } + + // Add discipline group error if discipline fields exist but are empty + if (!fieldAvailability.disciplineFields.isEmpty() && !hasDisciplineCodes) { + addError(errors, ERROR_VALIDATION_EVYUKA_CODES_DISCIPLINE_ONLY_REQUIRED, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + + "/evyuka.discipline.group"); + } + } + } + + /** + * Adds additional L2 error messages based on form type. + */ + private void addAdditionalL2Errors(List errors, SubmissionStepConfig config, FormType formType, + boolean hasSubjectCodes, boolean hasDisciplineCodes) { + String additionalErrorCode = null; + + switch (formType) { + case FULL_FORM: + if (!hasSubjectCodes && !hasDisciplineCodes) { + additionalErrorCode = "error.validation.evyuka.combined.groups.required"; + } + break; + case SUBJECT_ONLY: + if (!hasSubjectCodes) { + additionalErrorCode = "error.validation.evyuka.subject.codes.required"; + } + break; + case DISCIPLINE_ONLY: + if (!hasDisciplineCodes) { + additionalErrorCode = "error.validation.evyuka.discipline.programme.codes.required"; + } + break; + default: + // No additional validation for unknown form types + break; + } + + if (additionalErrorCode != null) { + addError(errors, additionalErrorCode, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId()); + } + } + + /** + * Gets individual field error code for specific field. + */ + private String getIndividualFieldErrorCode(String fieldName) { + switch (fieldName) { + case EVYUKA_SUBJECT_VERSION: + return "error.validation.evyuka.subject.version.required"; + case EVYUKA_SUBJECT: + return "error.validation.evyuka.subject.required"; + case EVYUKA_DISCIPLINE: + return "error.validation.evyuka.discipline.required"; + case EVYUKA_PROGRAMME: + return "error.validation.evyuka.programme.required"; + default: + throw new IllegalArgumentException("Unknown evyuka field: " + fieldName); + } + } + + /** + * Checks if a single field has a non-empty value. + */ + private boolean hasValueInField(InProgressSubmission obj, DCInput field) { + List values = itemService.getMetadataByMetadataString(obj.getItem(), field.getFieldName()); + + if (CollectionUtils.isNotEmpty(values)) { + for (MetadataValue value : values) { + if (value != null && value.getValue() != null && !value.getValue().trim().isEmpty()) { + return true; + } + } + } + return false; + } + + /** + * Helper class to track available evyuka fields in the form. + */ + private static class FieldAvailability { + List subjectFields = new ArrayList<>(); + List disciplineFields = new ArrayList<>(); + } + + /** + * Determines if this is an evyuka form based on the form name. + * Evyuka forms typically have names like "e-vyuka-FAST", "e-vyuka-FBI", etc. + * + * @param formName the name of the submission form + * @return true if this is an evyuka form + */ + private boolean isEvyukaForm(String formName) { + return formName != null && formName.startsWith("e-vyuka"); + } + + public DCInputsReader getInputReader() { + if (inputReader == null) { + try { + inputReader = new DCInputsReader(); + } catch (DCInputsReaderException e) { + throw new IllegalStateException("Cannot initialize DCInputsReader", e); + } + } + return inputReader; + } + + public void setInputReader(DCInputsReader inputReader) { + this.inputReader = inputReader; + } + + public void setItemService(ItemService itemService) { + this.itemService = itemService; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaDisciplineProgrammeCodesValidation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaDisciplineProgrammeCodesValidation.java new file mode 100644 index 000000000000..7a781419ba72 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaDisciplineProgrammeCodesValidation.java @@ -0,0 +1,172 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.submit.step.validation; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.app.rest.model.ErrorRest; +import org.dspace.app.rest.repository.WorkspaceItemRestRepository; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.util.DCInput; +import org.dspace.app.util.DCInputSet; +import org.dspace.app.util.DCInputsReader; +import org.dspace.app.util.DCInputsReaderException; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; + +/** + * Validates E-výuka discipline and programme codes using validation group-based field detection. + * + * This validator handles discipline/programme code validation for evyuka forms by detecting fields + * with the "evyuka-discipline-programme-codes" validation group and ensuring at least one contains a value: + * - evyuka.discipline (Kód studijního oboru) + * - evyuka.programme (Kód studijního programu) + * + * Validation Strategy: + * - Searches for fields with validation-group="evyuka-discipline-programme-codes" + * - Requires at least one discipline/programme field to have a non-empty value + * - Generates both step-level and field-level error messages + * + * Error Generation: + * - Step-level error: General message for the entire submission step + * - Field-level errors: Specific messages for each empty discipline/programme field + * + * Note: This is a legacy validator. The current implementation uses the master + * EvyukaCodesValidation class with intelligent form type detection and multi-level error generation. + * + * Used by E-výuka collections that have discipline/programme code fields configured with validation groups. + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public class EvyukaDisciplineProgrammeCodesValidation extends AbstractValidation { + + // Error messages for individual fields + private static final String ERROR_VALIDATION_EVYUKA_DISCIPLINE_REQUIRED = + "error.validation.evyuka.discipline.required"; + private static final String ERROR_VALIDATION_EVYUKA_PROGRAMME_REQUIRED = + "error.validation.evyuka.programme.required"; + + // Error message when no discipline/programme codes are provided at all + private static final String ERROR_VALIDATION_EVYUKA_DISCIPLINE_PROGRAMME_CODES_REQUIRED = + "error.validation.evyuka.discipline.programme.codes.required"; + + // The metadata fields for evyuka discipline/programme codes + private static final String EVYUKA_DISCIPLINE = "evyuka.discipline"; + private static final String EVYUKA_PROGRAMME = "evyuka.programme"; + + private DCInputsReader inputReader; + private ItemService itemService; + + @Override + public List validate(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) throws DCInputsReaderException, SQLException { + + List errors = new ArrayList<>(); + + DCInputSet inputConfig = getInputReader().getInputsByFormName(config.getId()); + if (inputConfig == null) { + return errors; + } + + // Find all fields that belong to the "evyuka-discipline-programme-codes" validation group + List evyukaDisciplineProgrammeFields = new ArrayList<>(); + + for (DCInput[] row : inputConfig.getFields()) { + for (DCInput input : row) { + if ("evyuka-discipline-programme-codes".equals(input.getValidationGroup())) { + evyukaDisciplineProgrammeFields.add(input); + } + } + } + + // If no fields have evyuka-discipline-programme-codes validation group, skip validation + if (evyukaDisciplineProgrammeFields.isEmpty()) { + return errors; + } + + boolean hasDisciplineCode = false; + boolean hasProgrammeCode = false; + boolean hasAnyDisciplineProgrammeCode = false; + + // Check if any of the evyuka discipline/programme code fields have values + for (DCInput input : evyukaDisciplineProgrammeFields) { + String fieldName = input.getFieldName(); + List values = itemService.getMetadataByMetadataString(obj.getItem(), fieldName); + + if (CollectionUtils.isNotEmpty(values)) { + // Check if any value is not empty + for (MetadataValue value : values) { + if (value != null && value.getValue() != null && !value.getValue().trim().isEmpty()) { + hasAnyDisciplineProgrammeCode = true; + + if (EVYUKA_DISCIPLINE.equals(fieldName)) { + hasDisciplineCode = true; + } else if (EVYUKA_PROGRAMME.equals(fieldName)) { + hasProgrammeCode = true; + } + break; + } + } + } + } + + // If no discipline/programme codes are provided at all, add general error for the step + if (!hasAnyDisciplineProgrammeCode) { + // Add general error message that will be displayed at the top of the step + addError(errors, ERROR_VALIDATION_EVYUKA_DISCIPLINE_PROGRAMME_CODES_REQUIRED, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId()); + + // Also add specific field errors for individual fields + for (DCInput input : evyukaDisciplineProgrammeFields) { + String fieldName = input.getFieldName(); + String errorKey = getFieldSpecificErrorKey(fieldName); + addError(errors, errorKey, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + fieldName); + } + } + + return errors; + } + + /** + * Get field-specific error key based on the field name + */ + private String getFieldSpecificErrorKey(String fieldName) { + if (EVYUKA_DISCIPLINE.equals(fieldName)) { + return ERROR_VALIDATION_EVYUKA_DISCIPLINE_REQUIRED; + } else if (EVYUKA_PROGRAMME.equals(fieldName)) { + return ERROR_VALIDATION_EVYUKA_PROGRAMME_REQUIRED; + } + return ERROR_VALIDATION_EVYUKA_DISCIPLINE_PROGRAMME_CODES_REQUIRED; + } + + public DCInputsReader getInputReader() { + if (inputReader == null) { + try { + inputReader = new DCInputsReader(); + } catch (DCInputsReaderException e) { + throw new IllegalStateException("Cannot initialize DCInputsReader", e); + } + } + return inputReader; + } + + public void setInputReader(DCInputsReader inputReader) { + this.inputReader = inputReader; + } + + public void setItemService(ItemService itemService) { + this.itemService = itemService; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaSubjectCodesValidation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaSubjectCodesValidation.java new file mode 100644 index 000000000000..e57688caffa1 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/EvyukaSubjectCodesValidation.java @@ -0,0 +1,171 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.submit.step.validation; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.app.rest.model.ErrorRest; +import org.dspace.app.rest.repository.WorkspaceItemRestRepository; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.util.DCInput; +import org.dspace.app.util.DCInputSet; +import org.dspace.app.util.DCInputsReader; +import org.dspace.app.util.DCInputsReaderException; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; + +/** + * Validates E-výuka subject codes using validation group-based field detection. + * + * This validator handles subject code validation for evyuka forms by detecting fields + * with the "evyuka-subject-codes" validation group and ensuring at least one contains a value: + * - evyuka.subject.version (Kód verze předmětu) + * - evyuka.subject (Kód předmětu) + * + * Validation Strategy: + * - Searches for fields with validation-group="evyuka-subject-codes" + * - Requires at least one subject field to have a non-empty value + * - Generates both step-level and field-level error messages + * + * Error Generation: + * - Step-level error: General message for the entire submission step + * - Field-level errors: Specific messages for each empty subject field + * + * Note: This is a legacy validator. The current implementation uses the master + * EvyukaCodesValidation class with intelligent form type detection. + * + * Used by E-výuka collections that have subject code fields configured with validation groups. + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public class EvyukaSubjectCodesValidation extends AbstractValidation { + + // Error messages for individual fields + private static final String ERROR_VALIDATION_EVYUKA_SUBJECT_VERSION_REQUIRED = + "error.validation.evyuka.subject.version.required"; + private static final String ERROR_VALIDATION_EVYUKA_SUBJECT_REQUIRED = "error.validation.evyuka.subject.required"; + + // Error message when no subject codes are provided at all + private static final String ERROR_VALIDATION_EVYUKA_SUBJECT_CODES_REQUIRED = + "error.validation.evyuka.subject.codes.required"; + + // The metadata fields for evyuka subject codes + private static final String EVYUKA_SUBJECT_VERSION = "evyuka.subject.version"; + private static final String EVYUKA_SUBJECT = "evyuka.subject"; + + private DCInputsReader inputReader; + private ItemService itemService; + + @Override + public List validate(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) throws DCInputsReaderException, SQLException { + + List errors = new ArrayList<>(); + + DCInputSet inputConfig = getInputReader().getInputsByFormName(config.getId()); + if (inputConfig == null) { + return errors; + } + + // Find all fields that belong to the "evyuka-subject-codes" validation group + List evyukaSubjectFields = new ArrayList<>(); + + for (DCInput[] row : inputConfig.getFields()) { + for (DCInput input : row) { + if ("evyuka-subject-codes".equals(input.getValidationGroup())) { + evyukaSubjectFields.add(input); + } + } + } + + // If no fields have evyuka-subject-codes validation group, skip validation + if (evyukaSubjectFields.isEmpty()) { + return errors; + } + + boolean hasSubjectVersionCode = false; + boolean hasSubjectCode = false; + boolean hasAnySubjectCode = false; + + // Check if any of the evyuka subject code fields have values + for (DCInput input : evyukaSubjectFields) { + String fieldName = input.getFieldName(); + List values = itemService.getMetadataByMetadataString(obj.getItem(), fieldName); + + if (CollectionUtils.isNotEmpty(values)) { + // Check if any value is not empty + for (MetadataValue value : values) { + if (value != null && value.getValue() != null && !value.getValue().trim().isEmpty()) { + hasAnySubjectCode = true; + + if (EVYUKA_SUBJECT_VERSION.equals(fieldName)) { + hasSubjectVersionCode = true; + } else if (EVYUKA_SUBJECT.equals(fieldName)) { + hasSubjectCode = true; + } + break; + } + } + } + } + + // If no subject codes are provided at all, add general error for the step + if (!hasAnySubjectCode) { + // Add general error message that will be displayed at the top of the step + addError(errors, ERROR_VALIDATION_EVYUKA_SUBJECT_CODES_REQUIRED, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId()); + + // Also add specific field errors for individual fields + for (DCInput input : evyukaSubjectFields) { + String fieldName = input.getFieldName(); + String errorKey = getFieldSpecificErrorKey(fieldName); + addError(errors, errorKey, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + fieldName); + } + } + + return errors; + } + + /** + * Get field-specific error key based on the field name + */ + private String getFieldSpecificErrorKey(String fieldName) { + if (EVYUKA_SUBJECT_VERSION.equals(fieldName)) { + return ERROR_VALIDATION_EVYUKA_SUBJECT_VERSION_REQUIRED; + } else if (EVYUKA_SUBJECT.equals(fieldName)) { + return ERROR_VALIDATION_EVYUKA_SUBJECT_REQUIRED; + } + return ERROR_VALIDATION_EVYUKA_SUBJECT_CODES_REQUIRED; + } + + public DCInputsReader getInputReader() { + if (inputReader == null) { + try { + inputReader = new DCInputsReader(); + } catch (DCInputsReaderException e) { + throw new IllegalStateException("Cannot initialize DCInputsReader", e); + } + } + return inputReader; + } + + public void setInputReader(DCInputsReader inputReader) { + this.inputReader = inputReader; + } + + public void setItemService(ItemService itemService) { + this.itemService = itemService; + } +} diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml index 968ce46a43ee..671dfcb87053 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml @@ -42,4 +42,10 @@ + + + + + + diff --git a/dspace/config/submission-forms.dtd b/dspace/config/submission-forms.dtd index 01bb8ea60d9f..ba89c1152f3a 100644 --- a/dspace/config/submission-forms.dtd +++ b/dspace/config/submission-forms.dtd @@ -9,7 +9,8 @@ - + + Kód verze předmětu dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes diff --git a/dspace/config/vsb/evyuka_form_AUD.xml b/dspace/config/vsb/evyuka_form_AUD.xml index fe501772261a..e2f1382e6a65 100644 --- a/dspace/config/vsb/evyuka_form_AUD.xml +++ b/dspace/config/vsb/evyuka_form_AUD.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_EKF.xml b/dspace/config/vsb/evyuka_form_EKF.xml index 91f72d669f2e..c697ae8a9013 100644 --- a/dspace/config/vsb/evyuka_form_EKF.xml +++ b/dspace/config/vsb/evyuka_form_EKF.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_FAST.xml b/dspace/config/vsb/evyuka_form_FAST.xml index dde4f5eaf0d8..4b34f8232f1b 100644 --- a/dspace/config/vsb/evyuka_form_FAST.xml +++ b/dspace/config/vsb/evyuka_form_FAST.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_FBI.xml b/dspace/config/vsb/evyuka_form_FBI.xml index b621b69079f0..05e8ec3f5b07 100644 --- a/dspace/config/vsb/evyuka_form_FBI.xml +++ b/dspace/config/vsb/evyuka_form_FBI.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_FEI.xml b/dspace/config/vsb/evyuka_form_FEI.xml index 8bc913415e48..755653db60a4 100644 --- a/dspace/config/vsb/evyuka_form_FEI.xml +++ b/dspace/config/vsb/evyuka_form_FEI.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_FMT.xml b/dspace/config/vsb/evyuka_form_FMT.xml index 509d52a5f1fa..e3baef37a9ce 100644 --- a/dspace/config/vsb/evyuka_form_FMT.xml +++ b/dspace/config/vsb/evyuka_form_FMT.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_FS.xml b/dspace/config/vsb/evyuka_form_FS.xml index 115aed94e8b9..e44e03b63d02 100644 --- a/dspace/config/vsb/evyuka_form_FS.xml +++ b/dspace/config/vsb/evyuka_form_FS.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_HGF.xml b/dspace/config/vsb/evyuka_form_HGF.xml index f04e59a72566..5896ab688bc7 100644 --- a/dspace/config/vsb/evyuka_form_HGF.xml +++ b/dspace/config/vsb/evyuka_form_HGF.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/evyuka_form_USP.xml b/dspace/config/vsb/evyuka_form_USP.xml index ae95c7fe5ef9..2b204b5cb465 100644 --- a/dspace/config/vsb/evyuka_form_USP.xml +++ b/dspace/config/vsb/evyuka_form_USP.xml @@ -98,6 +98,7 @@ dropdown Vyberte kódy všech verzí předmětu, kterých se dokument týká. Dokument bude dostupný pouze vyjmenovaným verzím předmětů. + evyuka-subject-codes @@ -110,6 +111,7 @@ dropdown Vyberte kódy všech předmětů, kterých se dokument týká. Dokument bude rovněž společný pro všechny verze těchto předmětů. + evyuka-subject-codes @@ -122,6 +124,7 @@ dropdown Vyberte kódy všech studijních oborů, kterých se dokument týká. + evyuka-discipline-programme-codes @@ -134,6 +137,7 @@ dropdown Vyberte kódy všech studijních programů, kterých se dokument týká. + evyuka-discipline-programme-codes diff --git a/dspace/config/vsb/update_forms.py b/dspace/config/vsb/update_forms.py new file mode 100644 index 000000000000..b1cc27994fbc --- /dev/null +++ b/dspace/config/vsb/update_forms.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +""" +Python script to update E-výuka forms with correct validation groups. + +This script ensures that all evyuka forms in the /vsb folder have the proper +validation groups configured for the E-výuka validation system. + +Validation Groups: +- evyuka-subject-codes: Applied to evyuka.subject.version and evyuka.subject fields +- evyuka-discipline-programme-codes: Applied to evyuka.discipline and evyuka.programme fields + +The script supports both the master EvyukaCodesValidation (field name-based detection) +and legacy validators (validation group-based detection). + +Author: Milan Majchrak (dspace at dataquest.sk) +""" + +import os +import re +import xml.etree.ElementTree as ET +from pathlib import Path +import argparse +import logging +from datetime import datetime + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') +logger = logging.getLogger(__name__) + +class EvyukaFormUpdater: + """Updates E-výuka forms with correct validation groups.""" + + # Validation group mappings based on current validation system + VALIDATION_GROUPS = { + 'evyuka.subject.version': 'evyuka-subject-codes', + 'evyuka.subject': 'evyuka-subject-codes', + 'evyuka.discipline': 'evyuka-discipline-programme-codes', + 'evyuka.programme': 'evyuka-discipline-programme-codes' + } + + def __init__(self, forms_dir: str): + """Initialize with forms directory path.""" + self.forms_dir = Path(forms_dir) + if not self.forms_dir.exists(): + raise FileNotFoundError(f"Forms directory not found: {forms_dir}") + + # Create backup directory with timestamp (consistent with existing naming) + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M") + self.backup_dir = self.forms_dir / f"update-forms-backup-{timestamp}" + self.backup_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Backup directory created: {self.backup_dir}") + + def get_evyuka_forms(self): + """Get list of all evyuka form files.""" + pattern = "evyuka_form_*.xml" + forms = list(self.forms_dir.glob(pattern)) + + # Exclude template file from processing + forms = [f for f in forms if 'template' not in f.name] + + # Extra safety check - ensure we only process evyuka_form_*.xml files + valid_forms = [] + for f in forms: + if f.name.startswith('evyuka_form_') and f.name.endswith('.xml'): + valid_forms.append(f) + else: + logger.warning(f"Skipping unexpected file: {f.name}") + + logger.info(f"Found {len(valid_forms)} evyuka forms to process") + if logger.isEnabledFor(logging.DEBUG): + for form in sorted(valid_forms): + logger.debug(f"Will process: {form.name}") + + return sorted(valid_forms) + + def backup_file(self, file_path: Path): + """Create backup of original file in dedicated backup directory.""" + backup_path = self.backup_dir / file_path.name + backup_path.write_text(file_path.read_text(encoding='utf-8'), encoding='utf-8') + logger.debug(f"Created backup: {backup_path}") + + def is_evyuka_field(self, field_element): + """Check if field element is an evyuka code field.""" + schema_elem = field_element.find('dc-schema') + element_elem = field_element.find('dc-element') + qualifier_elem = field_element.find('dc-qualifier') + + if schema_elem is None or element_elem is None or qualifier_elem is None: + return False, None + + schema = schema_elem.text + element = element_elem.text + qualifier = qualifier_elem.text or "" + + if schema == 'evyuka': + field_name = f"{schema}.{element}" + if qualifier: + field_name += f".{qualifier}" + + if field_name in self.VALIDATION_GROUPS: + return True, field_name + + return False, None + + + + def process_form(self, form_path: Path, create_backup=True): + """Process a single evyuka form file using text-based processing to preserve formatting.""" + # Safety check - ensure we only process evyuka_form_*.xml files + if not (form_path.name.startswith('evyuka_form_') and form_path.name.endswith('.xml')): + logger.error(f"SECURITY: Refusing to process non-evyuka form file: {form_path.name}") + return False + + logger.info(f"Processing {form_path.name}...") + + if create_backup: + self.backup_file(form_path) + + # Read the original file content + try: + original_content = form_path.read_text(encoding='utf-8') + except Exception as e: + logger.error(f"Failed to read {form_path}: {e}") + return False + + # Parse XML only for field detection + try: + tree = ET.parse(form_path) + root = tree.getroot() + except ET.ParseError as e: + logger.error(f"Failed to parse {form_path}: {e}") + return False + + validation_updates = [] + evyuka_fields_found = 0 + + # Find all field elements for analysis + for field in root.findall('.//field'): + is_evyuka, field_name = self.is_evyuka_field(field) + + if is_evyuka: + evyuka_fields_found += 1 + logger.debug(f"Found evyuka field: {field_name}") + + expected_group = self.VALIDATION_GROUPS[field_name] + hint_elem = field.find('hint') + hint_text = hint_elem.text if hint_elem is not None else "" + + validation_elem = field.find('validation-group') + + if validation_elem is not None: + # Check if update is needed + current_group = validation_elem.text + if current_group != expected_group: + validation_updates.append({ + 'field_name': field_name, + 'validation_group': expected_group, + 'old_validation_group': current_group, + 'hint_text': hint_text, + 'action': 'update' + }) + else: + # Add new validation group + if hint_text: # Only if hint exists + validation_updates.append({ + 'field_name': field_name, + 'validation_group': expected_group, + 'hint_text': hint_text, + 'action': 'add' + }) + else: + logger.warning(f"No hint element found for {field_name}, cannot add validation-group") + + if validation_updates: + # Apply only validation group changes + self.save_file_with_validation_groups_only(form_path, original_content, validation_updates) + logger.info(f"Updated {len(validation_updates)} validation groups in {form_path.name}") + else: + logger.info(f"No changes needed for {form_path.name}") + + logger.info(f"Found {evyuka_fields_found} evyuka fields in {form_path.name}") + return True + + def save_file_with_validation_groups_only(self, file_path, original_content, validation_updates): + """Save file with only validation group changes, preserving all other formatting.""" + updated_content = original_content + + for update in validation_updates: + field_name = update['field_name'] + validation_group = update['validation_group'] + hint_text = update['hint_text'] + action = update['action'] # 'add' or 'update' + + if action == 'add': + # Add validation-group after the hint element - escape special regex characters in hint text + escaped_hint = re.escape(hint_text) + pattern = f'(]*>{escaped_hint})' + replacement = f'\\1\n {validation_group}' + updated_content = re.sub(pattern, replacement, updated_content, count=1) + logger.debug(f"Added validation-group '{validation_group}' to {field_name}") + + elif action == 'update': + # Update existing validation-group + old_group = update['old_validation_group'] + pattern = f'{re.escape(old_group)}' + replacement = f'{validation_group}' + updated_content = re.sub(pattern, replacement, updated_content, count=1) + logger.debug(f"Updated validation-group for {field_name}: '{old_group}' -> '{validation_group}'") + + # Write the updated content + file_path.write_text(updated_content, encoding='utf-8') + + def process_all_forms(self, create_backup=True): + """Process all evyuka forms in the directory.""" + forms = self.get_evyuka_forms() + + if not forms: + logger.warning("No evyuka forms found to process") + return + + success_count = 0 + total_forms = len(forms) + + for form_path in forms: + try: + if self.process_form(form_path, create_backup): + success_count += 1 + except Exception as e: + logger.error(f"Error processing {form_path}: {e}") + + logger.info(f"Successfully processed {success_count}/{total_forms} forms") + + def validate_forms(self): + """Validate that all forms have correct validation groups.""" + logger.info("Validating evyuka forms...") + + forms = self.get_evyuka_forms() + validation_errors = [] + + for form_path in forms: + try: + tree = ET.parse(form_path) + root = tree.getroot() + + form_errors = [] + + # Check each field + for field in root.findall('.//field'): + is_evyuka, field_name = self.is_evyuka_field(field) + + if is_evyuka: + validation_elem = field.find('validation-group') + expected_group = self.VALIDATION_GROUPS[field_name] + + if validation_elem is None: + form_errors.append(f"Missing validation-group for {field_name}") + elif validation_elem.text != expected_group: + form_errors.append(f"Incorrect validation-group for {field_name}: got '{validation_elem.text}', expected '{expected_group}'") + + if form_errors: + validation_errors.append(f"{form_path.name}: {', '.join(form_errors)}") + else: + logger.info(f"✓ {form_path.name} - validation groups correct") + + except Exception as e: + validation_errors.append(f"{form_path.name}: Error parsing file - {e}") + + if validation_errors: + logger.error("Validation errors found:") + for error in validation_errors: + logger.error(f" {error}") + return False + else: + logger.info("All forms have correct validation groups!") + return True + + +def main(): + """Main function with command line interface.""" + parser = argparse.ArgumentParser( + description="Update E-výuka forms with correct validation groups", + epilog=""" +Examples: + %(prog)s /path/to/vsb # Process all forms in directory + %(prog)s /path/to/vsb --validate # Only validate forms + %(prog)s /path/to/vsb --no-backup # Process without creating backups + %(prog)s /path/to/vsb --verbose # Enable debug logging + """ + ) + + parser.add_argument('forms_dir', + help='Directory containing evyuka form XML files') + parser.add_argument('--validate', action='store_true', + help='Only validate forms, do not modify') + parser.add_argument('--no-backup', action='store_true', + help='Do not create backup files') + parser.add_argument('--verbose', action='store_true', + help='Enable verbose logging') + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + try: + updater = EvyukaFormUpdater(args.forms_dir) + + if args.validate: + success = updater.validate_forms() + exit(0 if success else 1) + else: + updater.process_all_forms(create_backup=not args.no_backup) + + # Validate after processing + logger.info("\nValidating processed forms...") + success = updater.validate_forms() + if not success: + logger.error("Some validation errors remain after processing") + exit(1) + + except FileNotFoundError as e: + logger.error(str(e)) + exit(1) + except Exception as e: + logger.error(f"Unexpected error: {e}") + exit(1) + + +if __name__ == '__main__': + main() \ No newline at end of file