/** * 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.ui.searchui.query.service; import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.inject.Inject; import org.apache.commons.lang.StringUtils; import org.codice.ddf.ui.searchui.query.controller.SearchController; import org.codice.ddf.ui.searchui.query.model.Search; import org.codice.ddf.ui.searchui.query.model.SearchRequest; import org.cometd.annotation.Listener; import org.cometd.annotation.Service; import org.cometd.annotation.Session; import org.cometd.bayeux.Message; import org.cometd.bayeux.server.BayeuxContext; import org.cometd.bayeux.server.BayeuxServer; import org.cometd.bayeux.server.ConfigurableServerChannel; import org.cometd.bayeux.server.ServerMessage; import org.cometd.bayeux.server.ServerSession; import org.cometd.server.ServerMessageImpl; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.Metacard; import ddf.catalog.data.Result; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.filter.FilterDelegate; import ddf.catalog.filter.impl.SortByImpl; import ddf.catalog.operation.Query; import ddf.catalog.operation.SourceInfoResponse; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.SourceInfoRequestEnterprise; import ddf.catalog.source.SourceDescriptor; import ddf.catalog.source.SourceUnavailableException; import ddf.security.SecurityConstants; import ddf.security.Subject; /** * This class performs the searches when a client communicates with the cometd endpoint */ @Service public class SearchService { public static final String LOCAL_SOURCE = "local"; private static final Logger LOGGER = LoggerFactory.getLogger(SearchService.class); private static final String ID = "id"; private static final String SOURCES = "src"; private static final String MAX_TIMEOUT = "timeout"; private static final String START_INDEX = "start"; private static final String COUNT = "count"; private static final String CQL_FILTER = "cql"; private static final String SORT = "sort"; private static final String DEFAULT_SORT_ORDER = "desc"; private static final long DEFAULT_TIMEOUT = 300000; private static final int DEFAULT_COUNT = 10; private static final int DEFAULT_START_INDEX = 1; private final FilterBuilder filterBuilder; private final SearchController searchController; @Inject private BayeuxServer bayeux; @Session private ServerSession serverSession; /** * Creates a new SearchService * * @param filterBuilder * - FilterBuilder to use for queries * @param searchController * - SearchController to handle async queries */ public SearchService(FilterBuilder filterBuilder, SearchController searchController) { this.filterBuilder = filterBuilder; this.searchController = searchController; } /** * Service method called by Cometd when something arrives on the service channel * * @param remote * - Client session * @param message * - JSON message */ @Listener("/service/query") public void processQuery(final ServerSession remote, Message message) { ServerMessage.Mutable reply = new ServerMessageImpl(); Map<String, Object> queryMessage = message.getDataAsMap(); if (queryMessage != null && queryMessage.containsKey(Search.ID)) { bayeux.createChannelIfAbsent("/" + queryMessage.get(Search.ID), new ConfigurableServerChannel.Initializer() { public void configureChannel(ConfigurableServerChannel channel) { channel.setPersistent(true); } }); BayeuxContext context = bayeux.getContext(); Subject subject = null; if (context != null) { subject = (Subject) context.getRequestAttribute(SecurityConstants.SECURITY_SUBJECT); } // kick off the query executeQuery(queryMessage, subject); reply.put(Search.SUCCESSFUL, true); remote.deliver(serverSession, reply); } else { reply.put(Search.SUCCESSFUL, false); reply.put("status", "ERROR: unable to return results, no id in query request"); remote.deliver(serverSession, reply); } } @SuppressWarnings("unchecked") private <T> T castObject(Class<T> targetClass, Object o) { if (o != null) { if (o instanceof Number) { if (targetClass.equals(Double.class)) { return (T) Double.valueOf(((Number) o).doubleValue()); } else if (targetClass.equals(Long.class)) { return (T) Long.valueOf(((Number) o).longValue()); } else { // unhandled conversion so trying best effort return (T) o; } } else { return (T) o.toString(); } } else { return null; } } /** * Creates the query requests for each source and hands off the query to the Search Controller * * @param queryMessage * - JSON message received from cometd */ public void executeQuery(Map<String, Object> queryMessage, Subject subject) { String sources = castObject(String.class, queryMessage.get(SOURCES)); Long maxTimeout = castObject(Long.class, queryMessage.get(MAX_TIMEOUT)); Long startIndex = castObject(Long.class, queryMessage.get(START_INDEX)); Long count = castObject(Long.class, queryMessage.get(COUNT)); String cql = castObject(String.class, queryMessage.get(CQL_FILTER)); String sort = castObject(String.class, queryMessage.get(SORT)); String id = castObject(String.class, queryMessage.get(ID)); Set<String> sourceIds = getSourceIds(sources); Filter filter = null; try { if (StringUtils.isNotBlank(cql)) { filter = ECQL.toFilter(cql); } } catch (CQLException e) { LOGGER.debug("Unable to parse CQL filter", e); return; } Query query = createQuery(filter, startIndex, count, sort, maxTimeout); SearchRequest searchRequest = new SearchRequest(sourceIds, query, id); try { // Hand off to the search controller for the actual query searchController.executeQuery(searchRequest, serverSession, subject); } catch (Exception e) { LOGGER.debug("Exception while executing a query", e); } } private Set<String> getSourceIds(String sources) { Set<String> sourceIds; if (StringUtils.equalsIgnoreCase(sources, LOCAL_SOURCE)) { LOGGER.debug("Received local query"); sourceIds = new HashSet<String>(Arrays.asList(searchController.getFramework() .getId())); } else if (!(StringUtils.isEmpty(sources))) { LOGGER.debug("Received source names from client: {}", sources); sourceIds = new HashSet<String>(Arrays.asList(StringUtils.stripAll(sources.split(",")))); } else { LOGGER.debug("Received enterprise query"); SourceInfoResponse sourceInfo = null; try { sourceInfo = searchController.getFramework() .getSourceInfo(new SourceInfoRequestEnterprise(true)); } catch (SourceUnavailableException e) { LOGGER.debug("Exception while getting source status. Defaulting to all sources. " + "This could include unavailable sources.", e); } if (sourceInfo != null) { sourceIds = new HashSet<String>(); for (SourceDescriptor source : sourceInfo.getSourceInfo()) { if (source.isAvailable()) { sourceIds.add(source.getSourceId()); } } } else { sourceIds = searchController.getFramework() .getSourceIds(); } } return sourceIds; } /** * Creates a new query from the incoming parameters * * @param filter * - Filter to query * @param startIndexLng * - Start index for the query * @param countLng * - number of results for the query * @param sortStr * - How to sort the query results * @param maxTimeoutLng * - timeout value on the query execution * @return - the new query */ private Query createQuery(Filter filter, Long startIndexLng, Long countLng, String sortStr, Long maxTimeoutLng) { // default values String sortField = Result.TEMPORAL; String sortOrder = DEFAULT_SORT_ORDER; Long startIndex = startIndexLng == null ? Long.valueOf(DEFAULT_START_INDEX) : startIndexLng; Long count = countLng == null ? Long.valueOf(DEFAULT_COUNT) : countLng; long maxTimeout = maxTimeoutLng == null ? DEFAULT_TIMEOUT : maxTimeoutLng; // Updated to use the passed in index if valid (=> 1) // and to use the default if no value, or an invalid value (< 1) // is specified if (!(StringUtils.isEmpty(sortStr))) { String[] sortAry = sortStr.split(":"); if (sortAry.length > 1) { sortField = sortAry[0]; sortOrder = sortAry[1]; } } // Query must specify a valid sort order if a sort field was specified, i.e., query // cannot specify just "date:", must specify "date:asc" SortBy sort; if ("asc".equalsIgnoreCase(sortOrder)) { sort = new SortByImpl(sortField, SortOrder.ASCENDING); } else if ("desc".equalsIgnoreCase(sortOrder)) { sort = new SortByImpl(sortField, SortOrder.DESCENDING); } else { throw new IllegalArgumentException( "Incorrect sort order received, must be 'asc' or 'desc'"); } LOGGER.debug("Retrieved query settings: \n sortField: {} \nsortOrder: {}", sortField, sortOrder); if (filter == null) { LOGGER.debug("Received an empty filter. Using a wildcard contextual filter instead."); filter = filterBuilder.attribute(Metacard.ANY_TEXT) .is() .like() .text(FilterDelegate.WILDCARD_CHAR); } return new QueryImpl(filter, startIndex.intValue(), count.intValue(), sort, true, maxTimeout); } }