blocks and variants import enhancements

This commit is contained in:
Bane Stojkovic
2026-11-06 09:39:07 +01:00
parent 35bf22edea
commit bf7fdafef5
11 changed files with 528 additions and 129 deletions
@@ -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>();
+35 -61
View File
@@ -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
-4
View File
@@ -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
View File
@@ -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>