AppController.java

/*
 * Copyright 2020 Global Crop Diversity Trust
 *
 * 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
 *
 *   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.gringlobal.api.v1.impl;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.api.PageableAsQueryParam;
import org.gringlobal.api.v1.ApiBaseController;
import org.gringlobal.api.v1.CRUDController;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.AppSetting;
import org.gringlobal.model.QAppSetting;
import org.gringlobal.model.SysLang;
import org.gringlobal.oauth2.server.TenantRepository;
import org.gringlobal.persistence.AppResourceRepository.AppResourceInfo;
import org.gringlobal.service.AppSettingsService;
import org.gringlobal.service.CodeValueService;
import org.gringlobal.service.CodeValueTranslationService.TranslatedCodeValue;
import org.gringlobal.service.LanguageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.querydsl.core.types.OrderSpecifier;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController("appConfigApi1")
@RequestMapping(AppController.API_URL)
@PreAuthorize("isAuthenticated()")
@Tag(name = AppController.API_TAG)
@Slf4j
public class AppController extends ApiBaseController {
	public static final String API_TAG = "Application";

	/** The Constant API_URL. */
	public static final String API_URL = ApiBaseController.APIv1_BASE + "/app";

	@Autowired
	private AppSettingsService settingsService;

	@Autowired
	private CodeValueService codeValueService;

	@Autowired
	private LanguageService languageService;

	@Autowired
	private TenantRepository oauthTenantRegistrationRepository;

	@GetMapping(value = "/server-info", produces = { MediaType.APPLICATION_JSON_VALUE })
	public Map<String, Object> getServerInfo() throws SearchException {
		var serverInfo = new HashMap<String, Object>();
		serverInfo.put("time.timezone", TimeZone.getDefault().getDisplayName());
		serverInfo.put("time.zoneId", ZoneId.systemDefault().toString());
		serverInfo.put("time.zoneOffset", ZoneOffset.systemDefault().toString());
		serverInfo.put("time.current.offsetDateTime", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
		serverInfo.put("time.current.localDateTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
		serverInfo.put("time.current.zonedDateTime", ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
		return serverInfo;
	}

	@GetMapping(value = "/codes", produces = { MediaType.APPLICATION_JSON_VALUE })
	public LookupMap<String, String, TranslatedCodeValue> codeValuesList() throws SearchException {

		List<TranslatedCodeValue> codeInfos = codeValueService.listFiltered(null, Pageable.ofSize(Integer.MAX_VALUE)).getContent();
		LookupMap<String, String, TranslatedCodeValue> l10n = new LookupMap<>();
		codeInfos.forEach(ci -> {
			l10n.add(ci.entity.getGroupName(), ci.entity.getValue(), ci);
		});
		return l10n;
	}

	@GetMapping(value = "/languages", produces = { MediaType.APPLICATION_JSON_VALUE })
	public Iterable<SysLang> listLanguage() {
		return languageService.listEnabledLanguages();
	}

	@GetMapping(value = "/resources/{appName}", produces = { MediaType.APPLICATION_JSON_VALUE })
	public LookupMap<String, String, AppResourceInfo> appResourcesList(@PathVariable("appName") String appName) {

		SysLang sysLang = languageService.getLanguage(LocaleContextHolder.getLocale());

		LookupMap<String, String, AppResourceInfo> l10n = new LookupMap<>();
		settingsService.listResources(appName, sysLang).forEach(appRes -> {
			l10n.add(appRes.formName, appRes.resourceName, appRes);
		});
		if (l10n.size() == 0) {
			throw new NotFoundElement("No resources for app " + appName);
		}
		return l10n;
	}

	@GetMapping(value = "/setting", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "getSettings", description = "Get settings by category and name", summary = "Get")
	public List<AppSetting> getSettings(@RequestParam(value="categoryTag", required = false) final String categoryTag, @RequestParam(value="name") final String name) {
		return settingsService.getSettings(categoryTag, name);
	}

	@GetMapping(value = "/client-registration", produces = { MediaType.APPLICATION_JSON_VALUE })
	public Map<String, String> getClientRegistrations() {
		var registrations = new HashMap<String, String>();
		oauthTenantRegistrationRepository.forEach(registration -> registrations.put(registration.getRegistrationId(), registration.getClientName()));
		return registrations;
	}
	
	public static class LookupMap<K1, K2, V> extends TreeMap<K1, Map<K2, V>> {
		private static final long serialVersionUID = -533944504263416611L;

		public V add(K1 key, K2 key2, V element) {
			Map<K2, V> map = get(key);
			if (map == null) {
				put(key, map = new TreeMap<>());
			}
			map.put(key2, element);
			return element;
		}
	}

	@RestController("appSettingsCrudApi1")
	@RequestMapping(AppSettingsCRUDController.API_URL)
	@PreAuthorize("hasAuthority('GROUP_ADMINS')")
	@Tag(name = AppController.API_TAG)
	public static class AppSettingsCRUDController extends CRUDController<AppSetting, AppSettingsService> {
		public static final String API_URL = AppController.API_URL + "/settings";

		@Override
		protected OrderSpecifier<?>[] defaultSort() {
			return new OrderSpecifier[] { QAppSetting.appSetting.id.asc() };
		}

		@Override
		@PageableAsQueryParam
		public Page<AppSetting> list(@Parameter(hidden = true) final Pagination page) {
			return super.list(page);
		}

		@SuppressWarnings("unchecked")
		@PreAuthorize("isAuthenticated()")
		@GetMapping(value = "", produces = { MediaType.APPLICATION_JSON_VALUE })
		public Map<String, Object> appSettingsMini() {
			final Map<String, Object> settingsMap = new TreeMap<>();
			for (AppSetting setting : crudService.settingList()) {
				final String categoryTag = setting.getCategoryTag();
				if (StringUtils.isBlank(categoryTag)) {
					settingsMap.put(setting.getName(), setting.getValue());
					continue;
				}
				Object categoryGroup = settingsMap.computeIfAbsent(categoryTag, k -> new TreeMap<>());
				if (categoryGroup instanceof String) {
					log.error("AppSetting {} conflicts with a categoryTag", categoryTag);
					continue;
				}
				((TreeMap<String, String>) categoryGroup).put(setting.getName(), setting.getValue());
				settingsMap.put(categoryTag, categoryGroup);
			}
			return settingsMap;
		}
	}

}