mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
blocks and variants import enhancements
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -29,6 +30,10 @@ public class BuildingBlock {
|
||||
|
||||
private String name;
|
||||
private Collection<IDIYComponent<?>> components;
|
||||
// the dates are set only on factory blocks in import-defaults/blocks.xml, and a withdrawn
|
||||
// factory block carries nothing but its name and deletedOn
|
||||
private LocalDateTime createdOn;
|
||||
private LocalDateTime deletedOn;
|
||||
|
||||
public BuildingBlock(String name, Collection<IDIYComponent<?>> components) {
|
||||
super();
|
||||
@@ -52,6 +57,22 @@ public class BuildingBlock {
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedOn() {
|
||||
return createdOn;
|
||||
}
|
||||
|
||||
public void setCreatedOn(LocalDateTime createdOn) {
|
||||
this.createdOn = createdOn;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedOn() {
|
||||
return deletedOn;
|
||||
}
|
||||
|
||||
public void setDeletedOn(LocalDateTime deletedOn) {
|
||||
this.deletedOn = deletedOn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
||||
@@ -27,6 +27,7 @@ public interface IBlockProcessor {
|
||||
|
||||
public static final String BLOCKS_KEY = "buildingBlocks";
|
||||
public static final String DEFAULT_BLOCKS_IMPORTED_KEY = "defaultBlocksImported";
|
||||
public static final String DEFAULT_BLOCKS_IMPORT_DATE_KEY = "defaultBlocksImportDate";
|
||||
|
||||
/**
|
||||
* Prefix that marks a string as a reference to a building block rather than a component class
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.io.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
@@ -28,6 +29,7 @@ import com.thoughtworks.xstream.io.xml.DomDriver;
|
||||
import com.thoughtworks.xstream.security.AnyTypePermission;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.BuildingBlock;
|
||||
import org.diylc.common.BuildingBlockPackage;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
@@ -42,7 +44,7 @@ public class BuildingBlockManager {
|
||||
|
||||
private static final String BLOCKS_FILE_NAME = "import-defaults/blocks.xml";
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(VariantManager.class);
|
||||
private static final Logger LOG = Logger.getLogger(BuildingBlockManager.class);
|
||||
|
||||
private IConfigurationManager<?> configManager;
|
||||
private XStream xStream;
|
||||
@@ -58,44 +60,80 @@ public class BuildingBlockManager {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void importDefaultBlocks() {
|
||||
// import default templates from variants.xml file only if we didn't do it already
|
||||
if (!configManager.readBoolean(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, false)) {
|
||||
try {
|
||||
InputStream inputStream = Presenter.class.getResourceAsStream("/" + BLOCKS_FILE_NAME);
|
||||
if (inputStream == null) {
|
||||
return;
|
||||
}
|
||||
BufferedInputStream in = new BufferedInputStream(inputStream);
|
||||
Map<String, List<IDIYComponent<?>>> defaults =
|
||||
(Map<String, List<IDIYComponent<?>>>) ProjectFileManager.xStreamSerializer
|
||||
.fromXML(in);
|
||||
in.close();
|
||||
|
||||
Map<String, List<IDIYComponent<?>>> blocksMap =
|
||||
(Map<String, List<IDIYComponent<?>>>) configManager.readObject(IPlugInPort.BLOCKS_KEY,
|
||||
null);
|
||||
if (blocksMap == null)
|
||||
blocksMap = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
|
||||
// merge default blocks with user's
|
||||
for (Map.Entry<String, List<IDIYComponent<?>>> entry : defaults.entrySet()) {
|
||||
if (!blocksMap.containsKey(entry.getKey()))
|
||||
blocksMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// update templates and a flag marking that we imported them
|
||||
configManager.writeValue(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, true);
|
||||
configManager.writeValue(IPlugInPort.BLOCKS_KEY, blocksMap);
|
||||
int importCount = defaults.size();
|
||||
if (importCount > 0) {
|
||||
LOG.info(String.format("Imported %d default building blocks", importCount));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not load default blocks", e);
|
||||
try {
|
||||
InputStream inputStream = Presenter.class.getResourceAsStream("/" + BLOCKS_FILE_NAME);
|
||||
if (inputStream == null) {
|
||||
return;
|
||||
}
|
||||
BufferedInputStream in = new BufferedInputStream(inputStream);
|
||||
List<BuildingBlock> defaults =
|
||||
(List<BuildingBlock>) ProjectFileManager.xStreamSerializer.fromXML(in);
|
||||
in.close();
|
||||
importDefaultBlocks(defaults);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not load default blocks", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
void importDefaultBlocks(List<BuildingBlock> defaults) {
|
||||
LocalDateTime importDate = (LocalDateTime) configManager
|
||||
.readObject(IPlugInPort.DEFAULT_BLOCKS_IMPORT_DATE_KEY, LocalDateTime.MIN);
|
||||
|
||||
Map<String, List<IDIYComponent<?>>> blocksMap =
|
||||
(Map<String, List<IDIYComponent<?>>>) configManager.readObject(IPlugInPort.BLOCKS_KEY,
|
||||
null);
|
||||
if (blocksMap == null) {
|
||||
blocksMap = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
}
|
||||
|
||||
// as with variants, the import date is the latest date in the bundled file rather than the
|
||||
// clock, and never moves backwards
|
||||
LocalDateTime newImportDate = importDate;
|
||||
List<String> deletedNames = new ArrayList<String>();
|
||||
int importCount = 0;
|
||||
|
||||
for (BuildingBlock block : defaults) {
|
||||
if (block.getCreatedOn() != null && block.getCreatedOn().isAfter(newImportDate)) {
|
||||
newImportDate = block.getCreatedOn();
|
||||
}
|
||||
if (block.getDeletedOn() != null && block.getDeletedOn().isAfter(newImportDate)) {
|
||||
newImportDate = block.getDeletedOn();
|
||||
}
|
||||
// a deletion stub keeps nothing but its name, so a missing createdOn only means an old
|
||||
// entry when the block has not been deleted
|
||||
boolean created = block.getCreatedOn() == null ? block.getDeletedOn() == null
|
||||
: importDate.compareTo(block.getCreatedOn()) < 0;
|
||||
boolean deleted =
|
||||
block.getDeletedOn() != null && importDate.compareTo(block.getDeletedOn()) < 0;
|
||||
if (deleted && !created) {
|
||||
if (blocksMap.remove(block.getName()) != null) {
|
||||
deletedNames.add(block.getName());
|
||||
}
|
||||
} else if (created && block.getDeletedOn() == null
|
||||
&& !blocksMap.containsKey(block.getName())) {
|
||||
// a user who had the block before the import date existed keeps their copy, edited or
|
||||
// not, and one who deleted it gets it back once
|
||||
blocksMap.put(block.getName(), new ArrayList<IDIYComponent<?>>(block.getComponents()));
|
||||
importCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// the flag stays so that older versions, which know nothing else, do not import again
|
||||
configManager.writeValue(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, true);
|
||||
configManager.writeValue(IPlugInPort.DEFAULT_BLOCKS_IMPORT_DATE_KEY, newImportDate);
|
||||
configManager.writeValue(IPlugInPort.BLOCKS_KEY, blocksMap);
|
||||
for (String name : deletedNames) {
|
||||
removeFromRecent(name);
|
||||
}
|
||||
if (importCount > 0) {
|
||||
LOG.info(String.format("Imported %d default building blocks", importCount));
|
||||
}
|
||||
if (!deletedNames.isEmpty()) {
|
||||
LOG.info(String.format("Deleted %d withdrawn default building blocks", deletedNames.size()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public int importBlocks(String fileName) throws IOException {
|
||||
LOG.debug(String.format("importBlocks(%s)", fileName));
|
||||
@@ -221,14 +259,15 @@ public class BuildingBlockManager {
|
||||
blocks.put(blockName, blockComponents);
|
||||
|
||||
if (System.getProperty("org.diylc.WriteStaticBlocks", "false").equalsIgnoreCase("true")) {
|
||||
Map<String, List<IDIYComponent<?>>> defaultBlockMap =
|
||||
new HashMap<String, List<IDIYComponent<?>>>();
|
||||
defaultBlockMap.put(blockName, blockComponents);
|
||||
BuildingBlock defaultBlock = new BuildingBlock(blockName, blockComponents);
|
||||
defaultBlock.setCreatedOn(LocalDateTime.now().withNano(0));
|
||||
List<BuildingBlock> defaultBlocks = new ArrayList<BuildingBlock>();
|
||||
defaultBlocks.add(defaultBlock);
|
||||
try {
|
||||
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(BLOCKS_FILE_NAME));
|
||||
XStream xStream = new XStream(new DomDriver());
|
||||
xStream.addPermission(AnyTypePermission.ANY);
|
||||
xStream.toXML(defaultBlockMap, out);
|
||||
xStream.toXML(defaultBlocks, out);
|
||||
out.close();
|
||||
LOG.info("Saved default blocks");
|
||||
} catch (IOException e) {
|
||||
@@ -248,11 +287,16 @@ public class BuildingBlockManager {
|
||||
blocks.remove(blockName);
|
||||
configManager.writeValue(IBlockProcessor.BLOCKS_KEY, blocks);
|
||||
}
|
||||
removeFromRecent(blockName);
|
||||
}
|
||||
|
||||
// drop the block from the recently used list too, so it doesn't linger as a dead entry
|
||||
// drop the block from the recently used list too, so it doesn't linger as a dead entry
|
||||
@SuppressWarnings("unchecked")
|
||||
private void removeFromRecent(String blockName) {
|
||||
List<String> recent =
|
||||
(List<String>) configManager.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||
if (recent != null && recent.remove(IBlockProcessor.BLOCK_PREFIX + blockName))
|
||||
if (recent != null && recent.remove(IBlockProcessor.BLOCK_PREFIX + blockName)) {
|
||||
configManager.writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,11 @@ public class VariantManager {
|
||||
if (variantMap == null)
|
||||
variantMap = new HashMap<String, List<Template>>();
|
||||
|
||||
// the import date records how far the bundled file reaches, not when it was read, so that
|
||||
// running an older build first does not mark the variants of a newer build as already seen;
|
||||
// it never moves backwards, so running an older build after a newer one changes nothing
|
||||
LocalDateTime newImportDate = importDate;
|
||||
|
||||
// merge default variants with user's
|
||||
for (Map.Entry<String, List<Template>> entry : defaults.entrySet()) {
|
||||
List<Template> existingVariants = variantMap.computeIfAbsent(entry.getKey(), key -> new ArrayList<Template>());
|
||||
@@ -94,6 +99,12 @@ public class VariantManager {
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (Template variant : entry.getValue()) {
|
||||
if (variant.getCreatedOn() != null && variant.getCreatedOn().isAfter(newImportDate)) {
|
||||
newImportDate = variant.getCreatedOn();
|
||||
}
|
||||
if (variant.getDeletedOn() != null && variant.getDeletedOn().isAfter(newImportDate)) {
|
||||
newImportDate = variant.getDeletedOn();
|
||||
}
|
||||
// a deletion stub keeps nothing but its name, so a missing createdOn only means an old
|
||||
// entry when the variant has not been deleted
|
||||
boolean created = variant.getCreatedOn() == null ? variant.getDeletedOn() == null
|
||||
@@ -116,8 +127,7 @@ public class VariantManager {
|
||||
}
|
||||
|
||||
// update templates and a flag marking that we imported them
|
||||
configManager.writeValue(IPlugInPort.DEFAULT_TEMPLATES_IMPORT_DATE_KEY,
|
||||
LocalDateTime.now());
|
||||
configManager.writeValue(IPlugInPort.DEFAULT_TEMPLATES_IMPORT_DATE_KEY, newImportDate);
|
||||
configManager.writeValue(IPlugInPort.TEMPLATES_KEY, variantMap);
|
||||
if (importCount > 0) {
|
||||
LOG.info(String.format("Imported default %d variants", importCount));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<map>
|
||||
<entry>
|
||||
<string>TO-92 w/ 3 Leads</string>
|
||||
<list>
|
||||
<list>
|
||||
<org.diylc.common.BuildingBlock>
|
||||
<name>TO-92 w/ 3 Leads</name>
|
||||
<createdOn>2026-01-01T00:00:00</createdOn>
|
||||
<components class="list">
|
||||
<diylc.connectivity.Jumper>
|
||||
<name>J1</name>
|
||||
<alpha>127</alpha>
|
||||
@@ -73,11 +74,12 @@
|
||||
<folded>false</folded>
|
||||
<pinSpacing value="0.05" unit="in"/>
|
||||
</diylc.semiconductors.TransistorTO92>
|
||||
</list>
|
||||
</entry>
|
||||
<entry>
|
||||
<string>TO-92 w/ 2 Leads</string>
|
||||
<list>
|
||||
</components>
|
||||
</org.diylc.common.BuildingBlock>
|
||||
<org.diylc.common.BuildingBlock>
|
||||
<name>TO-92 w/ 2 Leads</name>
|
||||
<createdOn>2026-01-01T00:00:00</createdOn>
|
||||
<components class="list">
|
||||
<diylc.connectivity.Jumper>
|
||||
<name>J1</name>
|
||||
<alpha>127</alpha>
|
||||
@@ -131,11 +133,12 @@
|
||||
<folded>false</folded>
|
||||
<pinSpacing value="0.05" unit="in"/>
|
||||
</diylc.semiconductors.TransistorTO92>
|
||||
</list>
|
||||
</entry>
|
||||
<entry>
|
||||
<string>Title Block</string>
|
||||
<list>
|
||||
</components>
|
||||
</org.diylc.common.BuildingBlock>
|
||||
<org.diylc.common.BuildingBlock>
|
||||
<name>Title Block</name>
|
||||
<createdOn>2026-01-01T00:00:00</createdOn>
|
||||
<components class="list">
|
||||
<org.diylc.components.shapes.Rectangle>
|
||||
<name>RECT1</name>
|
||||
<alpha>127</alpha>
|
||||
@@ -735,6 +738,6 @@
|
||||
<verticalAlignment>CENTER</verticalAlignment>
|
||||
<orientation>DEFAULT</orientation>
|
||||
</org.diylc.components.misc.Label>
|
||||
</list>
|
||||
</entry>
|
||||
</map>
|
||||
</components>
|
||||
</org.diylc.common.BuildingBlock>
|
||||
</list>
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.diylc.appframework.miscutils.IConfigListener;
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
import org.diylc.common.BuildingBlock;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.testcomponents.TwoPointTestComponent;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Covers {@link BuildingBlockManager#importDefaultBlocks(List)}: factory blocks reach existing
|
||||
* users by their {@code createdOn}, withdrawn ones leave by their {@code deletedOn}, and the
|
||||
* recorded import date comes from the bundled file rather than the clock.
|
||||
*/
|
||||
public class DefaultBlockImportTest {
|
||||
|
||||
private static final LocalDateTime OLDER_RELEASE = LocalDateTime.of(2026, 1, 1, 0, 0);
|
||||
private static final LocalDateTime NEWER_RELEASE = LocalDateTime.of(2026, 6, 1, 0, 0);
|
||||
|
||||
private MapConfigurationManager configManager;
|
||||
private BuildingBlockManager manager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
configManager = new MapConfigurationManager();
|
||||
manager = new BuildingBlockManager(configManager, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freshInstallImportsEveryBlock() {
|
||||
manager.importDefaultBlocks(
|
||||
Arrays.asList(block("Old", OLDER_RELEASE), block("New", NEWER_RELEASE)));
|
||||
|
||||
assertTrue(blocks().containsKey("Old"));
|
||||
assertTrue(blocks().containsKey("New"));
|
||||
assertEquals(NEWER_RELEASE, importDate());
|
||||
assertTrue(configManager.readBoolean(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configFromTheOneTimeImportGetsTheMissingBlocksWithoutDuplicates() {
|
||||
List<IDIYComponent<?>> kept = components();
|
||||
Map<String, List<IDIYComponent<?>>> userBlocks = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
userBlocks.put("Old", kept);
|
||||
configManager.writeValue(IPlugInPort.BLOCKS_KEY, userBlocks);
|
||||
configManager.writeValue(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, true);
|
||||
|
||||
manager.importDefaultBlocks(
|
||||
Arrays.asList(block("Old", OLDER_RELEASE), block("New", OLDER_RELEASE)));
|
||||
|
||||
assertEquals(2, blocks().size());
|
||||
assertSame(kept, blocks().get("Old"));
|
||||
assertTrue(blocks().containsKey("New"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockOfANewerReleaseArrivesAfterAnOlderReleaseRanFirst() {
|
||||
manager.importDefaultBlocks(Arrays.asList(block("Old", OLDER_RELEASE)));
|
||||
manager.importDefaultBlocks(
|
||||
Arrays.asList(block("Old", OLDER_RELEASE), block("New", NEWER_RELEASE)));
|
||||
|
||||
assertTrue(blocks().containsKey("New"));
|
||||
assertEquals(NEWER_RELEASE, importDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void olderReleaseRunAfterANewerOneLeavesTheImportDate() {
|
||||
manager.importDefaultBlocks(
|
||||
Arrays.asList(block("Old", OLDER_RELEASE), block("New", NEWER_RELEASE)));
|
||||
manager.importDefaultBlocks(Arrays.asList(block("Old", OLDER_RELEASE)));
|
||||
|
||||
assertEquals(NEWER_RELEASE, importDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factoryBlockDeletedByTheUserIsNotImportedAgain() {
|
||||
manager.importDefaultBlocks(Arrays.asList(block("Old", OLDER_RELEASE)));
|
||||
blocks().remove("Old");
|
||||
|
||||
manager.importDefaultBlocks(Arrays.asList(block("Old", OLDER_RELEASE)));
|
||||
|
||||
assertFalse(blocks().containsKey("Old"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userBlockOfTheSameNameIsKept() {
|
||||
List<IDIYComponent<?>> own = components();
|
||||
Map<String, List<IDIYComponent<?>>> userBlocks = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
userBlocks.put("New", own);
|
||||
configManager.writeValue(IPlugInPort.BLOCKS_KEY, userBlocks);
|
||||
|
||||
manager.importDefaultBlocks(Arrays.asList(block("New", OLDER_RELEASE)));
|
||||
|
||||
assertSame(own, blocks().get("New"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withdrawnBlockIsRemovedFromTheRecentlyUsedListToo() {
|
||||
manager.importDefaultBlocks(Arrays.asList(block("Old", OLDER_RELEASE)));
|
||||
List<String> recent =
|
||||
new ArrayList<String>(Arrays.asList(IBlockProcessor.BLOCK_PREFIX + "Old"));
|
||||
configManager.writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recent);
|
||||
|
||||
manager.importDefaultBlocks(Arrays.asList(withdrawn("Old", NEWER_RELEASE)));
|
||||
|
||||
assertFalse(blocks().containsKey("Old"));
|
||||
assertTrue(recent.isEmpty());
|
||||
assertEquals(NEWER_RELEASE, importDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockRecreatedByTheUserAfterTheWithdrawalIsKept() {
|
||||
manager.importDefaultBlocks(Arrays.asList(withdrawn("Old", OLDER_RELEASE)));
|
||||
blocks().put("Old", components());
|
||||
|
||||
manager.importDefaultBlocks(Arrays.asList(withdrawn("Old", OLDER_RELEASE)));
|
||||
|
||||
assertTrue(blocks().containsKey("Old"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockCreatedAndWithdrawnSinceTheLastImportNeverArrives() {
|
||||
BuildingBlock shortLived = withdrawn("Short-lived", NEWER_RELEASE);
|
||||
shortLived.setCreatedOn(OLDER_RELEASE);
|
||||
|
||||
manager.importDefaultBlocks(Arrays.asList(shortLived));
|
||||
|
||||
assertFalse(blocks().containsKey("Short-lived"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, List<IDIYComponent<?>>> blocks() {
|
||||
return (Map<String, List<IDIYComponent<?>>>) configManager.readObject(IPlugInPort.BLOCKS_KEY,
|
||||
null);
|
||||
}
|
||||
|
||||
private LocalDateTime importDate() {
|
||||
return (LocalDateTime) configManager.readObject(IPlugInPort.DEFAULT_BLOCKS_IMPORT_DATE_KEY,
|
||||
null);
|
||||
}
|
||||
|
||||
private static List<IDIYComponent<?>> components() {
|
||||
return new ArrayList<IDIYComponent<?>>(Arrays.asList(
|
||||
new TwoPointTestComponent("R1", new Point2D.Double(0, 0), new Point2D.Double(10, 0))));
|
||||
}
|
||||
|
||||
private static BuildingBlock block(String name, LocalDateTime createdOn) {
|
||||
BuildingBlock block = new BuildingBlock(name, components());
|
||||
block.setCreatedOn(createdOn);
|
||||
return block;
|
||||
}
|
||||
|
||||
private static BuildingBlock withdrawn(String name, LocalDateTime deletedOn) {
|
||||
BuildingBlock block = new BuildingBlock(name, null);
|
||||
block.setDeletedOn(deletedOn);
|
||||
return block;
|
||||
}
|
||||
|
||||
private static class MapConfigurationManager implements IConfigurationManager<Object> {
|
||||
|
||||
private final Map<String, Object> values = new HashMap<String, Object>();
|
||||
|
||||
@Override
|
||||
public void addConfigListener(String key, IConfigListener listener) {}
|
||||
|
||||
@Override
|
||||
public boolean readBoolean(String key, boolean defaultValue) {
|
||||
return (Boolean) readObject(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readString(String key, String defaultValue) {
|
||||
return (String) readObject(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readInt(String key, int defaultValue) {
|
||||
return (Integer) readObject(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float readFloat(String key, float defaultValue) {
|
||||
return (Float) readObject(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double readDouble(String key, double defaultValue) {
|
||||
return (Double) readObject(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readObject(String key, Object defaultValue) {
|
||||
return values.containsKey(key) ? values.get(key) : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeValue(String key, Object value) {
|
||||
values.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(String appName) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.TestConfiguration;
|
||||
import org.diylc.common.BuildingBlock;
|
||||
import org.diylc.serialization.ProjectFileManager;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Checks that {@code import-defaults/blocks.xml} loads as factory blocks, each either a full block
|
||||
* with its {@code createdOn} or a withdrawal stub carrying only its name and {@code deletedOn}.
|
||||
*/
|
||||
public class DefaultBlocksTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp() {
|
||||
TestConfiguration.initialize();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void everyFactoryBlockIsCompleteOrAStub() {
|
||||
InputStream in = DefaultBlocksTest.class.getClassLoader()
|
||||
.getResourceAsStream("import-defaults/blocks.xml");
|
||||
Assert.assertNotNull("blocks.xml is not on the classpath", in);
|
||||
List<BuildingBlock> blocks =
|
||||
(List<BuildingBlock>) ProjectFileManager.xStreamSerializer.fromXML(in);
|
||||
|
||||
Assert.assertFalse(blocks.isEmpty());
|
||||
Set<String> names = new HashSet<String>();
|
||||
for (BuildingBlock block : blocks) {
|
||||
Assert.assertNotNull(block.getName());
|
||||
Assert.assertTrue("duplicate " + block.getName(), names.add(block.getName()));
|
||||
if (block.getDeletedOn() == null) {
|
||||
Assert.assertNotNull(block.getName(), block.getCreatedOn());
|
||||
Assert.assertNotNull(block.getName(), block.getComponents());
|
||||
Assert.assertFalse(block.getName(), block.getComponents().isEmpty());
|
||||
} else {
|
||||
Assert.assertNull(block.getName(), block.getComponents());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import org.junit.Test;
|
||||
/**
|
||||
* Checks that a factory variant marked {@code deletedOn} in {@code import-defaults/variants.xml} is
|
||||
* taken out of the user's configuration only when the last import predates the deletion, and is
|
||||
* never imported.
|
||||
* never imported, and that the recorded import date comes from the file rather than the clock.
|
||||
*/
|
||||
public class DeletedVariantsTest {
|
||||
|
||||
@@ -140,6 +140,39 @@ public class DeletedVariantsTest {
|
||||
Assert.assertTrue(remaining.stream().noneMatch(t -> t.getDeletedOn() != null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importDateIsTheLatestDateInTheFile() {
|
||||
LocalDateTime latest = LocalDateTime.MIN;
|
||||
for (List<Template> variants : loadVariants().values()) {
|
||||
for (Template variant : variants) {
|
||||
if (variant.getCreatedOn() != null && variant.getCreatedOn().isAfter(latest)) {
|
||||
latest = variant.getCreatedOn();
|
||||
}
|
||||
if (variant.getDeletedOn() != null && variant.getDeletedOn().isAfter(latest)) {
|
||||
latest = variant.getDeletedOn();
|
||||
}
|
||||
}
|
||||
}
|
||||
InMemoryConfigurationManager config =
|
||||
configWith(new ArrayList<Template>(), LocalDateTime.MIN);
|
||||
|
||||
importInto(config);
|
||||
|
||||
Assert.assertEquals(latest,
|
||||
config.readObject(IPlugInPort.DEFAULT_TEMPLATES_IMPORT_DATE_KEY, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importDateNeverMovesBackwards() {
|
||||
LocalDateTime later = LocalDateTime.of(2999, 1, 1, 0, 0);
|
||||
InMemoryConfigurationManager config = configWith(new ArrayList<Template>(), later);
|
||||
|
||||
importInto(config);
|
||||
|
||||
Assert.assertEquals(later,
|
||||
config.readObject(IPlugInPort.DEFAULT_TEMPLATES_IMPORT_DATE_KEY, null));
|
||||
}
|
||||
|
||||
private static class InMemoryConfigurationManager implements IConfigurationManager<Object> {
|
||||
|
||||
private final Map<String, Object> values = new HashMap<String, Object>();
|
||||
|
||||
@@ -223,30 +223,31 @@ for auto-sizing a component from its value. **It has no production caller** —
|
||||
|
||||
Building blocks are named groups of components, saved from a selection and re-placed as a unit.
|
||||
`BuildingBlockManager.importDefaultBlocks()` ships a default set in
|
||||
`diylc-core/src/main/resources/import-defaults/blocks.xml`, stored as a
|
||||
`Map<String, List<IDIYComponent<?>>>` using the `diylc` package alias
|
||||
(`aliasPackage("diylc", "org.diylc.components")`), and merges it into the user's own blocks by name.
|
||||
`diylc-core/src/main/resources/import-defaults/blocks.xml`, stored as a `List<BuildingBlock>`, each
|
||||
with its `name`, `components` (`class="list"`) and `createdOn`, using the `diylc` package alias
|
||||
(`aliasPackage("diylc", "org.diylc.components")`) for the components. It is merged into the user's
|
||||
own blocks (the unchanged `Map<String, List<IDIYComponent<?>>>` under `buildingBlocks`) by name.
|
||||
|
||||
Three ship today: `TO-92 w/ 3 Leads`, `TO-92 w/ 2 Leads`, `Title Block`.
|
||||
Three ship today, all `createdOn` 2026-01-01: `TO-92 w/ 3 Leads`, `TO-92 w/ 2 Leads`, `Title Block`.
|
||||
|
||||
Authoring one is a UI job, not a code job: place the components, select them, *Save as Building
|
||||
Block*, then lift the entry out of the user config into `blocks.xml`. Per D7 this is the default
|
||||
route for anything composable.
|
||||
Block*, then lift the entry out of the user config into `blocks.xml`. Running with
|
||||
`-Dorg.diylc.WriteStaticBlocks=true` writes the saved block in the bundled shape, dated now. Per D7
|
||||
this is the default route for anything composable.
|
||||
|
||||
**The blocker (fixed in §6.6).** `importDefaultBlocks()` is wrapped in a one-shot flag:
|
||||
The import runs on every start and mirrors `VariantManager.importDefaultVariants()`:
|
||||
|
||||
```java
|
||||
if (!configManager.readBoolean(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, false)) {
|
||||
```
|
||||
- A block is imported when its `createdOn` is after the `defaultBlocksImportDate` watermark
|
||||
(`LocalDateTime.MIN` when absent) and the user has no block of that name. A same-named user block
|
||||
is never overwritten or duplicated.
|
||||
- A withdrawn block is a stub with only `name` and `deletedOn`; it is removed from the user's blocks
|
||||
and recently used list when `deletedOn` is after the watermark.
|
||||
- The watermark becomes the latest `createdOn`/`deletedOn` in the file, never the clock, and never
|
||||
moves backwards, so the order in which versions are launched does not matter.
|
||||
- `defaultBlocksImported` is still written so that versions before the watermark do not import.
|
||||
|
||||
Once `defaultBlocksImported` is true it never runs again, so **a factory block added to `blocks.xml`
|
||||
reaches new installs only** — never anyone who has already launched the app. Any plan that
|
||||
distributes composites this way needs §6.6 first.
|
||||
|
||||
`VariantManager.importDefaultVariants()` does not have this problem, and the asymmetry is worth
|
||||
knowing: it keeps a `defaultTemplatesImportDate` watermark, re-reads `variants.xml` on every start,
|
||||
and imports any variant whose `Template.createdOn` is newer than the watermark and whose name the
|
||||
user does not already have. New factory *variants* do reach existing users today.
|
||||
Because configs from those versions have no watermark, the first start of this version offers every
|
||||
factory block once, and a user who deleted one gets it back once.
|
||||
|
||||
### 5.5 Tests
|
||||
|
||||
@@ -281,8 +282,7 @@ block for the unreleased version. `NEW_FEATURE` for components and variants, `BU
|
||||
Enum constants, catalogue data, building blocks and uncommenting. Cheapest fidelity per hour in
|
||||
the roadmap, and where D6 and D7 send most of the work.
|
||||
|
||||
§6.1, §6.5 and §6.6 are correctness fixes and should go first regardless of the rest; §6.6 also
|
||||
gates §6.7, which is the channel the composable parts travel through.
|
||||
§6.1 and §6.5 are correctness fixes and should go first regardless of the rest.
|
||||
|
||||
### 6.1 Restore the `ElectrolyticCanCapacitor` alias — **bug, not a gap**
|
||||
|
||||
@@ -416,38 +416,14 @@ value-based auto-sizing, not a live fault.
|
||||
|
||||
**~3 h including the test.**
|
||||
|
||||
### 6.6 Make factory building blocks reach existing users — **bug, and a prerequisite**
|
||||
### 6.6 Make factory building blocks reach existing users — **done**
|
||||
|
||||
Per §5.4, `importDefaultBlocks()` runs once per installation and never again, so every factory
|
||||
building block this roadmap proposes would land only on fresh installs. D7 routes most composable
|
||||
parts through that mechanism, so this is a prerequisite for §6.7, §7.5 and §8.9, not a nicety.
|
||||
|
||||
Two ways to fix it, in `BuildingBlockManager`:
|
||||
|
||||
**Recommended — drop the guard.** Delete the `DEFAULT_BLOCKS_IMPORTED_KEY` check and let the
|
||||
existing merge run on every start. The merge body already skips any name the user has
|
||||
(`if (!blocksMap.containsKey(entry.getKey()))`), so user blocks and user edits survive untouched and
|
||||
new factory names simply appear. Leave the flag being written so a downgrade still sees it.
|
||||
|
||||
One behavioural cost, which should be a deliberate choice rather than a surprise: a user who
|
||||
*deletes* a factory block gets it back on the next launch. Mitigate by recording deleted factory
|
||||
names in config if that turns out to annoy anyone — not before.
|
||||
|
||||
**Thorough — mirror the variant watermark.** Copy `VariantManager.importDefaultVariants()`: a
|
||||
`defaultBlocksImportDate` watermark plus a per-block `createdOn`. The obstacle is that `blocks.xml`
|
||||
is a `Map<String, List<IDIYComponent<?>>>` and `BuildingBlock` — which *does* have a name field but
|
||||
is not used in the stored format at all — carries no timestamp. Adding one means changing the shape
|
||||
of the `BLOCKS_KEY` config value, which also holds the user's own blocks. That is a config-format
|
||||
migration for a gain the simpler option already delivers.
|
||||
|
||||
Take the recommended option. Extend `BuildingBlockManagerTest` (8 tests, `diylc-core`) with a case
|
||||
asserting that a second `importDefaultBlocks()` on a config that already has the flag set still
|
||||
picks up a newly added default name. **~3 h.**
|
||||
Factory blocks carry `createdOn`/`deletedOn` behind a `defaultBlocksImportDate` watermark; see §5.4.
|
||||
|
||||
### 6.7 Ship the first factory building blocks
|
||||
|
||||
Data, not code, and per D7 this is where composable parts go. Each is authored in the app and lifted
|
||||
into `blocks.xml`. Gated on §6.6.
|
||||
into `blocks.xml`; each gets a `createdOn` later than the newest one already in the file.
|
||||
|
||||
| Block | Built from | Why |
|
||||
|---|---|---|
|
||||
@@ -458,7 +434,7 @@ into `blocks.xml`. Gated on §6.6.
|
||||
| Strat / Tele / LP harness | `guitar` pickups + pots + switch + jack | `guitar` components appear in only 8 of 425 files, which may be discoverability as much as demand |
|
||||
|
||||
Author the push-pull pot first: it is the one with measured demand, and it is the worked example that
|
||||
proves D7 before the rest follow. **~2-3 h each once §6.6 lands.**
|
||||
proves D7 before the rest follow. **~2-3 h each.**
|
||||
|
||||
---
|
||||
|
||||
@@ -596,20 +572,18 @@ joins the §6.7 list rather than this one once someone asks for it.
|
||||
|---|---|---|
|
||||
| 1 | §6.5 six malformed datasheet rows, plus the validation test | 3 h |
|
||||
| 2 | §6.1 alias fix, verified by regression run | 2 h |
|
||||
| 3 | §6.6 factory building block import guard | 3 h |
|
||||
| 4 | §6.7 push-pull pot block, as the D7 worked example | 3 h |
|
||||
| 5 | §6.2 revive three, decide on the fourth | 3 h |
|
||||
| 6 | §6.3 UX4, then UX5 / B8G / B9D | 6-8 h |
|
||||
| 7 | §6.4 datasheets, `RadialCeramicDiskCapacitor` first | 6-10 h each |
|
||||
| 8 | §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses | 2-3 h each |
|
||||
| 9 | §7.1 `Transformer`, with §7.3 Choke as its third type | 20-24 h |
|
||||
| 10 | §7.2 Speaker, physical plus symbol | 10-12 h |
|
||||
| 11 | §7.4 Footswitch | 10-12 h |
|
||||
| 12 | §7.5 `Photoresistor`, then the Vactrol block | 6 h |
|
||||
| 3 | §6.7 push-pull pot block, as the D7 worked example | 3 h |
|
||||
| 4 | §6.2 revive three, decide on the fourth | 3 h |
|
||||
| 5 | §6.3 UX4, then UX5 / B8G / B9D | 6-8 h |
|
||||
| 6 | §6.4 datasheets, `RadialCeramicDiskCapacitor` first | 6-10 h each |
|
||||
| 7 | §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses | 2-3 h each |
|
||||
| 8 | §7.1 `Transformer`, with §7.3 Choke as its third type | 20-24 h |
|
||||
| 9 | §7.2 Speaker, physical plus symbol | 10-12 h |
|
||||
| 10 | §7.4 Footswitch | 10-12 h |
|
||||
| 11 | §7.5 `Photoresistor`, then the Vactrol block | 6 h |
|
||||
|
||||
Steps 1-4 are about a day and a half: both known bugs cleared, the building block channel unblocked,
|
||||
and the highest-demand composable shipped as data rather than as a class. Step 9 is the one that
|
||||
matters most.
|
||||
Steps 1-4 are about a day and a half: both known bugs cleared and the highest-demand composable
|
||||
shipped as data rather than as a class. Step 8 is the one that matters most.
|
||||
|
||||
Three items that looked like components when this roadmap started are now data (§6.7, §7.6, §8.9),
|
||||
and one is split into a primitive plus a block (§7.5). That is D7 working as intended; expect the
|
||||
|
||||
@@ -119,10 +119,6 @@ top by about 22 mm).
|
||||
- HWCHAS walnut-frame chassis (HWCHAS1310 / 1710; aluminium top 298.45 x 222.25 / 400.05 x
|
||||
222.25 mm on a 19.05 mm walnut frame): a different construction from the folded sheet metal.
|
||||
- Hammond rack cases (RM, RMCS) and the 1458 instrument enclosure.
|
||||
- Factory building blocks are still imported once, behind the boolean
|
||||
`DEFAULT_BLOCKS_IMPORTED_KEY`, so a block added to `blocks.xml` later never reaches existing
|
||||
users. Fix by giving factory blocks `createdOn`/`deletedOn` and an import date, as `VariantManager`
|
||||
does, when a new factory block is next needed.
|
||||
- Configs older than class-keyed variants store keys like `Electro-Mechanical.Chassis Panel`;
|
||||
`VariantManager`'s upgrade maps them by the current category and name, so such variants would
|
||||
not carry over. Map the legacy name if it turns out to matter.
|
||||
|
||||
+3
-2
@@ -140,8 +140,9 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<configuration>
|
||||
<!-- XStream reflects into JDK collections when reading update.xml; mirrors the run configuration -->
|
||||
<argLine>--add-opens java.base/java.util=ALL-UNNAMED</argLine>
|
||||
<!-- XStream reflects into JDK collections when reading update.xml, and into fonts when reading
|
||||
import-defaults/blocks.xml; mirrors the run configuration -->
|
||||
<argLine>--add-opens java.base/java.util=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
Reference in New Issue
Block a user