| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.solr.handler.component; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.lucene.index.ExitableDirectoryReader; |
| import org.apache.lucene.search.TotalHits; |
| import org.apache.solr.client.solrj.SolrServerException; |
| import org.apache.solr.cloud.ZkController; |
| import org.apache.solr.common.SolrDocumentList; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.params.CursorMarkParams; |
| import org.apache.solr.common.params.ModifiableSolrParams; |
| import org.apache.solr.common.params.ShardParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.common.util.SimpleOrderedMap; |
| import org.apache.solr.core.CloseHook; |
| import org.apache.solr.core.CoreContainer; |
| import org.apache.solr.core.PluginInfo; |
| import org.apache.solr.core.SolrCore; |
| import org.apache.solr.handler.RequestHandlerBase; |
| import org.apache.solr.pkg.PackageAPI; |
| import org.apache.solr.pkg.PackageListeners; |
| import org.apache.solr.pkg.PackageLoader; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.response.SolrQueryResponse; |
| import org.apache.solr.search.CursorMark; |
| import org.apache.solr.search.SolrQueryTimeoutImpl; |
| import org.apache.solr.search.SortSpec; |
| import org.apache.solr.search.facet.FacetModule; |
| import org.apache.solr.security.AuthorizationContext; |
| import org.apache.solr.security.PermissionNameProvider; |
| import org.apache.solr.util.RTimerTree; |
| import org.apache.solr.util.SolrPluginUtils; |
| import org.apache.solr.util.circuitbreaker.CircuitBreaker; |
| import org.apache.solr.util.circuitbreaker.CircuitBreakerManager; |
| import org.apache.solr.util.plugin.PluginInfoInitialized; |
| import org.apache.solr.util.plugin.SolrCoreAware; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.lang.invoke.MethodHandles; |
| import java.util.*; |
| import java.util.concurrent.atomic.AtomicLong; |
| |
| import static org.apache.solr.common.params.CommonParams.*; |
| |
| |
| /** |
| * Refer SOLR-281 |
| */ |
| public class SearchHandler extends RequestHandlerBase implements SolrCoreAware, PluginInfoInitialized, PermissionNameProvider { |
| static final String INIT_COMPONENTS = "components"; |
| static final String INIT_FIRST_COMPONENTS = "first-components"; |
| static final String INIT_LAST_COMPONENTS = "last-components"; |
| |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| |
| /** |
| * A counter to ensure that no RID is equal, even if they fall in the same millisecond |
| */ |
| private static final AtomicLong ridCounter = new AtomicLong(); |
| |
| protected volatile List<SearchComponent> components; |
| private ShardHandlerFactory shardHandlerFactory; |
| private PluginInfo shfInfo; |
| private SolrCore core; |
| |
| protected List<String> getDefaultComponents() { |
| ArrayList<String> names = new ArrayList<>(8); |
| names.add(QueryComponent.COMPONENT_NAME); |
| names.add(FacetComponent.COMPONENT_NAME); |
| names.add(FacetModule.COMPONENT_NAME); |
| names.add(MoreLikeThisComponent.COMPONENT_NAME); |
| names.add(HighlightComponent.COMPONENT_NAME); |
| names.add(StatsComponent.COMPONENT_NAME); |
| names.add(DebugComponent.COMPONENT_NAME); |
| names.add(ExpandComponent.COMPONENT_NAME); |
| names.add(TermsComponent.COMPONENT_NAME); |
| |
| return names; |
| } |
| |
| @Override |
| public void init(PluginInfo info) { |
| init(info.initArgs); |
| for (PluginInfo child : info.children) { |
| if ("shardHandlerFactory".equals(child.type)) { |
| this.shfInfo = child; |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public PermissionNameProvider.Name getPermissionName(AuthorizationContext ctx) { |
| return PermissionNameProvider.Name.READ_PERM; |
| } |
| |
| /** |
| * Initialize the components based on name. Note, if using <code>INIT_FIRST_COMPONENTS</code> or <code>INIT_LAST_COMPONENTS</code>, |
| * then the {@link DebugComponent} will always occur last. If this is not desired, then one must explicitly declare all components using |
| * the <code>INIT_COMPONENTS</code> syntax. |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| public void inform(SolrCore core) { |
| this.core = core; |
| List<String> c = (List<String>) initArgs.get(INIT_COMPONENTS); |
| Set<String> missing = new HashSet<>(core.getSearchComponents().checkContains(c)); |
| List<String> first = (List<String>) initArgs.get(INIT_FIRST_COMPONENTS); |
| missing.addAll(core.getSearchComponents().checkContains(first)); |
| List<String> last = (List<String>) initArgs.get(INIT_LAST_COMPONENTS); |
| missing.addAll(core.getSearchComponents().checkContains(last)); |
| if (!missing.isEmpty()) throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, |
| "Missing SearchComponents named : " + missing); |
| if (c != null && (first != null || last != null)) throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, |
| "First/Last components only valid if you do not declare 'components'"); |
| |
| if (shfInfo == null) { |
| shardHandlerFactory = core.getCoreContainer().getShardHandlerFactory(); |
| } else { |
| shardHandlerFactory = core.createInitInstance(shfInfo, ShardHandlerFactory.class, null, null); |
| core.addCloseHook(new CloseHook() { |
| @Override |
| public void preClose(SolrCore core) { |
| shardHandlerFactory.close(); |
| } |
| |
| @Override |
| public void postClose(SolrCore core) { |
| } |
| }); |
| } |
| |
| if (core.getCoreContainer().isZooKeeperAware()) { |
| core.getPackageListeners().addListener(new PackageListeners.Listener() { |
| @Override |
| public String packageName() { |
| return null; |
| } |
| |
| @Override |
| public Map<String , PackageAPI.PkgVersion> packageDetails() { |
| return Collections.emptyMap(); |
| } |
| |
| @Override |
| public void changed(PackageLoader.Package pkg, Ctx ctx) { |
| //we could optimize this by listening to only relevant packages, |
| // but it is not worth optimizing as these are lightweight objects |
| components = null; |
| } |
| }); |
| } |
| |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| private void initComponents() { |
| Object declaredComponents = initArgs.get(INIT_COMPONENTS); |
| List<String> first = (List<String>) initArgs.get(INIT_FIRST_COMPONENTS); |
| List<String> last = (List<String>) initArgs.get(INIT_LAST_COMPONENTS); |
| |
| List<String> list = null; |
| boolean makeDebugLast = true; |
| if( declaredComponents == null ) { |
| // Use the default component list |
| list = getDefaultComponents(); |
| |
| if( first != null ) { |
| List<String> clist = first; |
| clist.addAll( list ); |
| list = clist; |
| } |
| |
| if( last != null ) { |
| list.addAll( last ); |
| } |
| } |
| else { |
| list = (List<String>)declaredComponents; |
| if( first != null || last != null ) { |
| throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, |
| "First/Last components only valid if you do not declare 'components'"); |
| } |
| makeDebugLast = false; |
| } |
| |
| // Build the component list |
| List<SearchComponent> components = new ArrayList<>(list.size()); |
| DebugComponent dbgCmp = null; |
| for(String c : list){ |
| SearchComponent comp = core.getSearchComponent( c ); |
| if (comp instanceof DebugComponent && makeDebugLast == true){ |
| dbgCmp = (DebugComponent) comp; |
| } else { |
| components.add(comp); |
| log.debug("Adding component:{}", comp); |
| } |
| } |
| if (makeDebugLast == true && dbgCmp != null){ |
| components.add(dbgCmp); |
| log.debug("Adding debug component:{}", dbgCmp); |
| } |
| this.components = components; |
| } |
| |
| public List<SearchComponent> getComponents() { |
| List<SearchComponent> result = components; // volatile read |
| if (result == null) { |
| synchronized (this) { |
| if (components == null) { |
| initComponents(); |
| } |
| result = components; |
| } |
| } |
| return result; |
| } |
| |
| private ShardHandler getAndPrepShardHandler(SolrQueryRequest req, ResponseBuilder rb) { |
| ShardHandler shardHandler = null; |
| |
| CoreContainer cc = req.getCore().getCoreContainer(); |
| boolean isZkAware = cc.isZooKeeperAware(); |
| rb.isDistrib = req.getParams().getBool(DISTRIB, isZkAware); |
| if (!rb.isDistrib) { |
| // for back compat, a shards param with URLs like localhost:8983/solr will mean that this |
| // search is distributed. |
| final String shards = req.getParams().get(ShardParams.SHARDS); |
| rb.isDistrib = ((shards != null) && (shards.indexOf('/') > 0)); |
| } |
| |
| if (rb.isDistrib) { |
| shardHandler = shardHandlerFactory.getShardHandler(); |
| shardHandler.prepDistributed(rb); |
| if (!rb.isDistrib) { |
| shardHandler = null; // request is not distributed after all and so the shard handler is not needed |
| } |
| } |
| |
| if (isZkAware) { |
| String shardsTolerant = req.getParams().get(ShardParams.SHARDS_TOLERANT); |
| boolean requireZkConnected = shardsTolerant != null && shardsTolerant.equals(ShardParams.REQUIRE_ZK_CONNECTED); |
| ZkController zkController = cc.getZkController(); |
| boolean zkConnected = zkController != null && ! zkController.getZkClient().getConnectionManager().isLikelyExpired(); |
| if (requireZkConnected && false == zkConnected) { |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "ZooKeeper is not connected"); |
| } else { |
| NamedList<Object> headers = rb.rsp.getResponseHeader(); |
| if (headers != null) { |
| headers.add("zkConnected", zkConnected); |
| } |
| } |
| } |
| |
| return shardHandler; |
| } |
| |
| /** |
| * Override this method if you require a custom {@link ResponseBuilder} e.g. for use by a custom {@link SearchComponent}. |
| */ |
| protected ResponseBuilder newResponseBuilder(SolrQueryRequest req, SolrQueryResponse rsp, List<SearchComponent> components) { |
| return new ResponseBuilder(req, rsp, components); |
| } |
| |
| @Override |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception |
| { |
| List<SearchComponent> components = getComponents(); |
| ResponseBuilder rb = newResponseBuilder(req, rsp, components); |
| if (rb.requestInfo != null) { |
| rb.requestInfo.setResponseBuilder(rb); |
| } |
| |
| boolean dbg = req.getParams().getBool(CommonParams.DEBUG_QUERY, false); |
| rb.setDebug(dbg); |
| if (dbg == false){//if it's true, we are doing everything anyway. |
| SolrPluginUtils.getDebugInterests(req.getParams().getParams(CommonParams.DEBUG), rb); |
| } |
| |
| final RTimerTree timer = rb.isDebug() ? req.getRequestTimer() : null; |
| |
| final CircuitBreakerManager circuitBreakerManager = req.getCore().getCircuitBreakerManager(); |
| if (circuitBreakerManager.isEnabled()) { |
| List<CircuitBreaker> trippedCircuitBreakers; |
| |
| if (timer != null) { |
| RTimerTree subt = timer.sub("circuitbreaker"); |
| rb.setTimer(subt); |
| |
| trippedCircuitBreakers = circuitBreakerManager.checkTripped(); |
| |
| rb.getTimer().stop(); |
| } else { |
| trippedCircuitBreakers = circuitBreakerManager.checkTripped(); |
| } |
| |
| if (trippedCircuitBreakers != null) { |
| String errorMessage = CircuitBreakerManager.toErrorMessage(trippedCircuitBreakers); |
| rsp.add(STATUS, FAILURE); |
| rsp.setException(new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, "Circuit Breakers tripped " + errorMessage)); |
| return; |
| } |
| } |
| |
| final ShardHandler shardHandler1 = getAndPrepShardHandler(req, rb); // creates a ShardHandler object only if it's needed |
| |
| tagRequestWithRequestId(rb); |
| |
| if (timer == null) { |
| // non-debugging prepare phase |
| for( SearchComponent c : components ) { |
| c.prepare(rb); |
| } |
| } else { |
| // debugging prepare phase |
| RTimerTree subt = timer.sub( "prepare" ); |
| for( SearchComponent c : components ) { |
| rb.setTimer(subt.sub( c.getName() ) ); |
| c.prepare(rb); |
| rb.getTimer().stop(); |
| } |
| subt.stop(); |
| } |
| |
| { // Once all of our components have been prepared, check if this requset involves a SortSpec. |
| // If it does, and if our request includes a cursorMark param, then parse & init the CursorMark state |
| // (This must happen after the prepare() of all components, because any component may have modified the SortSpec) |
| final SortSpec spec = rb.getSortSpec(); |
| final String cursorStr = rb.req.getParams().get(CursorMarkParams.CURSOR_MARK_PARAM); |
| if (null != spec && null != cursorStr) { |
| final CursorMark cursorMark = new CursorMark(rb.req.getSchema(), spec); |
| cursorMark.parseSerializedTotem(cursorStr); |
| rb.setCursorMark(cursorMark); |
| } |
| } |
| |
| if (!rb.isDistrib) { |
| // a normal non-distributed request |
| |
| SolrQueryTimeoutImpl.set(req); |
| try { |
| // The semantics of debugging vs not debugging are different enough that |
| // it makes sense to have two control loops |
| if(!rb.isDebug()) { |
| // Process |
| for( SearchComponent c : components ) { |
| c.process(rb); |
| } |
| } |
| else { |
| // Process |
| RTimerTree subt = timer.sub( "process" ); |
| for( SearchComponent c : components ) { |
| rb.setTimer( subt.sub( c.getName() ) ); |
| c.process(rb); |
| rb.getTimer().stop(); |
| } |
| subt.stop(); |
| |
| // add the timing info |
| if (rb.isDebugTimings()) { |
| rb.addDebugInfo("timing", timer.asNamedList() ); |
| } |
| } |
| } catch (ExitableDirectoryReader.ExitingReaderException ex) { |
| log.warn("Query: {}; ", req.getParamString(), ex); |
| if( rb.rsp.getResponse() == null) { |
| rb.rsp.addResponse(new SolrDocumentList()); |
| |
| // If a cursorMark was passed, and we didn't progress, set |
| // the nextCursorMark to the same position |
| String cursorStr = rb.req.getParams().get(CursorMarkParams.CURSOR_MARK_PARAM); |
| if (null != cursorStr) { |
| rb.rsp.add(CursorMarkParams.CURSOR_MARK_NEXT, cursorStr); |
| } |
| } |
| if(rb.isDebug()) { |
| NamedList debug = new NamedList(); |
| debug.add("explain", new NamedList()); |
| rb.rsp.add("debug", debug); |
| } |
| rb.rsp.getResponseHeader().asShallowMap() |
| .put(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, Boolean.TRUE); |
| } finally { |
| SolrQueryTimeoutImpl.reset(); |
| } |
| } else { |
| // a distributed request |
| |
| if (rb.outgoing == null) { |
| rb.outgoing = new LinkedList<>(); |
| } |
| rb.finished = new ArrayList<>(); |
| |
| int nextStage = 0; |
| do { |
| rb.stage = nextStage; |
| nextStage = ResponseBuilder.STAGE_DONE; |
| |
| // call all components |
| for( SearchComponent c : components ) { |
| // the next stage is the minimum of what all components report |
| nextStage = Math.min(nextStage, c.distributedProcess(rb)); |
| } |
| |
| |
| // check the outgoing queue and send requests |
| while (rb.outgoing.size() > 0) { |
| |
| // submit all current request tasks at once |
| while (rb.outgoing.size() > 0) { |
| ShardRequest sreq = rb.outgoing.remove(0); |
| sreq.actualShards = sreq.shards; |
| if (sreq.actualShards==ShardRequest.ALL_SHARDS) { |
| sreq.actualShards = rb.shards; |
| } |
| sreq.responses = new ArrayList<>(sreq.actualShards.length); // presume we'll get a response from each shard we send to |
| |
| // TODO: map from shard to address[] |
| for (String shard : sreq.actualShards) { |
| ModifiableSolrParams params = new ModifiableSolrParams(sreq.params); |
| params.remove(ShardParams.SHARDS); // not a top-level request |
| params.set(DISTRIB, "false"); // not a top-level request |
| params.remove("indent"); |
| params.remove(CommonParams.HEADER_ECHO_PARAMS); |
| params.set(ShardParams.IS_SHARD, true); // a sub (shard) request |
| params.set(ShardParams.SHARDS_PURPOSE, sreq.purpose); |
| params.set(ShardParams.SHARD_URL, shard); // so the shard knows what was asked |
| params.set(CommonParams.OMIT_HEADER, false); |
| if (rb.requestInfo != null) { |
| // we could try and detect when this is needed, but it could be tricky |
| params.set("NOW", Long.toString(rb.requestInfo.getNOW().getTime())); |
| } |
| String shardQt = params.get(ShardParams.SHARDS_QT); |
| if (shardQt != null) { |
| params.set(CommonParams.QT, shardQt); |
| } else { |
| // for distributed queries that don't include shards.qt, use the original path |
| // as the default but operators need to update their luceneMatchVersion to enable |
| // this behavior since it did not work this way prior to 5.1 |
| String reqPath = (String) req.getContext().get(PATH); |
| if (!"/select".equals(reqPath)) { |
| params.set(CommonParams.QT, reqPath); |
| } // else if path is /select, then the qt gets passed thru if set |
| } |
| shardHandler1.submit(sreq, shard, params); |
| } |
| } |
| |
| |
| // now wait for replies, but if anyone puts more requests on |
| // the outgoing queue, send them out immediately (by exiting |
| // this loop) |
| boolean tolerant = ShardParams.getShardsTolerantAsBool(rb.req.getParams()); |
| while (rb.outgoing.size() == 0) { |
| ShardResponse srsp = tolerant ? |
| shardHandler1.takeCompletedIncludingErrors(): |
| shardHandler1.takeCompletedOrError(); |
| if (srsp == null) break; // no more requests to wait for |
| |
| // Was there an exception? |
| if (srsp.getException() != null) { |
| // If things are not tolerant, abort everything and rethrow |
| if(!tolerant) { |
| shardHandler1.cancelAll(); |
| if (srsp.getException() instanceof SolrException) { |
| throw (SolrException)srsp.getException(); |
| } else { |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, srsp.getException()); |
| } |
| } else { |
| rsp.getResponseHeader().asShallowMap().put(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, Boolean.TRUE); |
| } |
| } |
| |
| rb.finished.add(srsp.getShardRequest()); |
| |
| // let the components see the responses to the request |
| for(SearchComponent c : components) { |
| c.handleResponses(rb, srsp.getShardRequest()); |
| } |
| } |
| } |
| |
| for(SearchComponent c : components) { |
| c.finishStage(rb); |
| } |
| |
| // we are done when the next stage is MAX_VALUE |
| } while (nextStage != Integer.MAX_VALUE); |
| } |
| |
| // SOLR-5550: still provide shards.info if requested even for a short circuited distrib request |
| if(!rb.isDistrib && req.getParams().getBool(ShardParams.SHARDS_INFO, false) && rb.shortCircuitedURL != null) { |
| NamedList<Object> shardInfo = new SimpleOrderedMap<Object>(); |
| SimpleOrderedMap<Object> nl = new SimpleOrderedMap<Object>(); |
| if (rsp.getException() != null) { |
| Throwable cause = rsp.getException(); |
| if (cause instanceof SolrServerException) { |
| cause = ((SolrServerException)cause).getRootCause(); |
| } else { |
| if (cause.getCause() != null) { |
| cause = cause.getCause(); |
| } |
| } |
| nl.add("error", cause.toString() ); |
| StringWriter trace = new StringWriter(); |
| cause.printStackTrace(new PrintWriter(trace)); |
| nl.add("trace", trace.toString() ); |
| } else if (rb.getResults() != null) { |
| nl.add("numFound", rb.getResults().docList.matches()); |
| nl.add("numFoundExact", rb.getResults().docList.hitCountRelation() == TotalHits.Relation.EQUAL_TO); |
| nl.add("maxScore", rb.getResults().docList.maxScore()); |
| } |
| nl.add("shardAddress", rb.shortCircuitedURL); |
| nl.add("time", req.getRequestTimer().getTime()); // elapsed time of this request so far |
| |
| int pos = rb.shortCircuitedURL.indexOf("://"); |
| String shardInfoName = pos != -1 ? rb.shortCircuitedURL.substring(pos+3) : rb.shortCircuitedURL; |
| shardInfo.add(shardInfoName, nl); |
| rsp.getValues().add(ShardParams.SHARDS_INFO,shardInfo); |
| } |
| } |
| |
| private void tagRequestWithRequestId(ResponseBuilder rb) { |
| final boolean ridTaggingDisabled = rb.req.getParams().getBool(CommonParams.DISABLE_REQUEST_ID, false); |
| if (! ridTaggingDisabled) { |
| String rid = getOrGenerateRequestId(rb.req); |
| if (StringUtils.isBlank(rb.req.getParams().get(CommonParams.REQUEST_ID))) { |
| ModifiableSolrParams params = new ModifiableSolrParams(rb.req.getParams()); |
| params.add(CommonParams.REQUEST_ID, rid);//add rid to the request so that shards see it |
| rb.req.setParams(params); |
| } |
| if (rb.isDistrib) { |
| rb.rsp.addToLog(CommonParams.REQUEST_ID, rid); //to see it in the logs of the landing core |
| } |
| } |
| } |
| |
| /** |
| * Returns a String to use as an identifier for this request. |
| * |
| * If the provided {@link SolrQueryRequest} contains a non-blank {@link CommonParams#REQUEST_ID} param value this is |
| * used. This is especially useful for users who deploy Solr as one component in a larger ecosystem, and want to use |
| * an external ID utilized by other components as well. If no {@link CommonParams#REQUEST_ID} value is present, one |
| * is generated from scratch for the request. |
| * <p> |
| * Callers are responsible for storing the returned value in the {@link SolrQueryRequest} object if they want to |
| * ensure that ID generation is not redone on subsequent calls. |
| */ |
| public static String getOrGenerateRequestId(SolrQueryRequest req) { |
| String rid = req.getParams().get(CommonParams.REQUEST_ID); |
| return StringUtils.isNotBlank(rid) ? rid : generateRid(req); |
| } |
| |
| private static String generateRid(SolrQueryRequest req) { |
| String hostName = req.getCore().getCoreContainer().getHostName(); |
| return hostName + "-" + ridCounter.getAndIncrement(); |
| } |
| |
| //////////////////////// SolrInfoMBeans methods ////////////////////// |
| |
| @Override |
| public String getDescription() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Search using components: "); |
| if( components != null ) { |
| for(SearchComponent c : components){ |
| sb.append(c.getName()); |
| sb.append(","); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| @Override |
| public Boolean registerV2() { |
| return Boolean.TRUE; |
| } |
| } |
| |
| |
| // TODO: generalize how a comm component can fit into search component framework |
| // TODO: statics should be per-core singletons |
| |
| |