diff --git a/.gitignore b/.gitignore index b7371f7a4..c22de3b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ ganttzk/target .project .settings/ -# ignore IDEA configuration files +# ignore Intellij IDEA configuration files .idea *.iml diff --git a/libreplan-business/src/main/java/org/libreplan/business/common/VersionInformation.java b/libreplan-business/src/main/java/org/libreplan/business/common/VersionInformation.java index 414c0b505..6abf8f22a 100644 --- a/libreplan-business/src/main/java/org/libreplan/business/common/VersionInformation.java +++ b/libreplan-business/src/main/java/org/libreplan/business/common/VersionInformation.java @@ -36,6 +36,7 @@ import org.apache.commons.logging.LogFactory; * published. It checks the last version against a URL. * * @author Susana Montes Pedreira + * @author Vova Perebykivskiy */ public class VersionInformation { @@ -118,14 +119,12 @@ public class VersionInformation { /** * Returns true if a new version of the project is published. * - * @param allowToGatherUsageStatsEnabled + * @param isCheckNewVersionEnabled * If true LibrePlan developers will process the requests to check * the new versions to generate usages statistics */ - public static boolean isNewVersionAvailable( - boolean allowToGatherUsageStatsEnabled) { - return singleton - .checkIsNewVersionAvailable(allowToGatherUsageStatsEnabled); + public static boolean isNewVersionAvailable(boolean isCheckNewVersionEnabled) { + return singleton.checkIsNewVersionAvailable(isCheckNewVersionEnabled); } /** @@ -133,13 +132,12 @@ public class VersionInformation { * Otherwise, during one day it returns the cached value. And it checks it * again after that time. */ - private boolean checkIsNewVersionAvailable( - boolean allowToGatherUsageStatsEnabled) { - if (!newVersionCached) { + private boolean checkIsNewVersionAvailable(boolean isCheckNewVersionEnabled) { + if ( !newVersionCached ) { long oneDayLater = lastVersionCachedDate.getTime() + DELAY_TO_CHECK_URL; - if (oneDayLater < new Date().getTime()) { - loadNewVersionFromURL(allowToGatherUsageStatsEnabled); + if ( oneDayLater < new Date().getTime() ) { + loadNewVersionFromURL(isCheckNewVersionEnabled); } } return newVersionCached; diff --git a/libreplan-business/src/main/java/org/libreplan/business/common/entities/Configuration.java b/libreplan-business/src/main/java/org/libreplan/business/common/entities/Configuration.java index e6a811070..f78c4fe3a 100644 --- a/libreplan-business/src/main/java/org/libreplan/business/common/entities/Configuration.java +++ b/libreplan-business/src/main/java/org/libreplan/business/common/entities/Configuration.java @@ -37,6 +37,7 @@ import org.libreplan.business.costcategories.entities.TypeOfWorkHours; * @author Cristina Alvarino Perez * @author Ignacio Diaz Teijido * @author Susana Montes Pedreira + * @author Vova Perebykivskiy */ public class Configuration extends BaseEntity { @@ -102,7 +103,7 @@ public class Configuration extends BaseEntity { private Boolean checkNewVersionEnabled = true; - private Boolean allowToGatherUsageStatsEnabled = false; + private Boolean allowToGatherUsageStatsEnabled = true; private Boolean generateCodeForExpenseSheets = true; diff --git a/libreplan-business/src/main/java/org/libreplan/business/users/daos/UserDAO.java b/libreplan-business/src/main/java/org/libreplan/business/users/daos/UserDAO.java index 2f3bab8cf..2418f0987 100644 --- a/libreplan-business/src/main/java/org/libreplan/business/users/daos/UserDAO.java +++ b/libreplan-business/src/main/java/org/libreplan/business/users/daos/UserDAO.java @@ -182,6 +182,7 @@ public class UserDAO extends GenericDAOHibernate } @Override + @Transactional(readOnly = true) public Number getRowCount() { return (Number) getSession().createCriteria(User.class).setProjection(Projections.rowCount()).uniqueResult(); } diff --git a/libreplan-business/src/main/resources/libreplan.properties b/libreplan-business/src/main/resources/libreplan.properties new file mode 100644 index 000000000..6955f5c72 --- /dev/null +++ b/libreplan-business/src/main/resources/libreplan.properties @@ -0,0 +1 @@ +statsPage http://stats.libreplan-enterprise.com/ \ No newline at end of file diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/common/ConfigurationController.java b/libreplan-webapp/src/main/java/org/libreplan/web/common/ConfigurationController.java index 45f5b8da1..8c69d2eea 100644 --- a/libreplan-webapp/src/main/java/org/libreplan/web/common/ConfigurationController.java +++ b/libreplan-webapp/src/main/java/org/libreplan/web/common/ConfigurationController.java @@ -31,6 +31,8 @@ import java.util.ArrayList; import java.util.Set; import java.util.Map; import java.util.Collections; +import java.util.Comparator; +import java.util.ConcurrentModificationException; import java.util.HashSet; import java.util.Arrays; @@ -49,6 +51,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.cxf.jaxrs.client.WebClient; import org.libreplan.business.calendars.entities.BaseCalendar; +import org.libreplan.business.common.daos.IConfigurationDAO; import org.libreplan.business.common.entities.Configuration; import org.libreplan.business.common.entities.Connector; import org.libreplan.business.common.entities.ConnectorProperty; @@ -61,10 +64,12 @@ import org.libreplan.business.common.entities.PredefinedConnectors; import org.libreplan.business.common.entities.ProgressType; import org.libreplan.business.common.exceptions.ValidationException; import org.libreplan.business.costcategories.entities.TypeOfWorkHours; +import org.libreplan.business.users.daos.IUserDAO; import org.libreplan.business.users.entities.UserRole; import org.libreplan.importers.JiraRESTClient; import org.libreplan.importers.TimSoapClient; import org.libreplan.web.common.components.bandboxsearch.BandboxSearch; +import org.libreplan.web.orders.IOrderModel; import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.DefaultDirObjectFactory; @@ -125,6 +130,12 @@ public class ConfigurationController extends GenericForwardComposer { private IConfigurationModel configurationModel; + private IConfigurationDAO configurationDAO; + + private IUserDAO userDAO; + + private IOrderModel orderModel; + private IMessagesForUser messages; private Component messagesContainer; @@ -157,6 +168,8 @@ public class ConfigurationController extends GenericForwardComposer { private Textbox emailSenderTextbox; + private boolean isGatheredStatsAlreadySent = false; + @Override public void doAfterCompose(Component comp) throws Exception { super.doAfterCompose(comp); @@ -242,24 +255,34 @@ public class ConfigurationController extends GenericForwardComposer { public void save() throws InterruptedException { - if ( getSelectedConnector() != null && getSelectedConnector().getName().equals("E-mail") && - isEmailFieldsValid() == false ) { + if (getSelectedConnector() != null && getSelectedConnector().getName().equals("E-mail") && + isEmailFieldsValid() == false) { messages.showMessage(Level.ERROR, _("Check all fields")); } else { - ConstraintChecker.isValid(configurationWindow); - if (checkValidEntitySequenceRows()) { - try { - configurationModel.confirm(); - configurationModel.init(); - messages.showMessage(Level.INFO, _("Changes saved")); - if (getSelectedConnector() != null - && !configurationModel - .scheduleOrUnscheduleJobs(getSelectedConnector())) { - messages.showMessage( - Level.ERROR, - _("Scheduling or unscheduling of jobs for this connector is not completed")); - } + ConstraintChecker.isValid(configurationWindow); + if (checkValidEntitySequenceRows()) { + try { + configurationModel.confirm(); + configurationModel.init(); + messages.showMessage(Level.INFO, _("Changes saved")); + + // Send data to server + if (!isGatheredStatsAlreadySent && configurationDAO.getConfigurationWithReadOnlyTransaction() + .isAllowToGatherUsageStatsEnabled()) { + GatheredUsageStats gatheredUsageStats = new GatheredUsageStats(); + gatheredUsageStats.setupNotAutowiredClasses(userDAO, orderModel); + gatheredUsageStats.sendGatheredUsageStatsToServer(); + isGatheredStatsAlreadySent = true; + } + + if (getSelectedConnector() != null + && !configurationModel + .scheduleOrUnscheduleJobs(getSelectedConnector())) { + messages.showMessage( + Level.ERROR, + _("Scheduling or unscheduling of jobs for this connector is not completed")); + } reloadWindow(); reloadEntitySequences(); reloadConnectors(); @@ -274,7 +297,6 @@ public class ConfigurationController extends GenericForwardComposer { } } } - } public void cancel() throws InterruptedException { @@ -1186,7 +1208,6 @@ public class ConfigurationController extends GenericForwardComposer { private void appendValueTextbox(Row row, final ConnectorProperty property) { - final Textbox textbox = new Textbox(); textbox.setWidth("400px"); textbox.setConstraint(checkPropertyValue(property)); diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/common/GatheredUsageStats.java b/libreplan-webapp/src/main/java/org/libreplan/web/common/GatheredUsageStats.java new file mode 100644 index 000000000..c8532d396 --- /dev/null +++ b/libreplan-webapp/src/main/java/org/libreplan/web/common/GatheredUsageStats.java @@ -0,0 +1,185 @@ +package org.libreplan.web.common; + +import org.libreplan.business.common.VersionInformation; +import org.libreplan.business.users.daos.IUserDAO; +import org.libreplan.web.orders.IOrderModel; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.zkoss.json.JSONObject; +import org.zkoss.zk.ui.Execution; +import org.zkoss.zk.ui.Executions; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; + +/** + * Class represents all data that will be sent to server. + * + * If you want to use it, just create a new object and set 2 private variables. + * All needed data will be already in object. + * + * Created by + * @author Vova Perebykivskiy + * on 02/08/2016. + */ + +public class GatheredUsageStats { + + private IUserDAO userDAO; + + private IOrderModel orderModel; + + + // Version of this statistics implementation + private int jsonObjectVersion = 1; + + // Unique system identifier (MD5 - ip + hostname) + private String id; + + // Version of LibrePlan + private String version = VersionInformation.getVersion(); + + // Number of users in application + private Number users; + + // Number of projects in application + private int projects; + + private Number getUserRows(){ + return userDAO.getRowCount(); + } + + private String generateID(){ + // Make hash of ip + hostname + WebAuthenticationDetails details = (WebAuthenticationDetails) SecurityContextHolder.getContext() + .getAuthentication().getDetails(); + String ip = details.getRemoteAddress(); + + Execution execution = Executions.getCurrent(); + String hostname = execution.getServerName(); + + String message = ip + hostname; + byte[] encoded = null; + StringBuffer sb = null; + + try { + byte[] bytesOfMessage = message.getBytes("UTF-8"); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + encoded = md5.digest(bytesOfMessage); + + // Convert bytes to hex format + sb = new StringBuffer(); + for (int i = 0; i < encoded.length; i++) sb.append(Integer.toString((encoded[i] & 0xff) + 0x100, 16) + .substring(1)); + + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return sb.toString(); + } + + // It needed because i do not need to call default constructor on Autowiring + private void myConstructor(){ + setId(generateID()); + setUsers(getUserRows()); + setProjects(orderModel.getOrders().size()); + } + + public void sendGatheredUsageStatsToServer(){ + JSONObject json = new JSONObject(); + + json.put("json-version", jsonObjectVersion); + json.put("id", id); + json.put("version", version); + json.put("users", users); + json.put("projects", projects); + + HttpURLConnection connection = null; + + Properties properties = new Properties(); + InputStream inputStream = null; + + try { + String filename = "libreplan.properties"; + inputStream = GatheredUsageStats.class.getClassLoader().getResourceAsStream(filename); + properties.load(inputStream); + + URL url = new URL(properties.getProperty("statsPage")); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/x-www-urlencoded"); + connection.setRequestProperty("Content-Language", "en-GB"); + connection.setRequestProperty("Content-Length", Integer.toString(json.toJSONString().getBytes().length)); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + // If the connection lasts > 2 sec throw Exception + connection.setConnectTimeout(2000); + + // Send request + DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); + dataOutputStream.writeBytes(json.toJSONString()); + dataOutputStream.flush(); + dataOutputStream.close(); + + // No needed code, but it is not working without id + connection.getInputStream(); + + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + finally { + if ( connection != null ) { + connection.disconnect(); + } + } + } + + public void setupNotAutowiredClasses(IUserDAO userDAO, IOrderModel orderModel){ + this.userDAO = userDAO; + this.orderModel = orderModel; + myConstructor(); + } + + public int getJsonObjectVersion() { + return jsonObjectVersion; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getVersion() { + return version; + } + + public Number getUsers() { + return users; + } + public void setUsers(Number users) { + this.users = users; + } + + public int getProjects() { + return projects; + } + public void setProjects(int projects) { + this.projects = projects; + } +} diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/common/TemplateController.java b/libreplan-webapp/src/main/java/org/libreplan/web/common/TemplateController.java index c3ea12442..90f85d5ff 100644 --- a/libreplan-webapp/src/main/java/org/libreplan/web/common/TemplateController.java +++ b/libreplan-webapp/src/main/java/org/libreplan/web/common/TemplateController.java @@ -72,7 +72,7 @@ public class TemplateController extends GenericForwardComposer { if (templateModel.isScenariosVisible()) { window = (Window) comp.getFellow("changeScenarioWindow"); windowMessages = new MessagesForUser(window - .getFellow("messagesContainer")); + .getFellow("messagesContainer")); } } @@ -219,12 +219,11 @@ public class TemplateController extends GenericForwardComposer { } public boolean isNewVersionAvailable() { - if (!templateModel.isCheckNewVersionEnabled()) { + if ( !templateModel.isCheckNewVersionEnabled() ) { return false; } - return VersionInformation.isNewVersionAvailable(templateModel - .isAllowToGatherUsageStatsEnabled()); + return VersionInformation.isNewVersionAvailable(templateModel.isCheckNewVersionEnabled()); } public String getUsername() { diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/planner/tabs/MultipleTabsPlannerController.java b/libreplan-webapp/src/main/java/org/libreplan/web/planner/tabs/MultipleTabsPlannerController.java index 1ea0f107a..e73dcfb8a 100644 --- a/libreplan-webapp/src/main/java/org/libreplan/web/planner/tabs/MultipleTabsPlannerController.java +++ b/libreplan-webapp/src/main/java/org/libreplan/web/planner/tabs/MultipleTabsPlannerController.java @@ -35,8 +35,10 @@ import org.libreplan.business.orders.entities.OrderElement; import org.libreplan.business.planner.entities.TaskElement; import org.libreplan.business.resources.daos.IResourcesSearcher; import org.libreplan.business.templates.entities.OrderTemplate; +import org.libreplan.business.users.daos.IUserDAO; import org.libreplan.business.users.entities.UserRole; import org.libreplan.web.common.ConfirmCloseUtil; +import org.libreplan.web.common.GatheredUsageStats; import org.libreplan.web.common.entrypoints.EntryPointsHandler; import org.libreplan.web.common.entrypoints.URLHandlerRegistry; import org.libreplan.web.dashboard.DashboardController; @@ -44,6 +46,7 @@ import org.libreplan.web.dashboard.DashboardControllerGlobal; import org.libreplan.web.limitingresources.LimitingResourcesController; import org.libreplan.web.logs.LogsController; import org.libreplan.web.montecarlo.MonteCarloController; +import org.libreplan.web.orders.IOrderModel; import org.libreplan.web.orders.OrderCRUDController; import org.libreplan.web.planner.allocation.AdvancedAllocationController.IBack; import org.libreplan.web.planner.company.CompanyPlanningController; @@ -76,18 +79,16 @@ import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.util.Composer; -import javax.swing.*; - /** * Creates and handles several tabs * * @author Óscar González Fernández * @author Lorenzo Tilve Álvaro + * @author Vova Perebykivskiy */ @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class - MultipleTabsPlannerController implements Composer, +public class MultipleTabsPlannerController implements Composer, IGlobalViewEntryPoints { public static String WELCOME_URL = "-- no URL provided --"; @@ -219,6 +220,15 @@ public class @Autowired private URLHandlerRegistry registry; + // Cannot Autowire it in GatheredUsageStats class + @Autowired + private IUserDAO userDAO; + + @Autowired + private IOrderModel orderModel; + + private boolean isGatheredStatsAlreadySent = false; + private TabsConfiguration buildTabsConfiguration(final Desktop desktop) { Map parameters = getURLQueryParametersMap(); @@ -266,7 +276,7 @@ public class @Override public void goToTaskResourceAllocation(Order order, - TaskElement task) { + TaskElement task) { orderPlanningController.setShowedTask(task); orderPlanningController.setCurrentControllerToShow(orderPlanningController.getEditTaskController()); getTabsRegistry() @@ -499,6 +509,13 @@ public class } } + + if ( !isGatheredStatsAlreadySent && configurationDAO.getConfiguration().isAllowToGatherUsageStatsEnabled() ){ + GatheredUsageStats gatheredUsageStats = new GatheredUsageStats(); + gatheredUsageStats.setupNotAutowiredClasses(userDAO, orderModel); + gatheredUsageStats.sendGatheredUsageStatsToServer(); + isGatheredStatsAlreadySent = true; + } } private TabsRegistry getTabsRegistry() { @@ -516,11 +533,6 @@ public class getTabsRegistry().show(resourceLoadTab); } - /*@Override - public void goToLogs() { - getTabsRegistry().show(logsTab); - }*/ - @Override public void goToCompanyLimitingResources() { getTabsRegistry().show(limitingResourcesTab);