ExportImportManager.java

/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * 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.keycloak.exportimport;

import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER_DEFAULT;

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class ExportImportManager {

    private static final Logger logger = Logger.getLogger(ExportImportManager.class);

    private final KeycloakSessionFactory sessionFactory;
    private final KeycloakSession session;

    private ExportProvider exportProvider;
    private ImportProvider importProvider;

    public ExportImportManager(KeycloakSession session) {
        this.sessionFactory = session.getKeycloakSessionFactory();
        this.session = session;

        String exportImportAction = ExportImportConfig.getAction();

        if (ExportImportConfig.ACTION_EXPORT.equals(exportImportAction)) {
            // Future Refactoring: If the system properties are no longer needed for integration tests, refactor to use
            // a default provider in its standard way.
            // Setting this to "provider" doesn't work yet when instrumenting Keycloak with Quarkus as it leads to
            // "java.lang.NullPointerException: Cannot invoke "String.indexOf(String)" because "value" is null"
            // when calling "Config.getProvider()" from "KeycloakProcessor.loadFactories()"
            String providerId = System.getProperty(PROVIDER, Config.scope("export").get("exporter", PROVIDER_DEFAULT));
            exportProvider = session.getProvider(ExportProvider.class, providerId);
            if (exportProvider == null) {
                throw new RuntimeException("Export provider '" + providerId + "' not found");
            }
        } else if (ExportImportConfig.ACTION_IMPORT.equals(exportImportAction)) {
            String providerId = System.getProperty(PROVIDER, Config.scope("import").get("importer", PROVIDER_DEFAULT));
            importProvider = session.getProvider(ImportProvider.class, providerId);
            if (importProvider == null) {
                throw new RuntimeException("Import provider '" + providerId + "' not found");
            }
        } else if (ExportImportConfig.getDir().isPresent()) { // import at startup
            ExportImportConfig.setStrategy(Strategy.IGNORE_EXISTING);
            ExportImportConfig.setReplacePlaceholders(true);
            // enables logging of what is imported
            ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
        }
    }

    public boolean isImportMasterIncluded() {
        if (importProvider != null) {
            try {
                return importProvider.isMasterRealmExported();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            return isImportMasterIncludedAtStartup();
        }
        
    }
    
    boolean isImportMasterIncludedAtStartup() {
        return getStartupImportProviders().map(Supplier::get)
                .anyMatch(provider -> {
                    try {
                        return provider.isMasterRealmExported();
                    } catch (IOException e) {
                        throw new RuntimeException("Failed to run import", e);
                    }
                });
    }

    public boolean isRunExport() {
        return exportProvider != null;
    }

    public void runImport() {
        if (importProvider != null) {
            try {
                importProvider.importModel();
            } catch (IOException e) {
                throw new RuntimeException("Failed to run import", e);
            }
        } else {
            runImportAtStartup();
        }
    }
    
    public void runImportAtStartup() {
        getStartupImportProviders().map(Supplier::get).forEach(ip -> {
            try {
                ip.importModel();
            } catch (IOException e) {
                throw new RuntimeException("Failed to run import", e);
            }
        });
    }

    private Stream<Supplier<ImportProvider>> getStartupImportProviders() {
        var dirProp = ExportImportConfig.getDir();
        if (dirProp.isEmpty()) {
            return Stream.empty();
        }
        String dir = dirProp.get();
        
        Stream<ProviderFactory> factories = sessionFactory.getProviderFactoriesStream(ImportProvider.class);

        return factories.flatMap(factory -> {
            String providerId = factory.getId();

            if ("dir".equals(providerId)) {
                Supplier<ImportProvider> func = () -> session.getProvider(ImportProvider.class, providerId);
                return Stream.of(func);
            }
            if ("singleFile".equals(providerId)) {
                Set<String> filesToImport = new HashSet<>();

                File[] files = Paths.get(dir).toFile().listFiles();
                Objects.requireNonNull(files, "directory not found");
                for (File file : files) {
                    Path filePath = file.toPath();

                    if (!(Files.exists(filePath) && Files.isRegularFile(filePath) && filePath.toString().endsWith(".json"))) {
                        logger.debugf("Ignoring import file because it is not a valid file: %s", file);
                        continue;
                    }

                    String fileName = file.getName();

                    if (fileName.contains("-realm.json") || fileName.contains("-users-")) {
                        continue;
                    }

                    filesToImport.add(file.getAbsolutePath());
                }
                
                return filesToImport.stream().map(file -> () -> {
                    // we need a new session to pickup the static system property
                    // file setting - it is picked up by the provider only at create time
                    // this will eventually need to be consolidated with the master existance check
                    // to prevent double parsing
                    KeycloakSession newSession = session.getKeycloakSessionFactory().create();
                    try {
                        ExportImportConfig.setFile(file);
                        return newSession.getProvider(ImportProvider.class, providerId);
                    } finally {
                        newSession.close();
                    }
                });
            }
            return Stream.empty();
        });
    }

    public void runExport() {
        try {
            exportProvider.exportModel();
        } catch (IOException e) {
            throw new RuntimeException("Failed to run export", e);
        }
    }

}