| /* |
| * 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 java.io.File; |
| import java.io.IOException; |
| import java.lang.invoke.MethodHandles; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.atomic.AtomicLong; |
| |
| import org.apache.lucene.search.suggest.Lookup; |
| import org.apache.lucene.search.suggest.Lookup.LookupResult; |
| import org.apache.lucene.util.Accountable; |
| import org.apache.lucene.util.Accountables; |
| import org.apache.lucene.util.BytesRef; |
| import org.apache.lucene.util.CharsRef; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.params.ModifiableSolrParams; |
| import org.apache.solr.common.params.ShardParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.common.util.SimpleOrderedMap; |
| import org.apache.solr.core.SolrCore; |
| import org.apache.solr.core.SolrEventListener; |
| import org.apache.solr.metrics.MetricsMap; |
| import org.apache.solr.metrics.SolrMetricProducer; |
| import org.apache.solr.metrics.SolrMetricsContext; |
| import org.apache.solr.search.SolrIndexSearcher; |
| import org.apache.solr.spelling.suggest.SolrSuggester; |
| import org.apache.solr.spelling.suggest.SuggesterOptions; |
| import org.apache.solr.spelling.suggest.SuggesterParams; |
| import org.apache.solr.spelling.suggest.SuggesterResult; |
| import org.apache.solr.util.plugin.SolrCoreAware; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * SuggestComponent: interacts with multiple {@link SolrSuggester} to serve up suggestions |
| * Responsible for routing commands and queries to the appropriate {@link SolrSuggester} |
| * and for initializing them as specified by SolrConfig |
| */ |
| public class SuggestComponent extends SearchComponent implements SolrCoreAware, SuggesterParams, Accountable, SolrMetricProducer { |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| |
| /** Name used to identify whether the user query concerns this component */ |
| public static final String COMPONENT_NAME = "suggest"; |
| |
| /** Name assigned to an unnamed suggester (at most one suggester) can be unnamed */ |
| private static final String DEFAULT_DICT_NAME = SolrSuggester.DEFAULT_DICT_NAME; |
| |
| /** SolrConfig label to identify Config time settings */ |
| private static final String CONFIG_PARAM_LABEL = "suggester"; |
| |
| /** SolrConfig label to identify boolean value to build suggesters on commit */ |
| private static final String BUILD_ON_COMMIT_LABEL = "buildOnCommit"; |
| |
| /** SolrConfig label to identify boolean value to build suggesters on optimize */ |
| private static final String BUILD_ON_OPTIMIZE_LABEL = "buildOnOptimize"; |
| |
| /** SolrConfig label to identify boolean value to build suggesters on startup */ |
| private static final String BUILD_ON_STARTUP_LABEL = "buildOnStartup"; |
| |
| @SuppressWarnings({"rawtypes"}) |
| protected NamedList initParams; |
| |
| /** |
| * Key is the dictionary name used in SolrConfig, value is the corresponding {@link SolrSuggester} |
| */ |
| protected Map<String, SolrSuggester> suggesters = new ConcurrentHashMap<>(); |
| |
| /** Container for various labels used in the responses generated by this component */ |
| private static class SuggesterResultLabels { |
| static final String SUGGEST = "suggest"; |
| static final String SUGGESTIONS = "suggestions"; |
| static final String SUGGESTION_NUM_FOUND = "numFound"; |
| static final String SUGGESTION_TERM = "term"; |
| static final String SUGGESTION_WEIGHT = "weight"; |
| static final String SUGGESTION_PAYLOAD = "payload"; |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public void init(@SuppressWarnings({"rawtypes"})NamedList args) { |
| super.init(args); |
| this.initParams = args; |
| } |
| |
| @Override |
| public void inform(SolrCore core) { |
| if (initParams != null) { |
| log.info("Initializing SuggestComponent"); |
| boolean hasDefault = false; |
| for (int i = 0; i < initParams.size(); i++) { |
| if (initParams.getName(i).equals(CONFIG_PARAM_LABEL)) { |
| @SuppressWarnings({"rawtypes"}) |
| NamedList suggesterParams = (NamedList) initParams.getVal(i); |
| SolrSuggester suggester = new SolrSuggester(); |
| String dictionary = suggester.init(suggesterParams, core); |
| if (dictionary != null) { |
| boolean isDefault = dictionary.equals(DEFAULT_DICT_NAME); |
| if (isDefault && !hasDefault) { |
| hasDefault = true; |
| } else if (isDefault){ |
| throw new RuntimeException("More than one dictionary is missing name."); |
| } |
| suggesters.put(dictionary, suggester); |
| } else { |
| if (!hasDefault){ |
| suggesters.put(DEFAULT_DICT_NAME, suggester); |
| hasDefault = true; |
| } else { |
| throw new RuntimeException("More than one dictionary is missing name."); |
| } |
| } |
| boolean buildOnStartup; |
| Object buildOnStartupObj = suggesterParams.get(BUILD_ON_STARTUP_LABEL); |
| if (buildOnStartupObj == null) { |
| File storeFile = suggester.getStoreFile(); |
| buildOnStartup = storeFile == null || !storeFile.exists(); |
| } else { |
| buildOnStartup = Boolean.parseBoolean((String) buildOnStartupObj); |
| } |
| boolean buildOnCommit = Boolean.parseBoolean((String) suggesterParams.get(BUILD_ON_COMMIT_LABEL)); |
| boolean buildOnOptimize = Boolean.parseBoolean((String) suggesterParams.get(BUILD_ON_OPTIMIZE_LABEL)); |
| |
| if (buildOnCommit || buildOnOptimize || buildOnStartup) { |
| SuggesterListener listener = new SuggesterListener(core, suggester, buildOnCommit, buildOnOptimize, buildOnStartup, core.isReloaded()); |
| if (log.isInfoEnabled()) { |
| log.info("Registering searcher listener for suggester: {} = {}", suggester.getName(), listener); |
| } |
| core.registerFirstSearcherListener(listener); |
| core.registerNewSearcherListener(listener); |
| } |
| } |
| } |
| } |
| } |
| |
| /** Responsible for issuing build and rebuild command to the specified {@link SolrSuggester} */ |
| @Override |
| public void prepare(ResponseBuilder rb) throws IOException { |
| SolrParams params = rb.req.getParams(); |
| log.info("SuggestComponent prepare with : {}", params); |
| if (!params.getBool(COMPONENT_NAME, false)) { |
| return; |
| } |
| |
| boolean buildAll = params.getBool(SUGGEST_BUILD_ALL, false); |
| boolean reloadAll = params.getBool(SUGGEST_RELOAD_ALL, false); |
| |
| final Collection<SolrSuggester> querysuggesters; |
| if (buildAll || reloadAll) { |
| querysuggesters = suggesters.values(); |
| } else { |
| querysuggesters = getSuggesters(params); |
| } |
| |
| if (params.getBool(SUGGEST_BUILD, false) || buildAll) { |
| for (SolrSuggester suggester : querysuggesters) { |
| suggester.build(rb.req.getCore(), rb.req.getSearcher()); |
| } |
| rb.rsp.add("command", (!buildAll) ? "build" : "buildAll"); |
| } else if (params.getBool(SUGGEST_RELOAD, false) || reloadAll) { |
| for (SolrSuggester suggester : querysuggesters) { |
| suggester.reload(rb.req.getCore(), rb.req.getSearcher()); |
| } |
| rb.rsp.add("command", (!reloadAll) ? "reload" : "reloadAll"); |
| } |
| } |
| |
| /** Dispatch shard request in <code>STAGE_EXECUTE_QUERY</code> stage */ |
| @Override |
| public int distributedProcess(ResponseBuilder rb) { |
| SolrParams params = rb.req.getParams(); |
| log.info("SuggestComponent distributedProcess with : {}", params); |
| if (rb.stage < ResponseBuilder.STAGE_EXECUTE_QUERY) |
| return ResponseBuilder.STAGE_EXECUTE_QUERY; |
| if (rb.stage == ResponseBuilder.STAGE_EXECUTE_QUERY) { |
| ShardRequest sreq = new ShardRequest(); |
| sreq.purpose = ShardRequest.PURPOSE_GET_TOP_IDS; |
| sreq.params = new ModifiableSolrParams(rb.req.getParams()); |
| sreq.params.remove(ShardParams.SHARDS); |
| rb.addRequest(this, sreq); |
| return ResponseBuilder.STAGE_GET_FIELDS; |
| } |
| |
| return ResponseBuilder.STAGE_DONE; |
| } |
| |
| /** |
| * Responsible for using the specified suggester to get the suggestions |
| * for the query and write the results |
| * */ |
| @Override |
| public void process(ResponseBuilder rb) throws IOException { |
| SolrParams params = rb.req.getParams(); |
| log.info("SuggestComponent process with : {}", params); |
| if (!params.getBool(COMPONENT_NAME, false) || suggesters.isEmpty()) { |
| return; |
| } |
| |
| boolean buildAll = params.getBool(SUGGEST_BUILD_ALL, false); |
| boolean reloadAll = params.getBool(SUGGEST_RELOAD_ALL, false); |
| Set<SolrSuggester> querySuggesters; |
| try { |
| querySuggesters = getSuggesters(params); |
| } catch(SolrException ex) { |
| if (!buildAll && !reloadAll) { |
| throw ex; |
| } else { |
| querySuggesters = new HashSet<>(); |
| } |
| } |
| |
| String query = params.get(SUGGEST_Q); |
| if (query == null) { |
| query = rb.getQueryString(); |
| if (query == null) { |
| query = params.get(CommonParams.Q); |
| } |
| } |
| |
| if (query != null) { |
| int count = params.getInt(SUGGEST_COUNT, 1); |
| boolean highlight = params.getBool(SUGGEST_HIGHLIGHT, false); |
| boolean allTermsRequired = params.getBool(SUGGEST_ALL_TERMS_REQUIRED, true); |
| String contextFilter = params.get(SUGGEST_CONTEXT_FILTER_QUERY); |
| if (contextFilter != null) { |
| contextFilter = contextFilter.trim(); |
| if (contextFilter.length() == 0) { |
| contextFilter = null; |
| } |
| } |
| |
| SuggesterOptions options = new SuggesterOptions(new CharsRef(query), count, contextFilter, allTermsRequired, highlight); |
| Map<String, SimpleOrderedMap<NamedList<Object>>> namedListResults = |
| new HashMap<>(); |
| for (SolrSuggester suggester : querySuggesters) { |
| SuggesterResult suggesterResult = suggester.getSuggestions(options); |
| toNamedList(suggesterResult, namedListResults); |
| } |
| rb.rsp.add(SuggesterResultLabels.SUGGEST, namedListResults); |
| } |
| } |
| |
| /** |
| * Used in Distributed Search, merges the suggestion results from every shard |
| * */ |
| @Override |
| public void finishStage(ResponseBuilder rb) { |
| SolrParams params = rb.req.getParams(); |
| log.info("SuggestComponent finishStage with : {}", params); |
| if (!params.getBool(COMPONENT_NAME, false) || rb.stage != ResponseBuilder.STAGE_GET_FIELDS) |
| return; |
| int count = params.getInt(SUGGEST_COUNT, 1); |
| |
| List<SuggesterResult> suggesterResults = new ArrayList<>(); |
| |
| // Collect Shard responses |
| for (ShardRequest sreq : rb.finished) { |
| for (ShardResponse srsp : sreq.responses) { |
| NamedList<Object> resp; |
| if((resp = srsp.getSolrResponse().getResponse()) != null) { |
| @SuppressWarnings("unchecked") |
| Map<String, SimpleOrderedMap<NamedList<Object>>> namedList = |
| (Map<String, SimpleOrderedMap<NamedList<Object>>>) resp.get(SuggesterResultLabels.SUGGEST); |
| if (log.isInfoEnabled()) { |
| log.info("{} : {}", srsp.getShard(), namedList); |
| } |
| suggesterResults.add(toSuggesterResult(namedList)); |
| } |
| } |
| } |
| |
| // Merge Shard responses |
| SuggesterResult suggesterResult = merge(suggesterResults, count); |
| Map<String, SimpleOrderedMap<NamedList<Object>>> namedListResults = |
| new HashMap<>(); |
| toNamedList(suggesterResult, namedListResults); |
| |
| rb.rsp.add(SuggesterResultLabels.SUGGEST, namedListResults); |
| } |
| |
| /** |
| * Given a list of {@link SuggesterResult} and <code>count</code> |
| * returns a {@link SuggesterResult} containing <code>count</code> |
| * number of {@link LookupResult}, sorted by their associated |
| * weights |
| * */ |
| private static SuggesterResult merge(List<SuggesterResult> suggesterResults, int count) { |
| SuggesterResult result = new SuggesterResult(); |
| Set<String> allTokens = new HashSet<>(); |
| Set<String> suggesterNames = new HashSet<>(); |
| |
| // collect all tokens |
| for (SuggesterResult shardResult : suggesterResults) { |
| for (String suggesterName : shardResult.getSuggesterNames()) { |
| allTokens.addAll(shardResult.getTokens(suggesterName)); |
| suggesterNames.add(suggesterName); |
| } |
| } |
| |
| // Get Top N for every token in every shard (using weights) |
| for (String suggesterName : suggesterNames) { |
| for (String token : allTokens) { |
| Lookup.LookupPriorityQueue resultQueue = new Lookup.LookupPriorityQueue( |
| count); |
| for (SuggesterResult shardResult : suggesterResults) { |
| List<LookupResult> suggests = shardResult.getLookupResult(suggesterName, token); |
| if (suggests == null) { |
| continue; |
| } |
| for (LookupResult res : suggests) { |
| resultQueue.insertWithOverflow(res); |
| } |
| } |
| List<LookupResult> sortedSuggests = new LinkedList<>(); |
| Collections.addAll(sortedSuggests, resultQueue.getResults()); |
| result.add(suggesterName, token, sortedSuggests); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public String getDescription() { |
| return "Suggester component"; |
| } |
| |
| @Override |
| public void initializeMetrics(SolrMetricsContext parentContext, String scope) { |
| super.initializeMetrics(parentContext, scope); |
| |
| this.solrMetricsContext.gauge(() -> ramBytesUsed(), true, "totalSizeInBytes", getCategory().toString()); |
| MetricsMap suggestersMap = new MetricsMap((detailed, map) -> { |
| for (Map.Entry<String, SolrSuggester> entry : suggesters.entrySet()) { |
| SolrSuggester suggester = entry.getValue(); |
| map.put(entry.getKey(), suggester.toString()); |
| } |
| }); |
| this.solrMetricsContext.gauge(suggestersMap, true, "suggesters", getCategory().toString(), scope); |
| } |
| |
| @Override |
| public long ramBytesUsed() { |
| long sizeInBytes = 0; |
| for (SolrSuggester suggester : suggesters.values()) { |
| sizeInBytes += suggester.ramBytesUsed(); |
| } |
| return sizeInBytes; |
| } |
| |
| @Override |
| public Collection<Accountable> getChildResources() { |
| return Accountables.namedAccountables("field", suggesters); |
| } |
| |
| private Set<SolrSuggester> getSuggesters(SolrParams params) { |
| Set<SolrSuggester> solrSuggesters = new HashSet<>(); |
| for(String suggesterName : getSuggesterNames(params)) { |
| SolrSuggester curSuggester = suggesters.get(suggesterName); |
| if (curSuggester != null) { |
| solrSuggesters.add(curSuggester); |
| } else { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No suggester named " + suggesterName +" was configured"); |
| } |
| } |
| if (solrSuggesters.size() == 0) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, |
| "'" + SUGGEST_DICT + "' parameter not specified and no default suggester configured"); |
| } |
| return solrSuggesters; |
| |
| } |
| |
| private Set<String> getSuggesterNames(SolrParams params) { |
| Set<String> suggesterNames = new HashSet<>(); |
| String[] suggesterNamesFromParams = params.getParams(SUGGEST_DICT); |
| if (suggesterNamesFromParams == null) { |
| suggesterNames.add(DEFAULT_DICT_NAME); |
| } else { |
| for (String name : suggesterNamesFromParams) { |
| suggesterNames.add(name); |
| } |
| } |
| return suggesterNames; |
| } |
| |
| /** Convert {@link SuggesterResult} to NamedList for constructing responses */ |
| private void toNamedList(SuggesterResult suggesterResult, Map<String, SimpleOrderedMap<NamedList<Object>>> resultObj) { |
| for(String suggesterName : suggesterResult.getSuggesterNames()) { |
| SimpleOrderedMap<NamedList<Object>> results = new SimpleOrderedMap<>(); |
| for (String token : suggesterResult.getTokens(suggesterName)) { |
| SimpleOrderedMap<Object> suggestionBody = new SimpleOrderedMap<>(); |
| List<LookupResult> lookupResults = suggesterResult.getLookupResult(suggesterName, token); |
| suggestionBody.add(SuggesterResultLabels.SUGGESTION_NUM_FOUND, lookupResults.size()); |
| List<SimpleOrderedMap<Object>> suggestEntriesNamedList = new ArrayList<>(); |
| for (LookupResult lookupResult : lookupResults) { |
| String suggestionString = lookupResult.key.toString(); |
| long weight = lookupResult.value; |
| String payload = (lookupResult.payload != null) ? |
| lookupResult.payload.utf8ToString() |
| : ""; |
| |
| SimpleOrderedMap<Object> suggestEntryNamedList = new SimpleOrderedMap<>(); |
| suggestEntryNamedList.add(SuggesterResultLabels.SUGGESTION_TERM, suggestionString); |
| suggestEntryNamedList.add(SuggesterResultLabels.SUGGESTION_WEIGHT, weight); |
| suggestEntryNamedList.add(SuggesterResultLabels.SUGGESTION_PAYLOAD, payload); |
| suggestEntriesNamedList.add(suggestEntryNamedList); |
| |
| } |
| suggestionBody.add(SuggesterResultLabels.SUGGESTIONS, suggestEntriesNamedList); |
| results.add(token, suggestionBody); |
| } |
| resultObj.put(suggesterName, results); |
| } |
| } |
| |
| /** Convert NamedList (suggester response) to {@link SuggesterResult} */ |
| private SuggesterResult toSuggesterResult(Map<String, SimpleOrderedMap<NamedList<Object>>> suggestionsMap) { |
| SuggesterResult result = new SuggesterResult(); |
| if (suggestionsMap == null) { |
| return result; |
| } |
| // for each token |
| for(Map.Entry<String, SimpleOrderedMap<NamedList<Object>>> entry : suggestionsMap.entrySet()) { |
| String suggesterName = entry.getKey(); |
| for (Iterator<Map.Entry<String, NamedList<Object>>> suggestionsIter = entry.getValue().iterator(); suggestionsIter.hasNext();) { |
| Map.Entry<String, NamedList<Object>> suggestions = suggestionsIter.next(); |
| String tokenString = suggestions.getKey(); |
| List<LookupResult> lookupResults = new ArrayList<>(); |
| NamedList<Object> suggestion = suggestions.getValue(); |
| // for each suggestion |
| for (int j = 0; j < suggestion.size(); j++) { |
| String property = suggestion.getName(j); |
| if (property.equals(SuggesterResultLabels.SUGGESTIONS)) { |
| @SuppressWarnings("unchecked") |
| List<NamedList<Object>> suggestionEntries = (List<NamedList<Object>>) suggestion.getVal(j); |
| for(NamedList<Object> suggestionEntry : suggestionEntries) { |
| String term = (String) suggestionEntry.get(SuggesterResultLabels.SUGGESTION_TERM); |
| Long weight = (Long) suggestionEntry.get(SuggesterResultLabels.SUGGESTION_WEIGHT); |
| String payload = (String) suggestionEntry.get(SuggesterResultLabels.SUGGESTION_PAYLOAD); |
| LookupResult res = new LookupResult(new CharsRef(term), weight, new BytesRef(payload)); |
| lookupResults.add(res); |
| } |
| } |
| result.add(suggesterName, tokenString, lookupResults); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** Listener to build or reload the maintained {@link SolrSuggester} by this component */ |
| private static class SuggesterListener implements SolrEventListener { |
| private final SolrCore core; |
| private final SolrSuggester suggester; |
| private final boolean buildOnCommit; |
| private final boolean buildOnOptimize; |
| private final boolean buildOnStartup; |
| |
| // On core reload, immediately after the core is created a new searcher is opened, causing the suggester |
| // to trigger a "buildOnCommit". The only event that we want to trigger in that situation is "buildOnStartup" |
| // so if buildOnCommit is true and this is a core being reloaded, we will skip the first time this listener |
| // is called. |
| private final AtomicLong callCount = new AtomicLong(0); |
| private final boolean isCoreReload; |
| |
| |
| public SuggesterListener(SolrCore core, SolrSuggester checker, boolean buildOnCommit, boolean buildOnOptimize, boolean buildOnStartup, boolean isCoreReload) { |
| this.core = core; |
| this.suggester = checker; |
| this.buildOnCommit = buildOnCommit; |
| this.buildOnOptimize = buildOnOptimize; |
| this.buildOnStartup = buildOnStartup; |
| this.isCoreReload = isCoreReload; |
| } |
| |
| @Override |
| public void init(@SuppressWarnings({"rawtypes"})NamedList args) {} |
| |
| @Override |
| public void newSearcher(SolrIndexSearcher newSearcher, |
| SolrIndexSearcher currentSearcher) { |
| long thisCallCount = callCount.incrementAndGet(); |
| if (isCoreReload && thisCallCount == 1) { |
| log.info("Skipping first newSearcher call for suggester {} in core reload", suggester); |
| return; |
| } else if (thisCallCount == 1 || (isCoreReload && thisCallCount == 2)) { |
| if (buildOnStartup) { |
| if (log.isInfoEnabled()) { |
| log.info("buildOnStartup: {}", suggester.getName()); |
| } |
| buildSuggesterIndex(newSearcher); |
| } |
| } else { |
| if (buildOnCommit) { |
| if (log.isInfoEnabled()) { |
| log.info("buildOnCommit: {}", suggester.getName()); |
| } |
| buildSuggesterIndex(newSearcher); |
| } else if (buildOnOptimize) { |
| if (newSearcher.getIndexReader().leaves().size() == 1) { |
| if (log.isInfoEnabled()) { |
| log.info("buildOnOptimize: {}", suggester.getName()); |
| } |
| buildSuggesterIndex(newSearcher); |
| } |
| } |
| } |
| |
| } |
| |
| private void buildSuggesterIndex(SolrIndexSearcher newSearcher) { |
| try { |
| suggester.build(core, newSearcher); |
| } catch (Exception e) { |
| log.error("Exception in building suggester index for {}: ", suggester.getName(), e); |
| } |
| } |
| |
| @Override |
| public void postCommit() {} |
| |
| @Override |
| public void postSoftCommit() {} |
| |
| @Override |
| public String toString() { |
| return "SuggesterListener [core=" + core + ", suggester=" + suggester |
| + ", buildOnCommit=" + buildOnCommit + ", buildOnOptimize=" |
| + buildOnOptimize + ", buildOnStartup=" + buildOnStartup |
| + ", isCoreReload=" + isCoreReload + "]"; |
| } |
| |
| } |
| } |