| /* |
| * 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.geode.management.internal.cli.domain; |
| |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| |
| import com.fasterxml.jackson.core.JsonProcessingException; |
| import com.fasterxml.jackson.databind.JsonNode; |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| |
| import org.apache.geode.cache.query.Struct; |
| import org.apache.geode.cache.query.internal.StructImpl; |
| import org.apache.geode.cache.query.internal.Undefined; |
| import org.apache.geode.management.cli.Result; |
| import org.apache.geode.management.internal.cli.GfshParser; |
| import org.apache.geode.management.internal.cli.result.model.DataResultModel; |
| import org.apache.geode.management.internal.cli.result.model.ResultModel; |
| import org.apache.geode.management.internal.cli.result.model.TabularResultModel; |
| import org.apache.geode.management.internal.i18n.CliStrings; |
| import org.apache.geode.pdx.JSONFormatter; |
| import org.apache.geode.pdx.PdxInstance; |
| import org.apache.geode.util.internal.GeodeJsonMapper; |
| |
| |
| /** |
| * Domain object used for Data Commands Functions |
| */ |
| public class DataCommandResult implements Serializable { |
| |
| private static final long serialVersionUID = 2601227194108110936L; |
| |
| public static final Logger logger = LogManager.getLogger(); |
| public static final String DATA_INFO_SECTION = "data-info"; |
| public static final String QUERY_SECTION = "query"; |
| public static final String LOCATION_SECTION = "location"; |
| private String command; |
| private Object putResult; |
| private Object getResult; |
| private List<SelectResultRow> selectResult; |
| private String queryTraceString; |
| |
| public static final String RESULT_FLAG = "Result"; |
| public static final String NUM_ROWS = "Rows"; |
| |
| public static final String MISSING_VALUE = "<NULL>"; |
| |
| // Aggregated Data. |
| private List<KeyInfo> locateEntryLocations; |
| private KeyInfo locateEntryResult; |
| private boolean hasResultForAggregation; |
| |
| private Object removeResult; |
| private Object inputKey; |
| private Object inputValue; |
| private Object inputQuery; |
| private Throwable error; |
| private String errorString; |
| private String infoString; |
| private String keyClass; |
| private String valueClass; |
| private int limit; |
| private boolean operationCompletedSuccessfully; // used for validation purposes. |
| |
| |
| public static final String NEW_LINE = GfshParser.LINE_SEPARATOR; |
| |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| if (isGet()) { |
| sb.append(" Type : Get").append(NEW_LINE); |
| sb.append(" Key : ").append(inputKey).append(NEW_LINE); |
| if (getResult != null) { |
| sb.append(" ReturnValue Class : ").append(getResult.getClass()).append(NEW_LINE); |
| } |
| sb.append(" ReturnValue : ").append(getResult).append(NEW_LINE); |
| } else if (isPut()) { |
| sb.append(" Type : Put"); |
| sb.append(" Key : ").append(inputKey).append(NEW_LINE); |
| if (putResult != null) { |
| sb.append(" ReturnValue Class : ").append(putResult.getClass()).append(NEW_LINE); |
| } |
| sb.append(" ReturnValue : ").append(putResult).append(NEW_LINE); |
| sb.append(" Value : ").append(inputValue).append(NEW_LINE); |
| } else if (isRemove()) { |
| sb.append(" Type : Remove"); |
| sb.append(" Key : ").append(inputKey).append(NEW_LINE); |
| if (removeResult != null) { |
| sb.append(" ReturnValue Class : ").append(removeResult.getClass()).append(NEW_LINE); |
| } |
| sb.append(" ReturnValue : ").append(removeResult).append(NEW_LINE); |
| } else if (isLocateEntry()) { |
| sb.append(" Type : Locate Entry"); |
| sb.append(" Key : ").append(inputKey).append(NEW_LINE); |
| // Assume here that this is aggregated result |
| sb.append(" Results : ").append(locateEntryResult).append(NEW_LINE); |
| sb.append(" Locations : ").append(locateEntryLocations).append(NEW_LINE); |
| } |
| if (errorString != null) { |
| sb.append(" ERROR ").append(errorString); |
| } |
| return sb.toString(); |
| } |
| |
| public boolean isGet() { |
| return CliStrings.GET.equals(command); |
| } |
| |
| public boolean isPut() { |
| return CliStrings.PUT.equals(command); |
| } |
| |
| public boolean isRemove() { |
| return CliStrings.REMOVE.equals(command); |
| } |
| |
| |
| public boolean isLocateEntry() { |
| return CliStrings.LOCATE_ENTRY.equals(command); |
| } |
| |
| public boolean isSelect() { |
| return CliStrings.QUERY.equals(command); |
| } |
| |
| public List<SelectResultRow> getSelectResult() { |
| return selectResult; |
| } |
| |
| |
| public static DataCommandResult createGetResult(Object inputKey, Object value, Throwable error, |
| String errorString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.GET; |
| result.inputKey = inputKey; |
| result.getResult = value; |
| result.error = error; |
| result.errorString = errorString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createGetInfoResult(Object inputKey, Object value, |
| Throwable error, String infoString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.GET; |
| result.inputKey = inputKey; |
| result.getResult = value; |
| result.error = error; |
| result.infoString = infoString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createLocateEntryResult(Object inputKey, KeyInfo locationResult, |
| Throwable error, String errorString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.LOCATE_ENTRY; |
| result.inputKey = inputKey; |
| |
| if (flag) { |
| result.hasResultForAggregation = true; |
| } |
| |
| result.locateEntryResult = locationResult; |
| |
| result.error = error; |
| result.errorString = errorString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createLocateEntryInfoResult(Object inputKey, |
| KeyInfo locationResult, Throwable error, String infoString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.LOCATE_ENTRY; |
| result.inputKey = inputKey; |
| |
| if (flag) { |
| result.hasResultForAggregation = true; |
| } |
| |
| result.locateEntryResult = locationResult; |
| |
| result.error = error; |
| result.infoString = infoString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createPutResult(Object inputKey, Object value, Throwable error, |
| String errorString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.PUT; |
| result.inputKey = inputKey; |
| result.putResult = value; |
| result.error = error; |
| result.errorString = errorString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createPutInfoResult(Object inputKey, Object value, |
| Throwable error, String infoString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.PUT; |
| result.inputKey = inputKey; |
| result.putResult = value; |
| result.error = error; |
| result.infoString = infoString; |
| return result; |
| } |
| |
| public static DataCommandResult createRemoveResult(Object inputKey, Object value, Throwable error, |
| String errorString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.REMOVE; |
| result.inputKey = inputKey; |
| result.removeResult = value; |
| result.error = error; |
| result.errorString = errorString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createRemoveInfoResult(Object inputKey, Object value, |
| Throwable error, String infoString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.REMOVE; |
| result.inputKey = inputKey; |
| result.removeResult = value; |
| result.error = error; |
| result.infoString = infoString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createSelectResult(Object inputQuery, List<SelectResultRow> value, |
| String queryTraceString, Throwable error, String errorString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.QUERY; |
| result.inputQuery = inputQuery; |
| result.queryTraceString = queryTraceString; |
| result.selectResult = value; |
| result.error = error; |
| result.errorString = errorString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| public static DataCommandResult createSelectInfoResult(Object inputQuery, |
| List<SelectResultRow> value, int limit, Throwable error, String infoString, boolean flag) { |
| DataCommandResult result = new DataCommandResult(); |
| result.command = CliStrings.QUERY; |
| result.inputQuery = inputQuery; |
| result.limit = limit; |
| result.selectResult = value; |
| result.error = error; |
| result.infoString = infoString; |
| result.operationCompletedSuccessfully = flag; |
| return result; |
| } |
| |
| |
| public String getCommand() { |
| return command; |
| } |
| |
| public void setCommand(String command) { |
| this.command = command; |
| } |
| |
| public Object getPutResult() { |
| return putResult; |
| } |
| |
| public void setPutResult(Object putResult) { |
| this.putResult = putResult; |
| } |
| |
| public Object getGetResult() { |
| return getResult; |
| } |
| |
| public void setGetResult(Object getResult) { |
| this.getResult = getResult; |
| } |
| |
| public Object getRemoveResult() { |
| return removeResult; |
| } |
| |
| public void setRemoveResult(Object removeResult) { |
| this.removeResult = removeResult; |
| } |
| |
| public Object getInputKey() { |
| return inputKey; |
| } |
| |
| public void setInputKey(Object inputKey) { |
| this.inputKey = inputKey; |
| } |
| |
| public Object getInputValue() { |
| return inputValue; |
| } |
| |
| public void setInputValue(Object inputValue) { |
| this.inputValue = inputValue; |
| } |
| |
| public Throwable getErorr() { |
| return error; |
| } |
| |
| public void setErorr(Throwable erorr) { |
| error = erorr; |
| } |
| |
| public String getErrorString() { |
| return errorString; |
| } |
| |
| public void setErrorString(String errorString) { |
| this.errorString = errorString; |
| } |
| |
| public String getInfoString() { |
| return infoString; |
| } |
| |
| public void setInfoString(String infoString) { |
| this.infoString = infoString; |
| } |
| |
| public String getKeyClass() { |
| return keyClass; |
| } |
| |
| public void setKeyClass(String keyClass) { |
| this.keyClass = keyClass; |
| } |
| |
| public String getValueClass() { |
| return valueClass; |
| } |
| |
| public void setValueClass(String valueClass) { |
| this.valueClass = valueClass; |
| } |
| |
| public List<KeyInfo> getLocateEntryLocations() { |
| return locateEntryLocations; |
| } |
| |
| public ResultModel toResultModel() { |
| if (StringUtils.isEmpty(keyClass)) { |
| keyClass = "java.lang.String"; |
| } |
| |
| if (StringUtils.isEmpty(valueClass)) { |
| valueClass = "java.lang.String"; |
| } |
| |
| ResultModel result = new ResultModel(); |
| DataResultModel data = result.addData(DATA_INFO_SECTION); |
| |
| if (errorString != null) { |
| data.addData("Message", errorString); |
| data.addData(RESULT_FLAG, operationCompletedSuccessfully); |
| return result; |
| } |
| |
| data.addData(RESULT_FLAG, operationCompletedSuccessfully); |
| if (infoString != null) { |
| data.addData("Message", infoString); |
| } |
| |
| if (isGet()) { |
| toResultModel_isGet(data); |
| } else if (isLocateEntry()) { |
| toResultModel_isLocate(result, data); |
| } else if (isPut()) { |
| toResultModel_isPut(data); |
| } else if (isRemove()) { |
| toResultModel_isRemove(data); |
| } |
| |
| return result; |
| } |
| |
| |
| private void toResultModel_isGet(DataResultModel data) { |
| data.addData("Key Class", getKeyClass()); |
| data.addData("Key", inputKey); |
| data.addData("Value Class", getValueClass()); |
| data.addData("Value", getResult != null ? getResult.toString() : "null"); |
| } |
| |
| private void toResultModel_isLocate(ResultModel result, DataResultModel data) { |
| |
| data.addData("Key Class", getKeyClass()); |
| data.addData("Key", inputKey); |
| |
| if (locateEntryLocations != null) { |
| TabularResultModel locationTable = result.addTable(LOCATION_SECTION); |
| |
| int totalLocations = 0; |
| |
| for (KeyInfo info : locateEntryLocations) { |
| List<Object[]> locations = info.getLocations(); |
| |
| if (locations != null) { |
| if (locations.size() == 1) { |
| Object[] array = locations.get(0); |
| boolean found = (Boolean) array[1]; |
| if (found) { |
| totalLocations++; |
| boolean primary = (Boolean) array[3]; |
| String bucketId = (String) array[4]; |
| locationTable.accumulate("MemberName", info.getMemberName()); |
| locationTable.accumulate("MemberId", info.getMemberId()); |
| if (bucketId != null) {// PR |
| if (primary) { |
| locationTable.accumulate("Primary", "*Primary PR*"); |
| } else { |
| locationTable.accumulate("Primary", "No"); |
| } |
| locationTable.accumulate("BucketId", bucketId); |
| } |
| } |
| } else { |
| for (Object[] array : locations) { |
| String regionPath = (String) array[0]; |
| boolean found = (Boolean) array[1]; |
| if (found) { |
| totalLocations++; |
| boolean primary = (Boolean) array[3]; |
| String bucketId = (String) array[4]; |
| locationTable.accumulate("MemberName", info.getMemberName()); |
| locationTable.accumulate("MemberId", info.getMemberId()); |
| locationTable.accumulate("RegionPath", regionPath); |
| if (bucketId != null) {// PR |
| if (primary) { |
| locationTable.accumulate("Primary", "*Primary PR*"); |
| } else { |
| locationTable.accumulate("Primary", "No"); |
| } |
| locationTable.accumulate("BucketId", bucketId); |
| } |
| } |
| } |
| } |
| } |
| } |
| data.addData("Locations Found", totalLocations); |
| } else { |
| data.addData("Location Info ", "Could not find location information"); |
| } |
| } |
| |
| private void toResultModel_isPut(DataResultModel data) { |
| data.addData("Key Class", getKeyClass()); |
| data.addData("Key", inputKey); |
| data.addData("Value Class", getValueClass()); |
| data.addData("Old Value", putResult != null ? putResult.toString() : "null"); |
| } |
| |
| private void toResultModel_isRemove(DataResultModel data) { |
| if (inputKey != null) {// avoids printing key when remove ALL is called |
| data.addData("Key Class", getKeyClass()); |
| data.addData("Key", inputKey); |
| } |
| } |
| |
| /** |
| * This method returns result when flag interactive=false i.e. Command returns result in one go |
| * and does not goes through steps waiting for user input. Method returns CompositeResultData |
| * instead of Result as Command Step is required to add NEXT_STEP information to guide |
| * executionStrategy to route it through final step. |
| */ |
| public ResultModel toSelectCommandResult() { |
| ResultModel result = new ResultModel(); |
| DataResultModel data = result.addData(DATA_INFO_SECTION); |
| if (!operationCompletedSuccessfully) { |
| result.setStatus(Result.Status.ERROR); |
| data.addData(RESULT_FLAG, operationCompletedSuccessfully); |
| if (errorString != null) { |
| data.addData("Message", errorString); |
| } else if (infoString != null) { |
| data.addData("Message", infoString); |
| } |
| return result; |
| } else { |
| TabularResultModel table = result.addTable(DataCommandResult.QUERY_SECTION); |
| data.addData(RESULT_FLAG, operationCompletedSuccessfully); |
| if (infoString != null) { |
| data.addData("Message", infoString); |
| } |
| if (inputQuery != null) { |
| if (limit > 0) { |
| data.addData("Limit", limit); |
| } |
| if (selectResult != null) { |
| data.addData(NUM_ROWS, selectResult.size()); |
| if (queryTraceString != null) { |
| data.addData("Query Trace", queryTraceString); |
| } |
| buildTable(table, 0, selectResult.size()); |
| } |
| } |
| return result; |
| } |
| } |
| |
| private void buildTable(TabularResultModel table, int startCount, int endCount) { |
| int lastRowExclusive = Math.min(selectResult.size(), endCount + 1); |
| List<SelectResultRow> paginatedRows = selectResult.subList(startCount, lastRowExclusive); |
| |
| // First find all the possible columns - not a Set because we want them ordered consistently |
| List<String> possibleColumns = new ArrayList<>(); |
| for (SelectResultRow row : paginatedRows) { |
| for (String column : row.getColumnValues().keySet()) { |
| if (!possibleColumns.contains(column)) { |
| possibleColumns.add(column); |
| } |
| } |
| } |
| |
| for (SelectResultRow row : paginatedRows) { |
| Map<String, String> columnValues = row.getColumnValues(); |
| for (String column : possibleColumns) { |
| table.accumulate(column, |
| columnValues.getOrDefault(column, DataCommandResult.MISSING_VALUE)); |
| } |
| } |
| } |
| |
| public Object getInputQuery() { |
| return inputQuery; |
| } |
| |
| public void setInputQuery(Object inputQuery) { |
| this.inputQuery = inputQuery; |
| } |
| |
| public void setLimit(int limit) { |
| this.limit = limit; |
| } |
| |
| public static class KeyInfo implements Serializable { |
| |
| private String memberId; |
| private String memberName; |
| private String host; |
| private int pid; |
| |
| // Indexes : regionName = 0, found=1, value=2 primary=3 bucketId=4 |
| private ArrayList<Object[]> locations = null; |
| |
| public void addLocation(Object[] locationArray) { |
| if (locations == null) { |
| locations = new ArrayList<>(); |
| } |
| |
| locations.add(locationArray); |
| } |
| |
| public List<Object[]> getLocations() { |
| return locations; |
| } |
| |
| public String getMemberId() { |
| return memberId; |
| } |
| |
| public void setMemberId(String memberId) { |
| this.memberId = memberId; |
| } |
| |
| public String getMemberName() { |
| return memberName; |
| } |
| |
| public void setMemberName(String memberName) { |
| this.memberName = memberName; |
| } |
| |
| public String getHost() { |
| return host; |
| } |
| |
| public void setHost(String host) { |
| this.host = host; |
| } |
| |
| public int getPid() { |
| return pid; |
| } |
| |
| public void setPid(int pid) { |
| this.pid = pid; |
| } |
| |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("{ Member : ").append(host).append("(").append(memberId).append(") , "); |
| for (Object[] array : locations) { |
| boolean primary = (Boolean) array[3]; |
| String bucketId = (String) array[4]; |
| sb.append(" [ Primary : ").append(primary).append(" , BucketId : ").append(bucketId) |
| .append(" ]"); |
| } |
| sb.append(" }"); |
| return sb.toString(); |
| } |
| |
| public boolean hasLocation() { |
| if (locations == null) { |
| return false; |
| } else { |
| for (Object[] array : locations) { |
| boolean found = (Boolean) array[1]; |
| if (found) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| } |
| |
| public static final int ROW_TYPE_STRUCT_RESULT = 100; |
| public static final int ROW_TYPE_BEAN = 200; |
| public static final int ROW_TYPE_PRIMITIVE = 300; |
| |
| public static class SelectResultRow implements Serializable { |
| private static final long serialVersionUID = 1L; |
| private final int type; |
| private final Map<String, String> columnValues; |
| |
| public SelectResultRow(int type, Object value) { |
| this.type = type; |
| columnValues = createColumnValues(value); |
| } |
| |
| public Map<String, String> getColumnValues() { |
| return columnValues; |
| } |
| |
| private Map<String, String> createColumnValues(Object value) { |
| Map<String, String> result = new LinkedHashMap<>(); |
| |
| if (value == null || MISSING_VALUE.equals(value)) { |
| result.put("Value", MISSING_VALUE); |
| } else if (type == ROW_TYPE_PRIMITIVE) { |
| result.put(RESULT_FLAG, value.toString()); |
| } else if (value instanceof Undefined) { |
| result.put("Value", "UNDEFINED"); |
| } else { |
| resolveObjectToColumns(result, value); |
| } |
| |
| return result; |
| } |
| |
| private void resolveObjectToColumns(Map<String, String> columnData, Object value) { |
| if (value instanceof PdxInstance) { |
| resolvePdxToColumns(columnData, (PdxInstance) value); |
| } else if (value instanceof Struct) { |
| resolveStructToColumns(columnData, (StructImpl) value); |
| } else if (value instanceof UUID) { |
| columnData.put("Result", valueToJson(value)); |
| } else { |
| ObjectMapper mapper = GeodeJsonMapper.getMapperWithAlwaysInclusion(); |
| JsonNode node = mapper.valueToTree(value); |
| |
| node.fieldNames().forEachRemaining(field -> { |
| try { |
| columnData.put(field, mapper.writeValueAsString(node.get(field))); |
| } catch (JsonProcessingException e) { |
| columnData.put(field, e.getMessage()); |
| } |
| }); |
| } |
| } |
| |
| private void resolvePdxToColumns(Map<String, String> columnData, PdxInstance pdx) { |
| for (String field : pdx.getFieldNames()) { |
| columnData.put(field, valueToJson(pdx.getField(field))); |
| } |
| } |
| |
| private void resolveStructToColumns(Map<String, String> columnData, StructImpl struct) { |
| for (String field : struct.getFieldNames()) { |
| columnData.put(field, valueToJson(struct.get(field))); |
| } |
| } |
| |
| private String valueToJson(Object value) { |
| if (value == null) { |
| return "null"; |
| } |
| |
| if (value instanceof String) { |
| return (String) value; |
| } |
| |
| if (value instanceof PdxInstance) { |
| return JSONFormatter.toJSON((PdxInstance) value); |
| } |
| |
| ObjectMapper mapper = GeodeJsonMapper.getMapperWithAlwaysInclusion(); |
| try { |
| return mapper.writeValueAsString(value); |
| } catch (JsonProcessingException jex) { |
| return jex.getMessage(); |
| } |
| } |
| } |
| |
| public void aggregate(DataCommandResult result) { |
| /* Right now only called for LocateEntry */ |
| if (!isLocateEntry()) { |
| return; |
| } |
| |
| if (locateEntryLocations == null) { |
| locateEntryLocations = new ArrayList<>(); |
| } |
| |
| if (result == null) {// self-transform result from single to aggregate when numMember==1 |
| if (locateEntryResult != null) { |
| locateEntryLocations.add(locateEntryResult); |
| } |
| return; |
| } |
| |
| if (result.errorString != null && !result.errorString.equals(errorString)) { |
| // append errorString only if differs |
| errorString = result.errorString + " " + errorString; |
| } |
| |
| // append message only when it differs for negative results |
| if (!operationCompletedSuccessfully && result.infoString != null |
| && !result.infoString.equals(infoString)) { |
| infoString = result.infoString; |
| } |
| |
| if (result.hasResultForAggregation) { |
| operationCompletedSuccessfully = true; |
| infoString = result.infoString; |
| if (result.locateEntryResult != null) { |
| locateEntryLocations.add(result.locateEntryResult); |
| } |
| } |
| } |
| |
| } |