diff --git a/designer-base/src/main/java/com/fr/design/mainframe/DesktopCardPane.java b/designer-base/src/main/java/com/fr/design/mainframe/DesktopCardPane.java index a5f4a72ac7..58cd014e0a 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/DesktopCardPane.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/DesktopCardPane.java @@ -35,6 +35,7 @@ public class DesktopCardPane extends BasicPane implements TargetModifiedListener } DesignerFrameFileDealerPane.getInstance().setCurrentEditingTemplate(jt); if (component != null) { + component.onLostFocus(); remove(component); } add(component = jt, BorderLayout.CENTER); @@ -42,6 +43,7 @@ public class DesktopCardPane extends BasicPane implements TargetModifiedListener repaint(); revalidate(); component.requestGridFocus(); + component.onGetFocus(); } protected JTemplate getSelectedJTemplate() { diff --git a/designer-base/src/main/java/com/fr/design/mainframe/JTemplate.java b/designer-base/src/main/java/com/fr/design/mainframe/JTemplate.java index 9708dcb65a..9b6a761726 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/JTemplate.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/JTemplate.java @@ -36,6 +36,7 @@ import com.fr.design.i18n.Toolkit; import com.fr.design.layout.FRGUIPaneFactory; import com.fr.design.mainframe.template.info.TemplateInfoCollector; import com.fr.design.mainframe.template.info.TemplateProcessInfo; +import com.fr.design.mainframe.template.info.TimeConsumeTimer; import com.fr.design.mainframe.toolbar.ToolBarMenuDockPlus; import com.fr.design.mainframe.toolbar.VcsScene; import com.fr.design.menu.MenuDef; @@ -78,7 +79,6 @@ import java.util.regex.Pattern; public abstract class JTemplate> extends TargetComponent implements ToolBarMenuDockPlus, DesignerProxy { // TODO ALEX_SEP editingFILE这个属性一定要吗?如果非要不可,有没有可能保证不为null private static final int PREFIX_NUM = 3000; - private static final int ONE_THOUSAND = 1000; private FILE editingFILE = null; // alex:初始状态为saved,这样不管是新建模板,还是打开模板,如果未做任何操作直接关闭,不提示保存 private boolean saved = true; @@ -92,7 +92,7 @@ public abstract class JTemplate> private static short currentIndex = 0;// 此变量用于多次新建模板时,让名字不重复 private DesignModelAdapter designModel; private PreviewProvider previewType; - private long openTime = 0L; // 打开模板的时间点(包括新建模板) + private TimeConsumeTimer consumeTimer = new TimeConsumeTimer(); public int resolution = ScreenResolution.getScreenResolution(); public JTemplate() { @@ -100,7 +100,6 @@ public abstract class JTemplate> public JTemplate(T t, String defaultFileName) { this(t, new MemFILE(newTemplateNameByIndex(defaultFileName)), true); - openTime = System.currentTimeMillis(); } public JTemplate(T t, FILE file) { @@ -123,10 +122,24 @@ public abstract class JTemplate> this.add(createCenterPane(), BorderLayout.CENTER); this.undoState = createUndoState(); designModel = createDesignModel(); - // 如果不是新建模板,并且在收集列表中 - if (!isNewFile && TemplateInfoCollector.getInstance().contains(t.getTemplateID())) { - openTime = System.currentTimeMillis(); + + consumeTimer.setEnabled(shouldInitForCollectInfo(isNewFile)); + } + + void onGetFocus() { + consumeTimer.start(); + } + + void onLostFocus() { + consumeTimer.stop(); + } + + private boolean shouldInitForCollectInfo(boolean isNewFile) { + if (isNewFile) { + return true; } + // 不是新建模板,但是已经在收集列表中 + return TemplateInfoCollector.getInstance().contains(template.getTemplateID()); } // 刷新右侧属性面板 @@ -139,9 +152,8 @@ public abstract class JTemplate> // 为收集模版信息作准备 private void initForCollect() { generateTemplateId(); - if (openTime == 0) { - openTime = System.currentTimeMillis(); - } + consumeTimer.setEnabled(true); + consumeTimer.start(); } private void collectInfo() { // 执行收集操作 @@ -149,16 +161,15 @@ public abstract class JTemplate> } private void collectInfo(String originID) { // 执行收集操作 - if (openTime == 0) { // 旧模板,不收集数据 + if (!consumeTimer.isEnabled()) { return; } - long saveTime = System.currentTimeMillis(); // 保存模板的时间点 try { - long timeConsume = ((saveTime - openTime) / ONE_THOUSAND); // 制作模板耗时(单位:s) - TemplateInfoCollector.getInstance().collectInfo(template.getTemplateID(), originID, getProcessInfo(), (int)timeConsume); + int timeConsume = consumeTimer.popTime(); + TemplateInfoCollector.getInstance().collectInfo(template.getTemplateID(), originID, getProcessInfo(), timeConsume); } catch (Throwable th) { // 不管收集过程中出现任何异常,都不应该影响模版保存 } - openTime = saveTime; // 更新 openTime,准备下一次计算 + consumeTimer.start(); // 准备下一次计算 } public abstract TemplateProcessInfo getProcessInfo(); diff --git a/designer-base/src/main/java/com/fr/design/mainframe/template/info/DesignerOpenHistory.java b/designer-base/src/main/java/com/fr/design/mainframe/template/info/DesignerOpenHistory.java new file mode 100644 index 0000000000..096b6d16ae --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/mainframe/template/info/DesignerOpenHistory.java @@ -0,0 +1,117 @@ +package com.fr.design.mainframe.template.info; + +import com.fr.general.ComparatorUtils; +import com.fr.log.FineLoggerFactory; +import com.fr.stable.StringUtils; +import com.fr.stable.xml.XMLPrintWriter; +import com.fr.stable.xml.XMLReadable; +import com.fr.stable.xml.XMLWriter; +import com.fr.stable.xml.XMLableReader; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * 管理打开设计器的日期记录 + * Created by plough on 2019/4/19. + */ +class DesignerOpenHistory implements XMLReadable, XMLWriter { + static final String XML_TAG = "DesignerOpenHistory"; + private static DesignerOpenHistory singleton; + private static final String SPLITTER = ","; + private static final int LENGTH = 3; // 保留最近 3 次的记录 + // 最近的日期是 history[0],最早的日期是 history[LENGTH-1] + private String[] history = new String[LENGTH]; + + private DesignerOpenHistory() { + for (int i = 0; i < LENGTH; i++) { + history[i] = StringUtils.EMPTY; + } + } + + static DesignerOpenHistory getInstance() { + if (singleton == null) { + singleton = new DesignerOpenHistory(); + } + return singleton; + } + + void update() { + String today = getToday(); + if (ComparatorUtils.equals(history[0], today)) { + return; + } + shiftByOne(); + history[0] = today; + } + + /** + * 获取历史记录中囊括的日子数。即最早的历史记录 history[LENGTH - 1],到最晚的记录 history[0] 之间的时间跨度 + */ + int getHistorySpanDayCount() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + try { + Date earliestDate = sdf.parse(getEarliestDate()); + Date latestDate = sdf.parse(history[0]); + long diffInMillies = latestDate.getTime() - earliestDate.getTime(); + return (int)TimeUnit.DAYS.convert(diffInMillies,TimeUnit.MILLISECONDS) + 1; + } catch (ParseException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return 1; + } + + /** + * 今天是否已经打开过设计器 + */ + boolean hasOpenedToday() { + String today = getToday(); + return ComparatorUtils.equals(today, history[0]); + } + + private String getEarliestDate() { + for (int i = LENGTH - 1; i >= 0; i--) { + if (StringUtils.isNotEmpty(history[i])) { + return history[i]; + } + } + throw new AssertionError("Designer open history is empty!"); + } + + private void shiftByOne() { + for (int i = LENGTH - 1; i > 0; i--) { + history[i] = history[i-1]; + } + } + + private String getToday() { + return new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime()); + } + + @Override + public String toString() { + return StringUtils.join(SPLITTER, history); + } + + private void parseString(String s) { + String[] arr = s.split(SPLITTER); + System.arraycopy(arr, 0, history, 0, arr.length); + } + + @Override + public void readXML(XMLableReader reader) { + if (XML_TAG.equals(reader.getTagName())) { + parseString(reader.getElementValue()); + } + } + + @Override + public void writeXML(XMLPrintWriter writer) { + writer.startTAG(XML_TAG); + writer.textNode(toString()); + writer.end(); + } +} diff --git a/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfo.java b/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfo.java index af644a1e81..4c45ce823e 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfo.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfo.java @@ -203,7 +203,11 @@ class TemplateInfo implements XMLReadable, XMLWriter { } boolean isComplete() { - return idleDayCount > COMPLETE_DAY_COUNT; + // 条件 1. 超过15天未编辑 + // 条件 2. 设计器在这段未编辑的时间内启动超过 X 次(目前定的 X = 3)。即"设计器最近 X 次启动的时间跨度" < "未编辑时间"; + + return idleDayCount > COMPLETE_DAY_COUNT + && DesignerOpenHistory.getInstance().getHistorySpanDayCount() < idleDayCount; } String getConsumingMapJsonString() { @@ -245,4 +249,8 @@ class TemplateInfo implements XMLReadable, XMLWriter { void addIdleDayCountByOne() { this.idleDayCount += 1; } + + int getIdleDayCount() { + return this.idleDayCount; + } } \ No newline at end of file diff --git a/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfoCollector.java b/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfoCollector.java index 5d5e5dd5bc..30bfb5e1d6 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfoCollector.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/template/info/TemplateInfoCollector.java @@ -2,7 +2,6 @@ package com.fr.design.mainframe.template.info; import com.fr.base.FRContext; import com.fr.design.DesignerEnvManager; -import com.fr.general.ComparatorUtils; import com.fr.log.FineLoggerFactory; import com.fr.stable.ProductConstants; import com.fr.stable.StableUtils; @@ -19,9 +18,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Calendar; import java.util.HashMap; import java.util.Map; @@ -31,17 +28,23 @@ import java.util.Map; */ public class TemplateInfoCollector implements XMLReadable, XMLWriter { private static final String XML_TAG = "TplInfo"; - private static final String XML_DESIGNER_OPEN_DATE = "DesignerOpenDate"; private static final String XML_TEMPLATE_INFO_LIST = "TemplateInfoList"; private static final String XML_FILE_NAME = "tpl.info"; private static TemplateInfoCollector instance; private Map templateInfoMap; - private String designerOpenDate; //设计器最近一次打开日期 + private DesignerOpenHistory designerOpenHistory; private TemplateInfoCollector() { init(); } + private void init() { + templateInfoMap = new HashMap<>(); + designerOpenHistory = DesignerOpenHistory.getInstance(); + + loadFromFile(); + } + public static TemplateInfoCollector getInstance() { if (instance == null) { instance = new TemplateInfoCollector(); @@ -96,6 +99,7 @@ public class TemplateInfoCollector implements XMLReadable, XMLWriter { * 发送本地模板信息到服务器,并清空已发送模版的本地记录 */ public void sendTemplateInfo() { + // 每次启动设计器后,都会执行这个函数(被 InformationCollector 的 collectStartTime 调用) addIdleDayCount(); removeTestTemplates(); @@ -120,13 +124,6 @@ public class TemplateInfoCollector implements XMLReadable, XMLWriter { return new File(StableUtils.pathJoin(ProductConstants.getEnvHome(), XML_FILE_NAME)); } - private void init() { - templateInfoMap = new HashMap<>(); - setDesignerOpenDate(); - - loadFromFile(); - } - void loadFromFile() { if (!getInfoFile().exists()) { return; @@ -150,21 +147,6 @@ public class TemplateInfoCollector implements XMLReadable, XMLWriter { return templateInfo; } - /** - * 把设计器最近打开日期设定为当前日期 - */ - private void setDesignerOpenDate() { - designerOpenDate = new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime()); - } - - /** - * 判断今天是否第一次打开设计器,为了防止同一天内,多次 addIdleDayCount - */ - private boolean designerOpenFirstTime() { - String today = new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime()); - return !ComparatorUtils.equals(today, designerOpenDate); - } - private boolean shouldCollectInfo() { //只收集本地环境的 if (!WorkContext.getCurrent().isLocal()) { @@ -189,12 +171,14 @@ public class TemplateInfoCollector implements XMLReadable, XMLWriter { * 更新 day_count:打开设计器却未编辑模板的连续日子 */ private void addIdleDayCount() { - if (designerOpenFirstTime()) { - for (TemplateInfo templateInfo : templateInfoMap.values()) { - templateInfo.addIdleDayCountByOne(); - } - setDesignerOpenDate(); + // 判断今天是否第一次打开设计器,为了防止同一天内,多次 addIdleDayCount + if (designerOpenHistory.hasOpenedToday()) { + return; } + for (TemplateInfo templateInfo : templateInfoMap.values()) { + templateInfo.addIdleDayCountByOne(); + } + designerOpenHistory.update(); } // 删除所有已完成的测试模版 @@ -220,8 +204,8 @@ public class TemplateInfoCollector implements XMLReadable, XMLWriter { if (reader.isChildNode()) { try { String name = reader.getTagName(); - if (XML_DESIGNER_OPEN_DATE.equals(name)) { - this.designerOpenDate = reader.getElementValue(); + if (DesignerOpenHistory.XML_TAG.equals(name)) { + reader.readXMLObject(designerOpenHistory); } else if (TemplateInfo.XML_TAG.equals(name)) { TemplateInfo templateInfo = TemplateInfo.newInstanceByRead(reader); templateInfoMap.put(templateInfo.getTemplateID(), templateInfo); @@ -236,9 +220,7 @@ public class TemplateInfoCollector implements XMLReadable, XMLWriter { public void writeXML(XMLPrintWriter writer) { writer.startTAG(XML_TAG); - writer.startTAG(XML_DESIGNER_OPEN_DATE); - writer.textNode(designerOpenDate); - writer.end(); + designerOpenHistory.writeXML(writer); writer.startTAG(XML_TEMPLATE_INFO_LIST); for (TemplateInfo templateInfo : templateInfoMap.values()) { diff --git a/designer-base/src/main/java/com/fr/design/mainframe/template/info/TimeConsumeTimer.java b/designer-base/src/main/java/com/fr/design/mainframe/template/info/TimeConsumeTimer.java new file mode 100644 index 0000000000..527341411b --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/mainframe/template/info/TimeConsumeTimer.java @@ -0,0 +1,69 @@ +package com.fr.design.mainframe.template.info; + +/** + * Created by plough on 2019/4/19. + */ +public class TimeConsumeTimer { + private static final int ONE_THOUSAND = 1000; + private enum State { + RUNNING, STOPPED + } + private int timeConsume; // 单位 s + private long startMS; // 单位 ms + private long stopMS; + private State state; + private boolean enabled; + + public TimeConsumeTimer() { + reset(); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void start() { + if (!isEnabled() || isRunning()) { + return; + } + startMS = System.currentTimeMillis(); + state = State.RUNNING; + } + + public void stop() { + if (!isEnabled() || !isRunning()) { + return; + } + stopMS = System.currentTimeMillis(); + + timeConsume += ((stopMS - startMS) / ONE_THOUSAND); + startMS = 0; + stopMS = 0; + state = State.STOPPED; + } + + public int popTime() { + if (!isEnabled()) { + return 0; + } + stop(); + int result = timeConsume; + reset(); + return result; + } + + private boolean isRunning() { + return state == State.RUNNING; + } + + private void reset() { + timeConsume = 0; + startMS = 0; + stopMS = 0; + state = State.STOPPED; + } +} diff --git a/designer-base/src/test/java/com/fr/design/mainframe/template/info/DesignerOpenHistoryTest.java b/designer-base/src/test/java/com/fr/design/mainframe/template/info/DesignerOpenHistoryTest.java new file mode 100644 index 0000000000..e3f63ddc39 --- /dev/null +++ b/designer-base/src/test/java/com/fr/design/mainframe/template/info/DesignerOpenHistoryTest.java @@ -0,0 +1,108 @@ +package com.fr.design.mainframe.template.info; + +import com.fr.invoke.Reflect; +import com.fr.stable.StringUtils; +import com.fr.stable.xml.XMLPrintWriter; +import com.fr.stable.xml.XMLableReader; +import com.fr.third.javax.xml.stream.XMLStreamException; +import org.junit.Before; +import org.junit.Test; + +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Created by plough on 2019/4/21. + */ +public class DesignerOpenHistoryTest { + private DesignerOpenHistory openHistory; + private String[] mockHistory = new String[] { + "2019-04-08", "2019-04-03", "2019-03-29" + }; + + @Before + public void setUp() { + openHistory = DesignerOpenHistory.getInstance(); + Reflect.on(openHistory).set("history", mockHistory); + } + + @Test + public void testReadAndWrite() throws XMLStreamException { + // 写入 xml + StringWriter sw = new StringWriter(); + XMLPrintWriter writer = XMLPrintWriter.create(new PrintWriter(sw)); + openHistory.writeXML(writer); + writer.flush(); + writer.close(); + + String xmlText = sw.getBuffer().toString(); + + // 临时修改配置 + Reflect.on(openHistory).set("history", new String[] {"", "", ""}); + + // 从 xml 中读取 + StringReader sr = new StringReader(xmlText); + XMLableReader xmlReader = XMLableReader.createXMLableReader(sr); + xmlReader.readXMLObject(openHistory); + + // 验证:与写入时的配置一致 + assertArrayEquals(mockHistory, (String[])Reflect.on(openHistory).field("history").get()); + } + + @Test + public void testToString() { + assertEquals("2019-04-08,2019-04-03,2019-03-29", openHistory.toString()); + } + + @Test + public void testParseString() { + String[] mockDates = {"2020-04-08", "2019-04-03", "2016-03-29"}; + Reflect.on(openHistory).call("parseString", StringUtils.join(",", mockDates)); + assertArrayEquals(mockDates, (String[])Reflect.on(openHistory).field("history").get()); + } + + @Test + public void testGetHistorySpanDayCount() { + assertEquals(11, openHistory.getHistorySpanDayCount()); + + Reflect.on(openHistory).set("history", new String[] {"2019-05-03", "2019-05-02", ""}); + assertEquals(2, openHistory.getHistorySpanDayCount()); + + Reflect.on(openHistory).set("history", new String[] {"2019-05-03", "", ""}); + assertEquals(1, openHistory.getHistorySpanDayCount()); + + try { + Reflect.on(openHistory).set("history", new String[] {"", "", ""}); + fail("should not be here"); + } catch (AssertionError ignore) { + } + } + + @Test + public void testHasOpenedToday() { + assertFalse(openHistory.hasOpenedToday()); + + Reflect.on(openHistory).set("history", new String[] {getToday(), "2019-02-02", ""}); + assertTrue(openHistory.hasOpenedToday()); + } + + @Test + public void testUpdate() { + openHistory.update(); + String[] arr = { getToday(), "2019-04-08", "2019-04-03" }; + assertArrayEquals(arr, (String[])Reflect.on(openHistory).field("history").get()); + } + + private String getToday() { + return new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime()); + } +} diff --git a/designer-base/src/test/java/com/fr/design/mainframe/template/info/TemplateInfoCollectorTest.java b/designer-base/src/test/java/com/fr/design/mainframe/template/info/TemplateInfoCollectorTest.java index eb9aa23468..7b02f4bba2 100644 --- a/designer-base/src/test/java/com/fr/design/mainframe/template/info/TemplateInfoCollectorTest.java +++ b/designer-base/src/test/java/com/fr/design/mainframe/template/info/TemplateInfoCollectorTest.java @@ -150,4 +150,20 @@ public class TemplateInfoCollectorTest { assertEquals(329, consumingMap.get("time_consume")); assertEquals(129, consumingMap.get("originTime")); } + + @Test + public void testAddIdleDateCount() { + String templateID = "16a988ce-8529-42f5-b17c-2ee849355071"; + TemplateInfoCollector collecter = TemplateInfoCollector.getInstance(); + TemplateInfo templateInfo = collecter.getOrCreateTemplateInfoByID(templateID); + + assertEquals(9, templateInfo.getIdleDayCount()); + + Reflect.on(collecter).call("addIdleDayCount"); + assertEquals(10, templateInfo.getIdleDayCount()); + + // 同一天内多次调用无效 + Reflect.on(collecter).call("addIdleDayCount"); + assertEquals(10, templateInfo.getIdleDayCount()); + } } diff --git a/designer-base/src/test/java/com/fr/design/mainframe/template/info/TimeConsumeTimerTest.java b/designer-base/src/test/java/com/fr/design/mainframe/template/info/TimeConsumeTimerTest.java new file mode 100644 index 0000000000..5dcf60e7f6 --- /dev/null +++ b/designer-base/src/test/java/com/fr/design/mainframe/template/info/TimeConsumeTimerTest.java @@ -0,0 +1,62 @@ +package com.fr.design.mainframe.template.info; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Created by plough on 2019/4/19. + */ +public class TimeConsumeTimerTest { + + @Test + public void testNotEnabled() throws InterruptedException { + TimeConsumeTimer consumeTimer = new TimeConsumeTimer(); + consumeTimer.start(); + Thread.sleep(1100); + consumeTimer.stop(); + assertEquals(0, consumeTimer.popTime()); + } + + @Test + public void testEnabled() throws InterruptedException { + TimeConsumeTimer consumeTimer = new TimeConsumeTimer(); + consumeTimer.setEnabled(true); + consumeTimer.start(); + Thread.sleep(1100); + consumeTimer.stop(); + assertEquals(1, consumeTimer.popTime()); + assertEquals(0, consumeTimer.popTime()); + } + + @Test + public void testMultiTimes() throws InterruptedException { + TimeConsumeTimer consumeTimer = new TimeConsumeTimer(); + consumeTimer.setEnabled(true); + + consumeTimer.start(); + Thread.sleep(1010); + consumeTimer.stop(); + + Thread.sleep(2000); + + consumeTimer.start(); + Thread.sleep(1010); + assertEquals(2, consumeTimer.popTime()); + } + + @Test + public void testStartMultiTime() throws InterruptedException { + TimeConsumeTimer consumeTimer = new TimeConsumeTimer(); + consumeTimer.setEnabled(true); + + consumeTimer.start(); + Thread.sleep(1010); + consumeTimer.start(); + Thread.sleep(1010); + consumeTimer.start(); + Thread.sleep(1010); + + assertEquals(3, consumeTimer.popTime()); + } +}