/*
 * 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.qpid.server.security.group;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.qpid.server.util.ServerScopedRuntimeException;

/**
 * A group database that reads/writes the following file format:
 *
 * group1.users=user1,user2
 * group2.users=user2,user3
 */
public class FileGroupDatabase implements GroupDatabase
{
    private static final Logger LOGGER = Logger.getLogger(FileGroupDatabase.class);

    private Map<String, Set<String>> _groupToUserMap = new ConcurrentHashMap<String, Set<String>>();
    private Map<String, Set<String>> _userToGroupMap = new ConcurrentHashMap<String, Set<String>>();
    private String _groupFile;

    @Override
    public Set<String> getAllGroups()
    {
        return Collections.unmodifiableSet(_groupToUserMap.keySet());
    }

    public synchronized void setGroupFile(String groupFile) throws IOException
    {
        File file = new File(groupFile);

        if (!file.canRead())
        {
            throw new FileNotFoundException(groupFile
                    + " cannot be found or is not readable");
        }

        readGroupFile(groupFile);
    }

    @Override
    public Set<String> getUsersInGroup(String group)
    {
        if (group == null)
        {
            LOGGER.warn("Requested user set for null group. Returning empty set.");
            return Collections.emptySet();
        }

        Set<String> set = _groupToUserMap.get(group);
        if (set == null)
        {
            return Collections.emptySet();
        }
        else
        {
            return Collections.unmodifiableSet(set);
        }
    }

    @Override
    public synchronized void addUserToGroup(String user, String group)
    {
        Set<String> users = _groupToUserMap.get(group);
        if (users == null)
        {
            throw new IllegalArgumentException("Group " + group + " does not exist so could not add " + user + " to it");
        }

        users.add(user);

        Set<String> groups = _userToGroupMap.get(user);
        if (groups == null)
        {
            groups = new ConcurrentSkipListSet<String>();
            _userToGroupMap.put(user, groups);
        }
        groups.add(group);

        update();
    }

    @Override
    public synchronized void removeUserFromGroup(String user, String group)
    {
        Set<String> users = _groupToUserMap.get(group);
        if (users == null)
        {
            throw new IllegalArgumentException("Group " + group + " does not exist so could not remove " + user + " from it");
        }

        users.remove(user);

        Set<String> groups = _userToGroupMap.get(user);
        if (groups != null)
        {
            groups.remove(group);
        }

        update();
    }

    @Override
    public Set<String> getGroupsForUser(String user)
    {
        if(user == null)
        {
            LOGGER.warn("Requested group set for null user. Returning empty set.");
            return Collections.emptySet();
        }

        Set<String> groups = _userToGroupMap.get(user);
        if (groups == null)
        {
            return Collections.emptySet();
        }
        else
        {
            return Collections.unmodifiableSet(groups);
        }
    }

    @Override
    public synchronized void createGroup(String group)
    {
        Set<String> users = new ConcurrentSkipListSet<String>();
        _groupToUserMap.put(group, users);

        update();
    }

    @Override
    public synchronized void removeGroup(String group)
    {
        _groupToUserMap.remove(group);
        for (Set<String> groupsForUser : _userToGroupMap.values())
        {
            groupsForUser.remove(group);
        }

        update();
    }

    private synchronized void update()
    {
        if (_groupFile != null)
        {
            try
            {
                writeGroupFile(_groupFile);
            }
            catch (IOException e)
            {
                throw new ServerScopedRuntimeException("Unable to persist change to file " + _groupFile);
            }
        }
    }

    private synchronized void readGroupFile(String groupFile) throws IOException
    {
        _groupFile = groupFile;
        _groupToUserMap.clear();
        _userToGroupMap.clear();
        Properties propertiesFile = new Properties();
        FileInputStream fileInputStream = new FileInputStream(groupFile);
        try
        {
            propertiesFile.load(fileInputStream);
        }
        finally
        {
            if(fileInputStream != null)
            {
                fileInputStream.close();
            }
        }

        for (String propertyName : propertiesFile.stringPropertyNames())
        {
            validatePropertyNameIsGroupName(propertyName);

            String groupName = propertyName.replaceAll("\\.users$", "");
            String userString = propertiesFile.getProperty(propertyName);

            final Set<String> userSet = buildUserSetFromCommaSeparateValue(userString);

            _groupToUserMap.put(groupName, userSet);

            for (String userName : userSet)
            {
                Set<String> groupsForThisUser = _userToGroupMap.get(userName);

                if (groupsForThisUser == null)
                {
                    groupsForThisUser = new ConcurrentSkipListSet<String>();
                    _userToGroupMap.put(userName, groupsForThisUser);
                }

                groupsForThisUser.add(groupName);
            }
        }
    }

    private synchronized void writeGroupFile(String groupFile) throws IOException
    {
        Properties propertiesFile = new Properties();

        for (String group : _groupToUserMap.keySet())
        {
            Set<String> users = _groupToUserMap.get(group);
            String userList = StringUtils.join(users, ",");

            propertiesFile.setProperty(group + ".users", userList);
        }

        String comment = "Written " + new Date();
        FileOutputStream fileOutputStream = new FileOutputStream(groupFile);
        try
        {
            propertiesFile.store(fileOutputStream, comment);
        }
        finally
        {
            if(fileOutputStream != null)
            {
                fileOutputStream.close();
            }
        }
    }

    private void validatePropertyNameIsGroupName(String propertyName)
    {
        if (!propertyName.endsWith(".users"))
        {
            throw new IllegalArgumentException(
                    "Invalid definition with name '"
                            + propertyName
                            + "'. Group definitions must end with suffix '.users'");
        }
    }

    private ConcurrentSkipListSet<String> buildUserSetFromCommaSeparateValue(String userString)
    {
        String[] users = userString.split(",");
        final ConcurrentSkipListSet<String> userSet = new ConcurrentSkipListSet<String>();
        for (String user : users)
        {
            final String trimmed = user.trim();
            if (!trimmed.isEmpty())
            {
                userSet.add(trimmed);
            }
        }
        return userSet;
    }

}
