ImportSAML2MetaData.java revision 4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1c
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster/**
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * Copyright (c) 2008 Sun Microsystems Inc. All Rights Reserved
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * The contents of this file are subject to the terms
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * of the Common Development and Distribution License
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * (the License). You may not use this file except in
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * compliance with the License.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * You can obtain a copy of the License at
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * https://opensso.dev.java.net/public/CDDLv1.0.html or
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * opensso/legal/CDDLv1.0.txt
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * See the License for the specific language governing
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * permission and limitations under the License.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * When distributing Covered Code, include this CDDL
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * Header Notice in each file and include the License file
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * at opensso/legal/CDDLv1.0.txt.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * If applicable, add the following below the CDDL Header,
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * with the fields enclosed by brackets [] replaced by
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * your own identifying information:
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * "Portions Copyrighted [year] [name of copyright owner]"
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * $Id: ImportSAML2MetaData.java,v 1.5 2008/07/08 01:12:01 exu Exp $
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster */
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster/**
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * Portions Copyrighted 2011 ForgeRock AS
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster */
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterpackage com.sun.identity.workflow;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.common.SAML2Constants;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.meta.SAML2MetaConstants;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.meta.SAML2MetaException;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.meta.SAML2MetaManager;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.meta.SAML2MetaSecurityUtils;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.meta.SAML2MetaUtils;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.jaxb.entityconfig.BaseConfigType;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.jaxb.entityconfig.EntityConfigElement;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.saml2.jaxb.metadata.EntityDescriptorElement;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.shared.debug.Debug;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport com.sun.identity.shared.xml.XMLUtils;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport java.util.List;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport javax.xml.bind.JAXBException;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport org.w3c.dom.Document;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport org.w3c.dom.Element;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport org.w3c.dom.Node;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterimport org.w3c.dom.NodeList;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster/**
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * Import SAML2 Metadata.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster */
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Fosterpublic class ImportSAML2MetaData {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster private ImportSAML2MetaData() {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster /**
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * Imports meta and extended metadata.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster *
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * @param realm Realm of the entity.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * @param metadata Meta data.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * @param extended extended data.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster * @return realm and entity ID.
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster */
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster public static String[] importData(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String realm,
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String metadata,
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String extended
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster ) throws WorkflowException {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String entityID = null;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster try {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaManager metaManager = new SAML2MetaManager();
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster EntityConfigElement configElt = null;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (extended != null) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Object obj = SAML2MetaUtils.convertStringToJAXB(extended);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster configElt = (obj instanceof EntityConfigElement) ?
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster (EntityConfigElement)obj : null;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (configElt != null && configElt.isHosted()) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster List config =
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster configElt.getIDPSSOConfigOrSPSSOConfigOrAuthnAuthorityConfig();
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (!config.isEmpty()) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster BaseConfigType bConfig = (BaseConfigType)
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster config.iterator().next();
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster realm = SAML2MetaUtils.getRealmByMetaAlias(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster bConfig.getMetaAlias());
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster EntityDescriptorElement descriptor = null;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (metadata != null) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster descriptor = getEntityDescriptorElement(metadata);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (descriptor != null) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster entityID = descriptor.getEntityID();
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster metaManager.createEntity(realm, descriptor, configElt);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster } catch (SAML2MetaException e) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster throw new WorkflowException(e.getMessage());
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster } catch (JAXBException e) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster throw new WorkflowException(e.getMessage());
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String[] results = {realm, entityID};
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster return results;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster static EntityDescriptorElement getEntityDescriptorElement(String metadata)
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster throws SAML2MetaException, JAXBException, WorkflowException {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Debug debug = Debug.getInstance("workflow");
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Document doc = XMLUtils.toDOMDocument(metadata, debug);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (doc == null) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster throw new WorkflowException(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster "import-entity-exception-invalid-descriptor", null);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Element docElem = doc.getDocumentElement();
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if ((!SAML2MetaConstants.ENTITY_DESCRIPTOR.equals(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster docElem.getLocalName())) ||
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster (!SAML2MetaConstants.NS_METADATA.equals(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster docElem.getNamespaceURI()))
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster ) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster throw new WorkflowException(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster "import-entity-exception-invalid-descriptor", null);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaSecurityUtils.verifySignature(doc);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster workaroundAbstractRoleDescriptor(doc);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Object obj = SAML2MetaUtils.convertNodeToJAXB(doc);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster obj = workaroundJAXBBug(obj);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster return (obj instanceof EntityDescriptorElement) ?
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster (EntityDescriptorElement)obj : null;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster private static void workaroundAbstractRoleDescriptor(Document doc) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Debug debug = Debug.getInstance("workflow");
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster NodeList nl = doc.getDocumentElement().getElementsByTagNameNS(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaConstants.NS_METADATA,SAML2MetaConstants.ROLE_DESCRIPTOR);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster int length = nl.getLength();
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (length == 0) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster return;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster for(int i = 0; i < length; i++) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Element child = (Element)nl.item(i);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String type = child.getAttributeNS(SAML2Constants.NS_XSI, "type");
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (type != null) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if ((type.equals(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaConstants.ATTRIBUTE_QUERY_DESCRIPTOR_TYPE)) ||
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster (type.endsWith(":" +
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaConstants.ATTRIBUTE_QUERY_DESCRIPTOR_TYPE))) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String newTag = type.substring(0, type.length() - 4);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String xmlstr = XMLUtils.print(child);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster int index = xmlstr.indexOf(
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaConstants.ROLE_DESCRIPTOR);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster xmlstr = "<" + newTag + xmlstr.substring(index +
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster SAML2MetaConstants.ROLE_DESCRIPTOR.length());
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (!xmlstr.endsWith("/>")) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster index = xmlstr.lastIndexOf("</");
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster xmlstr = xmlstr.substring(0, index) + "</" + newTag +
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster ">";
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Document tmpDoc = XMLUtils.toDOMDocument(xmlstr, debug);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster Node newChild =
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster doc.importNode(tmpDoc.getDocumentElement(), true);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster child.getParentNode().replaceChild(newChild, child);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster private static Object workaroundJAXBBug(Object obj) throws JAXBException {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String metadata = SAML2MetaUtils.convertJAXBToString(obj);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster String replaced = metadata.replaceAll("<(.*:)?Extensions/>", "");
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster if (metadata.equalsIgnoreCase(replaced)) {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster return obj;
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster } else {
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster return SAML2MetaUtils.convertStringToJAXB(replaced);
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster }
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster}
4a2f0f0be43dfd4c1b490cbf3cc48b6ba6084b1cAllan Foster