/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.polygene.library.shiro.concerns;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.PermissionUtils;
import org.apache.polygene.api.common.AppliesTo;
import org.apache.polygene.api.common.Optional;
import org.apache.polygene.api.concern.ConcernOf;
import org.apache.polygene.api.injection.scope.Invocation;
import org.apache.polygene.library.shiro.Shiro;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@AppliesTo( { RequiresAuthentication.class,
              RequiresGuest.class,
              RequiresPermissions.class,
              RequiresRoles.class,
              RequiresUser.class } )
public class SecurityConcern
        extends ConcernOf<InvocationHandler>
        implements InvocationHandler
{

    private static final Logger LOGGER = LoggerFactory.getLogger( Shiro.LOGGER_NAME );

    @Optional
    @Invocation
    private RequiresAuthentication requiresAuthentication;

    @Optional
    @Invocation
    private RequiresGuest requiresGuest;

    @Optional
    @Invocation
    private RequiresPermissions requiresPermissions;

    @Optional
    @Invocation
    private RequiresRoles requiresRoles;

    @Optional
    @Invocation
    private RequiresUser requiresUser;

    @Override
    public Object invoke( Object proxy, Method method, Object[] args )
            throws Throwable
    {
        Subject subject = SecurityUtils.getSubject();

        handleRequiresGuest( subject );
        handleRequiresUser( subject );
        handleRequiresAuthentication( subject );
        handleRequiresRoles( subject );
        handleRequiresPermissions( subject );

        return next.invoke( proxy, method, args );
    }

    private void handleRequiresGuest( Subject subject )
    {
        if ( requiresGuest != null ) {
            LOGGER.debug( "SecurityConcern::RequiresGuest" );
            if ( subject.getPrincipal() != null ) {
                throw new UnauthenticatedException(
                        "Attempting to perform a guest-only operation. The current Subject is "
                        + "not a guest (they have been authenticated or remembered from a previous login).  Access "
                        + "denied." );

            }
        } else {
            LOGGER.debug( "SecurityConcern::RequiresGuest: not concerned" );
        }
    }

    private void handleRequiresUser( Subject subject )
    {
        if ( requiresUser != null ) {
            LOGGER.debug( "SecurityConcern::RequiresUser" );
            if ( subject.getPrincipal() == null ) {
                throw new UnauthenticatedException(
                        "Attempting to perform a user-only operation. The current Subject is "
                        + "not a user (they haven't been authenticated or remembered from a previous login).  "
                        + "Access denied." );
            }
        } else {
            LOGGER.debug( "SecurityConcern::RequiresUser: not concerned" );
        }
    }

    private void handleRequiresAuthentication( Subject subject )
    {
        if ( requiresAuthentication != null ) {
            LOGGER.debug( "SecurityConcern::RequiresAuthentication" );
            if ( !subject.isAuthenticated() ) {
                throw new UnauthenticatedException( "The current Subject is not authenticated.  Access denied." );
            }
        } else {
            LOGGER.debug( "SecurityConcern::RequiresAuthentication: not concerned" );
        }
    }

    private void handleRequiresRoles( Subject subject )
    {
        if ( requiresRoles != null ) {
            LOGGER.debug( "SecurityConcern::RequiresRoles" );
            String roleId = requiresRoles.value();
            String[] roles = roleId.split( "," );
            if ( roles.length == 1 ) {
                if ( !subject.hasRole( roles[ 0] ) ) {
                    String msg = "Calling Subject does not have required role [" + roleId + "].  "
                                 + "MethodInvocation denied.";
                    throw new UnauthorizedException( msg );
                }
            } else {
                Set<String> rolesSet = new LinkedHashSet<String>( Arrays.asList( roles ) );
                if ( !subject.hasAllRoles( rolesSet ) ) {
                    String msg = "Calling Subject does not have required roles [" + roleId + "].  "
                                 + "MethodInvocation denied.";
                    throw new UnauthorizedException( msg );
                }
            }
        } else {
            LOGGER.debug( "SecurityConcern::RequiresRoles: not concerned" );
        }

    }

    private void handleRequiresPermissions( Subject subject )
    {
        if ( requiresPermissions != null ) {
            LOGGER.debug( "SecurityConcern::RequiresPermissions" );
            String permsString = requiresPermissions.value();
            Set<String> permissions = PermissionUtils.toPermissionStrings( permsString );
            if ( permissions.size() == 1 ) {
                if ( !subject.isPermitted( permissions.iterator().next() ) ) {
                    String msg = "Calling Subject does not have required permission [" + permsString + "].  "
                                 + "Method invocation denied.";
                    throw new UnauthorizedException( msg );
                }
            } else {
                String[] permStrings = new String[ permissions.size() ];
                permStrings = permissions.toArray( permStrings );
                if ( !subject.isPermittedAll( permStrings ) ) {
                    String msg = "Calling Subject does not have required permissions [" + permsString + "].  "
                                 + "Method invocation denied.";
                    throw new UnauthorizedException( msg );
                }

            }
        } else {
            LOGGER.debug( "SecurityConcern::RequiresPermissions: not concerned" );
        }

    }

}
