AdminController.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.genesys.server.mvc.admin;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import javax.validation.constraints.NotNull;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.lang3.time.StopWatch;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.model.ImageGallery;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.persistence.ImageGalleryPersistence;
import org.genesys.filerepository.persistence.RepositoryFilePersistence;
import org.genesys.filerepository.persistence.RepositoryFolderRepository;
import org.genesys.filerepository.service.ImageGalleryService;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.model.dataset.Dataset;
import org.genesys.server.model.genesys.Accession;
import org.genesys.server.model.genesys.AccessionId;
import org.genesys.server.model.genesys.PDCI;
import org.genesys.server.model.genesys.QAccessionId;
import org.genesys.server.model.impl.FaoInstitute;
import org.genesys.server.persistence.AccessionIdRepository;
import org.genesys.server.persistence.AccessionRepository;
import org.genesys.server.persistence.FaoInstituteRepository;
import org.genesys.server.persistence.PDCIRepository;
import org.genesys.server.persistence.SubsetRepository;
import org.genesys.server.persistence.dataset.DatasetRepository;
import org.genesys.server.persistence.kpi.ExecutionRepository;
import org.genesys.server.service.AccessionService;
import org.genesys.server.service.AdminService;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.CountryNamesUpdater;
import org.genesys.server.service.DatasetService;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.GenesysService;
import org.genesys.server.service.GeoRegionService;
import org.genesys.server.service.GeoService;
import org.genesys.server.service.InstituteService;
import org.genesys.server.service.TaxonomyService;
import org.genesys.server.service.UserService;
import org.genesys.server.service.filter.AccessionFilter;
import org.genesys.server.exception.NonUniqueAccessionException;
import org.genesys.server.exception.SearchException;
import org.genesys.server.service.worker.AccessionCounter;
import org.genesys.server.service.worker.AccessionProcessor;
import org.genesys.server.service.worker.ITPGRFAStatusUpdater;
import org.genesys.server.service.worker.InstituteUpdater;
import org.genesys.server.service.worker.SGSVUpdate;
import org.genesys.server.service.worker.ScheduledGLISUpdater;
import org.genesys.server.service.worker.Taxonomy2GRINMatcher;
import org.genesys.server.service.worker.UsdaTaxonomyUpdater;
import org.genesys.server.service.worker.WorldClimUpdater;
import org.genesys.util.PDCICalculator;
import org.genesys.util.TileIndexCalculator;
import org.genesys.worldclim.WorldClimUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.xml.sax.SAXException;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.querydsl.jpa.impl.JPAQueryFactory;

@Controller
@RequestMapping("/admin")
@PreAuthorize("hasRole('ADMINISTRATOR')")
public class AdminController {

	public static final Logger LOG = LoggerFactory.getLogger(AdminController.class);

	@Autowired
	TaskExecutor taskExecutor;

	@Autowired
	InstituteUpdater instituteUpdater;

	@Autowired
	CountryNamesUpdater alternateNamesUpdater;

	@Autowired
	GenesysService genesysService;
	@Autowired
	AdminService adminService;

	@Autowired
	GeoService geoService;

	@Autowired
	SGSVUpdate sgsvUpdater;

	@Autowired
	ITPGRFAStatusUpdater itpgrfaUpdater;

	@Autowired
	private InstituteService instituteService;

	@Autowired
	private GeoRegionService geoRegionService;

	@Autowired
	private TaxonomyService taxonomyService;

	@Autowired
	private DatasetService datasetService;

	@Autowired
	private JPAQueryFactory jpaQueryFactory;

	@Autowired
	private AccessionCounter accessionCounter;

	ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());

	@Autowired
	private AccessionRepository accessionRepository;

	@Autowired
	private AccessionProcessor accessionProcessor;

	@Autowired
	private PDCIRepository pdciRepository;

	@Autowired
	private RepositoryService repositoryService;

	@Autowired
	private FaoInstituteRepository instituteRepository;

	@Autowired
	private DatasetRepository datasetRepository;

	@Autowired
	private ImageGalleryService imageGalleryService;

	@Autowired(required = false)
	private ElasticsearchService elasticsearch;

	@Autowired
	private CustomAclService aclService;

	@Autowired
	private WorldClimUpdater worldClimUpdater;

	@Autowired
	private ScheduledGLISUpdater scheduledGLISUpdater;

	@Autowired
	private AccessionService accessionService;

	@Autowired
	private UsdaTaxonomyUpdater usdaTaxonomyUpdater;

	@Autowired
	private Taxonomy2GRINMatcher genesysTaxonomy2GRIN;

	@Autowired
	private UserService userService;

	@Autowired
	private ArticleService articleService;

	@RequestMapping("/")
	public String root(Model model) {
		return "/admin/index";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/refreshWiews")
	public String refreshWiews() {
		try {
			instituteUpdater.updateFaoInstitutes();
		} catch (final IOException e) {
			LOG.error(e.getMessage(), e);
		}
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/refreshCountries")
	public String refreshCountries() {
		try {
			geoService.updateCountryData();
		} catch (final IOException e) {
			LOG.error(e.getMessage(), e);
		}
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/updateAccessionCountryRefs")
	public String updateAccessionCountryRefs() {
		genesysService.updateAccessionCountryRefs();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/updateInstituteCountryRefs")
	public String updateInstituteCountryRefs() {
		instituteService.updateCountryRefs();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/updateAccessionInstituteRefs")
	public String updateAccessionInstituteRefs() {
		genesysService.updateAccessionInstitueRefs();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/scanForSubsets")
	public String scanForSubsets() {
		accessionService.scanForPublishedSubsets();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/scanForDatasets")
	public String scanForDatasets() {
		accessionService.scanForPublishedDatasets();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/resetCounters")
	public String resetCounters() {
		accessionService.resetSubsetAndDatasetCounters();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/relinkDatasetAccessions")
	public String rematchDatasetAccessions() {
		datasetService.rematchDatasetAccessions();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/updateSGSV")
	public String updateSGSV() {
		sgsvUpdater.updateSGSV();
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/sanitize")
	public String sanitize() {
		LOG.info("Sanitizing content");
		articleService.sanitizeAll();
		LOG.info("Sanitizing content.. Done");
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/updateAlternateNames")
	public String updateAlternateNames() {
		LOG.info("Updating alternate GEO names");
		try {
			alternateNamesUpdater.updateAlternateNames();
		} catch (final IOException e) {
			LOG.error(e.getMessage(), e);
		}
		LOG.info("Updating alternate GEO names: done");
		return "redirect:/admin/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/updateITPGRFA")
	public String updateITPGRFA() {
		LOG.info("Updating country ITPGRFA status");
		try {
			itpgrfaUpdater.downloadAndUpdate();
		} catch (final IOException e) {
			LOG.error(e.getMessage(), e);
		}
		LOG.info("Updating done");
		return "redirect:/admin/";
	}

	@PostMapping(value = "/updateGRIN", params = "action=update")
	public String updateGRIN() {
		LOG.info("Updating GRIN Taxonomy");
		try {
			usdaTaxonomyUpdater.update();
		} catch (final Throwable e) {
			LOG.error(e.getMessage(), e);
		}
		LOG.info("Updating done");
		return "redirect:/admin/";
	}

	@PostMapping(value = "/updateGRIN", params = "action=mcpd2grin")
	public String mapToGrinTaxonomy() {
		LOG.info("Mapping MCPD Taxonomy2 to GRIN Taxonomy");
		try {
			genesysTaxonomy2GRIN.update();
			taxonomyService.updateFamilyNames();
		} catch (final Throwable e) {
			LOG.error(e.getMessage(), e);
		}
		LOG.info("Updating done");
		return "redirect:/admin/";
	}

	@RequestMapping(value = "/assign-uuid", method = RequestMethod.POST)
	public String assignUuid() {
		while (genesysService.assignMissingUuid(100) > 0) {

		}
		return "redirect:/admin/";
	}

	@RequestMapping(value = "/pdci", method = RequestMethod.POST, params = "action=institute-pdci")
	public String updatePDCI() {
		for (FaoInstitute institute : instituteService.listActive(PageRequest.of(0, Integer.MAX_VALUE))) {
			LOG.info("Updating PDCI for {}", institute.getCode());
			accessionCounter.recountInstitute(institute);
		}

		return "redirect:/admin/";
	}

	@RequestMapping(value = "/pdci", method = RequestMethod.POST, params = "action=filtered-pdci")
	public String updateFilteredPDCI(@RequestParam(name = "filter") String filters) throws JsonParseException, JsonMappingException, IOException {

		AccessionFilter filter = mapper.readValue(filters, AccessionFilter.class);
		LOG.warn("Recalculating PDCI for accessions matching filter: {}", filter);

		accessionProcessor.apply(filter, (accessions) -> {

			// Everything here is executed within a @Transaction(readOnly = false) context
			if (accessions == null) {
				return;
			}

			accessions.forEach(accession -> {
				AccessionId accessionId = accession.getAccessionId();
				PDCI pdci = accessionId.getPdci();
				// create new PDCI if missing
				PDCI resultingPdci = PDCICalculator.updatePdci(pdci == null ? new PDCI() : pdci, accession);

				updateAccessionPDCI(accession, resultingPdci);
				accessionCounter.recountInstitute(accession.getInstitute());
			});

			LOG.debug("Updated {} PDCI entries", accessions.size());
		});

		return "redirect:/admin/";
	}

	private void updateAccessionPDCI(Accession accession, PDCI pdci) {
		AccessionId accessionId = accession.getAccessionId();

		pdci.setAccession(accessionId);
		pdciRepository.save(pdci);

		if (accessionId.getPdci() == null) {
			accessionId.setPdci(pdci);
			LOG.trace("Assigning new PDCI for {}", accession);
			jpaQueryFactory.update(QAccessionId.accessionId).where(QAccessionId.accessionId.eq(accessionId)).set(QAccessionId.accessionId.pdci(), pdci).execute();

			// jpaQueryFactory.update(QAccession.accession).where(QAccession.accession.eq(accession)).set(QAccession.accession.pdci,
			// pdci).execute();
		}
	}

	/**
	 * Update tile indexes for filtered accessions.
	 * @throws SearchException 
	 */
	@RequestMapping(value = "/admin-action", method = RequestMethod.POST, params = "action=tile-index")
	public String updateTileIndex(@RequestParam(name = "filter") String filters) throws JsonParseException, JsonMappingException, IOException, SearchException {

		AccessionFilter filter = mapper.readValue(filters, AccessionFilter.class);
		LOG.warn("Recalculating tileIndex for {} accessions matching filter:\n\t{}\n\t{}", accessionService.countAccessions(filter), filters, filter);

		AtomicLong updates = new AtomicLong();
		accessionProcessor.apply(filter, (accessions) -> {
			if (accessions == null) {
				return;
			}

			ArrayList<AccessionId> toSave = new ArrayList<>();
			accessions.forEach(accession -> {
				AccessionId accessionId = accession.getAccessionId();
				var ti = WorldClimUtil.getWorldclim25Tile(accessionId.getLongitude(), accessionId.getLatitude());
				var ti3 = TileIndexCalculator.get3MinuteTileIndex(accessionId.getLongitude(), accessionId.getLatitude());

				if (! Objects.equals(ti, accessionId.getTileIndex()) || ! Objects.equals(ti3, accessionId.getTileIndex3min())) {
					if (LOG.isTraceEnabled()) LOG.trace("{}\t\t{}!={}\t\t\t{}!={}", accessionId.getId(), ti, accessionId.getTileIndex(), ti3, accessionId.getTileIndex3min());
					accessionId.setTileIndex(ti);
					accessionId.setTileIndex3min(ti3);
					toSave.add(accessionId);
				}
			});

			accessionIdRepository.saveAll(toSave);
			updates.addAndGet(toSave.size());
			if (toSave.size() > 0 && LOG.isDebugEnabled()) {
				LOG.debug("Assigned {}/{} tileIndexes", toSave.size(), accessions.size());
			}
		});

		LOG.warn("Updated tileIndexes for {} accessions", updates.get());
		return "redirect:/admin/";
	}

//	private void updateAccessionTileIndex(Accession accession, Long index) {
//		if (index == null) {
//			jpaQueryFactory.update(QAccession.accession).where(QAccession.accession.eq(accession)).setNull(QAccession.accession.tileIndex).execute();
//		} else {
//			jpaQueryFactory.update(QAccession.accession).where(QAccession.accession.eq(accession)).set(QAccession.accession.tileIndex, index).execute();
//		}
//	}

	@RequestMapping(value = "/admin-action", method = RequestMethod.POST, params = "georegion")
	public String updateGeoReg() throws IOException, ParserConfigurationException, SAXException {
		geoRegionService.updateGeoRegionData();
		return "redirect:/admin/";
	}

	@RequestMapping(value = "/taxonomy", method = RequestMethod.POST, params = { "action=taxonomy-cleanup" })
	public String cleanupTaxonomies() {
		taxonomyService.cleanupTaxonomies();
		return "redirect:/admin/";
	}

	@RequestMapping(value = "/cropname-crop", method = RequestMethod.POST)
	public String assignCropWithCropname() {
		LOG.info("Assigning crops to accessions with CROPNAME.");
		AtomicLong counter = new AtomicLong(0);
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		final Set<Long> batch = Collections.synchronizedSet(new HashSet<>(100));
		List<Long> list = accessionRepository.listAccessionIdsWithCropname();
		LOG.info("The list has {} elements", list.size());
		list.stream().parallel().forEach(accessionId -> {
			batch.add(accessionId);
			Set<Long> copy = null;
			synchronized (batch) {
				if (batch.size() > 100) {
					copy = new HashSet<>(batch);
					batch.clear();
				}
			}
			if (copy != null)
				genesysService.updateAccessionCrops(copy);
			if (counter.incrementAndGet() % 1000 == 0 && LOG.isInfoEnabled()) {
				LOG.info("Updated {} records overall_rate={} records/s", counter.get(), Math.round(1000.0 * counter.get() / (stopWatch.getTime())));
			}
		});

		// handle remaining
		genesysService.updateAccessionCrops(batch);

		LOG.info("Done assigning crops to accessions with CROPNAME.");
		return "redirect:/admin/";
	}

	@PostMapping(value = "/clear-dois")
	public String clearDois() {
		LOG.info("Clear DOIs");
		genesysService.removeDOIs();
		return "redirect:/admin/";
	}

	@PostMapping(value = "/ensureInstituteFolders")
	public String ensureInstituteFolders() throws Exception {
		LOG.info("Ensure institute folders");
		List<RepositoryFolder> wiewsFolders = repositoryService.getFolders(Paths.get("/wiews"), RepositoryFolder.DEFAULT_SORT);
		for (RepositoryFolder instFolder: wiewsFolders) {
			LOG.warn("Ensuring inheritance for {}", instFolder.getFolderPath());
			FaoInstitute institute = instituteRepository.findByCode(instFolder.getName());
			if (institute != null) {
				repositoryService.ensureFolder(instFolder.getFolderPath(), institute);
			} else {
				LOG.error("No institute " + instFolder.getName() + " for path " + instFolder.getPath());
			}
		}
		return "redirect:/admin/";
	}

	@PostMapping(value = "/ensureDatasetFolders")
	public String ensureDatasetFolders() throws Exception {
		LOG.info("Ensure dataset folders");
		for (Dataset dataset : datasetRepository.findAll()) {
			final Path datasetPath = datasetService.getDatasetRepositoryFolder(dataset);
			LOG.warn("Ensuring inheritance for {}", datasetPath);
			repositoryService.ensureFolder(datasetPath, dataset.getOwner());
		}
		return "redirect:/admin/";
	}

	/**
	 * We want all thumbnails to exist
	 */
	@PostMapping(value = "/ensureThumbnails")
	public String ensureThumbnails() {
		LOG.info("Ensure thumbnails");
		int page = 0;
		Page<ImageGallery> galleries = null;
		do {
			PageRequest p = PageRequest.of(page++, 10);
			galleries = imageGalleryService.listImageGalleries(p);
			galleries.forEach(gallery -> {
				LOG.warn("Ensuring all thumbnails for {}", gallery.getPath());
				imageGalleryService.ensureThumbnails(gallery);
			});
		} while (galleries.hasNext());
		return "redirect:/admin/";
	}

	@PostMapping(value = "/reindex-es")
	public String reindexElasticsearch() {
		LOG.info("Reindex Elasticsearch");
		elasticsearch.reindexAll();
		return "redirect:/admin/";
	}

	@PostMapping(value = "/cleanup-acl")
	public String cleanupAcl() {
		LOG.info("Cleanup ACL");
		aclService.cleanupAcl();
		return "redirect:/admin/";
	}
	
	@PostMapping(value = "/acl", params = { "institutes" })
	public String aclMakeInstitutesPublic() throws Exception {
		LOG.warn("Adding ACL for FaoInstitutes");

		instituteRepository.findAll().forEach(institute -> {
			// LOG.warn("Making FaoInstitute {} public", institute.getCode());
			aclService.createOrUpdatePermissions(institute);
		});

		LOG.warn("Added ACL to existing FaoInstitutes");
		return "redirect:/admin/";
	}
	

	@Autowired
	private RepositoryFolderRepository folderRepository;
	@Autowired
	private RepositoryFilePersistence fileRepository;
	@Autowired
	private ImageGalleryPersistence imageGalleryRepository;

	@PostMapping(value = "/acl", params = { "repository" })
	@Transactional
	public String aclFixRepositoryAcl() throws Exception {

		LOG.warn("Adding ACL for Repository folders");
		folderRepository.findAll().forEach(folder -> {
			aclService.createOrUpdatePermissions(folder);
		});

		LOG.warn("Adding ACL for Repository files");
		fileRepository.findAll().forEach(file -> {
			aclService.createOrUpdatePermissions(file);
		});

		LOG.warn("Adding ACL for Image galleries");
		imageGalleryRepository.findAll().forEach(gallery -> {
			aclService.createOrUpdatePermissions(gallery);
		});

		return "redirect:/admin/";
	}

	@Autowired
	private SubsetRepository subsetRepository;

	@PostMapping(value = "/acl", params = { "subsets" })
	@Transactional
	public String aclFixSubsetAcl() throws Exception {

		LOG.warn("Adding ACL for Subsets");
		subsetRepository.findAll().forEach(subset -> {
			LOG.warn("Setting ACL for Subset {}", subset.getTitle());
			aclService.makePubliclyReadable(subset, subset.isPublished());
		});

		return "redirect:/admin/";
	}

	@PostMapping(value = "/acl", params = { "datasets" })
	@Transactional
	public String aclFixDatasetAcl() throws Exception {

		LOG.warn("Adding ACL for Datasets");
		datasetRepository.findAll().forEach(dataset -> {
			LOG.warn("Setting ACL for Dataset {}", dataset.getTitle());
			aclService.makePubliclyReadable(dataset, dataset.isPublished());
		});

		return "redirect:/admin/";
	}
	
	@Autowired
	private ExecutionRepository kpiExecutionRepository;
	
	@PostMapping(value = "/acl", params = { "kpi" })
	@Transactional
	public String aclFixKPIAcl() throws Exception {
		LOG.warn("Adding ACL support to KPI Execution");
		
		kpiExecutionRepository.findAll().forEach(execution -> {
			LOG.warn("Making KPI Execution {} ACL-ready", execution.getName());
			aclService.createOrUpdatePermissions(execution);
		});

		return "redirect:/admin/";
	}

	@PostMapping(value = "/update-glis")
	public String updateGLIS(@RequestParam(value="from", required = true) @DateTimeFormat(pattern="yyyy-MM-dd") final LocalDate from) {
		LOG.info("Update GLIS with accessions with DOI");
		scheduledGLISUpdater.notifyGLIS(from.atStartOfDay().toInstant(ZoneOffset.UTC));
		return "redirect:/admin/";
	}

	@Autowired
	private AccessionIdRepository accessionIdRepository;
	
	@PostMapping(value = "/fix-repo", params = { "accession-folders" })
	@Transactional
	public String updateAccessionFolders() throws Exception {
		LOG.warn("Registering RepositoryFolders with Accessions");

		repositoryService.getFolder(Paths.get("/wiews")).getChildren().forEach(wiewsFolder -> {
			try {
				String instCode = wiewsFolder.getName();
				RepositoryFolder instAccnFolders = repositoryService.getFolder(wiewsFolder.getFolderPath().resolve("acn"));
				if (instAccnFolders != null) {
					LOG.warn("Processing acn folders for {}", instCode);
					instAccnFolders.getChildren().forEach(acceFolder -> {
						try {
							Accession accession = genesysService.getAccession(instCode, acceFolder.getName());
							if (accession != null) {
								LOG.debug("Folder for accession {}:{} is {}", instCode, accession.getAccessionNumber(), acceFolder.getPath());
								accession.getAccessionId().setRepositoryFolder(acceFolder);
								{
									// Get number of images in the gallery
									ImageGallery gallery = acceFolder.getGallery();
									if (gallery != null) {
										accession.getAccessionId().setImageCount(gallery.getImages().size());
									}
								}
								accessionIdRepository.save(accession.getAccessionId());
							} else {
								LOG.warn("No accession {}:{} for folder {}", instCode, acceFolder.getName(), acceFolder.getPath());
							}
						} catch (NonUniqueAccessionException e) {
							LOG.warn("Accession not unique {}:{}", instCode, acceFolder.getName());
						}
					});
				} else {
					LOG.info("No /wiews/{}/acn folder", instCode);
				}
			} catch (InvalidRepositoryPathException e) {
				LOG.warn("Invalid path {}", e.getMessage());
			}
		});

		return "redirect:/admin/";
	}

	@RequestMapping(value = "/updateClimate", method = RequestMethod.POST)
	public String worldClim() throws IOException {
		worldClimUpdater.update();
		return "redirect:/admin/";
	}


	@PostMapping(value = "/change-instcode")
	@Transactional
	public String changeInstituteCode(@RequestParam(name = "current", required = true) String currentInstCode, @RequestParam(name = "new", required = true) String newInstCode) {
		adminService.changeInstituteCode(currentInstCode, newInstCode);
		return "redirect:/admin/";
	}

	/**
	 * Regenerate all tile indexes.
	 */
	@PostMapping(value = "/regenerate-tileindex")
	public String updateTileIndexes() throws SearchException {
		var qAid = QAccessionId.accessionId;
		var idQuery = jpaQueryFactory.from(qAid).select(qAid.id)
			// where
			.where(qAid.latitude.isNotNull().and(qAid.longitude.isNotNull()))
			// sort
			.orderBy(qAid.id.asc());

		accessionProcessor.apply(idQuery, (accessions) -> {
			var accessionIds = accessions.stream().map(Accession::getAccessionId).collect(Collectors.toList());
			accessionIds.forEach(accessionId -> {
				accessionId.setTileIndex(WorldClimUtil.getWorldclim25Tile(accessionId.getLongitude(), accessionId.getLatitude()));
				accessionId.setTileIndex3min(TileIndexCalculator.get3MinuteTileIndex(accessionId.getLongitude(), accessionId.getLatitude()));
			});
			accessionIdRepository.saveAll(accessionIds);
		});
		return "redirect:/admin/";
	}

	@PostMapping(value = "/kill")
	@Transactional
	public String kill() {
		LOG.error("Killing the server");
		System.exit(-666);
		return "redirect:/admin/";
	}

	@PostMapping(value = "/send-email")
	public void sendEmail(@RequestBody @Validated SendEmailRequest sendEmailRequest) {
		userService.sendEmail(sendEmailRequest.uuids, sendEmailRequest.template, sendEmailRequest.subject);
	}

	public static class SendEmailRequest {
		@NotNull
		public Set<UUID> uuids;
		@NotNull
		public String template;
		@NotNull
		public String subject;
	}
}