content: /*
   * Copyright 2013-2023 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.net.URI;
  import java.net.URISyntaxException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import com.alibaba.cloud.nacos.NacosConfigManager;
  import com.alibaba.cloud.nacos.NacosConfigProperties;
  import com.alibaba.cloud.nacos.NacosPropertiesPrefixer;
  import com.alibaba.cloud.nacos.utils.StringUtils;
  import org.apache.commons.logging.Log;
  
  import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
  import org.springframework.boot.ConfigurableBootstrapContext;
  import org.springframework.boot.context.config.ConfigDataLocation;
  import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
  import org.springframework.boot.context.config.ConfigDataLocationResolver;
  import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
  import org.springframework.boot.context.config.ConfigDataResource;
  import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
  import org.springframework.boot.context.config.Profiles;
  import org.springframework.boot.context.properties.bind.BindHandler;
  import org.springframework.boot.context.properties.bind.Bindable;
  import org.springframework.boot.context.properties.bind.Binder;
  import org.springframework.boot.logging.DeferredLogFactory;
  import org.springframework.core.Ordered;
  
  import static com.alibaba.cloud.nacos.configdata.NacosConfigDataResource.NacosItemConfig;
  
  /**
   * Implementation of {@link ConfigDataLocationResolver}, load Nacos
   * {@link ConfigDataResource}.
   *
   * @author freeman
   * @since 2021.0.1.0
   */
  public class NacosConfigDataLocationResolver
  		implements ConfigDataLocationResolver<NacosConfigDataResource>, Ordered {
  	/**
  	 * Prefix for Config Server imports.
  	 */
  	public static final String PREFIX = "nacos:";
  	private static final String GROUP = "group";
  
  	// support params
  	private static final String REFRESH_ENABLED = "refreshEnabled";
  	private static final String PREFERENCE = "preference";
  	private final Log log;
  
  	public NacosConfigDataLocationResolver(DeferredLogFactory logFactory) {
  		this.log = logFactory.getLog(getClass());
  	}
  
  	@Override
  	public int getOrder() {
  		return -1;
  	}
  
  	protected NacosConfigProperties loadProperties(
  			ConfigDataLocationResolverContext context) {
  		Binder binder = context.getBinder();
  		BindHandler bindHandler = getBindHandler(context);
  
  		NacosConfigProperties nacosConfigProperties;
  		if (context.getBootstrapContext().isRegistered(NacosConfigDataLoadProperties.class)) {
  			nacosConfigProperties = context.getBootstrapContext()
  					.get(NacosConfigDataLoadProperties.class);
  		}
  		else {
  			String nacosPrefix = NacosPropertiesPrefixer.getPrefix(context.getBinder());
  
  			String nacosConfigPrefix = nacosPrefix + ".config";
  
  			nacosConfigProperties = binder
  					.bind(nacosPrefix, Bindable.of(NacosConfigDataLoadProperties.class),
  							bindHandler)
  					.map(properties -> binder
  							.bind(nacosConfigPrefix,
  									Bindable.ofInstance(properties), bindHandler)
  							.orElse(properties))
  					.orElseGet(() -> binder
  							.bind(nacosConfigPrefix,
  									Bindable.of(NacosConfigDataLoadProperties.class), bindHandler)
  							.orElseGet(NacosConfigDataLoadProperties::new));
  		}
  
  		return nacosConfigProperties;
  	}
  
  	private BindHandler getBindHandler(ConfigDataLocationResolverContext context) {
  		return context.getBootstrapContext().getOrElse(BindHandler.class, null);
  	}
  
  	protected Log getLog() {
  		return this.log;
  	}
  
  	@Override
  	public boolean isResolvable(ConfigDataLocationResolverContext context,
  			ConfigDataLocation location) {
  		if (!location.hasPrefix(getPrefix())) {
  			return false;
  		}
  		String prefix = NacosPropertiesPrefixer.getPrefix(context.getBinder());
  
  		return context.getBinder()
  				.bind(prefix + ".config.enabled", Boolean.class)
  				.orElse(true);
  	}
  
  	protected String getPrefix() {
  		return PREFIX;
  	}
  
  	@Override
  	public List<NacosConfigDataResource> resolve(
  			ConfigDataLocationResolverContext context, ConfigDataLocation location)
  			throws ConfigDataLocationNotFoundException,
  			ConfigDataResourceNotFoundException {
  		return Collections.emptyList();
  	}
  
  	@Override
  	public List<NacosConfigDataResource> resolveProfileSpecific(
  			ConfigDataLocationResolverContext resolverContext,
  			ConfigDataLocation location, Profiles profiles)
  			throws ConfigDataLocationNotFoundException {
  		NacosConfigProperties properties = loadProperties(resolverContext);
  
  		ConfigurableBootstrapContext bootstrapContext = resolverContext
  				.getBootstrapContext();
  
  		bootstrapContext.registerIfAbsent(NacosConfigProperties.class,
  				InstanceSupplier.of(properties));
  
  		registerConfigManager(properties, bootstrapContext);
  
  		return loadConfigDataResources(location, profiles, properties);
  	}
  
  	private List<NacosConfigDataResource> loadConfigDataResources(
  			ConfigDataLocation location, Profiles profiles,
  			NacosConfigProperties properties) {
  		List<NacosConfigDataResource> result = new ArrayList<>();
  		URI uri = getUri(location, properties);
  
  		if (StringUtils.isBlank(dataIdFor(uri))) {
  			throw new IllegalArgumentException("dataId must be specified");
  		}
  
  		NacosConfigDataResource resource = new NacosConfigDataResource(properties,
  				location.isOptional(), profiles, log,
  				new NacosItemConfig().setGroup(groupFor(uri, properties))
  						.setDataId(dataIdFor(uri)).setSuffix(suffixFor(uri, properties))
  						.setRefreshEnabled(refreshEnabledFor(uri, properties))
  						.setPreference(preferenceFor(uri)));
  		result.add(resource);
  
  		return result;
  	}
  
  	private String preferenceFor(URI uri) {
  		return getQueryMap(uri).get(PREFERENCE);
  	}
  
  	private URI getUri(ConfigDataLocation location, NacosConfigProperties properties) {
  		String path = location.getNonPrefixedValue(getPrefix());
  		if (StringUtils.isBlank(path)) {
  			path = "/";
  		}
  		if (!path.startsWith("/")) {
  			path = "/" + path;
  		}
  		String uri = properties.getServerAddr() + path;
  		return getUri(uri);
  	}
  
  	private void registerConfigManager(NacosConfigProperties properties,
  			ConfigurableBootstrapContext bootstrapContext) {
  		if (!bootstrapContext.isRegistered(NacosConfigManager.class)) {
  			bootstrapContext.register(NacosConfigManager.class,
  					InstanceSupplier.of(NacosConfigManager.getInstance(properties)));
  		}
  	}
  
  	private URI getUri(String uris) {
  		if (!uris.startsWith("http://") && !uris.startsWith("https://")) {
  			uris = "http://" + uris;
  		}
  		URI uri;
  		try {
  			uri = new URI(uris);
  		}
  		catch (URISyntaxException e) {
  			throw new IllegalArgumentException("illegal URI: " + uris);
  		}
  		return uri;
  	}
  
  	private String groupFor(URI uri, NacosConfigProperties properties) {
  		Map<String, String> queryMap = getQueryMap(uri);
  		return queryMap.containsKey(GROUP) ? queryMap.get(GROUP) : properties.getGroup();
  	}
  
  	private Map<String, String> getQueryMap(URI uri) {
  		String query = uri.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 result;
  	}
  
  	private String suffixFor(URI uri, NacosConfigProperties properties) {
  		String dataId = dataIdFor(uri);
  		if (dataId != null && dataId.contains(".")) {
  			return dataId.substring(dataId.lastIndexOf('.') + 1);
  		}
  		return properties.getFileExtension();
  	}
  
  	private boolean refreshEnabledFor(URI uri, NacosConfigProperties properties) {
  		Map<String, String> queryMap = getQueryMap(uri);
  		return queryMap.containsKey(REFRESH_ENABLED)
  				? Boolean.parseBoolean(queryMap.get(REFRESH_ENABLED))
  				: properties.isRefreshEnabled();
  	}
  
  	private String dataIdFor(URI uri) {
  		String path = uri.getPath();
  		// notice '/'
  		if (path == null || path.length() <= 1) {
  			return StringUtils.EMPTY;
  		}
  		String[] parts = path.substring(1).split("/");
  		if (parts.length != 1) {
  			throw new IllegalArgumentException("illegal dataId");
  		}
  		return parts[0];
  	}
  
  }
  