/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014 - 2016, Open Source Geospatial Foundation (OSGeo) * * This library 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; * version 2.1 of the License. * * This library 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. */ package org.geotools.data.solr; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections.comparators.ComparatorChain; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery.ORDER; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.request.LukeRequest; import org.apache.solr.client.solrj.response.LukeResponse; import org.apache.solr.client.solrj.response.LukeResponse.FieldInfo; import org.apache.solr.client.solrj.response.LukeResponse.FieldTypeInfo; import org.geotools.data.Query; import org.geotools.data.solr.SolrUtils.ExtendedFieldSchemaInfo; import org.geotools.data.store.ContentDataStore; import org.geotools.data.store.ContentEntry; import org.geotools.data.store.ContentFeatureSource; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.feature.NameImpl; import org.geotools.filter.FilterCapabilities; import org.geotools.filter.visitor.SimplifyingFilterVisitor; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import com.vividsolutions.jts.geom.Geometry; import java.util.logging.Logger; /** * Datastore implementation for SOLR document <br> * The types provided from the datastore are obtained querying with distinct a specific SOLR field */ public class SolrDataStore extends ContentDataStore { // Url of SOLR server private URL url; // Controlls how documents are mapped to layers private SolrLayerMapper layerMapper; // Types that the datastore provides obtained // Dependent on doc loader being used private List<Name> nativeTypeNames; // Attributes present in SOLR schema private ArrayList<SolrAttribute> solrAttributes = new ArrayList<SolrAttribute>(); // SOLR uuid attributes private SolrAttribute pk = null; // Attributes configurations of the store entries private Map<String, SolrLayerConfiguration> solrConfigurations = new ConcurrentHashMap<String, SolrLayerConfiguration>(); HttpSolrClient solrServer; /** * Create the data store, using the {@link FieldLayerMapper}. * * @param url the URL of SOLR server * @param field SOLR field to query to obtain the store types */ public SolrDataStore(URL url, String field) { this(url, new FieldLayerMapper(field)); } /** * Creates the datastore. * * @param url The URL of SOLR server * @param layerMapper The document loader. */ public SolrDataStore(URL url, SolrLayerMapper layerMapper) { // TODO: make connection timeouts configurable this.url = url; this.layerMapper = layerMapper; this.solrServer = new HttpSolrClient(url.toString()); this.solrServer.setAllowCompression(true); this.solrServer.setConnectionTimeout(10000); this.solrServer.setFollowRedirects(true); this.solrServer.setSoTimeout(10000); } /** * Retrieve SOLR attribute for specific type <br/> * Two SOLR LukeRequest are needed to discover SOLR fields and theirs schema for dynamic and * static kinds. <br/> * For each discovered field a SOLR request is needed to verify if the field has no values in * the actual type, this information will be stored in {@link SolrAttribute#setEmpty}. <br/> * SolrJ not extracts information about uniqueKey so custom class * {@link ExtendedFieldSchemaInfo} is used. <br/> * MultiValued SOLR field is mapped as String type * * @param layerName the type to use to query the SOLR field {@link SolrDataStore#field} * * @see {@link SolrUtils#decodeSolrFieldType} * @see {@link ExtendedFieldSchemaInfo#ExtendedFieldSchemaInfo} * */ public ArrayList<SolrAttribute> getSolrAttributes(String layerName) { if (solrAttributes.isEmpty()) { solrAttributes = new ArrayList<SolrAttribute>(); try { LukeRequest lq = new LukeRequest(); lq.setShowSchema(true); LukeResponse processSchema = lq.process(solrServer); lq = new LukeRequest(); lq.setShowSchema(false); LukeResponse processField = lq.process(solrServer); Map<String, FieldInfo> fis = processField.getFieldInfo(); SortedSet<String> keys = new TreeSet<String>(fis.keySet()); for (String k : keys) { FieldInfo fieldInfo = fis.get(k); String name = fieldInfo.getName(); String type = fieldInfo.getType(); FieldTypeInfo fty = processSchema.getFieldTypeInfo(type); if (fty != null) { String solrTypeName = fty.getClassName(); Class<?> objType = SolrUtils.decodeSolrFieldType(solrTypeName); if (objType != null) { ExtendedFieldSchemaInfo extendedFieldSchemaInfo = new SolrUtils.ExtendedFieldSchemaInfo( processSchema, processField, name); SolrAttribute at = new SolrAttribute(name, objType); at.setSolrType(solrTypeName); if (extendedFieldSchemaInfo.getUniqueKey()) { at.setPk(true); at.setUse(true); } if (extendedFieldSchemaInfo.getMultivalued() && !Geometry.class.isAssignableFrom(at.getType())) { at.setType(String.class); } at.setEmpty(fieldInfo.getDocs() == 0); solrAttributes.add(at); } else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Skipping attribute " + fty.getName() + " as we don't know how to map its type to a java object " + fty.getClassName()); } } } } // Reorder fields: empty after List<BeanComparator> sortFields = Arrays.asList(new BeanComparator("empty"), new BeanComparator("name")); ComparatorChain multiSort = new ComparatorChain(sortFields); Collections.sort(solrAttributes, multiSort); } catch (Exception ex) { LOGGER.log(Level.SEVERE, ex.getMessage(), ex); } } return solrAttributes; } @Override protected List<Name> createTypeNames() throws IOException { try { if (nativeTypeNames == null || nativeTypeNames.isEmpty()) { List<Name> temp = new ArrayList<>(); for (String name : layerMapper.createTypeNames(solrServer)) { temp.add(new NameImpl(namespaceURI, name)); } nativeTypeNames = temp; } } catch (Exception ex) { LOGGER.log(Level.SEVERE, ex.getMessage(), ex); } Set<Name> names = new TreeSet<>(nativeTypeNames); // also pick the configured layers for (SolrLayerConfiguration conf : solrConfigurations.values()) { String name = conf.getLayerName(); names.add(new NameImpl(namespaceURI, name)); } return new ArrayList<>(names); } @Override protected ContentFeatureSource createFeatureSource(ContentEntry entry) throws IOException { ContentEntry type = ensureEntry(entry.getName()); return new SolrFeatureSource(type); } @Override public FilterFactory getFilterFactory() { return CommonFactoryFinder.getFilterFactory2(); } /** * The filter capabilities which reports which spatial operations the underlying SOLR server can * handle natively. * * @return The filter capabilities, never <code>null</code>. */ public FilterCapabilities getFilterCapabilities() { FilterToSolr f2s = new FilterToSolr(null); return f2s.getCapabilities(); } /** * Gets the attributes configuration for the types in this datastore */ public Map<String, SolrLayerConfiguration> getSolrConfigurations() { return solrConfigurations; } /** * Add the type configuration to this datastore */ public void setSolrConfigurations(SolrLayerConfiguration configuration) { entries.remove(new NameImpl(namespaceURI, configuration.getLayerName())); this.solrConfigurations.put(configuration.getLayerName(), configuration); } /** * Get the url of SOLR server */ public URL getUrl() { return url; } /** * Get the field used to filter the types that the datastore provides. * * @deprecated */ public String getField() { if (layerMapper instanceof FieldLayerMapper) { return ((FieldLayerMapper) layerMapper).getField(); } throw new IllegalStateException("Layer mapper not instance of " + FieldLayerMapper.class.getName()); } /** * Gets the document loader controlling how documents are mapped to layers from the solr index. */ public SolrLayerMapper getLayerMapper() { return layerMapper; } /** * Gets the primary key attribute a type in this datastore.</br> If the key is not currently * available a call to {@link #getSolrAttributes} is needed. * * @param layerName the type to use to query the SOLR field {@link SolrDataStore#field} */ public SolrAttribute getPrimaryKey(String layerName) { if (pk == null) { ArrayList<SolrAttribute> attributes = getSolrAttributes(layerName); for (SolrAttribute at : attributes) { if (at.isPk()) { pk = at; break; } } } return pk; } /** * Builds the SolrJ query with support of subset of fields, limit/offset, sorting, OGC filter * encoding and viewParams <br> * The SOLR query always need the order by PK field to enable pagination and efficient data * retrieving <br> * Currently only additional "q" and "fq" SOLR parameters can be passed using vireParams, this * conditions are added in AND with others * * @param featureType the feature type to query * @param q the OGC query to translate in SOLR request * * @see {@link Hints#VIRTUAL_TABLE_PARAMETERS} * */ protected SolrQuery select(SimpleFeatureType featureType, Query q) { SolrQuery query = new SolrQuery(); query.setParam("omitHeader", true); try { // Column names if (q.getPropertyNames() != null) { for (String prop : q.getPropertyNames()) { query.addField(prop); } } query.setQuery("*:*"); // Encode limit/offset, if necessary if (q.getStartIndex() != null && q.getStartIndex() >= 0) { query.setStart(q.getStartIndex()); } if (q.getMaxFeatures() > 0) { query.setRows(q.getMaxFeatures()); } // Sort ORDER naturalSortOrder = ORDER.asc; if (q.getSortBy() != null) { for (SortBy sort : q.getSortBy()) { if (sort.getPropertyName() != null) { query.addSort(sort.getPropertyName().getPropertyName(), sort.getSortOrder() .equals(SortOrder.ASCENDING) ? ORDER.asc : ORDER.desc); } else { naturalSortOrder = sort.getSortOrder().equals(SortOrder.ASCENDING) ? ORDER.asc : ORDER.desc; } } } // Always add natural sort by PK to support pagination query.addSort(getPrimaryKey(featureType.getTypeName()).getName(), naturalSortOrder); // Encode OGC filer FilterToSolr f2s = initializeFilterToSolr(featureType); String fq = layerMapper.prepareFilterQuery(featureType); Filter simplified = SimplifyingFilterVisitor.simplify(q.getFilter(), featureType); String ffq = f2s.encodeToString(simplified); if (ffq != null && !ffq.isEmpty()) { fq = fq != null ? fq + " AND " + ffq : ffq; } query.setFilterQueries(fq); // Add viewpPrams addViewparams(q, query); } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } return query; } /** * Builds the SolrJ count query with support of limit/offset, OGC filter encoding and viewParams <br> * Currently only additional "q" and "fq" SOLR parameters can be passed using viewParams, this * conditions are added in AND with others * * @param featureType the feature type to query * @param q the OGC query to translate in SOLR request * * @see {@link Hints#VIRTUAL_TABLE_PARAMETERS} * */ protected SolrQuery count(SimpleFeatureType featureType, Query q) { SolrQuery query = new SolrQuery(); query.setParam("omitHeader", true); query.setQuery("*:*"); query.setFields(this.getPrimaryKey(featureType.getName().getLocalPart()).getName()); try { // Encode limit/offset, if necessary if (q.getStartIndex() != null && q.getStartIndex() >= 0) { query.setStart(q.getStartIndex()); } query.setRows(0); // Encode OGC filer FilterToSolr f2s = initializeFilterToSolr(featureType); String fq = layerMapper.prepareFilterQuery(featureType); String ffq = f2s.encodeToString(q.getFilter()); if (ffq != null && !ffq.isEmpty()) { fq = fq != null ? fq + " AND " + ffq : ffq; } query.setFilterQueries(fq); // Add viewparams parameters addViewparams(q, query); } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } return query; } /* * Set parameters for OGC filter encoder */ private FilterToSolr initializeFilterToSolr(SimpleFeatureType featureType) { FilterToSolr f2s = new FilterToSolr(featureType); f2s.setPrimaryKey(this.getPrimaryKey(featureType.getName().getLocalPart())); f2s.setFeatureTypeName(featureType.getName().getLocalPart()); return f2s; } /* * Add viewParams to SOLR query */ private void addViewparams(Query q, SolrQuery query) { String qViewParamers = null; String fqViewParamers = null; Hints hints = q.getHints(); if (hints != null) { Map<String, String> parameters = (Map<String, String>) hints .get(Hints.VIRTUAL_TABLE_PARAMETERS); if (parameters != null) { for (String param : parameters.keySet()) { // Accepts only q and fq query parameters if (param.equalsIgnoreCase("q")) { qViewParamers = parameters.get(param); } if (param.equalsIgnoreCase("fq")) { fqViewParamers = parameters.get(param); } } } } if (qViewParamers != null && !qViewParamers.isEmpty()) { query.set("q", query.get("q").concat(" AND ").concat(qViewParamers)); } if (fqViewParamers != null && !fqViewParamers.isEmpty()) { query.addFilterQuery(fqViewParamers); } } HttpSolrClient getSolrServer() { return solrServer; } @Override public void dispose() { try { solrServer.close(); } catch (IOException ex) { LOGGER.log(Level.SEVERE, ex.getMessage(), ex); } finally { super.dispose(); } } }