All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nakedobjects.plugins.remoting.server.ServerFacadeImpl Maven / Gradle / Ivy

package org.nakedobjects.plugins.remoting.server;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import org.apache.log4j.Logger;
import org.nakedobjects.applib.Identifier;
import org.nakedobjects.metamodel.adapter.NakedObject;
import org.nakedobjects.metamodel.adapter.ResolveState;
import org.nakedobjects.metamodel.adapter.oid.Oid;
import org.nakedobjects.metamodel.adapter.version.Version;
import org.nakedobjects.metamodel.authentication.AuthenticationSession;
import org.nakedobjects.metamodel.commons.ensure.Assert;
import org.nakedobjects.metamodel.commons.exceptions.NakedObjectException;
import org.nakedobjects.metamodel.commons.exceptions.UnexpectedCallException;
import org.nakedobjects.metamodel.commons.exceptions.UnknownTypeException;
import org.nakedobjects.metamodel.config.ConfigurationConstants;
import org.nakedobjects.metamodel.config.NakedObjectConfiguration;
import org.nakedobjects.metamodel.criteria.InstancesCriteria;
import org.nakedobjects.metamodel.facets.collections.modify.CollectionFacet;
import org.nakedobjects.metamodel.facets.object.encodeable.EncodeableFacet;
import org.nakedobjects.metamodel.spec.NakedObjectSpecification;
import org.nakedobjects.metamodel.spec.feature.NakedObjectAction;
import org.nakedobjects.metamodel.spec.feature.NakedObjectActionConstants;
import org.nakedobjects.metamodel.spec.feature.NakedObjectActionType;
import org.nakedobjects.metamodel.spec.feature.NakedObjectAssociation;
import org.nakedobjects.metamodel.spec.feature.NakedObjectMember;
import org.nakedobjects.metamodel.spec.feature.OneToManyAssociation;
import org.nakedobjects.metamodel.spec.feature.OneToOneAssociation;
import org.nakedobjects.metamodel.spec.identifier.IdentifierFactory;
import org.nakedobjects.metamodel.specloader.SpecificationLoader;
import org.nakedobjects.metamodel.specloader.internal.NakedObjectActionImpl;
import org.nakedobjects.metamodel.util.CollectionFacetUtils;
import org.nakedobjects.plugins.remoting.shared.NakedObjectsRemoteException;
import org.nakedobjects.plugins.remoting.shared.ObjectEncoder;
import org.nakedobjects.plugins.remoting.shared.ServerFacade;
import org.nakedobjects.plugins.remoting.shared.data.ClientActionResultData;
import org.nakedobjects.plugins.remoting.shared.data.CriteriaData;
import org.nakedobjects.plugins.remoting.shared.data.Data;
import org.nakedobjects.plugins.remoting.shared.data.EncodeableObjectData;
import org.nakedobjects.plugins.remoting.shared.data.IdentityData;
import org.nakedobjects.plugins.remoting.shared.data.KnownObjects;
import org.nakedobjects.plugins.remoting.shared.data.NullData;
import org.nakedobjects.plugins.remoting.shared.data.ObjectData;
import org.nakedobjects.plugins.remoting.shared.data.ReferenceData;
import org.nakedobjects.plugins.remoting.shared.data.ServerActionResultData;
import org.nakedobjects.plugins.remoting.shared.transaction.ClientTransactionEvent;
import org.nakedobjects.runtime.authentication.AuthenticationManager;
import org.nakedobjects.runtime.authentication.PasswordAuthenticationRequest;
import org.nakedobjects.runtime.context.NakedObjectsContext;
import org.nakedobjects.runtime.persistence.PersistenceConstants;
import org.nakedobjects.runtime.persistence.PersistenceSession;
import org.nakedobjects.runtime.transaction.NakedObjectTransactionManager;
import org.nakedobjects.runtime.transaction.messagebroker.MessageBroker;
import org.nakedobjects.runtime.transaction.updatenotifier.UpdateNotifier;


/**
 * previously called ServerDistribution.
 */
public class ServerFacadeImpl implements ServerFacade {
    
    private static final Logger LOG = Logger.getLogger(ServerFacadeImpl.class);
    
    private final AuthenticationManager authenticationManager;
    private ObjectEncoder encoder;
    


    public ServerFacadeImpl(final AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    ////////////////////////////////////////////////////////////////
    // init, shutdown
    ////////////////////////////////////////////////////////////////

    public void init() {}

    public void shutdown() {}


    ////////////////////////////////////////////////////////////////
    // Session
    ////////////////////////////////////////////////////////////////

    public void closeSession(final AuthenticationSession session) {
        authenticationManager.closeSession(session);
    }


    ////////////////////////////////////////////////////////////////
    // Authentication
    ////////////////////////////////////////////////////////////////

    public AuthenticationSession authenticate(final String userNameAndPassword) {
        final PasswordAuthenticationRequest request = extractAuthenticationRequest(userNameAndPassword);
        return authenticationManager.authenticate(request);
    }

    private PasswordAuthenticationRequest extractAuthenticationRequest(final String userNameAndPassword) {
        final int delimiter = userNameAndPassword.indexOf("::");
        final String username = userNameAndPassword.substring(0, delimiter);
        final String password = userNameAndPassword.substring(delimiter + 2);
        final PasswordAuthenticationRequest request = new PasswordAuthenticationRequest(username, password);
        return request;
    }

    ////////////////////////////////////////////////////////////////
    // Authorization
    ////////////////////////////////////////////////////////////////

    public boolean authoriseVisibility(final AuthenticationSession session, final String memberName) {
        return getMember(memberName).isVisible(session, null).isAllowed();
    }

    public boolean authoriseUsability(final AuthenticationSession session, final String memberName) {
        return getMember(memberName).isUsable(session, null).isAllowed();
    }

    private NakedObjectMember getMember(final String memberName) {
        final Identifier id = IdentifierFactory.fromIdentityString(memberName);
        final NakedObjectSpecification specification = getSpecificationLoader().loadSpecification(id.getClassName());
        if (id.isPropertyOrCollection()) {
            return getAssociationElseThrowException(id, specification);
        } else {
            return getActionElseThrowException(id, specification);
        }
    }

    private NakedObjectMember getActionElseThrowException(final Identifier id, final NakedObjectSpecification specification) {
        NakedObjectMember member = 
            specification.getObjectAction(
                    NakedObjectActionConstants.USER, id.getMemberName(), getMemberParameterSpecifications(id));
        if (member == null) {
            throw new NakedObjectException("No user action found for id " + id);
        }
        return member;
    }

    private NakedObjectMember getAssociationElseThrowException(final Identifier id, final NakedObjectSpecification specification) {
        NakedObjectMember member = specification.getAssociation(id.getMemberName());
        if (member == null) {
            throw new NakedObjectException("No property or collection found for id " + id);
        }
        return member;
    }

    private NakedObjectSpecification[] getMemberParameterSpecifications(final Identifier id) {
        final String[] parameters = id.getMemberParameterNames();
        final NakedObjectSpecification[] specifications = new NakedObjectSpecification[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            specifications[i] = getSpecificationLoader().loadSpecification(parameters[i]);
        }
        return specifications;
    }

    ////////////////////////////////////////////////////////////////
    // setAssociation, setValue, clearAssociation, clearValue
    ////////////////////////////////////////////////////////////////

    public ObjectData[] setAssociation(
            final AuthenticationSession session,
            final String fieldIdentifier,
            final IdentityData target,
            final IdentityData associated) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("request setAssociation " + fieldIdentifier + " on " + target + " with " + associated + " for " + session);
        }
        final NakedObject inObject = getPersistentNakedObject(session, target);
        final NakedObject associate = getPersistentNakedObject(session, associated);
        final NakedObjectAssociation association = inObject.getSpecification().getAssociation(fieldIdentifier);
        if (!association.isVisible(session, inObject).isAllowed() || 
             association.isUsable(session, inObject).isVetoed()) {
            throw new NakedObjectException("can't modify field as not visible or editable");
        }
        if (association instanceof OneToOneAssociation) {
            ((OneToOneAssociation) association).setAssociation(inObject, associate);
        } else {
            ((OneToManyAssociation) association).addElement(inObject, associate);
        }
        return getUpdates();
    }

    public ObjectData[] setValue(
            final AuthenticationSession session,
            final String fieldIdentifier,
            final IdentityData target,
            final EncodeableObjectData encodeableObjectData) {
        Assert.assertNotNull(encodeableObjectData);
        if (LOG.isDebugEnabled()) {
            LOG.debug("request setValue " + fieldIdentifier + " on " + target + " with " + encodeableObjectData + " for " + session);
        }
        final NakedObject inObject = getPersistentNakedObject(session, target);
        final OneToOneAssociation association = (OneToOneAssociation) inObject.getSpecification().getAssociation(fieldIdentifier);
        if (!association.isVisible(session, inObject).isAllowed() || association.isUsable(session, inObject).isVetoed()) {
            throw new NakedObjectException("can't modify field as not visible or editable");
        }

        final String encodedObject = encodeableObjectData.getEncodedObjectData();

        final NakedObjectSpecification specification = association.getSpecification();
        final NakedObject adapter = restoreLeafObject(encodedObject, specification);
        association.setAssociation(inObject, adapter);
        return getUpdates();
    }


    public ObjectData[] clearAssociation(
            final AuthenticationSession session,
            final String fieldIdentifier,
            final IdentityData target,
            final IdentityData associated) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("request clearAssociation " + fieldIdentifier + " on " + target + " of " + associated + " for " + session);
        }
        final NakedObject inObject = getPersistentNakedObject(session, target);
        final NakedObject associate = getPersistentNakedObject(session, associated);
        final NakedObjectSpecification specification = inObject.getSpecification();
        final NakedObjectAssociation association = specification.getAssociation(fieldIdentifier);

        if (!association.isVisible(session, inObject).isAllowed() || association.isUsable(session, inObject).isVetoed()) {
            throw new NakedObjectException("can't modify field as not visible or editable");
        }

        if (association instanceof OneToOneAssociation) {
            ((OneToOneAssociation) association).clearAssociation(inObject);
        } else {
            ((OneToManyAssociation) association).removeElement(inObject, associate);
        }
        return getUpdates();
    }

    public ObjectData[] clearValue(final AuthenticationSession session, final String fieldIdentifier, final IdentityData target) {
        LOG.debug("request clearValue " + fieldIdentifier + " on " + target + " for " + session);
        final NakedObject inObject = getPersistentNakedObject(session, target);
        final OneToOneAssociation association = (OneToOneAssociation) inObject.getSpecification().getAssociation(fieldIdentifier);

        if (!association.isVisible(session, inObject).isAllowed() || association.isUsable(session, inObject).isVetoed()) {
            throw new NakedObjectException("can't modify field as not visible or editable");
        }

        association.clearAssociation(inObject);
        return getUpdates();
    }

    private ObjectData[] convertToNakedCollection(final NakedObject instances) {
        final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(instances);
        final ObjectData[] data = new ObjectData[facet.size(instances)];
        final Enumeration elements = facet.elements(instances);
        int i = 0;
        while (elements.hasMoreElements()) {
            final NakedObject element = (NakedObject) elements.nextElement();
            data[i++] = encoder.createCompletePersistentGraph(element);
        }
        return data;
    }

    ////////////////////////////////////////////////////////////////
    // executeClientAction
    ////////////////////////////////////////////////////////////////

    public ClientActionResultData executeClientAction(final AuthenticationSession session, final ReferenceData[] data, final int[] types) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("execute client action for " + session);
            LOG.debug("start transaction");
        }
        getTransactionManager().startTransaction();
        try {
            final KnownObjects knownObjects = new KnownObjects();
            final NakedObject[] persistedObjects = new NakedObject[data.length];
            final NakedObject[] disposedObjects = new NakedObject[data.length];
            final NakedObject[] changedObjects = new NakedObject[data.length];
            for (int i = 0; i < data.length; i++) {
                NakedObject object;
                switch (types[i]) {
                case ClientTransactionEvent.ADD:
                    object = encoder.restore(data[i], knownObjects);
                    persistedObjects[i] = object;
                    if (object.getOid().isTransient()) { // confirm that the graph has not been made
                        // persistent earlier in this loop
                        LOG.debug("  makePersistent " + data[i]);
                        getPersistenceSession().makePersistent(object);
                    }
                    break;

                case ClientTransactionEvent.CHANGE:
                    final NakedObject obj = getPersistentNakedObject(data[i]);
                    obj.checkLock(data[i].getVersion());

                    object = encoder.restore(data[i], knownObjects);
                    LOG.debug("  objectChanged " + data[i]);
                    getPersistenceSession().objectChanged(object);
                    changedObjects[i] = object;
                    break;

                case ClientTransactionEvent.DELETE:
                    final NakedObject inObject = getPersistentNakedObject(data[i]);
                    inObject.checkLock(data[i].getVersion());

                    LOG.debug("  destroyObject " + data[i] + " for " + session);
                    disposedObjects[i] = inObject;
                    getPersistenceSession().destroyObject(inObject);
                    break;
                }

            }

            LOG.debug("  end transaction");
            getTransactionManager().endTransaction();

            final ReferenceData[] madePersistent = new ReferenceData[data.length];
            final Version[] changedVersion = new Version[data.length];

            for (int i = 0; i < data.length; i++) {
                switch (types[i]) {
                case ClientTransactionEvent.ADD:
                    madePersistent[i] = encoder.createIdentityData(persistedObjects[i]);
                    break;

                case ClientTransactionEvent.CHANGE:
                    changedVersion[i] = changedObjects[i].getVersion();
                    break;

                }
            }

            return encoder.createClientActionResult(madePersistent, changedVersion, getUpdates());
        } catch (final RuntimeException e) {
            LOG.info("abort transaction", e);
            getTransactionManager().abortTransaction();
            throw e;
        }
    }


    ////////////////////////////////////////////////////////////////
    // executeServerAction
    ////////////////////////////////////////////////////////////////

    public ServerActionResultData executeServerAction(
            final AuthenticationSession session,
            final String actionType,
            final String actionIdentifier,
            final ReferenceData target,
            final Data[] parameterData) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("request executeAction " + actionIdentifier + " on " + target + " for " + session);
        }

        NakedObject object;
        final KnownObjects knownObjects = new KnownObjects();
        if (target instanceof IdentityData) {
            object = getPersistentNakedObject(session, (IdentityData) target);
        } else if (target instanceof ObjectData) {
            object = encoder.restore(target, knownObjects);
        } else if (target == null) {
            object = null;
        } else {
            throw new NakedObjectException();
        }

        final NakedObjectAction action = getActionMethod(actionType, actionIdentifier, parameterData, object);
        final NakedObject[] parameters = getParameters(session, parameterData, knownObjects);

        if (action == null) {
            throw new NakedObjectsRemoteException("Could not find method " + actionIdentifier);
        }

        final NakedObject result = action.execute(object, parameters);

        ObjectData persistedTarget;
        if (target == null) {
            persistedTarget = null;
        } else if (target instanceof ObjectData) {
            persistedTarget = encoder.createMadePersistentGraph((ObjectData) target, object);
        } else {
            persistedTarget = null;
        }

        final ObjectData[] persistedParameters = new ObjectData[parameterData.length];
        for (int i = 0; i < persistedParameters.length; i++) {
            if (action.getParameters()[i].getSpecification().isObject() && parameterData[i] instanceof ObjectData) {
                persistedParameters[i] = encoder.createMadePersistentGraph((ObjectData) parameterData[i], parameters[i]);
            }
        }
        final List messages = getMessageBroker().getMessages();
        final List warnings = getMessageBroker().getWarnings();

        // TODO for efficiency, need to remove the objects in the results graph from the updates set
        return encoder.createServerActionResult(result, getUpdates(), getDisposed(), persistedTarget, persistedParameters,
                messages.toArray(new String[0]), warnings.toArray(new String[0]));
    }

    private MessageBroker getMessageBroker() {
        return NakedObjectsContext.getMessageBroker();
    }

    private NakedObjectAction getActionMethod(
            final String actionType,
            final String actionIdentifier,
            final Data[] parameterData,
            final NakedObject object) {
        final NakedObjectSpecification[] parameterSpecifiactions = new NakedObjectSpecification[parameterData.length];
        for (int i = 0; i < parameterSpecifiactions.length; i++) {
            parameterSpecifiactions[i] = getSpecification(parameterData[i].getType());
        }

        final NakedObjectActionType type = NakedObjectActionImpl.getType(actionType);

        final int pos = actionIdentifier.indexOf('#');
        final String className = actionIdentifier.substring(0, pos);
        final String methodName = actionIdentifier.substring(pos + 1);

        if (object == null) {
            throw new UnexpectedCallException("object not specified");
        }
        return object.getSpecification().getObjectAction(type, methodName, parameterSpecifiactions);
    }

    private NakedObject[] getParameters(final AuthenticationSession session, final Data[] parameterData, final KnownObjects knownObjects) {
        final NakedObject[] parameters = new NakedObject[parameterData.length];
        for (int i = 0; i < parameters.length; i++) {
            final Data data = parameterData[i];
            if (data instanceof NullData) {
                continue;
            }

            if (data instanceof IdentityData) {
                parameters[i] = getPersistentNakedObject(session, (IdentityData) data);
            } else if (data instanceof ObjectData) {
                parameters[i] = encoder.restore(data, knownObjects);
            } else if (data instanceof EncodeableObjectData) {
                final NakedObjectSpecification valueSpecification = getSpecificationLoader().loadSpecification(
                        data.getType());
                final String valueData = ((EncodeableObjectData) data).getEncodedObjectData();

                final NakedObject value = restoreLeafObject(valueData, valueSpecification);
                /*
                 * NakedValue value =
                 * NakedObjectsContext.getObjectLoader().createValueInstance(valueSpecification);
                 * value.restoreFromEncodedString(valueData);
                 */
                parameters[i] = value;
            } else {
                throw new UnknownTypeException(data);
            }
        }
        return parameters;
    }

    private ReferenceData[] getDisposed() {
    	final List list = new ArrayList();
        for(NakedObject element: getUpdateNotifier().getDisposedObjects()) {
            list.add(encoder.createIdentityData(element));
        }
        return (ReferenceData[]) list.toArray(new ReferenceData[list.size()]);
    }


    ////////////////////////////////////////////////////////////////
    // getObject, resolve
    ////////////////////////////////////////////////////////////////

    public ObjectData getObject(final AuthenticationSession session, final Oid oid, final String specificationName) {
        final NakedObjectSpecification specification = getSpecification(specificationName);
        final NakedObject object = getPersistenceSession().loadObject(oid, specification);
        return encoder.createForUpdate(object);
    }

    public Data resolveField(final AuthenticationSession session, final IdentityData target, final String fieldName) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("request resolveField " + target + "/" + fieldName + " for " + session);
        }

        final NakedObjectSpecification spec = getSpecification(target.getType());
        final NakedObjectAssociation field = spec.getAssociation(fieldName);
        // NakedObject object = NakedObjects.getObjectManager().getObject(target.getOid(), spec);
        final NakedObject object = NakedObjectsContext.getPersistenceSession().recreateAdapter(target.getOid(), spec);
        getPersistenceSession().resolveField(object, field);
        return encoder.createForResolveField(object, fieldName);
    }

    public ObjectData resolveImmediately(final AuthenticationSession session, final IdentityData target) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("request resolveImmediately " + target + " for " + session);
        }

        final NakedObjectSpecification spec = getSpecification(target.getType());
        final NakedObject object = getPersistenceSession().loadObject(target.getOid(), spec);
        if (object.getResolveState().canChangeTo(ResolveState.RESOLVING)) {
            // this is need when the object store does not load the object fully in the getObject() above
            getPersistenceSession().resolveImmediately(object);
        }

        return encoder.createCompletePersistentGraph(object);
    }

    
    ////////////////////////////////////////////////////////////////
    // findInstances, hasInstances
    ////////////////////////////////////////////////////////////////

    public ObjectData[] findInstances(final AuthenticationSession session, final CriteriaData criteriaData) {
        final InstancesCriteria criteria = encoder.restoreCriteria(criteriaData);
        LOG.debug("request findInstances " + criteria + " for " + session);
        final NakedObject instances = getPersistenceSession().findInstances(criteria);
        return convertToNakedCollection(instances);
    }

    public boolean hasInstances(final AuthenticationSession session, final String objectType) {
        LOG.debug("request hasInstances of " + objectType + " for " + session);
        return getPersistenceSession().hasInstances(getSpecification(objectType));
    }


    ////////////////////////////////////////////////////////////////
    // oidForService
    ////////////////////////////////////////////////////////////////

    public IdentityData oidForService(final AuthenticationSession session, final String id) {
        final NakedObject service = getPersistenceSession().getService(id);
        if (service == null) {
            throw new NakedObjectsRemoteException("Failed to find service " + id);
        } else {
            return encoder.createIdentityData(service);
        }
    }

    
    ////////////////////////////////////////////////////////////////
    // getProperties
    ////////////////////////////////////////////////////////////////

    public Properties getProperties() {
        final Properties properties = new Properties();
        properties.put("test-client", "true");

        // pass over services
        final NakedObjectConfiguration configuration = NakedObjectsContext.getConfiguration();
        final NakedObjectConfiguration serviceProperties = configuration.getProperties(ConfigurationConstants.ROOT + "services");
        final Enumeration e = serviceProperties.propertyNames();
        while (e.hasMoreElements()) {
            final String name = (String) e.nextElement();
            properties.put(name, serviceProperties.getString(name));
        }

        // pass over OID generator
        final String oidGeneratorClass = getPersistenceSession().getOidGenerator().getClass().getName();
        if (oidGeneratorClass != null) {
            properties.put(PersistenceConstants.OID_GENERATOR_CLASS_NAME, oidGeneratorClass);
        }

        // TODO load up client properties
        return properties;
    }


    ////////////////////////////////////////////////////////////////
    // Helpers
    ////////////////////////////////////////////////////////////////

    private NakedObjectSpecification getSpecification(final String fullName) {
        return getSpecificationLoader().loadSpecification(fullName);
    }

    private NakedObject getPersistentNakedObject(final AuthenticationSession session, final IdentityData object) {
        final NakedObject obj = getPersistentNakedObject(object);
        if (LOG.isDebugEnabled()) {
            LOG.debug("get object " + object + " for " + session + " --> " + obj);
        }
        obj.checkLock(object.getVersion());
        return obj;
    }

    private NakedObject getPersistentNakedObject(final ReferenceData object) {
        final NakedObjectSpecification spec = getSpecification(object.getType());
        final NakedObject obj = getPersistenceSession().loadObject(object.getOid(), spec);
        Assert.assertNotNull(obj);
        return obj;
    }

    private NakedObject restoreLeafObject(final String encodedObject, final NakedObjectSpecification specification) {
        final EncodeableFacet encoder = specification.getFacet(EncodeableFacet.class);
        if (encoder == null) {
            throw new NakedObjectException("No encoder for " + specification.getFullName());
        }
        final NakedObject object = encoder.fromEncodedString(encodedObject);
        return object;
    }

    private ObjectData[] getUpdates() {
    	final List list = new ArrayList();
    	for(NakedObject element: getUpdateNotifier().getChangedObjects()) {
    		list.add(encoder.createForUpdate(element));	
    	}
        return (ObjectData[]) list.toArray(new ObjectData[list.size()]);
    }

    

    ////////////////////////////////////////////////////////////////
    // Dependencies (injected)
    ////////////////////////////////////////////////////////////////

    public void setEncoder(final ObjectEncoder objectEncoder) {
        this.encoder = objectEncoder;
    }


    ////////////////////////////////////////////////////////////////
    // Dependencies (from context)
    ////////////////////////////////////////////////////////////////
    
    private static SpecificationLoader getSpecificationLoader() {
        return NakedObjectsContext.getSpecificationLoader();
    }

    private static PersistenceSession getPersistenceSession() {
        return NakedObjectsContext.getPersistenceSession();
    }

    private static NakedObjectTransactionManager getTransactionManager() {
        return getPersistenceSession().getTransactionManager();
    }
    
    private static UpdateNotifier getUpdateNotifier() {
        return NakedObjectsContext.getUpdateNotifier();
    }



}
// Copyright (c) Naked Objects Group Ltd.




© 2015 - 2025 Weber Informatics LLC | Privacy Policy