package org.fenixedu.bennu.core.groups;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.fenixedu.bennu.core.annotation.GroupOperator;
import org.fenixedu.bennu.core.domain.User;
import org.fenixedu.bennu.core.domain.exceptions.AuthorizationException;
import org.fenixedu.bennu.core.domain.exceptions.BennuCoreDomainException;
import org.fenixedu.bennu.core.domain.groups.PersistentGroup;
import org.fenixedu.bennu.core.security.Authenticate;
import org.joda.time.DateTime;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
/**
* <p>
* Groups represent access groups. They are immutable objects, all operations return a new group with the result.
* </p>
*
* <p>
* Every group has a persistent counter part, to be used when domain relations are needed to persist the group information. Groups
* can be converted back and forth to {@link PersistentGroup}s using {@link #toPersistentGroup()} and
* {@link PersistentGroup#toGroup()}.
* </p>
*
* <p>
* Groups can be translated to and from a DSL of groups, using methods {@link #getExpression()} and {@link #parse(String)},
* respectively. The language supports compositions ({@code |}), intersections ({@code &}), negations ({@code !}) and differences
* ({@code -}) over basic constructs, that can either be functions or the special link group: {@code #name} ({@link DynamicGroup}
* ). Functions have the general form: {@code id(argName=argValue, argName=argValue,...)} but for groups without arguments they
* loose the parenthesis completely, also the argNames can be skipped if a default argument is set. Common basic constructs are:
* {@code anonymous} ( {@link AnonymousGroup}), {@code logged} ({@link LoggedGroup}), {@code anyone} ({@link AnyoneGroup}),
* {@code nobody} ( {@link NobodyGroup}), {@code U(istxxx, istxxxx,...)} ({@link UserGroup}).
* </p>
*
* <p>
* Sub-classes should be annotated with {@link GroupOperator} for proper language support, and should extends {@link CustomGroup}.
* </p>
*
* @author Pedro Santos (pedro.miguel.santos@tecnico.ulisboa.pt)
* @see PersistentGroup
* @see GroupOperator
*/
public abstract class Group implements Serializable, Comparable<Group> {
private static final long serialVersionUID = 1177210800165802668L;
private static final Group ANONYMOUS = new AnonymousGroup();
private static final Group ANYONE = new AnyoneGroup();
private static final Group LOGGED = new LoggedGroup();
private static final Group NOBODY = new NobodyGroup();
public static final Group anonymous() {
return ANONYMOUS;
}
public static final Group anyone() {
return ANYONE;
}
public static final Group logged() {
return LOGGED;
}
public static final Group nobody() {
return NOBODY;
}
public static DynamicGroup managers() {
return DynamicGroup.MANAGERS;
}
public static Group users(User... members) {
return users(Arrays.stream(members));
}
public static Group users(Stream<User> members) {
Set<User> set = members.collect(Collectors.toSet());
if (set.size() == 0) {
return NOBODY;
}
return new UserGroup(Collections.unmodifiableSet(set));
}
public static DynamicGroup dynamic(String name) {
return new DynamicGroup(name);
}
/**
* Human readable, internationalized textual representation of this group.
*
* @return internationalized name of the group.
*/
public abstract String getPresentationName();
/**
* Textual representation of this group in the group language.
*
* @return this group in group language.
*/
public abstract String getExpression();
/**
* Obtains (creating if necessary) the corresponding group domain entity.
*
* @return An instance of {@link PersistentGroup}.
*/
public abstract PersistentGroup toPersistentGroup();
/**
* Computes the full member list of this group. Potentially processor consuming operation, preferably developers should orient
* code to {@link #isMember(User)} or {@link #isMember(User, DateTime)} methods.
*
* @return all member users in the system at the exact moment of the invocation
*/
public abstract Stream<User> getMembers();
/**
* Same as {@link #getMembers()} but at a given moment in time. This is like a time-machine for the groups domain.
*
* @param when
* moment when to fetch the user list.
* @return all member users in the system at the requested moment
*/
public abstract Stream<User> getMembers(DateTime when);
/**
* Tests if the given user is a member of the group.
*
* @param user
* the user to test, can be null
* @return <code>true</code> if member, <code>false</code> otherwise
*
* @see #verify()
*/
public abstract boolean isMember(User user);
/**
* Same as {@link #isMember(User)} but at a given moment in time. This is like a time-machine for the groups domain.
*
* @param user
* the user to test, can be null
* @param when
* moment when to test the user.
* @return <code>true</code> if member, <code>false</code> otherwise
*/
public abstract boolean isMember(User user, DateTime when);
/**
* Tests if the given user is a member of the group, throwing an exception if not.
*
* @throws AuthorizationException
* if user is not a member of the group.
*/
public void verify() throws AuthorizationException {
if (!isMember(Authenticate.getUser())) {
throw AuthorizationException.unauthorized();
}
}
/**
* Intersect with given group. Returns the resulting group without changing {@code this} or the argument.
*
* @param group group to intersect with
* @return group resulting of the intersection between '{@code this}' and '{@code group}'
*/
public Group and(Group group) {
if (this.equals(group)) {
return this;
}
if (group instanceof NobodyGroup) {
return group;
}
if (group instanceof AnyoneGroup) {
return this;
}
if (group instanceof LoggedGroup && !this.isMember(null)) {
return this;
}
return new IntersectionGroup(ImmutableSet.<Group> builder().add(this).add(group).build());
}
/**
* Unite with given group. Returns the resulting group without changing {@code this} or the argument.
*
* @param group
* group to unite with
* @return group resulting of the union between '{@code this}' and '{@code group}'
*/
public Group or(Group group) {
if (this.equals(group)) {
return this;
}
if (group instanceof NobodyGroup) {
return this;
}
if (group instanceof AnyoneGroup) {
return group;
}
if (group instanceof LoggedGroup && !this.isMember(null)) {
return group;
}
return new UnionGroup(ImmutableSet.<Group> builder().add(this).add(group).build());
}
/**
* Subtract with given group. Returns the resulting group without changing {@code this} or the argument.
*
* @param group
* group to subtract with
* @return group resulting of all members of '{@code this}' except members of '{@code group}'
*/
public Group minus(Group group) {
if (this.equals(group)) {
return NOBODY;
}
if (group instanceof NobodyGroup) {
return this;
}
if (group instanceof AnyoneGroup) {
return NOBODY;
}
return new DifferenceGroup(this, ImmutableSet.of(group));
}
/**
* Negate the group. Returns the resulting group without changing {@code this}.
*
* @return inverse group
*/
public Group not() {
return new NegationGroup(this);
}
/**
* Grants access to the given user. Returns the resulting group without changing {@code this}.
*
* @param user
* user to grant access to
* @return group resulting of the union between '{@code this}' and the group of the given user
*/
public Group grant(User user) {
return or(user.groupOf());
}
/**
* Revokes access to the given user. Returns the resulting group without changing {@code this}.
*
* @param user
* user to revoke access from
* @return group resulting of the difference between '{@code this}' and the group of the given user
*/
public Group revoke(User user) {
return minus(user.groupOf());
}
/**
* Parse group from the group language expression.
*
* @param expression
* the group in textual form
* @return group representing the semantics of the expression.
* @throws BennuCoreDomainException
* if a parsing error occurs
*/
public static Group parse(String expression) {
if (Strings.isNullOrEmpty(expression)) {
return NOBODY;
}
return GroupParser.parse(expression);
}
@Override
public String toString() {
return getPresentationName();
}
@Override
public abstract boolean equals(Object object);
@Override
public abstract int hashCode();
@Override
public int compareTo(Group other) {
int byname = getPresentationName().compareTo(other.getPresentationName());
if (byname != 0) {
return byname;
}
return getExpression().compareTo(getExpression());
}
static String compositeExpression(Group group) {
if (group instanceof UnionGroup || group instanceof IntersectionGroup || group instanceof DifferenceGroup || group
instanceof NegationGroup) {
return "(" + group.getExpression() + ")";
} else {
return group.getExpression();
}
}
}