/*
 * Copyright 2002-2016 the original author or authors.
 *
 * 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.springframework.security.web.authentication.preauth.j2ee;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.core.authority.mapping.MappableAttributesRetriever;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * This <tt>MappableAttributesRetriever</tt> implementation reads the list of defined J2EE
 * roles from a <tt>web.xml</tt> file and returns these from {
 * {@link #getMappableAttributes()}.
 *
 * @author Ruud Senden
 * @author Luke Taylor
 * @since 2.0
 */
public class WebXmlMappableAttributesRetriever implements ResourceLoaderAware,
		MappableAttributesRetriever, InitializingBean {
	protected final Log logger = LogFactory.getLog(getClass());

	private ResourceLoader resourceLoader;
	private Set<String> mappableAttributes;

	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	public Set<String> getMappableAttributes() {
		return mappableAttributes;
	}

	/**
	 * Loads the web.xml file using the configured <tt>ResourceLoader</tt> and parses the
	 * role-name elements from it, using these as the set of <tt>mappableAttributes</tt>.
	 */

	public void afterPropertiesSet() throws Exception {
		Resource webXml = resourceLoader.getResource("/WEB-INF/web.xml");
		Document doc = getDocument(webXml.getInputStream());
		NodeList webApp = doc.getElementsByTagName("web-app");
		if (webApp.getLength() != 1) {
			throw new IllegalArgumentException(
					"Failed to find 'web-app' element in resource" + webXml);
		}
		NodeList securityRoles = ((Element) webApp.item(0))
				.getElementsByTagName("security-role");

		ArrayList<String> roleNames = new ArrayList<>();

		for (int i = 0; i < securityRoles.getLength(); i++) {
			Element secRoleElt = (Element) securityRoles.item(i);
			NodeList roles = secRoleElt.getElementsByTagName("role-name");

			if (roles.getLength() > 0) {
				String roleName = ((Element) roles.item(0)).getTextContent().trim();
				roleNames.add(roleName);
				logger.info("Retrieved role-name '" + roleName + "' from web.xml");
			}
			else {
				logger.info("No security-role elements found in " + webXml);
			}
		}

		mappableAttributes = Collections.unmodifiableSet(new HashSet<>(roleNames));
	}

	/**
	 * @return Document for the specified InputStream
	 */
	private Document getDocument(InputStream aStream) {
		Document doc;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setValidating(false);
			DocumentBuilder db = factory.newDocumentBuilder();
			db.setEntityResolver(new MyEntityResolver());
			doc = db.parse(aStream);
			return doc;
		}
		catch (FactoryConfigurationError e) {
			throw new RuntimeException("Unable to parse document object", e);
		}
		catch (ParserConfigurationException e) {
			throw new RuntimeException("Unable to parse document object", e);
		}
		catch (SAXException e) {
			throw new RuntimeException("Unable to parse document object", e);
		}
		catch (IOException e) {
			throw new RuntimeException("Unable to parse document object", e);
		}
		finally {
			try {
				aStream.close();
			}
			catch (IOException e) {
				logger.warn("Failed to close input stream for web.xml", e);
			}
		}
	}

	/**
	 * We do not need to resolve external entities, so just return an empty String.
	 */
	private static final class MyEntityResolver implements EntityResolver {
		public InputSource resolveEntity(String publicId, String systemId)
				throws SAXException, IOException {
			return new InputSource(new StringReader(""));
		}
	}
}
