package de.skuzzle.polly.core.internal.users; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import org.apache.log4j.Logger; import de.skuzzle.jeve.EventProvider; import de.skuzzle.polly.core.internal.persistence.PersistenceManagerV2Impl; import de.skuzzle.polly.core.parser.InputParser; import de.skuzzle.polly.core.parser.InputScanner; import de.skuzzle.polly.core.parser.ast.declarations.Declaration; import de.skuzzle.polly.core.parser.ast.declarations.DeclarationReader; import de.skuzzle.polly.core.parser.ast.declarations.Namespace; import de.skuzzle.polly.core.parser.ast.expressions.Expression; import de.skuzzle.polly.core.parser.ast.expressions.literals.Literal; import de.skuzzle.polly.core.parser.ast.visitor.ASTTraversalException; import de.skuzzle.polly.core.parser.ast.visitor.ExecutionVisitor; import de.skuzzle.polly.core.parser.ast.visitor.ParentSetter; import de.skuzzle.polly.core.parser.ast.visitor.resolving.TypeResolver; import de.skuzzle.polly.core.parser.problems.ProblemReporter; import de.skuzzle.polly.core.parser.problems.SimpleProblemReporter; import de.skuzzle.polly.core.util.CaseInsensitiveStringKeyMap; import de.skuzzle.polly.core.util.TypeMapper; import de.skuzzle.polly.sdk.AbstractDisposable; import de.skuzzle.polly.sdk.Attribute; import de.skuzzle.polly.sdk.FormatManager; import de.skuzzle.polly.sdk.PersistenceManagerV2.Atomic; import de.skuzzle.polly.sdk.PersistenceManagerV2.Param; import de.skuzzle.polly.sdk.PersistenceManagerV2.Read; import de.skuzzle.polly.sdk.PersistenceManagerV2.Write; import de.skuzzle.polly.sdk.Types; import de.skuzzle.polly.sdk.User; import de.skuzzle.polly.sdk.UserManager; import de.skuzzle.polly.sdk.constraints.AttributeConstraint; import de.skuzzle.polly.sdk.eventlistener.IrcUser; import de.skuzzle.polly.sdk.eventlistener.UserEvent; import de.skuzzle.polly.sdk.eventlistener.UserListener; import de.skuzzle.polly.sdk.exceptions.AlreadySignedOnException; import de.skuzzle.polly.sdk.exceptions.ConstraintException; import de.skuzzle.polly.sdk.exceptions.DatabaseException; import de.skuzzle.polly.sdk.exceptions.DisposingException; import de.skuzzle.polly.sdk.exceptions.InvalidUserNameException; import de.skuzzle.polly.sdk.exceptions.RoleException; import de.skuzzle.polly.sdk.exceptions.UnknownAttributeException; import de.skuzzle.polly.sdk.exceptions.UnknownUserException; import de.skuzzle.polly.sdk.exceptions.UserExistsException; import de.skuzzle.polly.sdk.roles.RoleManager; import de.skuzzle.polly.sdk.time.Time; /** * * @author Simon * @version 27.07.2011 ae73250 */ public class UserManagerImpl extends AbstractDisposable implements UserManager { private static Logger logger = Logger.getLogger(UserManagerImpl.class.getName()); private final static AttributeConstraint NO_CONSTRAINT = new AttributeConstraint() { @Override public boolean accept(Types value) { return true; } }; private final static FileFilter DECLARATION_FILTER = new FileFilter() { @Override public boolean accept(File pathname) { return pathname.getName().toLowerCase().endsWith(".decl"); } }; private PersistenceManagerV2Impl persistence; /** * Stores the currently signed on users. Key: the nickname in lower case. */ private Map<String, User> onlineCache; private File declarationCachePath; private Map<String, AttributeConstraint> constraints; private EventProvider eventProvider; private User admin; private boolean registeredStale; private List<User> registeredUsers; private boolean attributesStale; private List<AttributeImpl> allAttributes; private RoleManager roleManager; private final FormatManager formatter; public UserManagerImpl(PersistenceManagerV2Impl persistence, String declarationCachePath, int tempVarLifeTime, boolean ignoreUnknownIdentifiers, EventProvider eventProvider, RoleManager roleManager, FormatManager formatter) { this.formatter = formatter; this.eventProvider = eventProvider; this.persistence = persistence; this.roleManager = roleManager; this.onlineCache = Collections.synchronizedMap( new CaseInsensitiveStringKeyMap<User>()); this.declarationCachePath = new File(declarationCachePath); this.constraints = new HashMap<String, AttributeConstraint>(); try { logger.info("Reading declarations..."); if (!this.declarationCachePath.exists()) { logger.warn("Declaration-cache directory does not exist. " + "Trying to create folder structure"); this.declarationCachePath.mkdirs(); } Namespace.setDeclarationFolder(this.declarationCachePath); readDeclarations(this.declarationCachePath); logger.trace("done"); } catch (IOException e) { logger.warn("No declarations restored", e); } } private static void readDeclarations(File folder) throws IOException { for (final File file : folder.listFiles(DECLARATION_FILTER)) { DeclarationReader dr = null; try { final String nsName = file.getName().substring( 0, file.getName().length() - 5); final Namespace ns = Namespace.forName(nsName); dr = new DeclarationReader(file, "ISO-8859-1", ns); dr.readAll(); } catch (IOException e) { e.printStackTrace(); } finally { if (dr != null) { dr.close(); } } } } @Override public synchronized Set<String> getDeclaredIdentifiers(String namespace) { final Namespace ns = Namespace.forName(namespace); final Set<String> result = new HashSet<String>(); for (final List<Declaration> decls : ns.getDeclarations().values()) { for (final Declaration decl : decls ) { result.add(decl.getName().getId()); } } return result; } public void setAdmin(User admin) { this.admin = admin; } public User getAdmin() { return this.admin; } @Override public User getUser(IrcUser user) { return this.onlineCache.get(user.getNickName()); } @Override public User getUser(int id) { UserImpl result = (UserImpl) this.persistence.atomic().find(UserImpl.class, id); if (result != null) { result.setUserManager(this); } return result; } @Override public User getUser(String registeredName) { if (registeredName == null) { return null; } try (final Read r = this.persistence.read()) { final UserImpl result = r.findSingle(UserImpl.class, "USER_BY_NAME", new Param(registeredName)); if (result != null) { result.setUserManager(this); } return result; } } @Override public User updateUser(final User old, final User updated) { try { this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) throws DatabaseException { old.setHashedPassword(updated.getHashedPassword()); } }); } catch (DatabaseException e) { e.printStackTrace(); } logger.info("User '" + old + "' updated to '" + updated + "'"); return old; } public void addUser(User user) throws UserExistsException, DatabaseException, InvalidUserNameException { Matcher m = USER_NAME_PATTERN.matcher(user.getName()); if (!m.matches()) { throw new InvalidUserNameException(user.getName()); } try (final Write w = this.persistence.write()) { User check = w.read().findSingle(User.class, "USER_BY_NAME", new Param(user.getName())); if (check != null) { logger.trace("User already exists."); throw new UserExistsException(check); } w.single(user); this.registeredStale = true; } // Assign registered role to new user. try { this.roleManager.assignRole(user, RoleManager.DEFAULT_ROLE); } catch (RoleException ignore) { logger.warn("Ignoring RoleException", ignore); } logger.info("Added user " + user); } @Override public User addUser(String name, String password) throws UserExistsException, DatabaseException, InvalidUserNameException { User newUser = this.createUser(name, password); this.addUser(newUser); return newUser; } @Override public void deleteUser(final User user) throws DatabaseException { this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) throws DatabaseException { logoff(user); write.remove(user); } }); this.registeredStale = true; logger.info("Deleted user " + user); } @Override public synchronized User logon(String from, String registeredName, String password) throws UnknownUserException, AlreadySignedOnException { logger.info("Trying to log on user '" + registeredName + "'."); User user = this.getUser(registeredName); if (user == null) { throw new UnknownUserException(registeredName); } this.checkAlreadySignedOn(user); if (user.checkPassword(password)) { user.setCurrentNickName(from); this.onlineCache.put(user.getCurrentNickName(), user); logger.info("Irc User " + from + " successfully logged in as " + registeredName); ((UserImpl) user).setLoginTime(Time.currentTimeMillis()); UserEvent e = new UserEvent(this, user); this.fireUserSignedOn(e); return user; } logger.warn("Login from '" + from + "' with user name '" + registeredName + "' failed: Invalid password."); return null; } public synchronized User logonWithoutPassword(String from) throws AlreadySignedOnException, UnknownUserException { logger.info("Trying to autologon user '" + from + "'."); User user = this.getUser(from); if (user == null) { throw new UnknownUserException(from); } this.checkAlreadySignedOn(user); user.setCurrentNickName(from); this.onlineCache.put(user.getCurrentNickName(), user); logger.info("Irc User " + from + " successfully logged in as " + from); ((UserImpl) user).setLoginTime(Time.currentTimeMillis()); UserEvent e = new UserEvent(this, user); this.fireUserSignedOn(e); return user; } private void checkAlreadySignedOn(User user) throws AlreadySignedOnException { if (this.onlineCache.containsKey(user.getCurrentNickName())) { throw new AlreadySignedOnException(user); } } @Override public void logoff(User user) { logger.info("User " + user + " logged off."); UserEvent e = new UserEvent(this, user); this.onlineCache.remove(user.getCurrentNickName()); this.fireUserSignedOff(e); } @Override public void logoff(IrcUser user) { this.logoff(user, false); } public synchronized void logoff(IrcUser user, boolean auto) { logger.info("User " + user + " logged off."); UserEvent e = new UserEvent(this, this.getUser(user), auto); this.onlineCache.remove(user.getNickName()); this.fireUserSignedOff(e); } public synchronized void logoffAll() { // HACK: copy users to not get a concurrent modification exception Collection<User> online = new ArrayList<User>(this.onlineCache.values()); for (User user : online) { IrcUser tmp = new IrcUser(user.getCurrentNickName(), "", ""); this.logoff(tmp, true); } } @Override public boolean isSignedOn(IrcUser user) { return this.onlineCache.containsKey(user.getNickName()); } @Override public boolean isSignedOn(User user) { return this.onlineCache.containsKey(user.getCurrentNickName()); } @Override public synchronized List<User> getRegisteredUsers() { if (this.registeredStale || this.registeredUsers == null) { final List<de.skuzzle.polly.core.internal.users.UserImpl> all = this.persistence.atomic().findList(UserImpl.class, UserImpl.ALL_USERS); for (final de.skuzzle.polly.core.internal.users.UserImpl u : all) { u.setUserManager(this); } this.registeredUsers = new ArrayList<User>(all); this.registeredStale = false; } return this.registeredUsers; } @Override public Collection<User> getOnlineUsers() { return Collections.unmodifiableCollection(this.onlineCache.values()); } public synchronized void traceNickChange(IrcUser oldUser, IrcUser newUser) { logger.debug("Tracing nickchange from '" + oldUser + "' to '" + newUser + "'"); User tmp = this.onlineCache.get(oldUser.getNickName()); tmp.setCurrentNickName(newUser.getNickName()); this.onlineCache.remove(oldUser.getNickName()); this.onlineCache.put(newUser.getNickName(), tmp); } @Override public synchronized Map<String, List<Attribute>> getAllAttributes() { if (this.attributesStale || this.allAttributes == null) { this.allAttributes = this.persistence.atomic().findList(AttributeImpl.class, AttributeImpl.ALL_ATTRIBUTES); } final Map<String, List<Attribute>> result = new HashMap<>(this.allAttributes.size()); for (final Attribute attr : this.allAttributes) { List<Attribute> lst = result.get(attr.getCategory()); if (lst == null) { lst = new ArrayList<>(); result.put(attr.getCategory(), lst); } lst.add(attr); } return result; } @Override public void addAttribute(final String name, final Types defaultValue, final String description, final String category, final AttributeConstraint constraint) throws DatabaseException { this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) throws DatabaseException { constraints.put(name.toLowerCase(), constraint); final AttributeImpl check = write.read().findSingle(AttributeImpl.class, AttributeImpl.ATTRIBUTE_BY_NAME, new Param(name)); if (check != null) { logger.trace("Tried to add an attribute that already existed: " + name + ". Existing attribute: " + check); return; } final String sDefaultValue = defaultValue.valueString( getPersistenceFormatter()); final AttributeImpl att = new AttributeImpl(name, sDefaultValue, description, category); final List<User> all = getRegisteredUsers(); write.single(att); logger.trace("Adding new attribute to each user."); for (User user : all) { final UserImpl u = (UserImpl) user; u.getAttributes().put(name, sDefaultValue); } logger.info("Attribute " + att + " added."); } }); this.attributesStale = true; } @Override public synchronized String setAttributeFor(User executor, final User user, final String attribute, String value) throws DatabaseException, ConstraintException { logger.trace("Trying to set attribute '" + attribute + "' to value '" + value + "'"); // check if attribute exists: user.getAttribute(attribute); if (value.equalsIgnoreCase("%default%")) { Attribute attr = this.persistence.atomic().findSingle(Attribute.class, AttributeImpl.ATTRIBUTE_BY_NAME, new Param(attribute)); value = attr.getDefaultValue(); } final Types valueCopy = this.parseValue(executor, value); final AttributeConstraint constraint = this.constraints.get( attribute.toLowerCase()); if (!constraint.accept(valueCopy)) { throw new ConstraintException("'" + value + "' ist kein g�ltiger Wert f�r das Attribut '" + attribute + "'"); } this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) { ((UserImpl) user).setAttribute(attribute, valueCopy); } }); return valueCopy.valueString(this.getPersistenceFormatter()); } public FormatManager getPersistenceFormatter() { return this.formatter; } Types parseValue(User executor, String value) { if (executor == null) { // use admin as namespace if no executor is specified. executor = this.admin; } final ProblemReporter reporter = new SimpleProblemReporter(); final InputScanner is = new InputScanner(value); final InputParser ip = new InputParser(is, reporter); is.setSkipWhiteSpaces(true); try { final Expression exp = ip.parseSingleExpression(); exp.visit(new ParentSetter()); final String nsName = executor.getCurrentNickName() == null ? executor.getName() : executor.getCurrentNickName(); final Namespace ns = Namespace.forName(nsName); final ExecutionVisitor exec = new ExecutionVisitor(ns, ns, reporter); // resolve types TypeResolver.resolveAST(exp, ns, reporter); exp.visit(exec); final Literal result = exec.getSingleResult(); return TypeMapper.literalToTypes(result); } catch (ASTTraversalException e) { // ignore the exception, just use plain value which was submitted return new Types.StringType(value); } } @Override public void addAttribute(String name, Types defaultValue, String description, String category) throws DatabaseException { this.addAttribute(name, defaultValue, description, category, NO_CONSTRAINT); } @Override public void removeAttribute(String name) throws DatabaseException { try (final Write w = this.persistence.write()) { List<User> all = w.read().findList(User.class, "ALL_USERS"); final Attribute att = w.read().findSingle( Attribute.class, AttributeImpl.ATTRIBUTE_BY_NAME, new Param(name)); if (att == null) { throw new UnknownAttributeException(name); } logger.trace("Removing attribute from all users."); for (User user : all) { final UserImpl u = (UserImpl) user; u.getAttributes().remove(name); } w.remove(att); this.constraints.remove(name); logger.info("Attribute " + att + " removed."); } } @Override protected void actualDispose() throws DisposingException { this.persistence = null; this.onlineCache.clear(); this.onlineCache = null; } @Override public User createUser(String name, String password) { final UserImpl result = new UserImpl(name, password); result.setUserManager(this); final List<AttributeImpl> all = this.persistence.atomic().findList( AttributeImpl.class, AttributeImpl.ALL_ATTRIBUTES); for (Attribute att : all) { result.getAttributes().put(att.getName(), att.getDefaultValue()); } return result; } @Override public void addUserListener(UserListener listener) { this.eventProvider.addListener(UserListener.class, listener); } @Override public void removeUserListener(UserListener listener) { this.eventProvider.removeListener(UserListener.class, listener); } protected void fireUserSignedOn(final UserEvent e) { this.eventProvider.dispatch(UserListener.class, e, UserListener::userSignedOn); } protected void fireUserSignedOff(final UserEvent e) { this.eventProvider.dispatch(UserListener.class, e, UserListener::userSignedOff); } }