UnixEntityManager.java

package eu.javaexperience.unix.user;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;

import eu.javaexperience.arrays.ArrayTools;
import eu.javaexperience.collection.CollectionTools;
import eu.javaexperience.io.IOTools;
import eu.javaexperience.io.file.FileReloadEntry;
import eu.javaexperience.log.JavaExperienceLoggingFacility;
import eu.javaexperience.log.LogLevel;
import eu.javaexperience.log.Loggable;
import eu.javaexperience.log.Logger;
import eu.javaexperience.log.LoggingTools;
import eu.javaexperience.reflect.Mirror;
import eu.javaexperience.regex.RegexTools;
import eu.javaexperience.semantic.references.MayNotNull;
import eu.javaexperience.semantic.references.MayNull;
import eu.javaexperience.unix.user.exception.GroupAlredyExistException;
import eu.javaexperience.unix.user.exception.GroupDoesnotExistException;
import eu.javaexperience.unix.user.exception.InsufficientPermissionException;
import eu.javaexperience.unix.user.exception.UserDoesnotExistException;

public final class UnixEntityManager
{
	protected static final Logger LOG = JavaExperienceLoggingFacility.getLogger(new Loggable("UnixEntityManager"));
	
	private static boolean useSUDO = false;

	protected static final File passwdFile = new File("/etc/passwd");
	protected static final File groupFile = new File("/etc/group");
	
	protected static final FileReloadEntry<PasswdIndex> passwdIndex = new FileReloadEntry<UnixEntityManager.PasswdIndex>(passwdFile)
	{
		@Override
		protected PasswdIndex processFile(File f) throws IOException
		{
			return PasswdIndex.parseAndIndexPasswdFile("/etc/passwd");
		}
	};
	
	protected static final FileReloadEntry<GroupIndex> groupIndex = new FileReloadEntry<UnixEntityManager.GroupIndex>(groupFile)
	{
		@Override
		protected GroupIndex processFile(File f) throws IOException
		{
			return GroupIndex.parseAndIndexGroupFile("/etc/group");
		}
	
	};
	protected static PasswdIndex getPasswdIndex() throws FileNotFoundException, IOException
	{
		return passwdIndex.get();
	}
	
	protected static GroupIndex getGroupIndex() throws FileNotFoundException, IOException
	{
		return groupIndex.get();
	}
	
	/**
	 * I've faced an interesting bug in the previous version:
	 * When i cached the passwd's file content, sometimes i get the older
	 * version of the from the cache.
	 * The reason is that the java returns the file last modification time in
	 * secounds resolution, so if the passwd file modified right since some
	 * milisecs the cache determines (badly) that the file not modified, so this
	 * case is important if we apply file cache based on last modification.
	 * */
	protected static boolean needReload(long entryTime, long fileTime)
	{
		entryTime /= 1000;
		fileTime /= 1000;
		long currentTime = System.currentTimeMillis()/1000;
		return fileTime == currentTime || fileTime > entryTime;
	}
	
	public static class struct_passwd
	{
		public String username;
		public int uid;
		public int gid;
		public String etcData;
		public String homeDir;
		public String shell;
		
		public static @MayNotNull struct_passwd parseLine(String line)
		{
			String[] data = RegexTools.COLON.split(line);
			struct_passwd ret = new struct_passwd();
			ret.username = data[0];
			ret.uid = Integer.parseInt(data[2]);
			ret.gid = Integer.parseInt(data[3]);
			ret.etcData = data[4];
			ret.homeDir = data[5];
			ret.shell = data[6];
			
			return ret;
		}
		
		public static @MayNull struct_passwd parseLineSafe(String line)
		{
			try
			{
				return parseLine(line);
			}
			catch(Throwable t)
			{
				LoggingTools.tryLogFormatException(LOG, LogLevel.WARNING, t, "Can't parse passwd line: %s", line);
				return null;
			}
		}
		
	}
	
	public static class PasswdIndex
	{
		public Map<String, struct_passwd> byUsername = new HashMap<>();
		public Map<Integer, struct_passwd> byUid = new HashMap<>();
		
		public static PasswdIndex parseAndIndexAll(String[] lines)
		{
			PasswdIndex ret = new PasswdIndex();
			for(String s: lines)
			{
				if(null != s)
				{
					struct_passwd p = struct_passwd.parseLineSafe(s);
					if(null != p)
					{
						ret.byUsername.put(p.username, p);
						ret.byUid.put(p.uid, p);
					}
				}
			}
			
			return ret;
		}
		
		public static PasswdIndex parseAndIndexPasswdFile(String file) throws FileNotFoundException, IOException
		{
			String[] lines = RegexTools.LINUX_NEW_LINE.split(IOTools.getFileContents(file));
			return parseAndIndexAll(lines);
		}
	}
	
	public static class struct_group
	{
		public String groupname;
		public int gid;
		public HashSet<String> users = new HashSet<>();
		
		public static @MayNotNull struct_group parseLine(String line)
		{
			String[] data = RegexTools.COLON.split(line);
			struct_group ret = new struct_group();
			ret.groupname = data[0];
			ret.gid = Integer.parseInt(data[2]);
			if(data.length > 3)
			{
				CollectionTools.copyInto(RegexTools.COMMA.split(data[3]), ret.users);
			}
			
			return ret;
		}
		
		public static @MayNull struct_group parseLineSafe(String line)
		{
			try
			{
				return parseLine(line);
			}
			catch(Throwable t)
			{
				LoggingTools.tryLogFormatException(LOG, LogLevel.WARNING, t, "Can't parse group line: %s", line);
				return null;
			}
		}
	}
	
	public static class GroupIndex
	{
		public Map<String, struct_group> byGroupname = new HashMap<>();
		public Map<Integer, struct_group> byGid = new HashMap<>();
		
		public static GroupIndex parseAndIndexAll(String[] lines)
		{
			GroupIndex ret = new GroupIndex();
			for(String s: lines)
			{
				if(null != s)
				{
					struct_group g = struct_group.parseLineSafe(s);
					if(null != g)
					{
						ret.byGroupname.put(g.groupname, g);
						ret.byGid.put(g.gid, g);
					}
				}
			}
			
			return ret;
		}
		
		public static GroupIndex parseAndIndexGroupFile(String file) throws FileNotFoundException, IOException
		{
			String[] lines = RegexTools.LINUX_NEW_LINE.split(IOTools.getFileContents(file));
			return parseAndIndexAll(lines);
		}
	}
	
/********************************* Error codes ********************************/
	
	private static void uidNonex(int uid) throws UserDoesnotExistException
	{
		throw new UserDoesnotExistException("UNIX user with UID: " + uid + " doesnot exist!");
	}

	private static void gidNonex(int gid) throws GroupDoesnotExistException
	{
		throw new GroupDoesnotExistException("UNIX group with GID: " + gid + " doesnot exist!");
	}

	private static void userNonex(String usr) throws UserDoesnotExistException
	{
		throw new UserDoesnotExistException("UNIX user with username: " + usr + " doesnot exist!");
	}

	private static void groupNonex(String grp)throws GroupDoesnotExistException
	{
		throw new GroupDoesnotExistException("UNIX group with groupname: "+ grp + " doesnot exist!");
	}
	
	public static struct_passwd getExistingUser(PasswdIndex index, String username) throws UserDoesnotExistException
	{
		struct_passwd p = index.byUsername.get(username);
		if(null == p)
		{
			userNonex(username);
		}
		
		return p;
	}
	
	public static struct_passwd getExistingUser(PasswdIndex index, int uid) throws UserDoesnotExistException
	{
		struct_passwd p = index.byUid.get(uid);
		if(null == p)
		{
			uidNonex(uid);
		}
		
		return p;
	}
	
	public static struct_group getExistingGroup(GroupIndex index, String grpname) throws GroupDoesnotExistException
	{
		struct_group grp = index.byGroupname.get(grpname);
		if(null == grp)
		{
			groupNonex(grpname);
		}
		return grp;
	}
	
	public static struct_group getExistingGroup(GroupIndex index, int gid) throws GroupDoesnotExistException
	{
		struct_group grp = index.byGid.get(gid);
		if(null == grp)
		{
			gidNonex(gid);
		}
		return grp;
	}
	
	public static boolean isUserExistByName(String username) throws IOException
	{
		return getPasswdIndex().byUsername.containsKey(username);
	}

	public static boolean isUserExistByUID(int UID) throws IOException
	{
		return getPasswdIndex().byUid.containsKey(UID);
	}

	public static int UIDByUsername(String username) throws IOException, UserDoesnotExistException
	{
		return getExistingUser(getPasswdIndex(), username).uid;
	}

	public static String usernameByUID(int UID) throws IOException, UserDoesnotExistException
	{
		return getExistingUser(getPasswdIndex(), UID).username;
	}

	public static String getUserHomePath(String username) throws IOException,UserDoesnotExistException
	{
		return getExistingUser(getPasswdIndex(), username).homeDir;
	}

	public static String getUserRealName(String username) throws IOException,	UserDoesnotExistException
	{
		String[] buf = getExistingUser(getPasswdIndex(), username).etcData.split(",");
		return buf.length > 0 ? buf[0] : null;
	}

	public static String[] usersBetween(int MIN_UID, int MAX_UID) throws IOException
	{
		PasswdIndex index = getPasswdIndex();
		
		ArrayList<String> users = new ArrayList<>();
		
		for(Entry<Integer, struct_passwd> kv:index.byUid.entrySet())
		{
			int uidbuf = kv.getValue().uid;
			if ((uidbuf >= MIN_UID) && (uidbuf <= MAX_UID))
			{
				users.add(kv.getValue().username);
			}
		}
		
		return users.toArray(Mirror.emptyStringArray);
	}

	public static String[] usersUsernameContains(String part) throws IOException
	{
		PasswdIndex index = getPasswdIndex();
		
		ArrayList<String> users = new ArrayList<>();
		
		for(Entry<Integer, struct_passwd> kv:index.byUid.entrySet())
		{
			struct_passwd p = kv.getValue();
			if(p.username.contains(part))
			{
				users.add(p.username);
			}
		}
		
		return users.toArray(Mirror.emptyStringArray);
	}

	public static boolean isGroupExistByName(String groupname)	throws IOException
	{
		return getGroupIndex().byGroupname.containsKey(groupname);
	}

	public static boolean isGroupExistByGID(int GID) throws IOException
	{
		return getGroupIndex().byGid.containsKey(GID);
	}

	public static int gidByGroupname(String groupname) throws IOException,	GroupDoesnotExistException
	{
		return getExistingGroup(getGroupIndex(), groupname).gid;
	}

	public static String groupnameByGID(int GID) throws IOException,GroupDoesnotExistException
	{
		return getExistingGroup(getGroupIndex(), GID).groupname;
	}

	public static String[] groupsBetween(int MIN_GID, int MAX_GID)throws IOException
	{
		GroupIndex index = getGroupIndex();
		
		ArrayList<String> users = new ArrayList<>();
		
		for(Entry<Integer, struct_group> kv:index.byGid.entrySet())
		{
			int uidbuf = kv.getValue().gid;
			if ((uidbuf >= MIN_GID) && (uidbuf <= MAX_GID))
			{
				users.add(kv.getValue().groupname);
			}
		}
		
		return users.toArray(Mirror.emptyStringArray);
	}

	public static String[] groupGroupnameContains(String part) throws IOException
	{
		GroupIndex index = getGroupIndex();
		
		ArrayList<String> users = new ArrayList<>();
		
		for(Entry<Integer, struct_group> kv:index.byGid.entrySet())
		{
			struct_group p = kv.getValue();
			if(p.groupname.contains(part))
			{
				users.add(p.groupname);
			}
		}
		
		return users.toArray(Mirror.emptyStringArray);
	}
	
	public static boolean isUserInGroup(String username, String groupname) throws IOException, GroupDoesnotExistException
	{
		struct_group grp = getExistingGroup(getGroupIndex(), groupname);
		return grp.users.contains(username);
	}
	
	public static String[] getUserGroups(String username) throws IOException, UserDoesnotExistException
	{
		ArrayList<String> ret = new ArrayList<>();
		for(Entry<String, struct_group> kv:getGroupIndex().byGroupname.entrySet())
		{
			if(kv.getValue().users.contains(username))
			{
				ret.add(kv.getKey());
			}
		}
		
		return ret.toArray(Mirror.emptyStringArray);
	}

	public static void addUserToGroup(String username, String groupname)throws IOException, GroupDoesnotExistException,	UserDoesnotExistException, InsufficientPermissionException
	{
		if (!isGroupExistByName(groupname))
			groupNonex(groupname);

		String[] groups = getUserGroups(username);

		StringBuilder gs = new StringBuilder();

		for (int i = 0; i < groups.length; i++)
		{
			gs.append(groups[i]);
			gs.append(",");
		}
		gs.append(groupname);

		String[] command = {"usermod", "-G", gs.toString(), username};

		if (useSUDO)
		{
			command = ArrayTools.arrayAppend("sudo", command);
		}

		try
		{
			new ProcessBuilder(command).start().waitFor();
		}
		catch (Exception e)
		{
			throw new InsufficientPermissionException(
					"Cannot perform useradd command");
		}
	}

	public static void delUserFromGroup(String username, String groupname)throws IOException, UserDoesnotExistException,InsufficientPermissionException
	{
		if (!isUserExistByName(username))
			userNonex(username);

		String[] groups = getUserGroups(username);
		StringBuilder gs = new StringBuilder();
		for (int i = 0; i < groups.length; i++)
			if (!groups[i].equals(groupname))
			{
				if(i !=0)
					gs.append(",");
				gs.append(groups[i]);
			}

		String[] command = {"usermod", "-G", gs.toString(), username};

		if (useSUDO)
		{
			command = ArrayTools.arrayAppend("sudo", command);
		}

		try
		{
			new ProcessBuilder(command).start().waitFor();
		}
		catch (Exception e)
		{
			throw new InsufficientPermissionException("Cannot perform useradd command");
		}
	}

	public static void createGroup(String groupname) throws IOException,InsufficientPermissionException
	{
		String[] command ={"addgroup", groupname};

		if (useSUDO)
		{
			command = ArrayTools.arrayAppend("sudo", command);
		}

		try
		{
			new ProcessBuilder(command).start();
		}
		catch (IOException e)
		{
			throw new InsufficientPermissionException("Cannot perform addgroup command");
		}
	}

	public static String[] usersInGroup(String group) throws IOException,GroupDoesnotExistException
	{
		return getExistingGroup(getGroupIndex(), group).users.toArray(Mirror.emptyStringArray);
	}

	public static boolean login(String user, String password) throws IOException, InterruptedException
	{
		Process p = new ProcessBuilder(new String[]{"pwauth"}).start();
		p.getOutputStream().write((user + "\n" + password + "\n").getBytes());
		p.getOutputStream().flush();
		if (p.waitFor() == 0)
		{
			return true;
		}
		
		return false;
	}

	public static boolean delGroup(String group) throws InterruptedException,IOException
	{
		return new ProcessBuilder(new String[]{"groupdel", group}).start().waitFor() == 0;
	}

	public static boolean lockUser(String user, boolean l_p_u)throws IOException, UserDoesnotExistException, InterruptedException
	{
		getExistingUser(getPasswdIndex(), user);
		return new ProcessBuilder(new String[]{"passwd", user, l_p_u ? "-l" : "-u"}).start().waitFor() == 0;
	}

	public static void kickUsersInRange(String username, int min, int max) throws IOException, InterruptedException
	{
		String[] users = usersBetween(min, max);
		for (int i = 0; i < users.length; i++)
			if (users[i].equals(username))
			{
				kickUser(username);
				return;
			}
	}

	public static void kickUser(String username) throws InterruptedException,IOException
	{
		new ProcessBuilder(new String[]{"/usr/bin/killall", "-9", "--user", username}).start().waitFor();
	}

	public static void renameGroup(String mirol, String mire)throws IOException, GroupDoesnotExistException,InterruptedException, GroupAlredyExistException
	{
		if (!isGroupExistByName(mirol))
			groupNonex(mirol);

		if (isGroupExistByName(mire))
			throw new GroupAlredyExistException(mire+ " nevű csoport már létezik, " + mirol	+ "-t nem nevezheted át erre.");
		new ProcessBuilder(new String[]	{"groupmod", mirol, "-n", mire}).start().waitFor();
	}

	protected static String[] loadShadowLines() throws FileNotFoundException, IOException
	{
		return IOTools.readAllLine(new File("/etc/shadow"));
	}
	
	public static boolean isUserLocked(String user) throws IOException, UserDoesnotExistException
	{
		String[] lines = loadShadowLines();
		String[] unit = lines;
		for (int i = 0; i < lines.length; i++)
			if ((unit = lines[i].split(":"))[0].equals(user))
			{
				return unit[1].charAt(0) == '!';
			}
		
		userNonex(user);
		return false;
	}

	public static String getUserDefaultGroupName(String username) throws GroupDoesnotExistException, UserDoesnotExistException, FileNotFoundException, IOException
	{
		return getExistingGroup(getGroupIndex(), getExistingUser(getPasswdIndex(), username).gid).groupname;
	}
	
	public static int getUserDefaultGroupGid(String username) throws UserDoesnotExistException, FileNotFoundException, IOException
	{
		return getExistingUser(getPasswdIndex(), username).gid;
	}
}