# EntityResource.java

    /*
     * Copyright 2008 Niclas Hedhman. 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.qi4j.library.rest.admin;

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.io.StringReader;
    import java.io.Writer;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.Date;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    import org.openrdf.model.Statement;
    import org.openrdf.rio.RDFHandlerException;
    import org.qi4j.api.association.AssociationDescriptor;
    import org.qi4j.api.entity.EntityDescriptor;
    import org.qi4j.api.entity.EntityReference;
    import org.qi4j.api.injection.scope.Service;
    import org.qi4j.api.injection.scope.Structure;
    import org.qi4j.api.injection.scope.Uses;
    import org.qi4j.api.property.PropertyDescriptor;
    import org.qi4j.api.usecase.Usecase;
    import org.qi4j.api.usecase.UsecaseBuilder;
    import org.qi4j.api.value.ValueSerialization;
    import org.qi4j.api.value.ValueSerializationException;
    import org.qi4j.functional.Iterables;
    import org.qi4j.library.rdf.entity.EntityStateSerializer;
    import org.qi4j.library.rdf.serializer.RdfXmlSerializer;
    import org.qi4j.spi.entity.EntityState;
    import org.qi4j.spi.entity.ManyAssociationState;
    import org.qi4j.spi.entity.NamedAssociationState;
    import org.qi4j.spi.entitystore.ConcurrentEntityStateModificationException;
    import org.qi4j.spi.entitystore.EntityNotFoundException;
    import org.qi4j.spi.entitystore.EntityStore;
    import org.qi4j.spi.entitystore.EntityStoreUnitOfWork;
    import org.qi4j.spi.entitystore.helpers.JSONEntityState;
    import org.qi4j.spi.module.ModuleSpi;
    import org.restlet.data.CharacterSet;
    import org.restlet.data.Form;
    import org.restlet.data.Language;
    import org.restlet.data.MediaType;
    import org.restlet.data.Method;
    import org.restlet.data.Status;
    import org.restlet.data.Tag;
    import org.restlet.representation.EmptyRepresentation;
    import org.restlet.representation.Representation;
    import org.restlet.representation.StringRepresentation;
    import org.restlet.representation.Variant;
    import org.restlet.representation.WriterRepresentation;
    import org.restlet.resource.ResourceException;
    import org.restlet.resource.ServerResource;

    public class EntityResource
        extends ServerResource
    {

        @Service
        private EntityStore entityStore;

        @Service
        private ValueSerialization valueSerialization;

        @Structure
        private ModuleSpi module;

        @Uses
        private EntityStateSerializer entitySerializer;
        private String identity;

        public EntityResource()
        {
            // Define the supported variant.
            getVariants().addAll( Arrays.asList(
                new Variant( MediaType.TEXT_HTML ),
                new Variant( MediaType.APPLICATION_RDF_XML ),
                new Variant( MediaType.APPLICATION_JSON ) ) );
            setNegotiated( true );
            setAllowedMethods( Collections.singleton( Method.ALL ) );
        }

        @Override
        protected void doInit()
            throws ResourceException
        {
            // /entity/{identity}
            Map<String, Object> attributes = getRequest().getAttributes();
            identity = (String) attributes.get( "identity" );
        }

        @Override
        protected Representation delete( Variant variant )
            throws ResourceException
        {
            Usecase usecase = UsecaseBuilder.newUsecase( "Remove entity" );
            EntityStoreUnitOfWork uow = entityStore.newUnitOfWork( usecase, module, System.currentTimeMillis() );
            try
            {
                EntityReference identityRef = EntityReference.parseEntityReference( identity );
                uow.entityStateOf( module, identityRef ).remove();
                uow.applyChanges().commit();
                getResponse().setStatus( Status.SUCCESS_NO_CONTENT );
            }
            catch( EntityNotFoundException e )
            {
                uow.discard();
                getResponse().setStatus( Status.CLIENT_ERROR_NOT_FOUND );
            }

            return new EmptyRepresentation();
        }

        @Override
        protected Representation get( Variant variant )
            throws ResourceException
        {
            EntityStoreUnitOfWork uow = entityStore.newUnitOfWork( UsecaseBuilder.newUsecase( "Get entity" ),
                                                                   module,
                                                                   System.currentTimeMillis() );

            try
            {
                EntityState entityState = getEntityState( uow );

                // Check modification date
                Date lastModified = getRequest().getConditions().getModifiedSince();
                if( lastModified != null )
                {
                    if( lastModified.getTime() / 1000 == entityState.lastModified() / 1000 )
                    {
                        throw new ResourceException( Status.REDIRECTION_NOT_MODIFIED );
                    }
                }

                // Generate the right representation according to its media type.
                if( MediaType.APPLICATION_RDF_XML.equals( variant.getMediaType() ) )
                {
                    return entityHeaders( representRdfXml( entityState ), entityState );
                }
                else if( MediaType.TEXT_HTML.equals( variant.getMediaType() ) )
                {
                    return entityHeaders( representHtml( entityState ), entityState );
                }
                else if( MediaType.APPLICATION_JSON.equals( variant.getMediaType() ) )
                {
                    return entityHeaders( representJson( entityState ), entityState );
                }
            }
            catch( ResourceException ex )
            {
                uow.discard();
                throw ex;
            }

            throw new ResourceException( Status.CLIENT_ERROR_NOT_FOUND );
        }

        private EntityState getEntityState( EntityStoreUnitOfWork unitOfWork )
            throws ResourceException
        {
            EntityState entityState;
            try
            {
                EntityReference entityReference = EntityReference.parseEntityReference( identity );
                entityState = unitOfWork.entityStateOf( module, entityReference );
            }
            catch( EntityNotFoundException e )
            {
                throw new ResourceException( Status.CLIENT_ERROR_NOT_FOUND );
            }
            return entityState;
        }

        private Representation entityHeaders( Representation representation, EntityState entityState )
        {
            representation.setModificationDate( new Date( entityState.lastModified() ) );
            representation.setTag( new Tag( "" + entityState.version() ) );
            representation.setCharacterSet( CharacterSet.UTF_8 );
            representation.setLanguages( Collections.singletonList( Language.ENGLISH ) );

            return representation;
        }

        private Representation representHtml( final EntityState entity )
        {
            return new WriterRepresentation( MediaType.TEXT_HTML )
            {
                @Override
                public void write( Writer writer )
                    throws IOException
                {
                    PrintWriter out = new PrintWriter( writer );
                    out.println( "<html><head><title>" + entity.identity() + "</title>"
                                 + "<link rel=\"alternate\" type=\"application/rdf+xml\" "
                                 + "href=\"" + entity.identity() + ".rdf\"/></head><body>" );
                    out.println( "<h1>" + entity.identity() + "</h1>" );

                    out.println( "<form method=\"post\" action=\"" + getRequest().getResourceRef().getPath() + "\">\n" );
                    out.println( "<fieldset><legend>Properties</legend>\n<table>" );

                    final EntityDescriptor descriptor = entity.entityDescriptor();

                    for( PropertyDescriptor persistentProperty : descriptor.state().properties() )
                    {
                        Object value = entity.propertyValueOf( persistentProperty.qualifiedName() );
                        out.println( "<tr><td>"
                                     + "<label for=\"" + persistentProperty.qualifiedName() + "\" >"
                                     + persistentProperty.qualifiedName().name()
                                     + "</label></td>\n"
                                     + "<td><input "
                                     + "size=\"80\" "
                                     + "type=\"text\" "
                                     + ( persistentProperty.isImmutable() ? "readonly=\"true\" " : "" )
                                     + "name=\"" + persistentProperty.qualifiedName() + "\" "
                                     + "value=\"" + ( value == null ? "" : valueSerialization.serialize( value ) )
                                     + "\"/></td></tr>" );
                    }
                    out.println( "</table></fieldset>\n" );

                    out.println( "<fieldset><legend>Associations</legend>\n<table>" );
                    for( AssociationDescriptor associationType : descriptor.state().associations() )
                    {
                        Object value = entity.associationValueOf( associationType.qualifiedName() );
                        if( value == null )
                        {
                            value = "";
                        }
                        out.println( "<tr><td>"
                                     + "<label for=\"" + associationType.qualifiedName() + "\" >"
                                     + associationType.qualifiedName().name()
                                     + "</label></td>\n"
                                     + "<td><input "
                                     + "type=\"text\" "
                                     + "size=\"80\" "
                                     + "name=\"" + associationType.qualifiedName() + "\" "
                                     + "value=\"" + value + "\"/></td></tr>" );
                    }
                    out.println( "</table></fieldset>\n" );

                    out.println( "<fieldset><legend>ManyAssociations</legend>\n<table>" );
                    for( AssociationDescriptor associationType : descriptor.state().manyAssociations() )
                    {
                        ManyAssociationState identities = entity.manyAssociationValueOf( associationType.qualifiedName() );
                        String value = "";
                        for( EntityReference identity : identities )
                        {
                            value += identity.identity() + "\n";
                        }

                        out.println( "<tr><td>"
                                     + "<label for=\"" + associationType.qualifiedName() + "\" >"
                                     + associationType.qualifiedName().name()
                                     + "</label></td>\n"
                                     + "<td><textarea "
                                     + "rows=\"10\" "
                                     + "cols=\"80\" "
                                     + "name=\"" + associationType.qualifiedName() + "\" >"
                                     + value
                                     + "</textarea></td></tr>" );
                    }
                    out.println( "</table></fieldset>\n" );

                    out.println( "<fieldset><legend>NamedAssociations</legend>\n<table>" );
                    for( AssociationDescriptor associationType : descriptor.state().namedAssociations() )
                    {
                        NamedAssociationState identities = entity.namedAssociationValueOf( associationType.qualifiedName() );
                        String value = "";
                        for( String name : identities )
                        {
                            value += name + "\n" + identities.get( name ).identity() + "\n";
                        }

                        out.println( "<tr><td>"
                                     + "<label for=\"" + associationType.qualifiedName() + "\" >"
                                     + associationType.qualifiedName().name()
                                     + "</label></td>\n"
                                     + "<td><textarea "
                                     + "rows=\"10\" "
                                     + "cols=\"80\" "
                                     + "name=\"" + associationType.qualifiedName() + "\" >"
                                     + value
                                     + "</textarea></td></tr>" );
                    }
                    out.println( "</table></fieldset>\n" );

                    out.println( "<input type=\"submit\" value=\"Update\"/></form>\n" );

                    out.println( "</body></html>\n" );
                }
            };
        }

        private Representation representJson( EntityState entityState )
        {
            if( entityState instanceof JSONEntityState )
            {
                JSONEntityState jsonState = (JSONEntityState) entityState;
                return new StringRepresentation( jsonState.state().toString(), MediaType.APPLICATION_JSON );
            }
            else
            {
                throw new ResourceException( Status.CLIENT_ERROR_NOT_ACCEPTABLE );
            }
        }

        private Representation representRdfXml( final EntityState entity )
            throws ResourceException
        {
            Representation representation = new WriterRepresentation( MediaType.APPLICATION_RDF_XML )
            {
                @Override
                public void write( Writer writer )
                    throws IOException
                {
                    try
                    {
                        Iterable<Statement> statements = entitySerializer.serialize( entity );
                        new RdfXmlSerializer().serialize( statements, writer );
                    }
                    catch( RDFHandlerException e )
                    {
                        throw new IOException( e );
                    }
                }
            };
            representation.setCharacterSet( CharacterSet.UTF_8 );
            return representation;
        }

        @Override
        protected Representation put( Representation representation, Variant variant )
            throws ResourceException
        {
            return post( representation, variant );
        }

        @Override
        public Representation post( Representation entityRepresentation, Variant variant )
            throws ResourceException
        {
            Usecase usecase = UsecaseBuilder.newUsecase( "Update entity" );
            EntityStoreUnitOfWork unitOfWork = entityStore.newUnitOfWork( usecase, module, System.currentTimeMillis() );
            EntityState entity = getEntityState( unitOfWork );

            Form form = new Form( entityRepresentation );

            try
            {
                final EntityDescriptor descriptor = entity.entityDescriptor();

                // Parse JSON into properties
                for( PropertyDescriptor persistentProperty : descriptor.state().properties() )
                {
                    if( !persistentProperty.isImmutable() )
                    {
                        String formValue = form.getFirstValue( persistentProperty.qualifiedName().name(), null );
                        if( formValue == null )
                        {
                            entity.setPropertyValue( persistentProperty.qualifiedName(), null );
                        }
                        else
                        {
                            entity.setPropertyValue(
                                persistentProperty.qualifiedName(),
                                valueSerialization.deserialize( persistentProperty.valueType(), formValue ) );
                        }
                    }
                }

                for( AssociationDescriptor associationType : descriptor.state().associations() )
                {
                    String newStringAssociation = form.getFirstValue( associationType.qualifiedName().name() );
                    if( newStringAssociation == null || newStringAssociation.isEmpty() )
                    {
                        entity.setAssociationValue( associationType.qualifiedName(), null );
                    }
                    else
                    {
                        entity.setAssociationValue( associationType.qualifiedName(),
                                                    EntityReference.parseEntityReference( newStringAssociation ) );
                    }
                }
                for( AssociationDescriptor associationType : descriptor.state().manyAssociations() )
                {
                    String newStringAssociation = form.getFirstValue( associationType.qualifiedName().name() );
                    ManyAssociationState manyAssociation = entity.manyAssociationValueOf( associationType.qualifiedName() );
                    if( newStringAssociation == null )
                    {
                        // Remove "left-overs"
                        for( EntityReference entityReference : manyAssociation )
                        {
                            manyAssociation.remove( entityReference );
                        }
                        continue;
                    }

                    BufferedReader bufferedReader = new BufferedReader( new StringReader( newStringAssociation ) );
                    String identity;

                    try
                    {
                        // Synchronize old and new association
                        int index = 0;
                        while( ( identity = bufferedReader.readLine() ) != null )
                        {
                            EntityReference reference = new EntityReference( identity );

                            if( manyAssociation.count() < index && manyAssociation.get( index ).equals( reference ) )
                            {
                                continue;
                            }

                            try
                            {
                                unitOfWork.entityStateOf( module, reference );

                                manyAssociation.remove( reference );
                                manyAssociation.add( index++, reference );
                            }
                            catch( EntityNotFoundException e )
                            {
                                // Ignore this entity - doesn't exist
                            }
                        }

                        // Remove "left-overs"
                        while( manyAssociation.count() > index )
                        {
                            manyAssociation.remove( manyAssociation.get( index ) );
                        }
                    }
                    catch( IOException e )
                    {
                        // Ignore
                    }
                }
                for( AssociationDescriptor associationType : descriptor.state().namedAssociations() )
                {
                    String newStringAssociation = form.getFirstValue( associationType.qualifiedName().name() );
                    NamedAssociationState namedAssociation = entity.namedAssociationValueOf( associationType.qualifiedName() );
                    if( newStringAssociation == null )
                    {
                        // Remove "left-overs"
                        for( String name : namedAssociation )
                        {
                            namedAssociation.remove( name );
                        }
                        continue;
                    }
                    Set<String> names = new HashSet<>();
                    BufferedReader bufferedReader = new BufferedReader( new StringReader( newStringAssociation ) );
                    String line;
                    try
                    {
                        while( ( line = bufferedReader.readLine() ) != null )
                        {
                            String name = line;
                            line = bufferedReader.readLine();
                            if( line == null )
                            {
                                break;
                            }
                            String identity = line;
                            EntityReference reference = new EntityReference( identity );
                            try
                            {
                                unitOfWork.entityStateOf( module, reference );

                                namedAssociation.remove( name );
                                namedAssociation.put( name, reference );

                                names.add( name );
                            }
                            catch( EntityNotFoundException e )
                            {
                                // Ignore this entity - doesn't exist
                            }
                        }

                        // Remove "left-overs"
                        for( String assocName : Iterables.toList( namedAssociation ) )
                        {
                            if( !names.contains( assocName ) )
                            {
                                namedAssociation.remove( assocName );
                            }
                        }
                    }
                    catch( IOException e )
                    {
                        // Ignore
                    }
                }
            }
            catch( ValueSerializationException | IllegalArgumentException e )
            {
                throw new ResourceException( Status.SERVER_ERROR_INTERNAL, e );
            }

            try
            {
                unitOfWork.applyChanges().commit();
            }
            catch( ConcurrentEntityStateModificationException e )
            {
                throw new ResourceException( Status.CLIENT_ERROR_CONFLICT );
            }
            catch( EntityNotFoundException e )
            {
                throw new ResourceException( Status.CLIENT_ERROR_GONE );
            }

            getResponse().setStatus( Status.SUCCESS_RESET_CONTENT );

            return new EmptyRepresentation();
        }
    }