Package nexj.core.rpc

Source Code of nexj.core.rpc.GenericServer

// Copyright 2010-2011 NexJ Systems Inc. This software is licensed under the terms of the Eclipse Public License 1.0
package nexj.core.rpc;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import nexj.core.meta.Argument;
import nexj.core.meta.Attribute;
import nexj.core.meta.Event;
import nexj.core.meta.Member;
import nexj.core.meta.Metaclass;
import nexj.core.meta.Metadata;
import nexj.core.meta.Selector;
import nexj.core.persistence.OIDHolder;
import nexj.core.rpc.jms.JMSSender;
import nexj.core.runtime.Instance;
import nexj.core.runtime.InstanceList;
import nexj.core.runtime.InvocationContext;
import nexj.core.runtime.InvocationContextAware;
import nexj.core.runtime.InvocationContextHolder;
import nexj.core.runtime.SecurityViolationException;
import nexj.core.runtime.UnitOfWork;
import nexj.core.scripting.ConstPair;
import nexj.core.scripting.Pair;
import nexj.core.scripting.Symbol;
import nexj.core.util.ExceptionHolder;
import nexj.core.util.HashTab;
import nexj.core.util.IdentityHashTab;
import nexj.core.util.Logger;
import nexj.core.util.Lookup;
import nexj.core.util.ObjUtil;
import nexj.core.util.PropertyIterator;
import nexj.core.util.Undefined;

/**
* The generic request server implementation.
*/
public class GenericServer implements Server, InvocationContextAware, InvocationContextHolder
{
   // constants

   /**
    * Empty pair.
    */
   protected final static Pair EMPTY_PAIR = new ConstPair(null);

   // attributes

   /**
    * The master server flag. True to complete the invocation context.
    */
   protected boolean m_bMaster = true;

   // associations

   /**
    * The invocation context.
    */
   protected InvocationContext m_context;
  
   /**
    * The server interceptor.
    */
   protected Interceptor m_interceptor;
  
   /**
    * The class logger.
    */
   private final static Logger s_logger = Logger.getLogger(GenericServer.class);

   // operations

   /**
    * Sets the master server flag.
    * @param bMaster The master server flag to set.
    */
   public void setMaster(boolean bMaster)
   {
      m_bMaster = bMaster;
   }

   /**
    * @return The master server flag.
    */
   public boolean isMaster()
   {
      return m_bMaster;
   }
  
   /**
    * Sets the invocation context.
    * @param context The invocation context to set.
    */
   public void setInvocationContext(InvocationContext context)
   {
      m_context = context;
   }

   /**
    * @return The invocation context.
    */
   public InvocationContext getInvocationContext()
   {
      return m_context;
   }

   /**
    * Sets the server interceptor.
    * @param interceptor The server interceptor to set.
    */
   public void setInterceptor(Interceptor interceptor)
   {
      m_interceptor = interceptor;
   }

   /**
    * @return The server interceptor.
    */
   public Interceptor getInterceptor()
   {
      return m_interceptor;
   }

   /**
    * @see nexj.core.rpc.Server#invoke(nexj.core.rpc.Request)
    */
   public Response invoke(Request request)
   {
      boolean bAuditedSaved = m_context.isAudited();
      boolean bTransientSaved = m_context.isTransient();
      final boolean bStealth = request.isStealth() || m_context.isStealth();
      Object[][] argArray = null;

      try
      {
         if (!bStealth)
         {
            s_logger.dump(request);
         }

         if (m_context.isProtected())
         {
            RPCUtil.validate(request, new IdentityHashTab(), m_context);
         }

         if (request.getLocale() != null)
         {
            m_context.setLocale(request.getLocale());
         }

         if (request.getTimeZone() != null)
         {
            m_context.setTimeZone(request.getTimeZone());
         }

         Metadata metadata = m_context.getMetadata();

         if (request.getNamespace() != null && metadata.getNamespace() != null)
         {
            if (!metadata.getNamespace().equals(request.getNamespace()))
            {
               throw new RequestVersionException("err.rpc.requestNamespace",
                  new Object[]{metadata.getNamespace(), request.getNamespace()});
            }

            if (!metadata.getVersion().equals(request.getVersion()))
            {
               throw new RequestVersionException("err.rpc.requestVersion",
                  new Object[]{metadata.getVersion(), request.getVersion()});
            }
         }

         UnitOfWork uow = m_context.initUnitOfWork();

         uow.checkLicense();

         if (m_interceptor != null && m_interceptor.isEnabled(m_context))
         {
            m_interceptor.interceptRequest(request);
         }

         if (request.isAsync())
         {
            if (request.isCommit())
            {
               if (s_logger.isDebugEnabled() && !bStealth)
               {
                  s_logger.debug("Asynchronous request - forwarding to " + UnitOfWork.SYSTEM_QUEUE);
               }

               TransferObject properties = new TransferObject(1);

               properties.setValue(JMSSender.USER, m_context.getPrincipal().getName());
               uow.addMessage(UnitOfWork.SYSTEM_QUEUE, request, properties, -1, -1, false);
               m_context.complete(true);
            }

            Response response = new Response();

            if (s_logger.isDumpEnabled() && !bStealth)
            {
               s_logger.dump(response);
            }

            return response;
         }

         m_context.setAudited(false);

         boolean bTransient = !request.isCommit();

         uow.setTransient(bTransient);

         // Add to the unit of work the classes to track
         int nFilterCount = request.getFilterCount();
         boolean bInstanceFilter = false;

         for (int i = 0; i < nFilterCount; ++i)
         {
            TransferObject tobj = request.getFilter(i);

            if (tobj == null || tobj.getClassName() == null)
            {
               throw new RequestException("err.rpc.filterTO");
            }

            m_context.track(metadata.getMetaclass(tobj.getClassName()));

            if (!bInstanceFilter)
            {
               bInstanceFilter = tobj.hasValue("instances");
            }
         }

         int nInvocationCount = request.getInvocationCount();
         Lookup identityMap = new HashTab(nInvocationCount << 1);
         Lookup eventMap = new HashTab(nInvocationCount);
         Event[] eventArray = new Event[nInvocationCount];

         argArray = new Object[nInvocationCount][];
         InstanceFactory instanceFactory = new InstanceFactory(identityMap,
            new ArrayList(nInvocationCount << 3), InstanceFactory.STATE | InstanceFactory.PRE,
            m_context);

         // Instantiate the request objects
         for (int i = 0; i < nInvocationCount; ++i)
         {
            Request.Invocation invocation = request.getInvocation(i);
            TransferObject tobj = invocation.getObject();
            String sEventName = invocation.getEventName();

            if (sEventName == null)
            {
               sEventName = tobj.getEventName();
            }

            if (tobj == null || tobj.getClassName() == null || sEventName == null)
            {
               throw new RequestException("err.rpc.requestTO");
            }

            m_context.setTransient(bTransient);

            Object[] arguments = invocation.getArguments();
            Metaclass metaclass = metadata.getMetaclass(tobj.getClassName());
            Selector selector = metaclass.getSelector(sEventName);
            Member member;
            int nArgCount;

            if (arguments != null)
            {
               nArgCount = arguments.length;
               member = selector.getMember(nArgCount);

               if (member.isStatic() && tobj.getValueCount() != 0)
               {
                  throw new RequestException("err.rpc.requestTO");
               }
            }
            else
            {
               nArgCount = tobj.getValueCount();
               member = selector.findMember(0);

               if (member == null || member.isStatic())
               {
                  member = selector.getMember(nArgCount);

                  if (!member.isStatic())
                  {
                     throw new RequestException("err.rpc.argCount",
                        new Object[]{member.getName(), metaclass.getName()});
                  }
               }
               else
               {
                  nArgCount = 0;
               }
            }

            if (member.isAttribute())
            {
               throw new RequestException("err.rpc.attributeInvocation",
                  new Object[]{member.getName(), metaclass.getName()});
            }

            if (m_context.isProtected() && member.getVisibility() != Metaclass.PUBLIC)
            {
               throw new SecurityViolationException("err.rpc.eventVisibility",
                  new Object[]{member.getName(), metaclass.getName()});
            }

            Event event = (Event)member;
            Object[] args = new Object[nArgCount + 1];

            if (arguments != null)
            {
               for (int k = 0; k < nArgCount; ++k)
               {
                  args[k + 1] = instanceFactory.instantiate(arguments[k]);
               }
            }

            if (event.isStatic())
            {
               args[0] = metaclass;

               if (arguments == null)
               {
                  int nEventArgCount = event.getArgumentCount();

                  if (event.isVarArg())
                  {
                     --nEventArgCount;
                  }

                  for (int k = 0; k < nEventArgCount; ++k)
                  {
                     args[k + 1] = instanceFactory.instantiate(tobj.getValue(event.getArgument(k).getName()));
                  }

                  if (event.isVarArg())
                  {
                     int nArg = nEventArgCount;

                     for (PropertyIterator itr = tobj.getIterator(); itr.hasNext();)
                     {
                        itr.next();

                        Argument arg = event.findArgument(itr.getName());

                        if (arg == null || arg == event.getArgument(nEventArgCount))
                        {
                           args[++nArg] = new Pair(Symbol.define(itr.getName()), instanceFactory.instantiate(itr.getValue()));
                        }
                     }
                  }
               }

               // Allow auditing of static events
               identityMap.put(tobj, null);
            }
            else
            {
               args[0] = instanceFactory.instantiate(tobj);
            }

            eventArray[i] = event;
            argArray[i] = args;
            eventMap.put(tobj, event);
         }

         Lookup diffMap = null;

         // Instantiate the transfer objects from the instance filters
         if (bInstanceFilter)
         {
            diffMap = new HashTab();

            for (int i = 0; i < nFilterCount; ++i)
            {
               m_context.setTransient(bTransient);

               TransferObject tobj = request.getFilter(i);
               Object instances = tobj.findValue("instances");

               if (instances != null)
               {
                  List instanceList = (List)instances;
                  int nCount = instanceList.size();

                  for (int k = 0; k < nCount; ++k)
                  {
                     tobj = (TransferObject)instanceList.get(k);

                     Instance inst = instanceFactory.instantiate(tobj);

                     instanceList.set(k, inst);

                     if (inst != null)
                     {
                        diffMap.put(inst, tobj);
                     }
                  }
               }
            }
         }

         instanceFactory.complete();
         instanceFactory = null;
         GenericServer.auditRequest(identityMap, eventMap, m_context);
         identityMap = null;

         Object[] resultArray = new Object[nInvocationCount << 1];

         // Unset the pending event flag on the request argument instances
         for (int i = 0; i < nInvocationCount; ++i)
         {
            Object[] args = argArray[i];
            Object obj = args[0];

            if (obj instanceof Instance)
            {
               Instance instance = (Instance)obj;

               if (args.length == 1 && eventArray[i].getName().equals(instance.getPendingEventName()))
               {
                  resultArray[i << 1] = instance;
               }

               instance.suspendEvent();
            }
         }

         m_context.setTransient(false);
         uow.invokePendingEvents(false, false);

         // Invoke the events and accumulate the results
         for (int i = 0; i < nInvocationCount; ++i)
         {
            TransferObject tobj = request.getObject(i);
            Event event = eventArray[i];
            Pair attributes = EMPTY_PAIR;
            Object[] args = argArray[i];
            Object obj = args[0];
            int nAttrIndex = -1;

            m_context.setTransient(event.isTransient(bTransient));

            if (event.getArgumentCount() != 0)
            {
               Argument arg = event.findArgument("attributes");

               if (arg != null)
               {
                  nAttrIndex = arg.getOrdinal() + 1;

                  Object value = args[nAttrIndex];

                  attributes = (value instanceof Pair) ? (Pair)value : null;
               }
            }

            if (nAttrIndex < 0)
            {
               Object value = tobj.findValue("attributes", EMPTY_PAIR);

               if (value != EMPTY_PAIR)
               {
                  nAttrIndex = 0;
                  attributes =  (Pair)value;
               }
            }

            if (event.isStatic() && m_context.isProtected() && m_context.isSecure())
            {
               Metaclass metaclass = event.getMetaclass();
               Argument result = event.getResult();

               if (result != null && !result.getType().isPrimitive())
               {
                  metaclass = (Metaclass)result.getType();
               }

               if (nAttrIndex > 0)
               {
                  Pair security = metaclass.checkReadAccess(attributes, m_context.getPrivilegeSet());
  
                  if (security != null)
                  {
                     args[nAttrIndex] = Pair.nconc(security, attributes);
                  }
               }

               metaclass.checkExpressionAccess(findArg(event, "where", args, tobj),  m_context.getPrivilegeSet());
               metaclass.checkOrderByAccess(findArg(event, "orderBy", args, tobj), m_context.getPrivilegeSet());
            }

            boolean bInvoked = false;
            boolean bDeleted = false;

            m_context.setUnitOfWork(uow);
            m_context.setTransient(event.isTransient(bTransient));

            if (obj instanceof Instance)
            {
               Instance instance = (Instance)obj;
               UnitOfWork instanceUOW = instance.getUnitOfWork();

               // Use the instance UoW
               if (!event.isStatic() && instanceUOW != null && instanceUOW != uow)
               {
                  m_context.setUnitOfWork(instanceUOW);
               }

               bInvoked = instance.invokeSuspendedEvent();
               bDeleted = (instance.getState() == Instance.DELETED);

               if (nAttrIndex <= 0)
               {
                  Attribute attribute = instance.getMetaclass().findAttribute("attributes");

                  if (attribute != null && !attribute.isStatic())
                  {
                     attributes = (Pair)instance.getValue(attribute.getOrdinal());
                  }
               }
            }

            if (resultArray[i << 1] == null)
            {
               if (!bDeleted)
               {
                  resultArray[i << 1] = event.invoke(args, m_context.getMachine());
               }
            }
            else
            {
               if (!bInvoked && !bDeleted)
               {
                  event.invoke(args, m_context.getMachine());
               }

               eventArray[i] = null;
            }

            resultArray[(i << 1) + 1] = attributes;
         }

         // Pre-commit all units of work in the context
         for (int i = m_context.getUnitOfWorkCount() - 1; i >= 0; i--)
         {
            UnitOfWork work = m_context.getUnitOfWork(i);

            m_context.setUnitOfWork(work);

            if (work.isTransient())
            {
               work.invokePendingEvents(true, false);
               work.computeHiddenness();
               work.accumulateChanges();
            }
            else
            {
               work.commit(false);
            }
         }

         m_context.setUnitOfWork(uow);

         // Transfer the results into the response object.
         // This is done as a separate step after the commit
         // so that the objects are in the correct state (e.g. with OIDs).
         Response response = new Response();

         identityMap = new HashTab();

         for (int i = 0; i < nInvocationCount; ++i)
         {
            Object result = resultArray[i << 1];
            Pair attributes = request.getInvocation(i).getAttributes();
            int nTF = RPCUtil.TF_HIDDEN;
           
            if (attributes == null)
            {
               attributes = (Pair)resultArray[(i << 1) + 1];

               if (attributes == EMPTY_PAIR)
               {
                  attributes = null;
                  nTF |= RPCUtil.TF_READABLE;
               }
            }
            else if (m_context.isProtected() && m_context.isSecure())
            {
               if (result instanceof Instance)
               {
                  Instance instance = (Instance)result;

                  instance.invoke("load", Pair.nconc(instance.getMetaclass()
                     .checkReadAccess(attributes, m_context.getPrivilegeSet()), attributes));
               }
               else if (result instanceof InstanceList)
               {
                  InstanceList list = (InstanceList)result;
                  Lookup classMap = new HashTab(4);

                  for (int k = 0, n = list.size(); k < n; ++k)
                  {
                     Instance instance = list.getInstance(k);
                     Metaclass metaclass = instance.getMetaclass();
                     Pair all = (Pair)classMap.get(metaclass);

                     if (all == null)
                     {
                        all = Pair.nconc(metaclass.checkReadAccess(
                           attributes, m_context.getPrivilegeSet()), attributes);

                        classMap.put(metaclass, all);
                     }

                     instance.invoke("load", all);
                  }
               }

               nTF |= RPCUtil.TF_READABLE;
            }

            response.addResult(RPCUtil.transfer(result, attributes,
               (eventArray[i] == null) ? diffMap : null, identityMap, nTF));
         }

         // Apply the change filters
         for (int i = 0; i < nFilterCount; ++i)
         {
            TransferObject tobj = request.getFilter(i);
            Metaclass metaclass = metadata.getMetaclass(tobj.getClassName());
            Lookup map = m_context.getClassChangeMap(metaclass);
            Object instances = tobj.findValue("instances");
            Pair attributes = (Pair)tobj.findValue("attributes");
            Pair all = attributes;
            List eventList = new ArrayList();

            if (m_context.isProtected() && m_context.isSecure())
            {
               all = Pair.nconc(metaclass.checkReadAccess(attributes, m_context.getPrivilegeSet()), attributes);
            }

            if (instances != null)
            {
               List instanceList = (List)instances;
               int nCount = instanceList.size();

               for (int k = 0; k < nCount; ++k)
               {
                  Instance instance = (Instance)instanceList.get(k);
                  Object state = map.get(instance);

                  if (state != null)
                  {
                     instance.invoke("load", all);
                     eventList.add(transferEvent(instance, ((Integer)state).byteValue(),
                        attributes, diffMap, identityMap));
                  }
                  else
                  {
                     eventList.add(null);
                  }
               }
            }
            else
            {
               for (Lookup.Iterator itr = map.iterator(); itr.hasNext();)
               {
                  Instance instance = (Instance)itr.next();

                  instance.invoke("load", all);

                  TransferObject obj = transferEvent(instance, ((Integer)itr.getValue()).byteValue(),
                     attributes, diffMap, identityMap);

                  if (obj != null)
                  {
                     eventList.add(obj);
                  }
               }
            }

            response.addEvent(eventList);
         }

         GenericServer.auditResponse(identityMap, m_context);

         if (m_bMaster)
         {
            m_context.complete(true);
         }

         if (s_logger.isDumpEnabled() && !bStealth)
         {
            s_logger.dump(response);
         }

         TransferObject corellator = request.getCorrelator();

         if (corellator != null)
         {
            s_logger.debug("Invoking the request correlator");

            Request coreq = new Request();

            if (corellator.findValue("response", Undefined.VALUE) == null)
            {
               corellator.setValue("response", response);
            }

            if (corellator.findValue("results", Undefined.VALUE) == null)
            {
               int nCount = response.getResultCount();
               Object[] results = new Object[nCount];

               for (int i = 0; i < nCount; ++i)
               {
                  results[i] = response.getResult(i);
               }

               corellator.setValue("results", results);
            }

            coreq.setLocale(request.getLocale());
            coreq.setTimeZone(request.getTimeZone());
            coreq.setCommit(request.isCommit());
            coreq.addInvocation(corellator);

            invoke(coreq);

            s_logger.debug("Completed the correlator invocation");
         }

         if (m_interceptor != null && m_interceptor.isEnabled(m_context))
         {
            m_interceptor.interceptResponse(response);
         }

         return response;
      }
      catch (Throwable t)
      {
         int nLevel = Logger.DEBUG;

         if (RPCUtil.isSystem(t) && !s_logger.isDumpEnabled())
         {
            s_logger.error(request);
            nLevel = Logger.ERROR;
         }

         s_logger.log(nLevel, "Server invocation exception", t);

         if (t instanceof ErrorLocationHolder)
         {
            completeErrorLocation((ErrorLocationHolder)t, argArray);
         }

         if (m_bMaster)
         {
            m_context.complete(false);
         }

         throw ObjUtil.rethrow(t);
      }
      finally
      {
         m_context.setTransient(bTransientSaved);
         m_context.setAudited(bAuditedSaved);
      }
   }

   /**
    * Finds an argument value.
    * @param event The event object.
    * @param sName The argument name.
    * @param args The argument array. The instance is at index 0.
    * @param tobj The transfer object.
    */
   protected static Object findArg(Event event, String sName, Object[] args, TransferObject tobj)
   {
      Argument arg = (event.getActionCount() == 0) ? null : event.findArgument(sName);

      if (arg != null)
      {
         return args[arg.getOrdinal() + 1];
      }

      return tobj.findValue(sName);
   }

   /**
    * Audits request objects.
    * @param identityMap Map of transfer object to instance.
    * @param eventMap Map of transfer object to event. Can be null.
    * @param context The invocation context.
    */
   public static void auditRequest(Lookup identityMap, Lookup eventMap, InvocationContext context)
   {
      for (Lookup.Iterator itr = identityMap.iterator(); itr.hasNext();)
      {
         if (itr.next() instanceof TransferObject)
         {
            TransferObject tobj = (TransferObject)itr.getKey();
            Instance instance = (Instance)itr.getValue();
            Event pendingEvent = null;
            Event event = null;

            if (instance != null && instance.isEventPending())
            {
               pendingEvent = (Event)instance.getMetaclass().getSelector(instance.getPendingEventName()).getMember(0);
            }

            if (eventMap != null)
            {
               event = (Event)eventMap.get(tobj);
            }
            else if (instance != null && tobj.getEventName() != null)
            {
               event = instance.getMetaclass().findEvent(tobj.getEventName(), 0);
            }

            if (pendingEvent != null && pendingEvent != event)
            {
               RPCUtil.audit(instance, tobj, pendingEvent, context);
            }

            if (event != null)
            {
               RPCUtil.audit(instance, tobj, event, context);
            }
         }
      }
   }

   /**
    * Audits response objects.
    * @param identityMap Map of instance to transfer object.
    * @param context The invocation context.
    */
   public static void auditResponse(Lookup identityMap, InvocationContext context)
   {
      for (Lookup.Iterator itr = identityMap.iterator(); itr.hasNext();)
      {
         if (itr.next() instanceof Instance)
         {
            Instance instance = (Instance)itr.getKey();
            TransferObject tobj = (TransferObject)itr.getValue();
  
            RPCUtil.audit(instance, tobj, (Event)instance.getMetaclass().getSelector("read").getMember(6), context);
         }
      }
   }

   /**
    * Creates a transfer object event for a given instance.
    * @param instance The instance for which to create the event.
    * @param nState The instance state, one of the Instance.* constants.
    * @param attributes The attribute list to transfer.
    * @param diffMap Map of instances to original transfer objects. If non-null, only the modified values are transferred.
    * @param identityMap Map for tracking object identity.
    * @return The converted object.
    */
   private static TransferObject transferEvent(Instance instance, byte nState,
      Pair attributes, Lookup diffMap, Lookup identityMap)
   {
      TransferObject tobj = RPCUtil.transfer(instance,
         (nState == Instance.DELETED) ? null : attributes, diffMap, identityMap,
            RPCUtil.TF_READABLE | RPCUtil.TF_HIDDEN);

      if (tobj != null)
      {
         if (tobj.getEventName() == null)
         {
            switch (nState)
            {
               case Instance.CLEAN:
                  break;

               case Instance.NEW:
                  tobj.setEventName("create");
                  break;

               case Instance.DIRTY:
                  tobj.setEventName("update");
                  break;

               case Instance.DELETED:
                  tobj.setEventName("delete");
                  break;

               default:
                  throw new IllegalStateException();
            }
         }
      }
      else if (instance != null && (instance.isHidden() || diffMap != null && diffMap.contains(instance)))
      {
         // Visibility change
         tobj = new TransferObject(instance.getMetaclass().getName(), 0);
         tobj.setOID(instance.getOID());
         tobj.setEventName("delete");
         identityMap.put(instance, tobj);
      }

      return tobj;
   }

   /**
    * Completes the ordinal numbers in error location holders.
    */
   private static void completeErrorLocation(ErrorLocationHolder e, Object[][] argArray)
   {
      OIDHolder holder = e.getOIDHolder();
     
      if (holder != null)
      {
         for (int i = 0, n = argArray.length; i != n; ++i)
         {
            Object[] invArray = argArray[i];
           
            if (invArray != null && invArray[0] == holder)
            {
               e.setOrdinal(i);

               break;
            }
         }
      }
     
      for (Iterator itr = e.getAttributeIterator(); itr.hasNext();)
      {
         Throwable t = e.findException((String)itr.next());
        
         if (t instanceof ErrorLocationHolder && t != e)
         {
            completeErrorLocation(((ErrorLocationHolder)t), argArray);
         }
      }
     
      if (e instanceof ExceptionHolder)
      {
         for (Iterator itr = ((ExceptionHolder)e).getExceptionIterator(); itr.hasNext();)
         {
            Throwable t = (Throwable)itr.next();
           
            if (t instanceof ErrorLocationHolder)
            {
               completeErrorLocation(((ErrorLocationHolder)t), argArray);
            }
         }
      }
   }
}
TOP

Related Classes of nexj.core.rpc.GenericServer

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.