diff --git a/dhis-2/dhis-api/pom.xml b/dhis-2/dhis-api/pom.xml index 397c0d4399e9..7de32b7e5330 100644 --- a/dhis-2/dhis-api/pom.xml +++ b/dhis-2/dhis-api/pom.xml @@ -97,6 +97,10 @@ org.hisp.dhis.rules rule-engine + + org.hisp.dhis + json-tree + org.springframework.security spring-security-core diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java index fd12a9b467d2..0e62820ab32d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java @@ -30,7 +30,9 @@ import java.io.InputStream; import java.util.List; import java.util.Map; +import javax.annotation.Nonnull; import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.schema.Property; import org.springframework.util.MimeType; @@ -160,6 +162,13 @@ String create(JobConfiguration config, MimeType contentType, InputStream content */ List getStaleConfigurations(int staleForSeconds); + /** + * @param params query parameters (criteria) to find + * @return all job configurations that match the query parameters + */ + @Nonnull + List findJobRunErrors(@Nonnull JobRunErrorsParams params); + /** * Get a map of parameter classes with appropriate properties This can be used for a frontend app * or for other appropriate applications which needs information about the jobs in the system. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java index a0d90d73df58..40402cdd79db 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java @@ -115,6 +115,13 @@ public interface JobConfigurationStore extends GenericDimensionalObjectStore getDueJobConfigurations(boolean includeWaiting); + /** + * @param params query parameters (criteria) to find + * @return all job configurations that match the query parameters + */ + @Nonnull + Stream findJobRunErrors(@Nonnull JobRunErrorsParams params); + /** * @return A list of all job types that are currently in {@link JobStatus#RUNNING} state. */ diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobProgress.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobProgress.java index 932cbf92a6f7..ee0f14d50f0f 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobProgress.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobProgress.java @@ -142,12 +142,18 @@ default boolean isSkipCurrentStage() { Error reporting API: */ - default void addError(ErrorCode code, String uid, String type, Integer index, String... args) { - addError(code, uid, type, index, List.of(args)); + default void addError( + @Nonnull ErrorCode code, @CheckForNull String uid, @Nonnull String type, String... args) { + addError(code, uid, type, List.of(args)); } - default void addError(ErrorCode code, String uid, String type, Integer index, List args) { - // default implementation is a NOOP, we don't remember or handle the error + default void addError( + @Nonnull ErrorCode code, + @CheckForNull String uid, + @Nonnull String type, + @Nonnull List args) { + // is overridden by a tracker that collects errors + // default is to not collect errors } /* @@ -590,10 +596,13 @@ public Progress( } public void addError(Error error) { - errors - .computeIfAbsent(error.getId(), key -> new ConcurrentHashMap<>()) - .computeIfAbsent(error.getCode(), key2 -> new ConcurrentLinkedQueue<>()) - .add(error); + Queue sameObjectAndCode = + errors + .computeIfAbsent(error.getId(), key -> new ConcurrentHashMap<>()) + .computeIfAbsent(error.getCode(), key2 -> new ConcurrentLinkedQueue<>()); + if (sameObjectAndCode.stream().noneMatch(e -> e.args.equals(error.args))) { + sameObjectAndCode.add(error); + } } public boolean hasErrors() { @@ -619,13 +628,6 @@ final class Error { /** The type of the object identified by #id that has the error */ @Nonnull @JsonProperty private final String type; - /** - * The row index in the payload of the import. This is the index in the list of objects of a - * single type. This means the same index occurs for each object type. For some imports this - * information is not available. - */ - @CheckForNull @JsonProperty private final Integer index; - /** The arguments used in the {@link #code}'s {@link ErrorCode#getMessage()} template */ @Nonnull @JsonProperty private final List args; @@ -642,12 +644,10 @@ public Error( @Nonnull @JsonProperty("code") ErrorCode code, @Nonnull @JsonProperty("id") String id, @Nonnull @JsonProperty("type") String type, - @CheckForNull @JsonProperty("index") Integer index, @Nonnull @JsonProperty("args") List args) { this.code = code; this.id = id; this.type = type; - this.index = index; this.args = args; } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrorsParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrorsParams.java new file mode 100644 index 000000000000..f7d0f28c74e1 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrorsParams.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.scheduling; + +import java.util.Date; +import java.util.List; +import javax.annotation.CheckForNull; +import lombok.Data; +import lombok.experimental.Accessors; +import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.feedback.ErrorCode; +import org.hisp.dhis.user.User; + +/** + * Query params when searching for {@link JobConfiguration}s with errors. + * + *

A match has to satisfy all filters (AND logic) but only one of the given codes or object + * {@link UID} (OR logic). + * + *

If any of the criteria is not defined it has no filter effect. + * + * @author Jan Bernitt + */ +@Data +@Accessors(chain = true) +public class JobRunErrorsParams { + + @OpenApi.Ignore @CheckForNull private UID job; + + /** The user that ran the job */ + @OpenApi.Property({UID.class, User.class}) + @CheckForNull + private UID user; + + /** The earliest date the job ran that should be included */ + @CheckForNull private Date from; + + /** The latest date the job ran that should be included */ + @CheckForNull private Date to; + + /** The codes to select, any match combined */ + @CheckForNull private List code; + + /** The object with errors to select, any match combined */ + @CheckForNull private List object; + + /** The {@link JobType} with errors to select, any match combined */ + @CheckForNull private List type; +} diff --git a/dhis-2/dhis-services/dhis-service-core/pom.xml b/dhis-2/dhis-services/dhis-service-core/pom.xml index 67b693b9aacb..4110907e6117 100644 --- a/dhis-2/dhis-services/dhis-service-core/pom.xml +++ b/dhis-2/dhis-services/dhis-service-core/pom.xml @@ -117,6 +117,11 @@ org.hibernate hibernate-core + + com.vladmihalcea + hibernate-types-52 + ${hibernate-types.version} + com.fasterxml.jackson.core jackson-core diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java index 68b40d57e50f..28adb9f5c462 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java @@ -40,6 +40,7 @@ import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -48,8 +49,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.beanutils.PropertyUtils; @@ -60,9 +63,11 @@ import org.hisp.dhis.common.NameableObject; import org.hisp.dhis.commons.util.TextUtils; import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.fileresource.FileResource; import org.hisp.dhis.fileresource.FileResourceDomain; import org.hisp.dhis.fileresource.FileResourceService; +import org.hisp.dhis.jsontree.*; import org.hisp.dhis.scheduling.JobType.Defaults; import org.hisp.dhis.schema.Property; import org.hisp.dhis.setting.SettingKey; @@ -247,6 +252,54 @@ public List getStaleConfigurations(int staleForSeconds) { return jobConfigurationStore.getStaleConfigurations(staleForSeconds); } + @Nonnull + @Override + @Transactional(readOnly = true) + public List findJobRunErrors(@Nonnull JobRunErrorsParams params) { + Function toObject = + json -> { + JsonObject obj = JsonMixed.of(json); + List flatErrors = new ArrayList<>(); + JsonObject errors = obj.getObject("errors"); + errors + .node() + .members() + .forEach( + byObject -> + byObject + .getValue() + .members() + .forEach( + byCode -> + byCode + .getValue() + .elements() + .forEach( + error -> { + ErrorCode code = + ErrorCode.valueOf( + JsonMixed.of(error).getString("code").string()); + Object[] args = + JsonMixed.of(error) + .getArray("args") + .stringValues() + .toArray(new String[0]); + String msg = + MessageFormat.format(code.getMessage(), args); + flatErrors.add( + error + .extract() + .addMembers(e -> e.addString("message", msg))); + }))); + return JsonMixed.of( + errors + .node() + .replaceWith( + JsonBuilder.createArray(arr -> flatErrors.forEach(arr::addElement)))); + }; + return jobConfigurationStore.findJobRunErrors(params).map(toObject).toList(); + } + @Override @Transactional(readOnly = true) public Map> getJobParametersSchema() { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerLoopService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerLoopService.java index 27c2aedb29fa..ceb9d77c38c2 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerLoopService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerLoopService.java @@ -285,7 +285,7 @@ private void updateProgress(@Nonnull String jobId) { try { JobProgress.Progress progress = job.getProgress(); String errorCodes = - progress.getErrorCodes().stream().map(ErrorCode::name).collect(joining(" ")); + progress.getErrorCodes().stream().map(ErrorCode::name).sorted().collect(joining(" ")); jobConfigurationStore.updateProgress( jobId, jsonMapper.writeValueAsString(progress), errorCodes); } catch (JsonProcessingException ex) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java index 44653469e464..0ea58d6f928b 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java @@ -31,6 +31,8 @@ import static java.util.stream.Collectors.toSet; import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; +import com.vladmihalcea.hibernate.type.array.StringArrayType; +import java.util.Date; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -40,7 +42,10 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.SessionFactory; import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; +import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.user.CurrentUserService; import org.springframework.context.ApplicationEventPublisher; @@ -243,6 +248,65 @@ public Stream getDueJobConfigurations(boolean includeWaiting) .stream(); } + @Nonnull + @Override + public Stream findJobRunErrors(@Nonnull JobRunErrorsParams params) { + // language=SQL + String sql = + """ + select jsonb_build_object( + 'id', c.uid, + 'type', c.jobType, + 'user', c.executedby, + 'created', c.created, + 'executed', c.lastexecuted, + 'finished', c.lastfinished, + 'filesize', fr.contentlength, + 'filetype', fr.contenttype, + 'errors', c.progress -> 'errors') #>> '{}' + from jobconfiguration c left join fileresource fr on c.uid = fr.uid + where c.errorcodes is not null and c.errorcodes != '' + and (:skipUid or c.uid = :uid) + and (:skipUser or c.executedby = :user) + and (:skipStart or c.lastexecuted >= :start) + and (:skipEnd or c.lastexecuted <= :end) + and (:skipObjects or jsonb_exists_any(c.progress -> 'errors', :objects )) + and (:skipCodes or string_to_array(c.errorcodes, ' ') && :codes) + and (:skipTypes or c.jobtype = any (:types)) + order by c.lastexecuted desc; + """; + List objectList = params.getObject(); + List errors = + objectList == null ? List.of() : objectList.stream().map(UID::getValue).toList(); + List codeList = params.getCode(); + List codes = + codeList == null ? List.of() : codeList.stream().map(ErrorCode::name).toList(); + List typeList = params.getType(); + List types = + typeList == null ? List.of() : typeList.stream().map(JobType::name).toList(); + Date start = params.getFrom(); + Date end = params.getTo(); + UID user = params.getUser(); + UID job = params.getJob(); + return getResultStream( + nativeQuery(sql) + .setParameter("skipUid", job == null) + .setParameter("uid", job == null ? "" : job.getValue()) + .setParameter("skipUser", user == null) + .setParameter("user", user == null ? "" : user.getValue()) + .setParameter("skipStart", start == null) + .setParameter("start", start == null ? new Date() : start) + .setParameter("skipEnd", end == null) + .setParameter("end", end == null ? new Date() : end) + .setParameter("skipObjects", errors.isEmpty()) + .setParameter("objects", errors.toArray(String[]::new), StringArrayType.INSTANCE) + .setParameter("skipCodes", codes.isEmpty()) + .setParameter("codes", codes.toArray(String[]::new), StringArrayType.INSTANCE) + .setParameter("skipTypes", types.isEmpty()) + .setParameter("types", types.toArray(String[]::new), StringArrayType.INSTANCE), + Object::toString); + } + @Override @Transactional(propagation = REQUIRES_NEW) public boolean tryExecuteNow(@Nonnull String jobId) { @@ -468,8 +532,14 @@ private static String getSingleResultOrNull(NativeQuery query) { } @SuppressWarnings("unchecked") - private static Set getResultSet(NativeQuery query, Function mapper) { + private static Set getResultSet(Query query, Function mapper) { Stream stream = (Stream) query.stream(); return stream.map(mapper).collect(toSet()); } + + @SuppressWarnings("unchecked") + private static Stream getResultStream(Query query, Function mapper) { + Stream stream = (Stream) query.stream(); + return stream.map(mapper); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java index 2d0938b9ceb7..f3367f15185a 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java @@ -149,13 +149,12 @@ public void addError( @Nonnull ErrorCode code, @CheckForNull String uid, @Nonnull String type, - @CheckForNull Integer index, @Nonnull List args) { try { // Note: we use empty string in case the UID is not known/defined yet to allow use in maps - progress.addError(new Error(code, uid == null ? "" : uid, type, index, args)); + progress.addError(new Error(code, uid == null ? "" : uid, type, args)); } catch (Exception ex) { - log.error("Failed to add error: %s %s %s %d %s".formatted(code, uid, type, index, args), ex); + log.error("Failed to add error: %s %s %s %s".formatted(code, uid, type, args), ex); } } diff --git a/dhis-2/dhis-support/dhis-support-commons/pom.xml b/dhis-2/dhis-support/dhis-support-commons/pom.xml index f10142563aa8..ceac056ad74f 100644 --- a/dhis-2/dhis-support/dhis-support-commons/pom.xml +++ b/dhis-2/dhis-support/dhis-support-commons/pom.xml @@ -24,6 +24,10 @@ org.hisp.dhis dhis-api + + org.hisp.dhis + json-tree + org.apache.commons commons-lang3 diff --git a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JacksonObjectMapperConfig.java b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JacksonObjectMapperConfig.java index 9cb1ed4fe58d..7a9027a9330d 100644 --- a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JacksonObjectMapperConfig.java +++ b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JacksonObjectMapperConfig.java @@ -49,6 +49,7 @@ import org.hisp.dhis.commons.jackson.config.geometry.JtsXmlModule; import org.hisp.dhis.dataexchange.aggregate.Api; import org.hisp.dhis.dataexchange.aggregate.ApiSerializer; +import org.hisp.dhis.jsontree.JsonValue; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.PrecisionModel; @@ -153,6 +154,7 @@ private static ObjectMapper configureMapper( module.addSerializer(Date.class, new WriteDateStdSerializer()); module.addSerializer(JsonPointer.class, new JsonPointerStdSerializer()); module.addSerializer(Api.class, new ApiSerializer()); + module.addSerializer(JsonValue.class, new JsonValueSerializer()); // Registering a custom Instant serializer/deserializer for DTOs JavaTimeModule javaTimeModule = new JavaTimeModule(); diff --git a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JsonValueSerializer.java b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JsonValueSerializer.java new file mode 100644 index 000000000000..cbd08c9b9ccc --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/config/JsonValueSerializer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.commons.jackson.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import org.hisp.dhis.jsontree.JsonValue; + +/** + * @author Jan Bernitt + */ +public class JsonValueSerializer extends JsonSerializer { + + @Override + public void serialize(JsonValue obj, JsonGenerator generator, SerializerProvider provider) + throws IOException { + if (obj == null) { + generator.writeNull(); + } else { + generator.writeRawValue(obj.node().getDeclaration()); + } + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/DhisControllerIntegrationTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/DhisControllerIntegrationTest.java index 512e4518a0cb..534feb02ea57 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/DhisControllerIntegrationTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/DhisControllerIntegrationTest.java @@ -27,6 +27,8 @@ */ package org.hisp.dhis.webapi; +import java.time.Duration; +import java.util.function.BooleanSupplier; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; import org.hisp.dhis.IntegrationTest; @@ -107,4 +109,14 @@ protected void integrationTestBefore() throws Exception { Configurator.setRootLevel(Level.INFO); } } + + protected static boolean await(Duration timeout, BooleanSupplier test) + throws InterruptedException { + while (!timeout.isNegative() && !test.getAsBoolean()) { + Thread.sleep(20); + timeout = timeout.minusMillis(20); + } + if (!timeout.isNegative()) return true; + return test.getAsBoolean(); + } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobConfigurationRunErrorsControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobConfigurationRunErrorsControllerTest.java new file mode 100644 index 000000000000..0a4971250222 --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobConfigurationRunErrorsControllerTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller; + +import static java.time.Duration.ofSeconds; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.BooleanSupplier; +import org.hisp.dhis.jsontree.JsonArray; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonNodeType; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.scheduling.JobStatus; +import org.hisp.dhis.web.HttpStatus; +import org.hisp.dhis.webapi.DhisControllerIntegrationTest; +import org.hisp.dhis.webapi.json.domain.JsonJobConfiguration; +import org.hisp.dhis.webapi.json.domain.JsonWebMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests the job run error result API. + * + * @author Jan Bernitt + */ +class JobConfigurationRunErrorsControllerTest extends DhisControllerIntegrationTest { + + private String jobId; + + @BeforeEach + void setUp() throws InterruptedException { + jobId = createAndRunImportWithErrors(); + } + + @Test + void testGetJobRunErrors_List() { + JsonArray list = GET("/jobConfigurations/errors").content(); + + assertEquals(1, list.size()); + JsonObject job = list.getObject(0); + assertEquals(jobId, job.getString("id").string()); + assertEquals(1, job.getArray("errors").size()); + } + + @Test + void testGetJobRunErrors_ListFilterUser() { + JsonArray list = + GET("/jobConfigurations/errors?user={user}", getCurrentUser().getUid()).content(); + assertEquals(1, list.size()); + assertEquals(0, GET("/jobConfigurations/errors?user=abcde123456").content().size()); + } + + @Test + void testGetJobRunErrors_ListFilterFrom() { + assertEquals(1, GET("/jobConfigurations/errors?from=2023-01-01").content().size()); + assertEquals(0, GET("/jobConfigurations/errors?from=2033-01-01").content().size()); + } + + @Test + void testGetJobRunErrors_ListFilterTo() { + assertEquals(1, GET("/jobConfigurations/errors?to=2033-01-01").content().size()); + assertEquals(0, GET("/jobConfigurations/errors?to=2023-01-01").content().size()); + } + + @Test + void testGetJobRunErrors_ListFilterCode() { + assertEquals(1, GET("/jobConfigurations/errors?code=E4000").content().size()); + assertEquals(0, GET("/jobConfigurations/errors?code=E5000").content().size()); + } + + @Test + void testGetJobRunErrors_ListFilterType() { + assertEquals(1, GET("/jobConfigurations/errors?type=METADATA_IMPORT").content().size()); + assertEquals(0, GET("/jobConfigurations/errors?type=DATA_INTEGRITY").content().size()); + } + + @Test + void testGetJobRunErrors_Object() { + JsonObject job = GET("/jobConfigurations/{uid}/errors", jobId).content(); + assertEquals(jobId, job.getString("id").string()); + assertEquals("METADATA_IMPORT", job.getString("type").string()); + assertTrue(job.has("created", "executed", "finished", "user", "filesize", "filetype")); + assertEquals(1, job.getArray("errors").size()); + } + + @Test + void testGetJobRunErrors_ObjectProgressErrors() { + JsonArray errors = GET("/jobConfigurations/{uid}/progress/errors", jobId).content(); + assertEquals(JsonNodeType.ARRAY, errors.node().getType()); + assertEquals(1, errors.size()); + } + + private String createAndRunImportWithErrors() throws InterruptedException { + JsonWebMessage message = + POST( + "/metadata?async=true", + "{'organisationUnits':[{'name':'My Unit', 'shortName':'OU1'}]}") + .content(HttpStatus.OK) + .as(JsonWebMessage.class); + String jobId = message.getString("response.id").string(); + + BooleanSupplier jobCompleted = + () -> isDone(GET("/jobConfigurations/{id}/gist?fields=id,jobStatus", jobId).content()); + assertTrue(await(ofSeconds(10), jobCompleted), "import did not run"); + return jobId; + } + + private static boolean isDone(JsonMixed config) { + JsonJobConfiguration c = config.as(JsonJobConfiguration.class); + return c.getJobStatus() == JobStatus.COMPLETED || c.getJobStatus() == JobStatus.DISABLED; + } +} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportJob.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportJob.java index 1aaa4126861f..4af6ddd09ca5 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportJob.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportJob.java @@ -108,7 +108,6 @@ public void execute(JobConfiguration config, JobProgress progress) { r.getErrorCode(), r.getMainId(), r.getMainKlass().getSimpleName(), - null, r.getArgs())); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java index 085f7f97c01a..11aae6ce13d6 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java @@ -34,15 +34,12 @@ import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.IdentifiableObjects; import org.hisp.dhis.common.OpenApi; -import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.feedback.ObjectReport; -import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobProgress; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.feedback.*; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.scheduling.*; import org.hisp.dhis.scheduling.JobProgress.Progress; -import org.hisp.dhis.scheduling.JobSchedulerService; import org.hisp.dhis.schema.Property; import org.hisp.dhis.schema.descriptors.JobConfigurationSchemaDescriptor; import org.hisp.dhis.user.CurrentUser; @@ -74,6 +71,25 @@ public class JobConfigurationController extends AbstractCrudController getJobRunErrors(JobRunErrorsParams params) { + return jobConfigurationService.findJobRunErrors(params); + } + + @GetMapping("{uid}/errors") + public JsonObject getJobRunErrors( + @PathVariable("uid") @OpenApi.Param({UID.class, JobConfiguration.class}) UID uid) + throws NotFoundException { + List errors = + jobConfigurationService.findJobRunErrors(new JobRunErrorsParams().setJob(uid)); + if (errors.isEmpty()) { + JobConfiguration obj = jobConfigurationService.getJobConfigurationByUid(uid.getValue()); + if (obj == null) throw new NotFoundException(JobConfiguration.class, uid.getValue()); + return JsonMixed.of("{}"); + } + return errors.get(0); + } + @GetMapping("/due") public List getDueJobConfigurations( @RequestParam int seconds, @@ -131,7 +147,7 @@ public Progress getProgress(@PathVariable("uid") String uid) { } @PreAuthorize("hasRole('ALL') or hasRole('F_PERFORM_MAINTENANCE')") - @GetMapping("{uid}/errors") + @GetMapping("{uid}/progress/errors") public List getErrors(@PathVariable("uid") String uid) { return jobSchedulerService.getErrors(uid); } diff --git a/dhis-2/dhis-web-embedded-jetty/pom.xml b/dhis-2/dhis-web-embedded-jetty/pom.xml index 247ccfd08fca..66fcb3563c24 100644 --- a/dhis-2/dhis-web-embedded-jetty/pom.xml +++ b/dhis-2/dhis-web-embedded-jetty/pom.xml @@ -34,11 +34,6 @@ dhis-service-dxf2 jar - - org.hisp.dhis - dhis-service-core - jar - org.hisp.dhis dhis-service-administration diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 22dcd0e7b27e..133c5d019a62 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -91,7 +91,7 @@ 1.4.1 2.0.0 - 0.10.1 + 0.10.3 5.8.8