Package org.wso2.carbon.mediation.registry

Source Code of org.wso2.carbon.mediation.registry.ESBRegistry

/**
* Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* 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.wso2.carbon.mediation.registry;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axiom.om.impl.llom.OMDocumentImpl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.SynapseException;
import org.apache.synapse.config.SynapseConfigUtils;
import org.apache.synapse.registry.AbstractRegistry;
import org.apache.synapse.registry.RegistryEntry;
import org.wso2.carbon.mediation.registry.persistence.PersistenceManager;
import org.wso2.carbon.mediation.registry.persistence.dataobject.RegistryEntryDO;
import org.wso2.carbon.registry.core.RegistryConstants;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Properties;

/**
* Registry implementation for ESB. This assumes that registry resources can be accessed using FILE
* system or http. Registry meta data are accessed using web services.
*/
public class ESBRegistry extends AbstractRegistry {

    public static final int FILE = 100;
    public static final int HTTP = 101;
    public static final int HTTPS = 102;
    public static final String URL_SEPARATOR = "/";
    public static final char URL_SEPARATOR_CHAR = '/';

    private static final int DELETE_RETRY_SLEEP_TIME = 10;
    private static final long DEFAULT_CACHABLE_DURATION = 0;
    private static final Log log = LogFactory.getLog(ESBRegistry.class);

    private static final int MAX_KEYS = 200;

    /**
     * File system path corresponding to the FILE url path. This is a system depending path
     * used for accessing resources as files.
     */
    private String localRegistry = null;

    /**
     * URL of the WS endpoind to get data about registry entries.
     */
    private String metaDataService = null;

    /**
     * Specifies whether the registry is in the local host or a remote registry.
     * Local host means the same computer as ESB is running.
     */
    private int registryType = ESBRegistryConstants.LOCAL_HOST_REGISTRY;

    /**
     * Contains the protocol for the registry. Allowd values are FILE, HTTP and HTTPS.
     */
    private int registryProtocol = FILE;

    public ESBRegistry() {
        this.localRegistry = RegistryHelper.getHome();
    }

    public void init(Properties properties) {

        super.init(properties);
        for (Object o : properties.keySet()) {
            if (o != null) {
                String name = (String) o;
                String value = (String) properties.get(name);
                addConfigProperty(name, value);
            }
        }

    }

    public OMNode lookup(String key) {

        if (log.isDebugEnabled()) {
            log.info("==> Repository fetch of resource with key : " + key);
        }
        URLConnection urlc;
        URL url = null;
        try {
            url = new URL(getRoot() + key);
        } catch (MalformedURLException e) {
            handleException("Invalid path '" + getRoot() + key + "' for URL", e);
        }

        if ("file".equals(url.getProtocol())) {
            try {
                url.openStream();
            } catch (IOException ignored) {
                if (!localRegistry.endsWith(URL_SEPARATOR)) {
                    localRegistry = localRegistry + URL_SEPARATOR;
                }
                try {
                    url = new URL(url.getProtocol() + ":" + localRegistry + key);
                } catch (MalformedURLException e) {
                    handleException("Invalid path '" + url.getProtocol() + ":" +
                            localRegistry + key + "' for URL", e);
                }
                try {
                    url.openStream();
                } catch (IOException e) {
                    return null;
                }
            }
        }

        try {
            urlc = url.openConnection();
            urlc.connect();
        } catch (IOException e) {
            return null;
        }

        InputStream input = null;
        try {
            input = urlc.getInputStream();
        } catch (IOException e) {
            handleException("Error when getting a stream from the URL", e);
        }

        if (input == null) {
            return null;
        }

        BufferedInputStream inputStream = new BufferedInputStream(input);
        OMNode result = null;
        try {
            XMLStreamReader parser = XMLInputFactory.newInstance().
                    createXMLStreamReader(inputStream);
            StAXOMBuilder builder = new StAXOMBuilder(parser);
            result = builder.getDocumentElement();

        } catch (OMException ignored) {

            if (log.isDebugEnabled()) {
                log.debug("The resource at the provided URL isn't " +
                        "well-formed XML,So,takes it as a text");
            }

            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("Error in closing the input stream. ", e);
            }

            result = SynapseConfigUtils.readNonXML(url);

        } catch (XMLStreamException ignored) {

            if (log.isDebugEnabled()) {
                log.debug("The resource at the provided URL isn't " +
                        "well-formed XML,So,takes it as a text");
            }

            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("Error in closing the input stream. ", e);
            }
            result = SynapseConfigUtils.readNonXML(url);

        } finally {
            try {
                result.detach();
                OMDocumentImpl parent = new OMDocumentImpl(OMAbstractFactory.getOMFactory());
                parent.addChild(result);
                inputStream.close();
            } catch (IOException e) {
                log.error("Error in closing the input stream.", e);
            }

        }
        return result;
    }

    public RegistryEntry getRegistryEntry(String key) {

        // get information from the actual resource
        MediationRegistryEntryImpl entryEmbedded = new MediationRegistryEntryImpl();

        try {
            URL url = new URL(getRoot() + key);
            if ("file".equals(url.getProtocol())) {
                try {
                    url.openStream();
                } catch (IOException ignored) {
                    if (!localRegistry.endsWith(URL_SEPARATOR)) {
                        localRegistry = localRegistry + URL_SEPARATOR;
                    }
                    url = new URL(url.getProtocol() + ":" + localRegistry + key);
                    try {
                        url.openStream();
                    } catch (IOException e) {
                        return null;
                    }
                }
            }
            URLConnection urlc = url.openConnection();

            entryEmbedded.setKey(key);
            entryEmbedded.setName(url.getFile());
            entryEmbedded.setType(ESBRegistryConstants.FILE);

            entryEmbedded.setDescription("Resource at : " + url.toString());
            entryEmbedded.setLastModified(urlc.getLastModified());
            entryEmbedded.setVersion(urlc.getLastModified());
            if (urlc.getExpiration() > 0) {
                entryEmbedded.setCachableDuration(
                        urlc.getExpiration() - System.currentTimeMillis());
            } else {
                entryEmbedded.setCachableDuration(getCachableDuration());
            }

        } catch (MalformedURLException e) {
            handleException("Invalid URL reference " + getRoot() + key, e);
        } catch (IOException e) {
            handleException("IO Error reading from URL " + getRoot() + key, e);
        }

        // get information from the database
        PersistenceManager persistenceManager = PersistenceManager.getInstance();
        RegistryEntryDO registryEntryDO = persistenceManager.getRegistryEntry(key);

        if (registryEntryDO != null) {

            if (registryEntryDO.getExpiryTime() != null) {
                entryEmbedded.setCachableDuration(registryEntryDO.getExpiryTime());
            } else {
                entryEmbedded.setCachableDuration(0);
            }
        }

        return entryEmbedded;
    }

    /**
     * Updates the metadata of the given registry entry
     *
     * @param entry RegistryEntry containing the new metadata
     */
    public void updateRegistryEntry(RegistryEntry entry) {

        RegistryEntryDO registryEntryDO = new RegistryEntryDO();
        registryEntryDO.setRegistryKey(entry.getKey());
        registryEntryDO.setExpiryTime(entry.getCachableDuration());

        PersistenceManager persistenceManager = PersistenceManager.getInstance();
        persistenceManager.saveOrUpdateRegistryEntry(registryEntryDO);
    }

    /**
     * Updates the registry resource pointed by the given key.
     *
     * @param key   Key of the resource to be updated
     * @param value New value of the resource
     */
    public void updateResource(String key, Object value) {

        if (registryType == ESBRegistryConstants.LOCAL_HOST_REGISTRY) {
            File file = new File(localRegistry + RegistryHelper.getSystemDependentPath(key));
            if (file.exists()) {
                BufferedWriter writer = null;
                try {
                    writer = new BufferedWriter(new FileWriter(file));
                    writer.write(value.toString());
                    writer.flush();
                } catch (IOException e) {
                    handleException("Couldn't write to registry entry: " + key, e);
                } finally {
                    if (writer != null) {
                        try {
                            writer.close();
                        } catch (IOException ignored) {
                        }
                    }
                }

            }
        } else {
            handleException("Remote registry is not supported.");
        }
    }

    /**
     * Adds a new resource to the registry.
     *
     * @param parentName   Key of the parent of the new resource
     * @param resourceName Name of the new resource
     * @param isLeaf       Specifies whether the new resource is a leaf or not. In a FILE system based
     *                     registry, leaf is a FILE and non-leaf is a FOLDER.
     * @throws Exception if an error occurs while creating the resources
     */
    private void addResource(String parentName,
                             String resourceName, boolean isLeaf) throws Exception {

        if (registryType == ESBRegistryConstants.LOCAL_HOST_REGISTRY) {

            if (isLeaf) {
                createFile(RegistryHelper.getSystemDependentPath(parentName), resourceName);
            } else {
                createFolder(RegistryHelper.getSystemDependentPath(parentName), resourceName);
            }
        }
    }

    /**
     * Removes the registry resource identified by the given key. If the key points to a directory,
     * all its subdirectories and files in those directoris will be deleted. All the database
     * entries for deleted registry resources will be removed.
     *
     * @param key resource key
     */
    private void removeResource(String key) {

        if (registryType == ESBRegistryConstants.LOCAL_HOST_REGISTRY) {
            File resource = new File(localRegistry + RegistryHelper.getSystemDependentPath(key));
            if (resource.exists()) {

                if (resource.isFile()) {
                    deleteFile(resource);
                } else if (resource.isDirectory()) {
                    deleteDirectory(resource);
                }

            } else {
                throw new SynapseException("Parent folder: " + key + " does not exists.");
            }
        }
    }

    /**
     * Returns the registry key in URL style (with "/" as the separator) for a given FILE.
     *
     * @param file File to get the key
     * @return Registry key
     */
    private String getRegsitryKey(File file) {

        String path = file.getAbsolutePath();
        String rootPath = new File(localRegistry).getAbsolutePath();
        return getURLPath(path.substring(rootPath.length() + 1)); // we have remove the preceeding "/"
    }

    private void deleteFile(File file) {

        boolean success = file.delete();
        if (!success) {
            // try with this work around to overcome a known bug in windows
            // work around:
            // run garbage collector and sleep for some time and delete
            // if still didn't delete,
            // rename FILE to a temp FILE in "temp" dir and mark it to delete on exist

            System.gc();
            try {
                Thread.sleep(DELETE_RETRY_SLEEP_TIME);
            } catch (InterruptedException e) {
                // ignore the exception
            }

            success = file.delete();
            if (!success) {
                int suffix = 1;
                File renamedFile;

                File tempDir = new File("temp");
                if (!tempDir.exists()) {
                    tempDir.mkdir();
                }

                do {
                    String changedName = "d" + suffix + file.getName();
                    renamedFile = new File(tempDir, changedName);
                    suffix++;
                } while (renamedFile.exists());

                if (file.renameTo(renamedFile)) {
                    renamedFile.deleteOnExit();
                } else {
                    handleException("Cannot delete the resource: " + file.getName());
                }
            }
        }

        PersistenceManager persistenceManager = PersistenceManager.getInstance();
        persistenceManager.deleteRegistryEntry(getRegsitryKey(file));
    }

    private void deleteDirectory(File dir) {

        File[] children = dir.listFiles();
        for (File aChildren : children) {
            if (aChildren != null) {
                if (aChildren.isFile()) {
                    deleteFile(aChildren);
                } else if (aChildren.isDirectory()) {
                    deleteDirectory(aChildren);
                }
            }
        }

        boolean success = dir.delete();
        if (success) {
            PersistenceManager persistenceManager = PersistenceManager.getInstance();
            persistenceManager.deleteRegistryEntry(getRegsitryKey(dir));
        } else {
            handleException("Cannot delete the resource: " + dir.getName());
        }
    }

    private void createFolder(String parentName, String newFolderName) throws Exception {

        /*
        search for parent. if found, create the new FOLDER in it. this depends on whether we are
        using a remote FILE system or not.
        add entry 'parentName/newFolderName' as key to the database.
        */

        if (registryType == ESBRegistryConstants.LOCAL_HOST_REGISTRY) {
            File parent = new File(localRegistry + parentName);
            if (parent.exists()) {

                File newEntry = new File(parent, newFolderName);

                boolean success = newEntry.mkdir();
                if (!success) {
                    handleException("Couldn't create folder: " + newFolderName);
                }

                // update meta data to the database
                // note that we are using the registry key part (path without the local registry
                // root as the key in db
                // if the new FOLDER has a parent FOLDER, use its values for defaults for all
                // possible properties

                RegistryEntryDO registryEntryDO = new RegistryEntryDO();
                registryEntryDO.setRegistryKey(parentName + URL_SEPARATOR + newFolderName);

                PersistenceManager persistenceManager = PersistenceManager.getInstance();
                RegistryEntryDO parentEntryDO =
                        persistenceManager.getRegistryEntry(getURLPath(parentName));
                if (parentEntryDO != null) {
                    registryEntryDO.setExpiryTime(parentEntryDO.getExpiryTime());
                } else {
                    registryEntryDO.setExpiryTime(getCachableDuration());
                }

                persistenceManager.addRegistryEntry(registryEntryDO);

            } else {
                handleException("Parent folder: " + parentName + " does not exists.");
            }
        }
    }

    private void createFile(String parentName, String newFileName) throws Exception {

        /*
        search for parent. if found, create the new FOLDER in it. this depends on whether we are
        using a remote FILE system or not.
        */

        if (registryType == ESBRegistryConstants.LOCAL_HOST_REGISTRY) {
            File parent = new File(localRegistry + parentName);
            if (parent.exists()) {

                File newFile = new File(parent, newFileName);
                boolean success = newFile.createNewFile();
                if (!success) {
                    handleException("Couldn't create resource: " + newFileName);
                }

                // update meta data to the database
                // note that we are using the registry key part (path without the local registry
                // root as the key in db
                // update the db only if we have some thing else to write other than the key

                RegistryEntryDO registryEntryDO = new RegistryEntryDO();
                registryEntryDO.setRegistryKey(parentName + URL_SEPARATOR + newFileName);

                PersistenceManager persistenceManager = PersistenceManager.getInstance();
                RegistryEntryDO parentEntryDO =
                        persistenceManager.getRegistryEntry(getURLPath(parentName));
                if (parentEntryDO != null) {
                    registryEntryDO.setExpiryTime(parentEntryDO.getExpiryTime());
                } else {
                    registryEntryDO.setExpiryTime(getCachableDuration());
                }

                persistenceManager.addRegistryEntry(registryEntryDO);

            } else {
                handleException("Parent folder: " + parentName + " does not exists.");
            }
        }
    }

    /**
     * Configure the ESB registry using registry parameters.
     * <p/>
     * root: FILE:directory -   registry is on local host
     * directory is used to access metadata
     * <p/>
     * root: http/https:location -  has to specify one of the following settings
     * localRegistry - location of the local registry
     * metadataService - url of the service to access metadata
     * If none of above parameters are given "registry" FOLDER is taken as the local registry.
     *
     * @param name name of the config
     * @param value value of the config
     */
    private void addConfigProperty(String name, String value) {

        if (localRegistry == null) {
            // registry root should always end with "/"
            if (ESBRegistryConstants.LOCAL_REGISTRY_ROOT.endsWith(URL_SEPARATOR)) {
                localRegistry = ESBRegistryConstants.LOCAL_REGISTRY_ROOT;
            } else {
                localRegistry = ESBRegistryConstants.LOCAL_REGISTRY_ROOT + URL_SEPARATOR;
            }
        }
        if (name != null && value != null) {
            if (name.equals("root")) {

                // root should always end with '/'
                // therefore, property keys do not have to begin with '/', which could be misleading
                try {
                    URL url = new URL(value);
                    if ("file".equals(url.getProtocol())) {
                        try {
                            url.openStream();
                        } catch (IOException ignored) {
                            if (!localRegistry.endsWith(URL_SEPARATOR)) {
                                localRegistry = localRegistry + URL_SEPARATOR;
                            }
                            url = new URL(url.getProtocol() + ":" + localRegistry + url.getPath());
                            try {
                                url.openStream();
                            } catch (IOException e) {
                                log.error("Unable to open a connection to url : " + url, e);
                            }
                        }
                    }
                    if (url.getProtocol().equals("file")) {
                        registryProtocol = FILE;

                        registryType = ESBRegistryConstants.LOCAL_HOST_REGISTRY;

                        if (url.getPath().endsWith(URL_SEPARATOR)) {
                            localRegistry = RegistryHelper.getSystemDependentPath(url.getPath());
                        } else {
                            localRegistry = RegistryHelper.getSystemDependentPath(url.getPath()) + File.separator;
                        }

                    } else if (url.getProtocol().equals("http")) {
                        registryProtocol = HTTP;
                    } else if (url.getProtocol().equals("https")) {
                        registryProtocol = HTTPS;
                    }


                    if (!value.endsWith(URL_SEPARATOR)) {
                        value = value + URL_SEPARATOR;
                    }


                } catch (MalformedURLException e) {
                    // don't set the root if this is not a valid URL
                    handleException("Registry root should be a valid URL.", e);
                }
            }

            if (name.equals("localRegistry")) {
                registryType = ESBRegistryConstants.LOCAL_HOST_REGISTRY;

                // registry root always ends with "/"
                if (!value.endsWith(File.separator)) {
                    value = value + File.separator;
                }
                localRegistry = value;
            }

            if (name.equals("matadataService")) {
                registryType = ESBRegistryConstants.REMOTE_HOST_REGISTRY;
                metaDataService = value;
            }

            // check if host name verification is disabled
            if (name.equalsIgnoreCase("disableHostNameVerification")) {
                if (value.equalsIgnoreCase("true")) {
                    HostnameVerifier hv = new HostnameVerifier() {
                        public boolean verify(String urlHostName, SSLSession session) {
                            return true;
                        }
                    };

                    HttpsURLConnection.setDefaultHostnameVerifier(hv);
                }
            }

        } else {
            log.debug("Name and Value must need");
        }
    }

    /**
     * Returns the root of the registry. This is always a URL.
     *
     * @return Registry root.
     */
    public String getRoot() {
        String root = (String) properties.get("root");
        if (root == null) {
            return "";
        } else {
            return root;
        }
    }

    public long getCachableDuration() {
        String cachableDuration = (String) properties.get("cachableDuration");
        return cachableDuration == null ? DEFAULT_CACHABLE_DURATION : Long.parseLong(cachableDuration);
    }

    /**
     * Gives the children of the given entry. If the registry is in the same host get the children
     * (subfolders and files) using the FILE system. If the registry is in a remote host, get
     * children using a WS call. Give null or registry entry with "" as the key to list the children
     * of the root.
     *
     * @param entry registry entry to get the children
     * @return children of the given entry
     */
    public RegistryEntry[] getChildren(RegistryEntry entry) {

        String registryRoot = localRegistry;

        if (entry == null) {
            // give the children of the root
            // null or key = "" stands for root

            MediationRegistryEntryImpl registryEntry = new MediationRegistryEntryImpl();
            registryEntry.setKey("");
            entry = registryEntry;
        }

        if (registryType == ESBRegistryConstants.LOCAL_HOST_REGISTRY) {

            // registry is in the local FILE system. access it directly.

            String entryPath = RegistryHelper.getSystemDependentPath(entry.getKey());
            File file = new File(registryRoot + entryPath);
            if (!file.isDirectory()) {
                return null;
            }

            try {

                String[] children = file.list();
                RegistryEntry[] entries = new RegistryEntry[children.length];
                for (int i = 0; i < children.length; i++) {

                    MediationRegistryEntryImpl registryEntry = new MediationRegistryEntryImpl();
                    if (entry.getKey().equals("")) {
                        // user asking for the children of the root
                        registryEntry.setKey(children[i]);
                    } else {
                        if (entryPath.endsWith(URL_SEPARATOR)) {
                            registryEntry.setKey(getURLPath(entry.getKey() + children[i]));
                        } else {
                            registryEntry.setKey(getURLPath(entry.getKey() + URL_SEPARATOR + children[i]));
                        }
                    }

                    // set if the registry entry is a FILE or a FOLDER
                    File entryFile = new File
                            (registryRoot + RegistryHelper.getSystemDependentPath(registryEntry.getKey()));
                    if (entryFile.isDirectory()) {
                        registryEntry.setType(ESBRegistryConstants.FOLDER);
                    }

                    entries[i] = registryEntry;
                }

                return entries;

            } catch (Exception e) {
                handleException("Error in reading the URL.");
            }


        } else if (registryType == ESBRegistryConstants.REMOTE_HOST_REGISTRY) {
            // implement for remote registries.
        }

        return null;
    }

    public RegistryEntry[] getDescendants(RegistryEntry entry) {

        ArrayList<RegistryEntry> list = new ArrayList<RegistryEntry>();
        RegistryEntry[] entries = getChildren(entry);
        if (entries != null) {
            for (RegistryEntry entry1 : entries) {

                if (list.size() > MAX_KEYS) {
                    break;
                }

                fillDescendants(entry1, list);
            }
        }

        RegistryEntry[] descendants = new RegistryEntry[list.size()];
        for (int i = 0; i < list.size(); i++) {
            descendants[i] = list.get(i);
        }

        return descendants;
    }

    public void delete(String path) {
        removeResource(path);
    }

    public void newResource(String path, boolean isDirectory) {
        String parent = getParentPath(path);
        String fileName = getResourceName(path);
        try {
            addResource(parent, fileName, !isDirectory);
        } catch (Exception e) {
            handleException("Error when adding a new resource", e);
        }
    }


    private void fillDescendants(RegistryEntry parent, ArrayList<RegistryEntry> list) {

        RegistryEntry[] entries = getChildren(parent);
        if (entries != null) {
            for (RegistryEntry entry : entries) {

                if (list.size() > MAX_KEYS) {
                    break;
                }

                fillDescendants(entry, list);
            }
        } else {
            list.add(parent);
        }
    }

    private String getURLPath(String filePath) {
        return filePath.replace(File.separatorChar, URL_SEPARATOR_CHAR);
    }

    private void handleException(String msg, Exception e) {
        log.error(msg, e);
        throw new SynapseException(msg, e);
    }

    private void handleException(String msg) {
        log.error(msg);
        throw new SynapseException(msg);
    }

    private String getParentPath(String resourcePath) {

        String parentPath;
        if (resourcePath.equals(RegistryConstants.ROOT_PATH)) {
            parentPath = null;
        } else {
            if (resourcePath.lastIndexOf(RegistryConstants.PATH_SEPARATOR) == 0) {
                parentPath = RegistryConstants.ROOT_PATH;
            } else {
                parentPath = resourcePath.substring(
                        0, resourcePath.lastIndexOf(RegistryConstants.PATH_SEPARATOR));
            }
        }
        return parentPath;
    }


    private String getResourceName(String path) {
        if (path != null) {
            String correctedPath = path;
            if (path.endsWith(RegistryConstants.PATH_SEPARATOR)) {
                correctedPath = path.substring(0,
                        path.lastIndexOf(RegistryConstants.PATH_SEPARATOR));
            }
            return correctedPath.substring(
                    correctedPath.lastIndexOf(RegistryConstants.PATH_SEPARATOR) + 1,
                    correctedPath.length());
        }
        return "";

    }

}
TOP

Related Classes of org.wso2.carbon.mediation.registry.ESBRegistry

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.