ref: Remove ConfigServiceIndexes

重构代码
dataId 不可缺省, 通过参数覆盖默认配置
pull/2349/head
Freeman Lau 3 years ago
parent 8958392914
commit 21e22e0696

@ -6,13 +6,10 @@ spring:
cloud:
nacos:
config:
name: test.yml
file-extension: yml
group: DEFAULT_GROUP
server-addr: localhost:8848
config:
import:
- optional:nacos:/
- optional:nacos:/group_02
- optional:nacos:group_03/test01.yml
- optional:nacos:group_04/test02.yml?refreshEnabled=false
- optional:nacos:test.yml
- optional:nacos:test01.yml?group=group_02
- optional:nacos:test02.yml?group=group_03&refreshEnabled=false

@ -1,32 +0,0 @@
/*
* Copyright 2015-2020 the original author or authors.
*
* Licensed 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
*
* https://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 com.alibaba.cloud.nacos.configdata;
import java.util.Map;
import com.alibaba.nacos.api.config.ConfigService;
/**
* namespace -> {@link ConfigService} mapping
*
* @author freeman
*/
public interface ConfigServiceIndexes {
Map<String, ConfigService> getIndexes();
}

@ -19,6 +19,7 @@ package com.alibaba.cloud.nacos.configdata;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
@ -35,7 +36,7 @@ import org.springframework.util.Assert;
*/
public class NacosBootstrapper implements BootstrapRegistryInitializer {
private Function<BootstrapContext, ConfigServiceIndexes> configServiceFactory;
private Function<BootstrapContext, ConfigService> configServiceFactory;
private LoaderInterceptor loaderInterceptor;
@ -44,8 +45,8 @@ public class NacosBootstrapper implements BootstrapRegistryInitializer {
}
public NacosBootstrapper withConfigServiceFactory(
Function<BootstrapContext, ConfigServiceIndexes> indexesFactory) {
this.configServiceFactory = indexesFactory;
Function<BootstrapContext, ConfigService> configServiceFactory) {
this.configServiceFactory = configServiceFactory;
return this;
}
@ -57,7 +58,7 @@ public class NacosBootstrapper implements BootstrapRegistryInitializer {
@Override
public void initialize(BootstrapRegistry registry) {
if (configServiceFactory != null) {
registry.register(ConfigServiceIndexes.class, configServiceFactory::apply);
registry.register(ConfigService.class, configServiceFactory::apply);
}
if (loaderInterceptor != null) {
registry.register(LoaderInterceptor.class,

@ -26,6 +26,7 @@ import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.NacosPropertySourceRepository;
import com.alibaba.cloud.nacos.client.NacosPropertySource;
import com.alibaba.cloud.nacos.parser.NacosDataParserHandler;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import org.apache.commons.logging.Log;
@ -41,8 +42,13 @@ import org.springframework.util.StringUtils;
import static com.alibaba.cloud.nacos.configdata.NacosBootstrapper.LoadContext;
import static com.alibaba.cloud.nacos.configdata.NacosBootstrapper.LoaderInterceptor;
import static com.alibaba.cloud.nacos.configdata.NacosConfigDataResource.NacosItemConfig;
/**
* Implementation of {@link ConfigDataLoader}.
*
* <p> Load {@link ConfigData} via {@link NacosConfigDataResource}
*
* @author freeman
*/
public class NacosConfigDataLoader implements ConfigDataLoader<NacosConfigDataResource> {
@ -73,23 +79,20 @@ public class NacosConfigDataLoader implements ConfigDataLoader<NacosConfigDataRe
public ConfigData doLoad(ConfigDataLoaderContext context,
NacosConfigDataResource resource) {
try {
ConfigServiceIndexes configServiceIndexes = getBean(context,
ConfigServiceIndexes.class);
ConfigService configService = getBean(context, ConfigService.class);
NacosConfigProperties properties = getBean(context,
NacosConfigProperties.class);
NacosItemConfig config = resource.getConfig();
// pull config from nacos
List<PropertySource<?>> propertySources = pullConfig(configServiceIndexes,
resource.getConfig().getNamespace(), resource.getConfig().getGroup(),
resource.getConfig().getDataId(), resource.getConfig().getSuffix(),
List<PropertySource<?>> propertySources = pullConfig(configService,
config.getGroup(), config.getDataId(), config.getSuffix(),
properties.getTimeout());
NacosPropertySource propertySource = new NacosPropertySource(propertySources,
resource.getConfig().getGroup(), resource.getConfig().getDataId(), new Date(),
resource.getConfig().isRefreshEnabled());
config.getGroup(), config.getDataId(), new Date(),
config.isRefreshEnabled());
// TODO support different namespace ?
// If Spring cloud version >= 2.0.3 add ConfigDataMissingEnvironmentPostProcessor
NacosPropertySourceRepository.collectNacosPropertySource(propertySource);
if (ALL_OPTIONS.size() == 1) {
@ -125,11 +128,10 @@ public class NacosConfigDataLoader implements ConfigDataLoader<NacosConfigDataRe
return null;
}
private List<PropertySource<?>> pullConfig(ConfigServiceIndexes indexes,
String namespace, String group, String dataId, String suffix, long timeout)
private List<PropertySource<?>> pullConfig(ConfigService configService,
String group, String dataId, String suffix, long timeout)
throws NacosException, IOException {
String config = indexes.getIndexes().get(namespace).getConfig(dataId, group,
timeout);
String config = configService.getConfig(dataId, group, timeout);
return NacosDataParserHandler.getInstance().parseNacosData(dataId, config,
suffix);
}

@ -19,7 +19,6 @@ package com.alibaba.cloud.nacos.configdata;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.nacos.api.config.ConfigFactory;
@ -40,23 +39,25 @@ import org.springframework.core.env.StandardEnvironment;
import static com.alibaba.cloud.nacos.configdata.NacosConfigDataResource.NacosItemConfig;
/**
* Implementation of {@link ConfigDataLocationResolver}, load Nacos {@link ConfigDataResource}.
*
* @author freeman
*/
public class NacosConfigDataLocationResolver
implements ConfigDataLocationResolver<NacosConfigDataResource>, Ordered {
private static final String HYPHEN = "-";
private static final String DOT = ".";
/**
* Prefix for Config Server imports.
*/
public static final String PREFIX = "nacos:";
public static final String DEFAULT_NAMESPACE = StringUtils.EMPTY;
private final Log log;
// support params
public static final String GROUP = "group";
public static final String REFRESH_ENABLED = "refreshEnabled";
public NacosConfigDataLocationResolver(Log log) {
this.log = log;
}
@ -134,57 +135,29 @@ public class NacosConfigDataLocationResolver
bootstrapContext.registerIfAbsent(NacosConfigProperties.class,
InstanceSupplier.of(properties));
registerOrUpdateIndexes(properties, bootstrapContext);
registerConfigService(properties, bootstrapContext);
// Retain the processing logic of the old version.
// Make sure to upgrade smoothly.
return loadConfigDataResources(resolverContext, location, profiles, properties);
return loadConfigDataResources(location, profiles, properties);
}
private List<NacosConfigDataResource> loadConfigDataResources(
ConfigDataLocationResolverContext resolverContext,
ConfigDataLocation location, Profiles profiles,
NacosConfigProperties properties) {
List<NacosConfigDataResource> result = new ArrayList<>();
String uris = getUri(location, properties);
if (StringUtils.isNotBlank(dataIdFor(uris))) {
// Can be considered as an extension-config.
NacosConfigDataResource resource = loadResource(properties,
location.isOptional(), profiles, uris, dataIdFor(uris));
result.add(resource);
if (StringUtils.isBlank(dataIdFor(uris))) {
throw new IllegalArgumentException("dataId must be specified");
}
else {
String prefix = dataIdPrefixFor(resolverContext.getBinder(), properties);
NacosConfigDataResource resource = loadResource(properties,
location.isOptional(), profiles, uris, prefix);
result.add(resource);
resource = loadResource(properties, location.isOptional(), profiles, uris,
prefix + DOT + properties.getFileExtension());
result.add(resource);
for (String profile : profiles.getActive()) {
String dataId = prefix + HYPHEN + profile + DOT
+ properties.getFileExtension();
resource = loadResource(properties, location.isOptional(), profiles, uris,
dataId);
result.add(resource);
}
}
return result;
}
private NacosConfigDataResource loadResource(NacosConfigProperties properties,
boolean optional, Profiles profiles, String uris, String dataId) {
return new NacosConfigDataResource(properties, optional, profiles,
log,
new NacosItemConfig()
.setNamespace(Objects.toString(properties.getNamespace(),
DEFAULT_NAMESPACE))
.setGroup(groupFor(uris, properties)).setDataId(dataId)
.setSuffix(suffixFor(uris, properties))
NacosConfigDataResource resource = new NacosConfigDataResource(properties,
location.isOptional(), profiles, log,
new NacosItemConfig().setGroup(groupFor(uris, properties))
.setDataId(dataIdFor(uris)).setSuffix(suffixFor(uris, properties))
.setRefreshEnabled(refreshEnabledFor(uris, properties)));
result.add(resource);
return result;
}
private String getUri(ConfigDataLocation location, NacosConfigProperties properties) {
@ -198,25 +171,14 @@ public class NacosConfigDataLocationResolver
return properties.getServerAddr() + path;
}
private void registerOrUpdateIndexes(NacosConfigProperties properties,
ConfigurableBootstrapContext bootstrapContext) {
String namespace = Objects.toString(properties.getNamespace(), DEFAULT_NAMESPACE);
ConfigService configService;
private void registerConfigService(NacosConfigProperties properties,
ConfigurableBootstrapContext bootstrapContext) {
try {
configService = ConfigFactory.createConfigService(properties.assembleConfigServiceProperties());
} catch (NacosException e) {
return;
}
if (bootstrapContext.isRegistered(ConfigServiceIndexes.class)) {
ConfigServiceIndexes indexes = bootstrapContext
.get(ConfigServiceIndexes.class);
indexes.getIndexes().putIfAbsent(namespace, configService);
} else {
bootstrapContext.register(ConfigServiceIndexes.class, (context) -> {
Map<String, ConfigService> indexes = new ConcurrentHashMap<>(4);
indexes.put(namespace, configService);
return () -> indexes;
});
if (!bootstrapContext.isRegistered(ConfigService.class)) {
ConfigService configService = ConfigFactory.createConfigService(properties.assembleConfigServiceProperties());
bootstrapContext.register(ConfigService.class, InstanceSupplier.of(configService));
}
} catch (NacosException ignore) {
}
}
@ -234,39 +196,29 @@ public class NacosConfigDataLocationResolver
}
private String groupFor(String uris, NacosConfigProperties properties) {
String[] part = split(uris);
if (part.length >= 1 && !part[0].isEmpty()) {
return part[0];
}
return properties.getGroup();
}
private String dataIdPrefixFor(Binder binder, NacosConfigProperties properties) {
String dataIdPrefix = properties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = properties.getName();
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = binder.bind("spring.application.name", String.class).get();
}
return dataIdPrefix;
}
private String dataIdFor(String uris) {
String[] part = split(uris);
if (part.length >= 2 && !part[1].isEmpty()) {
return part[1];
Map<String, String> queryMap = getQueryMap(uris);
return queryMap.containsKey(GROUP)
? queryMap.get(GROUP)
: properties.getGroup();
}
private Map<String, String> getQueryMap(String uris) {
String query = getUri(uris).getQuery();
if (StringUtils.isBlank(query)) {
return Collections.emptyMap();
}
Map<String, String> result = new HashMap<>(4);
for (String entry : query.split("&")) {
String[] kv = entry.split("=");
if (kv.length == 2) {
result.put(kv[0], kv[1]);
}
}
return null;
return result;
}
private String suffixFor(String uris, NacosConfigProperties properties) {
String[] part = split(uris);
String dataId = null;
if (part.length >= 2 && !part[1].isEmpty()) {
dataId = part[1];
}
String dataId = dataIdFor(uris);
if (dataId != null && dataId.contains(".")) {
return dataId.substring(dataId.lastIndexOf('.') + 1);
}
@ -274,22 +226,24 @@ public class NacosConfigDataLocationResolver
}
private boolean refreshEnabledFor(String uris, NacosConfigProperties properties) {
URI uri = getUri(uris);
if (uri.getQuery() != null && uri.getQuery().contains("refreshEnabled=false")) {
return false;
}
return properties.isRefreshEnabled();
Map<String, String> queryMap = getQueryMap(uris);
return queryMap.containsKey(REFRESH_ENABLED)
? Boolean.parseBoolean(queryMap.get(REFRESH_ENABLED))
: properties.isRefreshEnabled();
}
private String[] split(String uris) {
private String dataIdFor(String uris) {
URI uri = getUri(uris);
String path = uri.getPath();
// notice '/'
if (path == null || path.length() <= 1) {
return new String[0];
return StringUtils.EMPTY;
}
String[] parts = path.substring(1).split("/");
if (parts.length != 1) {
throw new IllegalArgumentException("illegal dataId");
}
path = path.substring(1);
return path.split("/");
return parts[0];
}
}

@ -83,7 +83,9 @@ public class NacosConfigDataResource extends ConfigDataResource {
return false;
}
NacosConfigDataResource that = (NacosConfigDataResource) o;
return optional == that.optional && Objects.equals(properties, that.properties) && Objects.equals(profiles, that.profiles) && Objects.equals(log, that.log) && Objects.equals(config, that.config);
return optional == that.optional && Objects.equals(properties, that.properties)
&& Objects.equals(profiles, that.profiles)
&& Objects.equals(log, that.log) && Objects.equals(config, that.config);
}
@Override
@ -93,16 +95,11 @@ public class NacosConfigDataResource extends ConfigDataResource {
@Override
public String toString() {
return "NacosConfigDataResource{" +
"properties=" + properties +
", optional=" + optional +
", profiles=" + profiles +
", config=" + config +
'}';
return "NacosConfigDataResource{" + "properties=" + properties + ", optional="
+ optional + ", profiles=" + profiles + ", config=" + config + '}';
}
public static class NacosItemConfig {
private String namespace;
private String group;
private String dataId;
private String suffix;
@ -111,20 +108,14 @@ public class NacosConfigDataResource extends ConfigDataResource {
public NacosItemConfig() {
}
public NacosItemConfig(String namespace, String group, String dataId,
String suffix, boolean refreshEnabled) {
this.namespace = namespace;
public NacosItemConfig(String group, String dataId, String suffix,
boolean refreshEnabled) {
this.group = group;
this.dataId = dataId;
this.suffix = suffix;
this.refreshEnabled = refreshEnabled;
}
public NacosItemConfig setNamespace(String namespace) {
this.namespace = namespace;
return this;
}
public NacosItemConfig setGroup(String group) {
this.group = group;
return this;
@ -145,10 +136,6 @@ public class NacosConfigDataResource extends ConfigDataResource {
return this;
}
public String getNamespace() {
return namespace;
}
public String getGroup() {
return group;
}
@ -174,23 +161,22 @@ public class NacosConfigDataResource extends ConfigDataResource {
return false;
}
NacosItemConfig that = (NacosItemConfig) o;
return refreshEnabled == that.refreshEnabled && Objects.equals(namespace, that.namespace) && Objects.equals(group, that.group) && Objects.equals(dataId, that.dataId) && Objects.equals(suffix, that.suffix);
return refreshEnabled == that.refreshEnabled
&& Objects.equals(group, that.group)
&& Objects.equals(dataId, that.dataId)
&& Objects.equals(suffix, that.suffix);
}
@Override
public int hashCode() {
return Objects.hash(namespace, group, dataId, suffix, refreshEnabled);
return Objects.hash(group, dataId, suffix, refreshEnabled);
}
@Override
public String toString() {
return "NacosItemConfig{" +
"namespace='" + namespace + '\'' +
", group='" + group + '\'' +
", dataId='" + dataId + '\'' +
", suffix='" + suffix + '\'' +
", refreshEnabled=" + refreshEnabled +
'}';
return "NacosItemConfig{" + "group='" + group + '\'' + ", dataId='" + dataId
+ '\'' + ", suffix='" + suffix + '\'' + ", refreshEnabled="
+ refreshEnabled + '}';
}
}

@ -17,6 +17,7 @@ import org.springframework.boot.logging.DeferredLog;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ -38,7 +39,6 @@ public class NacosConfigDataLocationResolverTest {
@BeforeEach
void setup() {
this.environment = new MockEnvironment();
this.environment.setProperty("spring.application.name", "app");
this.environmentBinder = Binder.get(this.environment);
this.resolver = new NacosConfigDataLocationResolver(new DeferredLog());
when(context.getBinder()).thenReturn(environmentBinder);
@ -75,62 +75,64 @@ public class NacosConfigDataLocationResolverTest {
}
@Test
void whenNotSetProfile_thenDataIdIsShorter() {
environment.setProperty("spring.application.name", "nacos-test");
String locationUri = "nacos:";
void testStartWithASlashIsOK() {
String locationUri = "nacos:/app";
List<NacosConfigDataResource> resources = testUri(locationUri);
assertThat(resources).hasSize(2);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("nacos-test");
assertThat(resources.get(1).getConfig().getDataId()).isEqualTo("nacos-test.properties");
assertThat(resources).hasSize(1);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("app");
locationUri = "nacos:app";
resources = testUri(locationUri);
assertThat(resources).hasSize(1);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("app");
}
@Test
void whenCustomizeSuffix_thenOverrideDefault() {
environment.setProperty("spring.application.name", "nacos-test");
environment.setProperty("spring.cloud.nacos.config.file-extension", "yml");
void testDataIdMustBeSpecified() {
String locationUri = "nacos:";
List<NacosConfigDataResource> resources = testUri(locationUri);
assertThat(resources).hasSize(2);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("nacos-test");
assertThat(resources.get(1).getConfig().getDataId()).isEqualTo("nacos-test.yml");
assertThatThrownBy(() -> testUri(locationUri)).hasMessage("dataId must be specified");
}
@Test
void testUrisInLocationShouldOverridesProperty() {
environment.setProperty("spring.application.name", "nacos-test");
String locationUri = "nacos:group01";
List<NacosConfigDataResource> resources = testUri(locationUri, "dev");
assertThat(resources).hasSize(3);
NacosConfigDataResource resource = resources.get(0);
assertThat(resource.getConfig().getGroup()).isEqualTo("group01");
assertThat(resource.getConfig().getSuffix()).isEqualTo("properties");
assertThat(resource.getConfig().getNamespace()).isEqualTo("");
assertThat(resource.getConfig().isRefreshEnabled()).isTrue();
assertThat(resource.getConfig().getDataId()).isEqualTo("nacos-test");
assertThat(resources.get(1).getConfig().getDataId()).isEqualTo("nacos-test.properties");
assertThat(resources.get(2).getConfig().getDataId()).isEqualTo("nacos-test-dev.properties");
locationUri = "nacos:group01/test.yml?refreshEnabled=false";
void testInvalidDataId() {
String locationUri = "nacos:test/test.yml";
assertThatThrownBy(() -> testUri(locationUri)).hasMessage("illegal dataId");
}
@Test
void whenCustomizeSuffix_thenOverrideDefault() {
String locationUri = "nacos:app";
List<NacosConfigDataResource> resources = testUri(locationUri);
assertThat(resources).hasSize(1);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("app");
assertThat(resources.get(0).getConfig().getSuffix()).isEqualTo("properties");
environment.setProperty("spring.cloud.nacos.config.file-extension", "yml");
locationUri = "nacos:app";
resources = testUri(locationUri);
assertThat(resources).hasSize(1);
resource = resources.get(0);
assertThat(resource.getConfig().getGroup()).isEqualTo("group01");
assertThat(resource.getConfig().getDataId()).isEqualTo("test.yml");
assertThat(resource.getConfig().getSuffix()).isEqualTo("yml");
assertThat(resource.getConfig().isRefreshEnabled()).isFalse();
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("app");
assertThat(resources.get(0).getConfig().getSuffix()).isEqualTo("yml");
locationUri = "nacos:app.json";
resources = testUri(locationUri);
assertThat(resources).hasSize(1);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("app.json");
assertThat(resources.get(0).getConfig().getSuffix()).isEqualTo("json");
}
@Test
void test_compatibleWithOlderVersion() {
environment.setProperty("spring.application.name", "nacos-test");
environment.setProperty("spring.cloud.nacos.config.name", "test");
environment.setProperty("spring.cloud.nacos.config.file-extension", "yml");
String locationUri = "nacos:";
List<NacosConfigDataResource> resources = testUri(locationUri, "dev");
assertThat(resources).hasSize(3);
assertThat(resources.get(0).getConfig().getDataId()).isEqualTo("test");
assertThat(resources.get(1).getConfig().getDataId()).isEqualTo("test.yml");
assertThat(resources.get(2).getConfig().getDataId()).isEqualTo("test-dev.yml");
void testUrisInLocationShouldOverridesProperty() {
environment.setProperty("spring.cloud.nacos.config.group", "default");
environment.setProperty("spring.cloud.nacos.config.refreshEnabled", "true");
String locationUri = "nacos:test.yml?group=not_default&refreshEnabled=false";
List<NacosConfigDataResource> resources = testUri(locationUri);
assertThat(resources).hasSize(1);
NacosConfigDataResource resource = resources.get(0);
assertThat(resource.getConfig().getGroup()).isEqualTo("not_default");
assertThat(resource.getConfig().getSuffix()).isEqualTo("yml");
assertThat(resource.getConfig().isRefreshEnabled()).isFalse();
assertThat(resource.getConfig().getDataId()).isEqualTo("test.yml");
}
private List<NacosConfigDataResource> testUri(String locationUri, String... activeProfiles) {
@ -152,12 +154,12 @@ public class NacosConfigDataLocationResolverTest {
when(bootstrapContext.get(eq(NacosConfigProperties.class)))
.thenReturn(new NacosConfigProperties());
List<NacosConfigDataResource> resources = this.resolver.resolveProfileSpecific(
context, ConfigDataLocation.of("nacos:group/test.yml"),
context, ConfigDataLocation.of("nacos:test.yml"),
mock(Profiles.class));
assertThat(resources).hasSize(1);
verify(bootstrapContext, times(0)).get(eq(NacosConfigProperties.class));
NacosConfigDataResource resource = resources.get(0);
assertThat(resource.getConfig().getGroup()).isEqualTo("group");
assertThat(resource.getConfig().getGroup()).isEqualTo("DEFAULT_GROUP");
assertThat(resource.getConfig().getDataId()).isEqualTo("test.yml");
}
@ -177,8 +179,8 @@ public class NacosConfigDataLocationResolverTest {
}
List<NacosConfigDataResource> resources = this.resolver.resolveProfileSpecific(
context, ConfigDataLocation.of("nacos:"), profiles);
assertThat(resources).hasSize(3);
context, ConfigDataLocation.of("nacos:test.yml"), profiles);
assertThat(resources).hasSize(1);
return resources.get(0);
}

Loading…
Cancel
Save