/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.catalog.ui.query.monitor.impl; import static org.apache.commons.lang3.Validate.notEmpty; import static org.apache.commons.lang3.Validate.notNull; import static org.quartz.CronScheduleBuilder.cronSchedule; import static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; import java.io.Serializable; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.RecursiveTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.codice.ddf.catalog.ui.metacard.workspace.QueryMetacardImpl; import org.codice.ddf.catalog.ui.metacard.workspace.WorkspaceMetacardImpl; import org.codice.ddf.catalog.ui.query.monitor.api.FilterService; import org.codice.ddf.catalog.ui.query.monitor.api.QueryUpdateSubscriber; import org.codice.ddf.catalog.ui.query.monitor.api.SecurityService; import org.codice.ddf.catalog.ui.query.monitor.api.WorkspaceQueryService; import org.codice.ddf.catalog.ui.query.monitor.api.WorkspaceService; import org.codice.ddf.security.common.Security; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; import org.opengis.filter.And; import org.opengis.filter.Filter; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import ddf.catalog.CatalogFramework; import ddf.catalog.federation.FederationException; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.operation.QueryRequest; import ddf.catalog.operation.QueryResponse; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.source.SourceUnavailableException; import ddf.catalog.source.UnsupportedQueryException; import ddf.security.Subject; public class WorkspaceQueryServiceImpl implements WorkspaceQueryService { public static final String JOB_IDENTITY = "WorkspaceQueryServiceJob"; private static final Logger LOGGER = LoggerFactory.getLogger(WorkspaceQueryServiceImpl.class); private static final String UNKNOWN_SOURCE = "unknown"; private static final String TRIGGER_NAME = "WorkspaceQueryTrigger"; private static final Security SECURITY = Security.getInstance(); private final QueryUpdateSubscriber queryUpdateSubscriber; private final WorkspaceService workspaceService; private final CatalogFramework catalogFramework; private final FilterBuilder filterBuilder; @SuppressWarnings("FieldCanBeLocal") private Scheduler scheduler; private SecurityService securityService; private FilterService filterService; private long queryTimeoutMinutes; private Integer queryTimeInterval; private JobDetail jobDetail; private Subject subject; /** * @param queryUpdateSubscriber must be non-null * @param workspaceService must be non-null * @param catalogFramework must be non-null * @param filterBuilder must be non-null * @param schedulerSupplier must be non-null * @param securityService must be non-null * @param filterService must be non-null */ public WorkspaceQueryServiceImpl(QueryUpdateSubscriber queryUpdateSubscriber, WorkspaceService workspaceService, CatalogFramework catalogFramework, FilterBuilder filterBuilder, Supplier<Optional<Scheduler>> schedulerSupplier, SecurityService securityService, FilterService filterService) throws SchedulerException { notNull(queryUpdateSubscriber, "queryUpdateSubscriber must be non-null"); notNull(workspaceService, "workspaceService must be non-null"); notNull(catalogFramework, "catalogFramework must be non-null"); notNull(filterBuilder, "filterBuilder must be non-null"); notNull(schedulerSupplier, "scheduleSupplier must be non-null"); notNull(securityService, "securityService must be non-null"); notNull(filterService, "filterService must be non-null"); this.queryUpdateSubscriber = queryUpdateSubscriber; this.workspaceService = workspaceService; this.catalogFramework = catalogFramework; this.filterBuilder = filterBuilder; this.securityService = securityService; this.filterService = filterService; Optional<Scheduler> schedulerOptional = schedulerSupplier.get(); if (schedulerOptional.isPresent()) { scheduler = schedulerOptional.get(); scheduler.getContext() .put(JOB_IDENTITY, this); jobDetail = newJob(QueryJob.class).withIdentity(JOB_IDENTITY) .build(); scheduler.start(); } else { LOGGER.warn("unable to get a quartz scheduler object, email notifications will not run"); } } public void setQueryTimeInterval(Integer queryTimeInterval) { notNull(queryTimeInterval, "queryTimeInterval must be non-null"); if (queryTimeInterval > 0 && queryTimeInterval <= 1440) { LOGGER.debug("Setting query time interval : {}", queryTimeInterval); this.queryTimeInterval = queryTimeInterval; } else if (this.queryTimeInterval == null) { this.queryTimeInterval = 1440; } } public Integer getQueryTimeInterval() { return this.queryTimeInterval; } /** * @param cronString cron string (must be non-null) */ @SuppressWarnings("unused") public void setCronString(String cronString) { notNull(cronString, "cronString must be non-null"); notNull(scheduler, "scheduler must be non-null"); notNull(jobDetail, "jobDetail must be non-null"); try { scheduler.deleteJob(jobDetail.getKey()); LOGGER.debug("Scheduling job {}", jobDetail); CronTrigger trigger = newTrigger().withIdentity(TRIGGER_NAME) .startNow() .withSchedule(cronSchedule(cronString)) .build(); scheduler.scheduleJob(jobDetail, trigger); LOGGER.debug("Setting cron string : {}", cronString); } catch (SchedulerException e) { LOGGER.warn("Unable to update scheduler with cron string: cron=[{}]", cronString, e); } } /** * @param queryTimeoutMinutes minutes (must be non-null) */ @SuppressWarnings("unused") public void setQueryTimeoutMinutes(Long queryTimeoutMinutes) { notNull(queryTimeoutMinutes, "queryTimeoutMinutes must be non-null"); LOGGER.debug("Setting queryTimeOutMinutes : {}", queryTimeoutMinutes); this.queryTimeoutMinutes = queryTimeoutMinutes; } public void setSubject(Subject subject) { this.subject = subject; } public void destroy() { LOGGER.trace("Shutting down"); try { scheduler.shutdown(); } catch (SchedulerException e) { LOGGER.warn("Unable to shut down scheduler", e); } } /** * Main entry point, should be called by a scheduler. */ public void run() { SECURITY.runAsAdmin(() -> { Subject runSubject = subject != null ? subject : SECURITY.getSystemSubject(); return runSubject.execute(() -> { LOGGER.trace("running workspace query service"); Map<String, Pair<WorkspaceMetacardImpl, List<QueryMetacardImpl>>> queryMetacards = workspaceService.getQueryMetacards(); LOGGER.debug("queryMetacards: size={}", queryMetacards.size()); List<WorkspaceTask> workspaceTasks = createWorkspaceTasks(queryMetacards); LOGGER.debug("workspaceTasks: size={}", workspaceTasks.size()); Map<String, Pair<WorkspaceMetacardImpl, Long>> results = executeWorkspaceTasks( workspaceTasks, queryTimeoutMinutes, TimeUnit.MINUTES); LOGGER.debug("results: {}", results); queryUpdateSubscriber.notify(results); return null; }); }); } private Map<String, Pair<WorkspaceMetacardImpl, Long>> executeWorkspaceTasks( List<WorkspaceTask> workspaceTasks, long timeout, TimeUnit timeoutUnit) { Map<String, Pair<WorkspaceMetacardImpl, Long>> results = new ConcurrentHashMap<>(); workspaceTasks.stream() .map(ForkJoinPool.commonPool()::submit) .map(task -> getTaskResult(task, timeout, timeoutUnit)) .filter(Objects::nonNull) .forEach(pair -> results.put(pair.getLeft() .getId(), new ImmutablePair<>(pair.getLeft(), pair.getRight()))); return results; } private Pair<WorkspaceMetacardImpl, Long> getTaskResult( ForkJoinTask<Pair<WorkspaceMetacardImpl, Long>> workspaceTask, long timeout, TimeUnit timeoutUnit) { try { return workspaceTask.get(timeout, timeoutUnit); } catch (TimeoutException e) { LOGGER.warn("Timeout", e); } catch (ExecutionException | InterruptedException e) { LOGGER.warn("ForkJoinPool error", e); } return null; } private List<WorkspaceTask> createWorkspaceTasks( Map<String, Pair<WorkspaceMetacardImpl, List<QueryMetacardImpl>>> queryMetacards) { List<WorkspaceTask> workspaceTasks = new ArrayList<>(); for (Pair<WorkspaceMetacardImpl, List<QueryMetacardImpl>> workspaceQueryPair : queryMetacards.values()) { Map<String, List<QueryMetacardImpl>> queryMetacardsGroupedBySource = groupBySource( workspaceQueryPair.getRight()); List<QueryRequest> queryRequests = getQueryRequests(queryMetacardsGroupedBySource.values() .stream()); if (!queryRequests.isEmpty()) { workspaceTasks.add(new WorkspaceTask(workspaceQueryPair.getLeft(), queryRequests)); } } return workspaceTasks; } private Map<String, List<QueryMetacardImpl>> groupBySource( List<QueryMetacardImpl> queryMetacards) { final Map<String, List<QueryMetacardImpl>> groupedBySource = new HashMap<>(); for (QueryMetacardImpl queryMetacard : queryMetacards) { List<String> sources = queryMetacard.getSources(); if (!sources.isEmpty()) { sources.forEach(sourceId -> groupedBySource.compute(sourceId, addToList(queryMetacard))); } else { groupedBySource.compute(UNKNOWN_SOURCE, addToList(queryMetacard)); } } return groupedBySource; } private BiFunction<String, List<QueryMetacardImpl>, List<QueryMetacardImpl>> addToList( QueryMetacardImpl queryMetacard) { return (id, queries) -> { if (queries == null) { return Lists.newArrayList(queryMetacard); } else { queries.add(queryMetacard); return queries; } }; } private List<QueryRequest> getQueryRequests( Stream<List<QueryMetacardImpl>> queriesGroupedBySource) { final Filter modifiedFilter = filterService.getModifiedDateFilter(calculateQueryTimeInterval()); return queriesGroupedBySource.map(this::queryMetacardsToFilters) .map(filterBuilder::anyOf) .map(filter -> filterBuilder.allOf(modifiedFilter, filter)) .map(this::filterToQuery) .map(this::queryToQueryRequest) .collect(Collectors.toList()); } private List<Filter> queryMetacardsToFilters(List<QueryMetacardImpl> queriesForSource) { return queriesForSource.stream() .map(this::metacardToFilter) .filter(Objects::nonNull) .collect(Collectors.toList()); } private QueryRequestImpl queryToQueryRequest(QueryImpl query) { final Map<String, Serializable> properties = securityService.addSystemSubject(new HashMap<>()); return new QueryRequestImpl(query, properties); } private QueryImpl filterToQuery(And filter) { final QueryImpl query = new QueryImpl(filter); query.setRequestsTotalResultsCount(true); return query; } private Filter metacardToFilter(QueryMetacardImpl queryMetacard) { try { return ECQL.toFilter(queryMetacard.getCql()); } catch (CQLException e) { LOGGER.warn("Error parsing CQL", e); return null; } } private Date calculateQueryTimeInterval() { return Date.from(Instant.now() .minus(queryTimeInterval, ChronoUnit.MINUTES)); } private class QueryTask extends RecursiveTask<Long> { private final QueryRequest queryRequest; private QueryTask(QueryRequest queryRequest) { this.queryRequest = queryRequest; } @Override protected Long compute() { try { final QueryResponse response = catalogFramework.query(queryRequest); return response.getHits(); } catch (UnsupportedQueryException | FederationException | SourceUnavailableException e) { LOGGER.warn("Query error", e); return 0L; } } } private class WorkspaceTask extends RecursiveTask<Pair<WorkspaceMetacardImpl, Long>> { private final WorkspaceMetacardImpl workspaceMetacard; private final List<QueryRequest> queryRequests; private WorkspaceTask(WorkspaceMetacardImpl workspaceMetacard, List<QueryRequest> queryRequests) { notNull(workspaceMetacard, "WorkspaceMetacardImpl must be non-null"); notNull(queryRequests, "queryRequests must be non-null"); notEmpty(queryRequests, "queryRequests must be non-empty"); this.workspaceMetacard = workspaceMetacard; this.queryRequests = queryRequests; } @Override protected Pair<WorkspaceMetacardImpl, Long> compute() { final long result = queryRequests.stream() .map(QueryTask::new) .map(QueryTask::fork) .mapToLong(ForkJoinTask::join) .sum(); return Pair.of(workspaceMetacard, result); } } }