blob: 8c8d58f89d6352425a9f9445c629036314efaa93 [file] [log] [blame]
Jin Gan79f75372017-01-05 15:08:11 -08001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2017-present Open Networking Foundation
Jin Gan79f75372017-01-05 15:08:11 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
jingan7c5bf1f2017-02-09 02:58:09 -080016
Jin Gan79f75372017-01-05 15:08:11 -080017package org.onosproject.restconf.restconfmanager;
18
19import com.fasterxml.jackson.databind.node.ObjectNode;
20import com.google.common.util.concurrent.ThreadFactoryBuilder;
21import org.apache.felix.scr.annotations.Activate;
22import org.apache.felix.scr.annotations.Component;
23import org.apache.felix.scr.annotations.Deactivate;
jingan7c5bf1f2017-02-09 02:58:09 -080024import org.apache.felix.scr.annotations.Reference;
25import org.apache.felix.scr.annotations.ReferenceCardinality;
Jin Gan79f75372017-01-05 15:08:11 -080026import org.apache.felix.scr.annotations.Service;
27import org.glassfish.jersey.server.ChunkedOutput;
Henry Yu14af7782017-03-09 19:33:36 -050028import org.onosproject.config.DynamicConfigService;
29import org.onosproject.config.FailedException;
30import org.onosproject.config.Filter;
Yuta HIGUCHIfa105b22018-03-01 21:16:51 -080031import org.onosproject.d.config.ResourceIds;
Sean Condon13b16812018-01-25 10:31:49 +000032import org.onosproject.restconf.api.RestconfError;
Henry Yu14af7782017-03-09 19:33:36 -050033import org.onosproject.restconf.api.RestconfException;
Henry Yuc10f7fc2017-07-26 13:42:08 -040034import org.onosproject.restconf.api.RestconfRpcOutput;
Henry Yu14af7782017-03-09 19:33:36 -050035import org.onosproject.restconf.api.RestconfService;
Henry Yuc10f7fc2017-07-26 13:42:08 -040036import org.onosproject.restconf.utils.RestconfUtils;
Henry Yu14af7782017-03-09 19:33:36 -050037import org.onosproject.yang.model.DataNode;
Henry Yuc10f7fc2017-07-26 13:42:08 -040038import org.onosproject.yang.model.DefaultResourceData;
sonugupta-huaweif0af7aa2017-03-17 00:54:52 +053039import org.onosproject.yang.model.InnerNode;
40import org.onosproject.yang.model.KeyLeaf;
41import org.onosproject.yang.model.ListKey;
42import org.onosproject.yang.model.NodeKey;
Henry Yu14af7782017-03-09 19:33:36 -050043import org.onosproject.yang.model.ResourceData;
44import org.onosproject.yang.model.ResourceId;
Henry Yuc10f7fc2017-07-26 13:42:08 -040045import org.onosproject.yang.model.RpcInput;
46import org.onosproject.yang.model.RpcOutput;
sonugupta-huaweif0af7aa2017-03-17 00:54:52 +053047import org.onosproject.yang.model.SchemaId;
Jin Gan79f75372017-01-05 15:08:11 -080048import org.slf4j.Logger;
49import org.slf4j.LoggerFactory;
50
Sean Condon13b16812018-01-25 10:31:49 +000051import javax.ws.rs.core.Response;
Henry Yuc10f7fc2017-07-26 13:42:08 -040052import java.net.URI;
Sean Condon13b16812018-01-25 10:31:49 +000053import java.util.Arrays;
jingan7c5bf1f2017-02-09 02:58:09 -080054import java.util.List;
Henry Yuc10f7fc2017-07-26 13:42:08 -040055import java.util.Map;
Sean Condon13b16812018-01-25 10:31:49 +000056import java.util.Optional;
Henry Yuc10f7fc2017-07-26 13:42:08 -040057import java.util.concurrent.CompletableFuture;
Jin Gan79f75372017-01-05 15:08:11 -080058import java.util.concurrent.ExecutorService;
59import java.util.concurrent.Executors;
Henry Yu14af7782017-03-09 19:33:36 -050060
Sean Condon13b16812018-01-25 10:31:49 +000061import static javax.ws.rs.core.Response.Status.CONFLICT;
Jin Gan79f75372017-01-05 15:08:11 -080062import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
Henry Yu830b5dc2017-11-16 10:44:45 -050063import static org.onosproject.d.config.ResourceIds.parentOf;
jingan7c5bf1f2017-02-09 02:58:09 -080064import static org.onosproject.restconf.utils.RestconfUtils.convertDataNodeToJson;
Henry Yu14af7782017-03-09 19:33:36 -050065import static org.onosproject.restconf.utils.RestconfUtils.convertJsonToDataNode;
Henry Yuc10f7fc2017-07-26 13:42:08 -040066import static org.onosproject.restconf.utils.RestconfUtils.rmLastPathSegment;
sonugupta-huaweif0af7aa2017-03-17 00:54:52 +053067import static org.onosproject.yang.model.DataNode.Type.MULTI_INSTANCE_NODE;
68import static org.onosproject.yang.model.DataNode.Type.SINGLE_INSTANCE_LEAF_VALUE_NODE;
69import static org.onosproject.yang.model.DataNode.Type.SINGLE_INSTANCE_NODE;
jingan7c5bf1f2017-02-09 02:58:09 -080070
Jin Gan79f75372017-01-05 15:08:11 -080071/*
jingan7c5bf1f2017-02-09 02:58:09 -080072 * ONOS RESTCONF application. The RESTCONF Manager
73 * implements the main logic of the RESTCONF application.
Jin Gan79f75372017-01-05 15:08:11 -080074 *
75 * The design of the RESTCONF subsystem contains 2 major bundles:
jingan7c5bf1f2017-02-09 02:58:09 -080076 * This bundle module is the back-end of the server.
Jin Gan79f75372017-01-05 15:08:11 -080077 * It provides the main logic of the RESTCONF server. It interacts with
jingan7c5bf1f2017-02-09 02:58:09 -080078 * the Dynamic Config Service and yang runtime service to run operations
79 * on the YANG data objects (i.e., resource id, yang data node).
Jin Gan79f75372017-01-05 15:08:11 -080080 */
81
jingan8a773322017-03-21 16:12:48 -070082@Component(immediate = true)
Jin Gan79f75372017-01-05 15:08:11 -080083@Service
84public class RestconfManager implements RestconfService {
85
86 private static final String RESTCONF_ROOT = "/onos/restconf";
Jin Gan79f75372017-01-05 15:08:11 -080087
88 private final int maxNumOfWorkerThreads = 5;
89
90 private final Logger log = LoggerFactory.getLogger(getClass());
91
jingan7c5bf1f2017-02-09 02:58:09 -080092 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
Henry Yu14af7782017-03-09 19:33:36 -050093 protected DynamicConfigService dynamicConfigService;
Jin Gan79f75372017-01-05 15:08:11 -080094
Jin Gan79f75372017-01-05 15:08:11 -080095 private ExecutorService workerThreadPool;
96
97 @Activate
98 protected void activate() {
99 workerThreadPool = Executors
100 .newFixedThreadPool(maxNumOfWorkerThreads,
101 new ThreadFactoryBuilder()
102 .setNameFormat("restconf-worker")
103 .build());
104 log.info("Started");
105 }
106
107 @Deactivate
108 protected void deactivate() {
Henry Yuc10f7fc2017-07-26 13:42:08 -0400109 workerThreadPool.shutdownNow();
Jin Gan79f75372017-01-05 15:08:11 -0800110 log.info("Stopped");
111 }
112
113 @Override
Henry Yuc10f7fc2017-07-26 13:42:08 -0400114 public ObjectNode runGetOperationOnDataResource(URI uri)
Jin Gan79f75372017-01-05 15:08:11 -0800115 throws RestconfException {
Henry Yu830b5dc2017-11-16 10:44:45 -0500116 DataResourceLocator rl = DataResourceLocator.newInstance(uri);
jingan7c5bf1f2017-02-09 02:58:09 -0800117 // TODO: define Filter (if there is any requirement).
Yuta HIGUCHIac85ee12017-08-03 20:07:35 -0700118 Filter filter = Filter.builder().build();
jingan7c5bf1f2017-02-09 02:58:09 -0800119 DataNode dataNode;
Henry Yuc10f7fc2017-07-26 13:42:08 -0400120
jingan7c5bf1f2017-02-09 02:58:09 -0800121 try {
Henry Yu830b5dc2017-11-16 10:44:45 -0500122 if (!dynamicConfigService.nodeExist(rl.ridForDynConfig())) {
Henry Yuc10f7fc2017-07-26 13:42:08 -0400123 return null;
124 }
Henry Yu830b5dc2017-11-16 10:44:45 -0500125 dataNode = dynamicConfigService.readNode(rl.ridForDynConfig(), filter);
jingan7c5bf1f2017-02-09 02:58:09 -0800126 } catch (FailedException e) {
127 log.error("ERROR: DynamicConfigService: ", e);
Sean Condon13b16812018-01-25 10:31:49 +0000128 throw new RestconfException("ERROR: DynamicConfigService", e,
129 RestconfError.ErrorTag.OPERATION_FAILED, INTERNAL_SERVER_ERROR,
130 Optional.of(uri.getPath()));
jingan7c5bf1f2017-02-09 02:58:09 -0800131 }
Henry Yu830b5dc2017-11-16 10:44:45 -0500132 ObjectNode rootNode = convertDataNodeToJson(rl.ridForYangRuntime(), dataNode);
jingan7c5bf1f2017-02-09 02:58:09 -0800133 return rootNode;
Jin Gan79f75372017-01-05 15:08:11 -0800134 }
135
136 @Override
Henry Yuc10f7fc2017-07-26 13:42:08 -0400137 public void runPostOperationOnDataResource(URI uri, ObjectNode rootNode)
Jin Gan79f75372017-01-05 15:08:11 -0800138 throws RestconfException {
Henry Yu830b5dc2017-11-16 10:44:45 -0500139 DataResourceLocator rl = DataResourceLocator.newInstance(uri);
140 ResourceData receivedData = convertJsonToDataNode(rl.uriForYangRuntime(), rootNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400141 ResourceId rid = receivedData.resourceId();
142 List<DataNode> dataNodeList = receivedData.dataNodes();
jingan7c5bf1f2017-02-09 02:58:09 -0800143 if (dataNodeList.size() > 1) {
Henry Yuc10f7fc2017-07-26 13:42:08 -0400144 log.warn("There are more than one Data Node can be proceed: {}", dataNodeList.size());
jingan7c5bf1f2017-02-09 02:58:09 -0800145 }
146 DataNode dataNode = dataNodeList.get(0);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400147
148 if (rid == null) {
149 rid = ResourceId.builder().addBranchPointSchema("/", null).build();
150 dataNode = removeTopNode(dataNode);
151 }
152
jingan7c5bf1f2017-02-09 02:58:09 -0800153 try {
Henry Yu830b5dc2017-11-16 10:44:45 -0500154 dynamicConfigService.createNode(rl.ridForDynConfig(), dataNode);
Yuta HIGUCHIfa105b22018-03-01 21:16:51 -0800155 } catch (Exception e) {
Sean Condon13b16812018-01-25 10:31:49 +0000156 if (e.getMessage().startsWith("Requested node already present")) {
157 throw new RestconfException("Already exists", e,
158 RestconfError.ErrorTag.DATA_EXISTS, CONFLICT,
159 Optional.of(uri.getPath()));
160 } else {
Yuta HIGUCHIfa105b22018-03-01 21:16:51 -0800161 log.error("ERROR: DynamicConfigService: creating {} with {}",
162 ResourceIds.toInstanceIdentifier(rl.ridForDynConfig()),
163 dataNode,
164 e);
Sean Condon13b16812018-01-25 10:31:49 +0000165 throw new RestconfException("ERROR: DynamicConfigService", e,
166 RestconfError.ErrorTag.OPERATION_FAILED, INTERNAL_SERVER_ERROR,
167 Optional.of(uri.getPath()));
168 }
jingan7c5bf1f2017-02-09 02:58:09 -0800169 }
Jin Gan79f75372017-01-05 15:08:11 -0800170 }
171
172 @Override
Henry Yuc10f7fc2017-07-26 13:42:08 -0400173 public void runPutOperationOnDataResource(URI uri, ObjectNode rootNode)
Jin Gan79f75372017-01-05 15:08:11 -0800174 throws RestconfException {
Henry Yu830b5dc2017-11-16 10:44:45 -0500175 DataResourceLocator rl = DataResourceLocator.newInstance(uri);
176 ResourceData receivedData = convertJsonToDataNode(rmLastPathSegment(rl.uriForYangRuntime()), rootNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400177 List<DataNode> dataNodeList = receivedData.dataNodes();
178 if (dataNodeList.size() > 1) {
179 log.warn("There are more than one Data Node can be proceed: {}", dataNodeList.size());
180 }
181 DataNode dataNode = dataNodeList.get(0);
182
jingan7c5bf1f2017-02-09 02:58:09 -0800183 try {
Henry Yuc10f7fc2017-07-26 13:42:08 -0400184 /*
185 * If the data node already exists, then replace it.
186 * Otherwise, create it.
187 */
Henry Yu830b5dc2017-11-16 10:44:45 -0500188 if (dynamicConfigService.nodeExist(rl.ridForDynConfig())) {
189 dynamicConfigService.replaceNode(parentOf(rl.ridForDynConfig()), dataNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400190 } else {
Henry Yu830b5dc2017-11-16 10:44:45 -0500191 dynamicConfigService.createNode(parentOf(rl.ridForDynConfig()), dataNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400192 }
193
jingan7c5bf1f2017-02-09 02:58:09 -0800194 } catch (FailedException e) {
195 log.error("ERROR: DynamicConfigService: ", e);
Sean Condon13b16812018-01-25 10:31:49 +0000196 throw new RestconfException("ERROR: DynamicConfigService", e,
197 RestconfError.ErrorTag.OPERATION_FAILED, INTERNAL_SERVER_ERROR,
198 Optional.of(uri.getPath()));
jingan7c5bf1f2017-02-09 02:58:09 -0800199 }
Jin Gan79f75372017-01-05 15:08:11 -0800200 }
201
202 @Override
Henry Yuc10f7fc2017-07-26 13:42:08 -0400203 public void runDeleteOperationOnDataResource(URI uri)
Jin Gan79f75372017-01-05 15:08:11 -0800204 throws RestconfException {
Henry Yu830b5dc2017-11-16 10:44:45 -0500205 DataResourceLocator rl = DataResourceLocator.newInstance(uri);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400206 try {
Henry Yu830b5dc2017-11-16 10:44:45 -0500207 if (dynamicConfigService.nodeExist(rl.ridForDynConfig())) {
208 dynamicConfigService.deleteNode(rl.ridForDynConfig());
Henry Yuc10f7fc2017-07-26 13:42:08 -0400209 }
210 } catch (FailedException e) {
211 log.error("ERROR: DynamicConfigService: ", e);
Sean Condon13b16812018-01-25 10:31:49 +0000212 throw new RestconfException("ERROR: DynamicConfigService", e,
213 RestconfError.ErrorTag.OPERATION_FAILED, INTERNAL_SERVER_ERROR,
214 Optional.of(uri.getPath()));
Henry Yuc10f7fc2017-07-26 13:42:08 -0400215 }
216 }
217
218 @Override
219 public void runPatchOperationOnDataResource(URI uri, ObjectNode rootNode)
220 throws RestconfException {
Henry Yu830b5dc2017-11-16 10:44:45 -0500221 DataResourceLocator rl = DataResourceLocator.newInstance(uri);
222 ResourceData receivedData = convertJsonToDataNode(rmLastPathSegment(rl.uriForYangRuntime()), rootNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400223 ResourceId rid = receivedData.resourceId();
224 List<DataNode> dataNodeList = receivedData.dataNodes();
225 if (dataNodeList.size() > 1) {
226 log.warn("There are more than one Data Node can be proceed: {}", dataNodeList.size());
227 }
228 DataNode dataNode = dataNodeList.get(0);
229
230 if (rid == null) {
231 rid = ResourceId.builder().addBranchPointSchema("/", null).build();
232 dataNode = removeTopNode(dataNode);
233 }
234
235 try {
Henry Yu830b5dc2017-11-16 10:44:45 -0500236 dynamicConfigService.updateNode(parentOf(rl.ridForDynConfig()), dataNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400237 } catch (FailedException e) {
238 log.error("ERROR: DynamicConfigService: ", e);
Sean Condon13b16812018-01-25 10:31:49 +0000239 throw new RestconfException("ERROR: DynamicConfigService", e,
240 RestconfError.ErrorTag.OPERATION_FAILED, INTERNAL_SERVER_ERROR,
241 Optional.of(uri.getPath()));
Henry Yuc10f7fc2017-07-26 13:42:08 -0400242 }
243 }
244
245 private DataNode removeTopNode(DataNode dataNode) {
246 if (dataNode instanceof InnerNode && dataNode.key().schemaId().name().equals("/")) {
247 Map.Entry<NodeKey, DataNode> entry = ((InnerNode) dataNode).childNodes().entrySet().iterator().next();
248 dataNode = entry.getValue();
249 }
250 return dataNode;
Jin Gan79f75372017-01-05 15:08:11 -0800251 }
252
253 @Override
254 public String getRestconfRootPath() {
255 return RESTCONF_ROOT;
256 }
257
Jin Gan79f75372017-01-05 15:08:11 -0800258 @Override
Henry Yuc10f7fc2017-07-26 13:42:08 -0400259 public void subscribeEventStream(String streamId, String clientIpAddr,
Jin Gan79f75372017-01-05 15:08:11 -0800260 ChunkedOutput<String> output)
261 throws RestconfException {
Henry Yuc10f7fc2017-07-26 13:42:08 -0400262 //TODO: to be completed
Sean Condon13b16812018-01-25 10:31:49 +0000263 throw new RestconfException("Not implemented",
264 RestconfError.ErrorTag.OPERATION_NOT_SUPPORTED,
265 Response.Status.NOT_IMPLEMENTED,
266 Optional.empty(), Optional.of("subscribeEventStream not yet implemented"));
Henry Yuc10f7fc2017-07-26 13:42:08 -0400267 }
268
269 @Override
270 public CompletableFuture<RestconfRpcOutput> runRpc(URI uri,
271 ObjectNode input,
272 String clientIpAddress) {
273 CompletableFuture<RestconfRpcOutput> result =
274 CompletableFuture.supplyAsync(() -> executeRpc(uri, input, clientIpAddress));
275 return result;
276 }
277
278 private RestconfRpcOutput executeRpc(URI uri, ObjectNode input, String clientIpAddress) {
279 ResourceData rpcInputNode = convertJsonToDataNode(uri, input);
280 ResourceId resourceId = rpcInputNode.resourceId();
281 List<DataNode> inputDataNodeList = rpcInputNode.dataNodes();
282 DataNode inputDataNode = inputDataNodeList.get(0);
Gaurav Agrawal142ceb02018-02-16 12:19:08 +0530283 RpcInput rpcInput = new RpcInput(resourceId, inputDataNode);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400284
285 RestconfRpcOutput restconfOutput = null;
286 try {
287 CompletableFuture<RpcOutput> rpcFuture =
Gaurav Agrawal142ceb02018-02-16 12:19:08 +0530288 dynamicConfigService.invokeRpc(rpcInput);
Henry Yuc10f7fc2017-07-26 13:42:08 -0400289 RpcOutput rpcOutput = rpcFuture.get();
290 restconfOutput = RestconfUtils.convertRpcOutput(resourceId, rpcOutput);
291 } catch (InterruptedException e) {
292 log.error("ERROR: computeResultQ.take() has been interrupted.");
293 log.debug("executeRpc Exception:", e);
Sean Condon13b16812018-01-25 10:31:49 +0000294 RestconfError error =
295 RestconfError.builder(RestconfError.ErrorType.RPC,
296 RestconfError.ErrorTag.OPERATION_FAILED)
297 .errorMessage("RPC execution has been interrupted")
298 .errorPath(uri.getPath())
299 .build();
300 restconfOutput = new RestconfRpcOutput(INTERNAL_SERVER_ERROR,
301 RestconfError.wrapErrorAsJson(Arrays.asList(error)));
Henry Yuc10f7fc2017-07-26 13:42:08 -0400302 restconfOutput.reason("RPC execution has been interrupted");
303 } catch (Exception e) {
304 log.error("ERROR: executeRpc: {}", e.getMessage());
305 log.debug("executeRpc Exception:", e);
Sean Condon13b16812018-01-25 10:31:49 +0000306 RestconfError error =
307 RestconfError.builder(RestconfError.ErrorType.RPC,
308 RestconfError.ErrorTag.OPERATION_FAILED)
309 .errorMessage(e.getMessage())
310 .errorPath(uri.getPath())
311 .build();
312 restconfOutput = new RestconfRpcOutput(INTERNAL_SERVER_ERROR,
313 RestconfError.wrapErrorAsJson(Arrays.asList(error)));
Henry Yuc10f7fc2017-07-26 13:42:08 -0400314 restconfOutput.reason(e.getMessage());
Jin Gan79f75372017-01-05 15:08:11 -0800315 }
316
Henry Yuc10f7fc2017-07-26 13:42:08 -0400317 return restconfOutput;
Jin Gan79f75372017-01-05 15:08:11 -0800318 }
319
sonugupta-huaweif0af7aa2017-03-17 00:54:52 +0530320 private ResourceData getDataForStore(ResourceData resourceData) {
321 List<DataNode> nodes = resourceData.dataNodes();
322 ResourceId rid = resourceData.resourceId();
323 DataNode.Builder dbr = null;
324 ResourceId parentId = null;
325 try {
326 NodeKey lastKey = rid.nodeKeys().get(rid.nodeKeys().size() - 1);
327 SchemaId sid = lastKey.schemaId();
328 if (lastKey instanceof ListKey) {
329 dbr = InnerNode.builder(
330 sid.name(), sid.namespace()).type(MULTI_INSTANCE_NODE);
331 for (KeyLeaf keyLeaf : ((ListKey) lastKey).keyLeafs()) {
332 Object val = keyLeaf.leafValue();
333 dbr = dbr.addKeyLeaf(keyLeaf.leafSchema().name(),
334 sid.namespace(), val);
335 dbr = dbr.createChildBuilder(keyLeaf.leafSchema().name(),
336 sid.namespace(), val)
337 .type(SINGLE_INSTANCE_LEAF_VALUE_NODE);
sonugupta-huawei6119ac72017-03-21 16:25:40 +0530338 //Exit for key leaf node
339 dbr = dbr.exitNode();
sonugupta-huaweif0af7aa2017-03-17 00:54:52 +0530340 }
341 } else {
342 dbr = InnerNode.builder(
343 sid.name(), sid.namespace()).type(SINGLE_INSTANCE_NODE);
344 }
345 if (nodes != null && !nodes.isEmpty()) {
346 // adding the parent node for given list of nodes
347 for (DataNode node : nodes) {
348 dbr = ((InnerNode.Builder) dbr).addNode(node);
349 }
350 }
351 parentId = rid.copyBuilder().removeLastKey().build();
352 } catch (CloneNotSupportedException e) {
Ray Milkey74e59132018-01-17 15:24:52 -0800353 log.error("getDataForStore()", e);
354 return null;
sonugupta-huaweif0af7aa2017-03-17 00:54:52 +0530355 }
356 ResourceData.Builder resData = DefaultResourceData.builder();
357 resData.addDataNode(dbr.build());
358 resData.resourceId(parentId);
359 return resData.build();
360 }
Jin Gan79f75372017-01-05 15:08:11 -0800361}