Java tutorial
/* * Copyright 2017 Red Hat, Inc. and/or its affiliates. * * Licensed under the Eclipse Public License version 1.0, available at * http://www.eclipse.org/legal/epl-v10.html */ package io.openshift.launchpad.catalog; import static io.openshift.launchpad.Files.removeFileExtension; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Singleton; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.jboss.forge.addon.projects.Project; import org.jboss.forge.addon.resource.DirectoryResource; import org.yaml.snakeyaml.Yaml; import io.openshift.launchpad.CopyFileVisitor; /** * This service reads from the Booster catalog Github repository in https://github.com/openshiftio/booster-catalog and * marshalls into {@link Booster} objects. * * @author <a href="mailto:ggastald@redhat.com">George Gastaldi</a> */ @Singleton public class BoosterCatalogService { private static final String GITHUB_URL = "https://github.com/"; private static final String CATALOG_INDEX_PERIOD_PROPERTY_NAME = "LAUNCHPAD_BACKEND_CATALOG_INDEX_PERIOD"; private static final String CATALOG_GIT_REF_PROPERTY_NAME = "LAUNCHPAD_BACKEND_CATALOG_GIT_REF"; private static final String CATALOG_GIT_REPOSITORY_PROPERTY_NAME = "LAUNCHPAD_BACKEND_CATALOG_GIT_REPOSITORY"; private static final String DEFAULT_INDEX_PERIOD = "0"; private static final String DEFAULT_GIT_REF = "master"; private static final String DEFAULT_GIT_REPOSITORY_URL = "https://github.com/openshiftio/booster-catalog.git"; private static final String CLONED_BOOSTERS_DIR = ".boosters"; private static final Yaml yaml = new Yaml(); /** * Files to be excluded from project creation */ private static final List<String> EXCLUDED_PROJECT_FILES = Arrays.asList(".git", ".travis", ".travis.yml", ".ds_store", ".obsidian", ".gitmodules"); private static final Logger logger = Logger.getLogger(BoosterCatalogService.class.getName()); private final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(); private volatile List<Booster> boosters = Collections.emptyList(); private ScheduledExecutorService executorService; /** * Clones the catalog git repository and reads the obsidian metadata on each quickstart repository */ private void index() { WriteLock lock = reentrantLock.writeLock(); try { lock.lock(); String catalogRepositoryURI = getEnvVarOrSysProp(CATALOG_GIT_REPOSITORY_PROPERTY_NAME, DEFAULT_GIT_REPOSITORY_URL); String catalogRef = getEnvVarOrSysProp(CATALOG_GIT_REF_PROPERTY_NAME, DEFAULT_GIT_REF); logger.log(Level.INFO, "Indexing contents from {0} using {1} ref", new Object[] { catalogRepositoryURI, catalogRef }); Path catalogPath = Files.createTempDirectory("booster-catalog"); // Remove this directory on JVM termination catalogPath.toFile().deleteOnExit(); logger.info(() -> "Created " + catalogPath); // Clone repository Git.cloneRepository().setURI(catalogRepositoryURI).setBranch(catalogRef).setCloneSubmodules(true) .setDirectory(catalogPath.toFile()).call().close(); this.boosters = indexBoosters(catalogPath); } catch (GitAPIException e) { logger.log(Level.SEVERE, "Error while performing Git operation", e); } catch (Exception e) { logger.log(Level.SEVERE, "Error while indexing", e); } finally { logger.info(() -> "Finished content indexing"); lock.unlock(); } } /** * @param moduleRoot * @return * @throws IOException */ private List<Booster> indexBoosters(Path catalogPath) throws IOException { Path moduleRoot = catalogPath.resolve(CLONED_BOOSTERS_DIR); List<Booster> boosters = new ArrayList<>(); Map<String, Mission> missions = new HashMap<>(); Map<String, Runtime> runtimes = new HashMap<>(); Files.walkFileTree(catalogPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { File ioFile = file.toFile(); String fileName = ioFile.getName().toLowerCase(); if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) { String id = removeFileExtension(fileName); Path modulePath = moduleRoot.resolve(id); indexBooster(id, file, modulePath, missions, runtimes).ifPresent(boosters::add); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return dir.startsWith(moduleRoot) ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE; } }); boosters.sort(Comparator.comparing(Booster::getName)); return Collections.unmodifiableList(boosters); } /** * Takes a YAML file from the repository and indexes it * * @param file A YAML file from the booster-catalog repository * @return an {@link Optional} containing a {@link Booster} */ @SuppressWarnings("unchecked") private Optional<Booster> indexBooster(String id, Path file, Path moduleDir, Map<String, Mission> missions, Map<String, Runtime> runtimes) { logger.info(() -> "Indexing " + file + " ..."); Booster booster = null; try (BufferedReader reader = Files.newBufferedReader(file)) { // Read YAML entry booster = yaml.loadAs(reader, Booster.class); } catch (IOException e) { logger.log(Level.SEVERE, "Error while reading " + file, e); } if (booster != null) { try { // Booster ID = filename without extension booster.setId(id); String runtimeId = file.getParent().toFile().getName(); String missionId = file.getParent().getParent().toFile().getName(); booster.setMission(missions.computeIfAbsent(missionId, (key) -> { ResourceBundle bundle = ResourceBundle.getBundle("missions", Locale.getDefault(), getClass().getClassLoader()); String name; try { name = bundle.getString(key); } catch (MissingResourceException mre) { name = key; } return new Mission(key, name); })); booster.setRuntime(runtimes.computeIfAbsent(runtimeId, (key) -> { ResourceBundle bundle = ResourceBundle.getBundle("runtimes", Locale.getDefault(), getClass().getClassLoader()); String name; try { name = bundle.getString(key); } catch (MissingResourceException mre) { name = key; } return new Runtime(key, name); })); booster.setContentPath(moduleDir); // Module does not exist. Clone it if (Files.notExists(moduleDir)) { try (Git git = Git.cloneRepository().setDirectory(moduleDir.toFile()) .setURI(GITHUB_URL + booster.getGithubRepo()).setCloneSubmodules(true) .setBranch(booster.getGitRef()).call()) { // Checkout on specified start point git.checkout().setName(booster.getGitRef()).setStartPoint(booster.getGitRef()).call(); } } Path metadataPath = moduleDir.resolve(booster.getBoosterDescriptorPath()); try (BufferedReader metadataReader = Files.newBufferedReader(metadataPath)) { Map<String, Object> metadata = yaml.loadAs(metadataReader, Map.class); booster.setMetadata(metadata); } Path descriptionPath = moduleDir.resolve(booster.getBoosterDescriptionPath()); if (Files.exists(descriptionPath)) { byte[] descriptionContent = Files.readAllBytes(descriptionPath); booster.setDescription(new String(descriptionContent)); } } catch (GitAPIException gitException) { logger.log(Level.SEVERE, "Error while reading git repository", gitException); } catch (Exception e) { logger.log(Level.SEVERE, "Error while reading metadata from " + file, e); } } return Optional.ofNullable(booster); } @PostConstruct void init() { long indexPeriod = Long .parseLong(getEnvVarOrSysProp(CATALOG_INDEX_PERIOD_PROPERTY_NAME, DEFAULT_INDEX_PERIOD)); if (indexPeriod > 0L) { executorService = Executors.newScheduledThreadPool(1); logger.info(() -> "Indexing every " + indexPeriod + " minutes"); executorService.scheduleAtFixedRate(this::index, 0, indexPeriod, TimeUnit.MINUTES); } else { index(); } } @PreDestroy void destroy() { if (executorService != null) { executorService.shutdown(); } } private static String getEnvVarOrSysProp(String name, String defaultValue) { return System.getProperty(name, System.getenv().getOrDefault(name, defaultValue)); } /** * Copies the {@link Booster} contents to the specified {@link Project} */ public Path copy(Booster booster, Project project) throws IOException { Path modulePath = booster.getContentPath(); Path to = project.getRoot().as(DirectoryResource.class).getUnderlyingResourceObject().toPath(); return Files.walkFileTree(modulePath, new CopyFileVisitor(to, (p) -> !EXCLUDED_PROJECT_FILES.contains(p.toFile().getName().toLowerCase()))); } public Set<Mission> getMissions() { return boosters.stream().map(Booster::getMission).collect(Collectors.toCollection(TreeSet::new)); } public Set<Runtime> getRuntimes(Mission mission) { if (mission == null) { return Collections.emptySet(); } return boosters.stream().filter(b -> mission.equals(b.getMission())).map(Booster::getRuntime) .collect(Collectors.toCollection(TreeSet::new)); } public Optional<Booster> getBooster(Mission mission, Runtime runtime) { Objects.requireNonNull(mission, "Mission should not be null"); Objects.requireNonNull(runtime, "Runtime should not be null"); return boosters.stream().filter(b -> mission.equals(b.getMission())) .filter(b -> runtime.equals(b.getRuntime())).findFirst(); } public List<Booster> getBoosters() { Lock readLock = reentrantLock.readLock(); try { readLock.lock(); return boosters; } finally { readLock.unlock(); } } }