/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 com.opensymphony.xwork2.validator;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.inject.Initializable;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsException;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Default validator factory
 *
 * @version $Date$ $Id$
 * @author Jason Carreira
 * @author James House
 */
public class DefaultValidatorFactory implements ValidatorFactory, Initializable {

    protected Map<String, String> validators = new HashMap<>();
    private static Logger LOG = LogManager.getLogger(DefaultValidatorFactory.class);
    protected ObjectFactory objectFactory;
    protected ValidatorFileParser validatorFileParser;

    @Inject
    public DefaultValidatorFactory(@Inject ObjectFactory objectFactory, @Inject ValidatorFileParser parser) {
        this.objectFactory = objectFactory;
        this.validatorFileParser = parser;
    }

    @Override
    public void init() {
        parseValidators();
    }

    public Validator getValidator(ValidatorConfig cfg) {

        String className = lookupRegisteredValidatorType(cfg.getType());

        Validator validator;

        try {
            // instantiate the validator, and set configured parameters
            //todo - can this use the ThreadLocal?
            validator = objectFactory.buildValidator(className, cfg.getParams(), ActionContext.getContext().getContextMap());
        } catch (Exception e) {
            final String msg = "There was a problem creating a Validator of type " + className + " : caused by " + e.getMessage();
            throw new StrutsException(msg, e, cfg);
        }

        // set other configured properties
        validator.setMessageKey(cfg.getMessageKey());
        validator.setDefaultMessage(cfg.getDefaultMessage());
        validator.setMessageParameters(cfg.getMessageParams());
        if (validator instanceof ShortCircuitableValidator) {
            ((ShortCircuitableValidator) validator).setShortCircuit(cfg.isShortCircuit());
        }

        return validator;
    }

    public void registerValidator(String name, String className) {
        LOG.debug("Registering validator of class {} with name {}", className, name);
        validators.put(name, className);
    }

    public String lookupRegisteredValidatorType(String name) {
        // lookup the validator class mapped to the type name
        String className = validators.get(name);

        if (className == null) {
            throw new IllegalArgumentException("There is no validator class mapped to the name " + name);
        }

        return className;
    }

    private void parseValidators() {
        LOG.debug("Loading validator definitions.");

        List<File> files = new ArrayList<>();
        try {
            // Get custom validator configurations via the classpath
            Iterator<URL> urls = ClassLoaderUtil.getResources("", DefaultValidatorFactory.class, false);
            while (urls.hasNext()) {
                URL u = urls.next();
                try {
                    URI uri = new URI(u.toExternalForm().replaceAll(" ", "%20"));
                    if (!uri.isOpaque() && "file".equalsIgnoreCase(uri.getScheme())) {
                        File f = new File(uri);
                        FilenameFilter filter = new FilenameFilter() {
                            public boolean accept(File file, String fileName) {
                                return fileName.contains("-validators.xml");
                            }
                        };
                        // First check if this is a directory
                        // If yes, then just do a "list" to get all files in this directory
                        // and match the filenames with *-validators.xml. If the filename
                        // matches then add to the list of files to be parsed
                        if (f.isDirectory()) {
                            try {
                                File[] ff = f.listFiles(filter);
                                if ( ff != null && ff.length > 0) {
                                    files.addAll(Arrays.asList(ff));
                                }
                            } catch (SecurityException se) {
                                LOG.error("Security Exception while accessing directory '{}'", f, se);
                            }

                        } else {
                            // If this is not a directory, then get hold of the inputstream.
                            // If its not a ZipInputStream, then create a ZipInputStream out
                            // of it. The intention is to allow nested jar files to be scanned
                            // for *-validators.xml.
                            // Ex: struts-app.jar -> MyApp.jar -> Login-validators.xml should be
                            // parsed and loaded.
                            ZipInputStream zipInputStream = null;
                            try (InputStream inputStream = u.openStream()) {
                                if (inputStream instanceof ZipInputStream) {
                                    zipInputStream = (ZipInputStream) inputStream;
                                } else {
                                    zipInputStream = new ZipInputStream(inputStream);
                                }
                                ZipEntry zipEntry = zipInputStream.getNextEntry();
                                while (zipEntry != null) {
                                    if (zipEntry.getName().endsWith("-validators.xml")) {
                                        LOG.trace("Adding validator {}", zipEntry.getName());
                                        files.add(new File(zipEntry.getName()));
                                    }
                                    zipEntry = zipInputStream.getNextEntry();
                                }
                            } finally {
                                //cleanup
                                if (zipInputStream != null) {
                                    zipInputStream.close();
                                }
                            }
                        }
                    }
                } catch (Exception ex) {
                    LOG.error("Unable to load {}", u, ex);
                }
            }
        } catch (IOException e) {
            throw new ConfigurationException("Unable to parse validators", e);
        }

        // Parse default validator configurations
        String resourceName = "com/opensymphony/xwork2/validator/validators/default.xml";
        retrieveValidatorConfiguration(resourceName);

        // Overwrite and extend defaults with application specific validator configurations
        resourceName = "validators.xml";
        retrieveValidatorConfiguration(resourceName);

        // Add custom (plugin) specific validator configurations
        for (File file : files) {
            retrieveValidatorConfiguration(file.getName());
        }
    }

    private void retrieveValidatorConfiguration(String resourceName) {
        InputStream is = ClassLoaderUtil.getResourceAsStream(resourceName, DefaultValidatorFactory.class);
        if (is != null) {
            validatorFileParser.parseValidatorDefinitions(validators, is, resourceName);
        }
    }
}
