package com.forgeessentials.playerlogger; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.NonUniqueResultException; import javax.persistence.Persistence; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; import javax.persistence.metamodel.SingularAttribute; import javax.sql.rowset.serial.SerialBlob; import net.minecraft.block.Block; import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import net.minecraftforge.common.util.BlockSnapshot; import net.minecraftforge.event.CommandEvent; import net.minecraftforge.event.entity.living.LivingHurtEvent; import net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn; import net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn; import net.minecraftforge.event.entity.player.AttackEntityEvent; import net.minecraftforge.event.entity.player.EntityInteractEvent; import net.minecraftforge.event.entity.player.EntityItemPickupEvent; import net.minecraftforge.event.entity.player.PlayerEvent.StartTracking; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.event.entity.player.PlayerOpenContainerEvent; import net.minecraftforge.event.world.BlockEvent; import net.minecraftforge.event.world.ExplosionEvent; import net.minecraftforge.event.world.WorldEvent; import com.forgeessentials.commons.selections.Selection; import com.forgeessentials.commons.selections.WorldPoint; import com.forgeessentials.playerlogger.entity.ActionBlock; import com.forgeessentials.playerlogger.entity.Action_; import com.forgeessentials.playerlogger.entity.BlockData; import com.forgeessentials.playerlogger.entity.BlockData_; import com.forgeessentials.playerlogger.entity.PlayerData; import com.forgeessentials.playerlogger.entity.PlayerData_; import com.forgeessentials.playerlogger.entity.WorldData; import com.forgeessentials.playerlogger.event.LogEventBreak; import com.forgeessentials.playerlogger.event.LogEventCommand; import com.forgeessentials.playerlogger.event.LogEventExplosion; import com.forgeessentials.playerlogger.event.LogEventInteract; import com.forgeessentials.playerlogger.event.LogEventPlace; import com.forgeessentials.util.events.PlayerChangedZone; import com.forgeessentials.util.events.ServerEventHandler; import com.forgeessentials.util.output.LoggingHandler; import com.google.common.base.Charsets; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event.Result; import cpw.mods.fml.common.eventhandler.EventPriority; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.PlayerEvent; import cpw.mods.fml.common.registry.GameData; import cpw.mods.fml.relauncher.Side; public class PlayerLogger extends ServerEventHandler implements Runnable { private Thread thread; private EntityManagerFactory entityManagerFactory; private EntityManager em; private Map<String, Long> blockCache = new HashMap<>(); private Map<Block, Long> blockTypeCache = new HashMap<>(); private Map<UUID, Long> playerCache = new HashMap<>(); /* ------------------------------------------------------------ */ private ConcurrentLinkedQueue<PlayerLoggerEvent<?>> eventQueue = new ConcurrentLinkedQueue<PlayerLoggerEvent<?>>(); /* ------------------------------------------------------------ */ /** * Closes any existing database connection and frees resources */ protected void close() { blockCache.clear(); blockTypeCache.clear(); playerCache.clear(); if (em != null && em.isOpen()) em.close(); if (entityManagerFactory != null && entityManagerFactory.isOpen()) entityManagerFactory.close(); } /** * Initialize the database connection */ protected void loadDatabase() { close(); // Set log level Logger.getLogger("org.hibernate").setLevel(Level.SEVERE); Properties properties = new Properties(); switch (PlayerLoggerConfig.databaseType) { case "h2": properties.setProperty("hibernate.connection.url", "jdbc:h2:" + PlayerLoggerConfig.databaseUrl); break; case "mysql": // e.g.: jdbc:mysql://localhost:3306/forgeessentials properties.setProperty("hibernate.connection.url", "jdbc:mysql://" + PlayerLoggerConfig.databaseUrl); break; default: throw new RuntimeException("PlayerLogger database type must be either h2 or mysql."); } properties.setProperty("hibernate.connection.username", PlayerLoggerConfig.databaseUsername); properties.setProperty("hibernate.connection.password", PlayerLoggerConfig.databasePassword); // properties.setProperty("hibernate.hbm2ddl.auto", "update"); // properties.setProperty("hibernate.format_sql", "false"); // properties.setProperty("hibernate.show_sql", "true"); entityManagerFactory = Persistence.createEntityManagerFactory("playerlogger_" + PlayerLoggerConfig.databaseType, properties); // entityManagerFactory = Persistence.createEntityManagerFactory("playerlogger_eclipselink_" + // PlayerLoggerConfig.databaseType, properties); em = entityManagerFactory.createEntityManager(); } @Override public void run() { while (!eventQueue.isEmpty()) { synchronized (this) { if (em == null) return; if (!em.isOpen()) { LoggingHandler.felog.error("[PL] Playerlogger database closed. Trying to reconnect..."); try { em = entityManagerFactory.createEntityManager(); } catch (IllegalStateException e) { LoggingHandler.felog.error("[PL] ------------------------------------------------------------------------"); LoggingHandler.felog.error("[PL] Fatal error! Database connection was lost and could not be reestablished"); LoggingHandler.felog.error("[PL] Stopping playerlogger!"); LoggingHandler.felog.error("[PL] ------------------------------------------------------------------------"); em = null; eventQueue.clear(); return; } } try { em.getTransaction().begin(); int count; for (count = 0; count < 1000; count++) { PlayerLoggerEvent<?> logEvent = eventQueue.poll(); if (logEvent == null) break; logEvent.process(em); } em.getTransaction().commit(); // System.out.println(String.format("%d: Wrote %d playerlogger entries", System.currentTimeMillis() // % (1000 * 60), count)); } catch (Exception e1) { LoggingHandler.felog.error("[PL] Exception while persisting playerlogger entries"); e1.printStackTrace(); try { em.getTransaction().rollback(); } catch (Exception e2) { LoggingHandler.felog.error("[PL] Exception while rolling back changes!"); e2.printStackTrace(); em.close(); return; } } finally { em.clear(); } } } } protected void startThread() { if (thread != null && thread.isAlive()) return; thread = new Thread(this, "Playerlogger"); thread.start(); } // ============================================================ // Utilities /** * <b>NEVER</b> call this and do write operations with this entity manager unless you do it in a synchronized block * with this object. * * <pre> * <code>synchronized (playerLogger) { * playerLogger.getEntityManager().doShit(); * }</code> * </pre> * * @return entity manager */ public EntityManager getEntityManager() { return em; } public <T> TypedQuery<T> buildSimpleQuery(Class<T> clazz, String fieldName, Object value) { CriteriaBuilder cBuilder = em.getCriteriaBuilder(); CriteriaQuery<T> cQuery = cBuilder.createQuery(clazz); Root<T> cRoot = cQuery.from(clazz); cQuery.select(cRoot).where(cBuilder.equal(cRoot.get(fieldName), value)); return em.createQuery(cQuery); } public <T, V> TypedQuery<T> buildSimpleQuery(Class<T> clazz, SingularAttribute<T, V> field, V value) { CriteriaBuilder cBuilder = em.getCriteriaBuilder(); CriteriaQuery<T> cQuery = cBuilder.createQuery(clazz); Root<T> cRoot = cQuery.from(clazz); cQuery.select(cRoot).where(cBuilder.equal(cRoot.get(field), value)); return em.createQuery(cQuery); } public <T, V> TypedQuery<Long> buildCountQuery(Class<T> clazz, SingularAttribute<T, V> field, V value) { CriteriaBuilder cBuilder = em.getCriteriaBuilder(); CriteriaQuery<Long> cQuery = cBuilder.createQuery(Long.class); Root<T> cRoot = cQuery.from(clazz); cQuery.select(cBuilder.count(cRoot)); if (field != null) cQuery.where(cBuilder.equal(cRoot.get(field), value)); return em.createQuery(cQuery); } public <T> T getOneOrNullResult(TypedQuery<T> query) { List<T> results = query.getResultList(); if (results.size() == 1) return results.get(0); if (results.isEmpty()) return null; throw new NonUniqueResultException(); } protected void logEvent(PlayerLoggerEvent<?> event) { if (em == null) return; eventQueue.add(event); startThread(); } protected synchronized WorldData getWorld(int dimensionId) { return em.getReference(WorldData.class, dimensionId); } protected synchronized PlayerData getPlayer(String uuid) { PlayerData data = getOneOrNullResult(buildSimpleQuery(PlayerData.class, PlayerData_.uuid, uuid)); if (data == null) { data = new PlayerData(); data.uuid = uuid; em.persist(data); } return data; } protected synchronized PlayerData getPlayer(UUID uuid) { Long id = playerCache.get(uuid); if (id != null) return em.getReference(PlayerData.class, id); PlayerData data = getPlayer(uuid.toString()); playerCache.put(uuid, data.id); return data; } protected synchronized BlockData getBlock(String name) { Long id = blockCache.get(name); if (id != null) return em.getReference(BlockData.class, id); BlockData data = getOneOrNullResult(buildSimpleQuery(BlockData.class, BlockData_.name, name)); if (data == null) { data = new BlockData(); data.name = name; em.persist(data); } blockCache.put(name, data.id); return data; } protected synchronized BlockData getBlock(Block block) { Long id = blockTypeCache.get(block); if (id != null) return em.getReference(BlockData.class, id); BlockData data = getBlock(GameData.getBlockRegistry().getNameForObject(block)); blockTypeCache.put(block, data.id); return data; } protected SerialBlob getTileEntityBlob(TileEntity tileEntity) { if (tileEntity == null) return null; NBTTagCompound nbt = new NBTTagCompound(); tileEntity.writeToNBT(nbt); nbt.setString("ENTITY_CLASS", tileEntity.getClass().getName()); try { return new SerialBlob(nbt.toString().getBytes(Charsets.UTF_8)); } catch (Exception ex) { LoggingHandler.felog.error(ex.toString()); ex.printStackTrace(); } return null; } /* ------------------------------------------------------------ */ /* data retrieval */ private synchronized <T> List<T> executeQuery(TypedQuery<T> query) { em.getTransaction().begin(); List<T> changes = query.getResultList(); em.getTransaction().commit(); return changes; } public List<ActionBlock> getBlockChanges(Selection area, Date startTime) { CriteriaBuilder cBuilder = em.getCriteriaBuilder(); CriteriaQuery<ActionBlock> cQuery = cBuilder.createQuery(ActionBlock.class); Root<ActionBlock> cRoot = cQuery.from(ActionBlock.class); cQuery.select(cRoot); cQuery.where(cBuilder.and(cBuilder.greaterThanOrEqualTo(cRoot.get(Action_.time), cBuilder.literal(startTime)), cBuilder.equal(cRoot.<Integer> get(Action_.world.getName()), cBuilder.literal(area.getDimension())), // cBuilder.between(cRoot.get(Action_.x), cBuilder.literal(area.getLowPoint().getX()), cBuilder.literal(area.getHighPoint().getX())), // cBuilder.between(cRoot.get(Action_.y), cBuilder.literal(area.getLowPoint().getY()), cBuilder.literal(area.getHighPoint().getY())), // cBuilder.between(cRoot.get(Action_.z), cBuilder.literal(area.getLowPoint().getZ()), cBuilder.literal(area.getHighPoint().getZ())))); cQuery.orderBy(cBuilder.desc(cRoot.get(Action_.time))); return executeQuery(em.createQuery(cQuery)); } public List<ActionBlock> getBlockChanges(WorldPoint point, Date startTime, int maxResults) { CriteriaBuilder cBuilder = em.getCriteriaBuilder(); CriteriaQuery<ActionBlock> cQuery = cBuilder.createQuery(ActionBlock.class); Root<ActionBlock> cRoot = cQuery.from(ActionBlock.class); cQuery.select(cRoot); cQuery.where(cBuilder.and(cBuilder.equal(cRoot.<Integer> get(Action_.world.getName()), cBuilder.literal(point.getDimension())), // cBuilder.equal(cRoot.get(Action_.x), cBuilder.literal(point.getX())), // cBuilder.equal(cRoot.get(Action_.y), cBuilder.literal(point.getY())), // cBuilder.equal(cRoot.get(Action_.z), cBuilder.literal(point.getZ())), // cBuilder.lessThan(cRoot.get(Action_.time), cBuilder.literal(startTime)))); cQuery.orderBy(cBuilder.desc(cRoot.get(Action_.time))); return executeQuery(em.createQuery(cQuery).setMaxResults(maxResults)); } /* ------------------------------------------------------------ */ /* World events */ @SubscribeEvent(priority = EventPriority.LOWEST) public synchronized void worldLoad(WorldEvent.Load e) { WorldData world = em.find(WorldData.class, e.world.provider.dimensionId); if (world == null) { em.getTransaction().begin(); world = new WorldData(); world.id = e.world.provider.dimensionId; world.name = e.world.provider.getDimensionName(); em.persist(world); em.getTransaction().commit(); } } @SubscribeEvent(priority = EventPriority.LOWEST) public void placeEvent(BlockEvent.PlaceEvent event) { logEvent(new LogEventPlace(event)); } @SubscribeEvent(priority = EventPriority.LOWEST) public void multiPlaceEvent(BlockEvent.MultiPlaceEvent e) { if (em == null) return; for (BlockSnapshot snapshot : e.getReplacedBlockSnapshots()) eventQueue.add(new LogEventPlace(new BlockEvent.PlaceEvent(snapshot, null, e.player))); startThread(); } @SubscribeEvent(priority = EventPriority.LOWEST) public void breakEvent(BlockEvent.BreakEvent e) { logEvent(new LogEventBreak(e)); } @SubscribeEvent(priority = EventPriority.LOWEST) public void explosionEvent(ExplosionEvent.Detonate e) { logEvent(new LogEventExplosion(e)); } @SubscribeEvent(priority = EventPriority.LOWEST) public void playerInteractEvent(PlayerInteractEvent event) { if (FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT || (event.useBlock == Result.DENY && event.useItem == Result.DENY)) return; ItemStack itemStack = event.entityPlayer.getCurrentEquippedItem(); if (event.useBlock != Result.ALLOW && itemStack != null && itemStack.getItem() instanceof ItemBlock) return; logEvent(new LogEventInteract(event)); } /* ------------------------------------------------------------ */ /* Other events */ @SubscribeEvent(priority = EventPriority.LOWEST) public void commandEvent(CommandEvent e) { logEvent(new LogEventCommand(e)); } /* ------------------------------------------------------------ */ /* Player events */ @SubscribeEvent(priority = EventPriority.LOWEST) public void playerLoggedInEvent(PlayerEvent.PlayerLoggedInEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void playerLoggedOutEvent(PlayerEvent.PlayerLoggedOutEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void playerRespawnEvent(PlayerEvent.PlayerRespawnEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void playerChangedDimensionEvent(PlayerEvent.PlayerChangedDimensionEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void playerChangedZoneEvent(PlayerChangedZone e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void playerOpenContainerEvent(PlayerOpenContainerEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void itemPickupEvent(EntityItemPickupEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void dropItemEvent(StartTracking e) { // TODO } /* ------------------------------------------------------------ */ /* Interact events */ @SubscribeEvent(priority = EventPriority.LOWEST) public void attackEntityEvent(AttackEntityEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void livingHurtEvent(LivingHurtEvent e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void entityInteractEvent(EntityInteractEvent e) { // TODO } /* ------------------------------------------------------------ */ /* Spawn events */ @SubscribeEvent(priority = EventPriority.LOWEST) public void checkSpawnEvent(CheckSpawn e) { // TODO } @SubscribeEvent(priority = EventPriority.LOWEST) public void specialSpawnEvent(SpecialSpawn e) { // TODO } }