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) {}
}
}