6
\$\begingroup\$

I was reading Head First Java. The book featured a project called BeatBox that allowed you to make, play, save, and load beat patterns using Java's Sequencer API and Swing API. While the idea belongs to the author, I implemented it vastly differently and made the app way more complex and flexible. For example, the app can dynamically adapt to match loaded patterns by adding missing instruments or changing the number of beats. Here's the code

Note. I removed some of the imports from Java's standard libraries to fit into the character limit. I hope you have the autoimport enabled

exceptions.IllegalPatternException
package org.example.demos.sequencer.beatBox.exceptions;

import org.example.demos.sequencer.beatBox.ManifestComponent;

public class IllegalPatternException extends Exception {
    private static final String FORMAT =
            "The pattern file doesn't specify a valid %s or does it in an illegal format";

    public IllegalPatternException(ManifestComponent problematicManifestComponent) {
        super(String.format(FORMAT, problematicManifestComponent));
    }
}
exceptions.IllegalPropertyException
package org.example.demos.sequencer.beatBox.exceptions;

import lombok.RequiredArgsConstructor;

public class IllegalPropertyException extends RuntimeException {
    private static final String FORMAT = "%s value is invalid: should be %s";

    public IllegalPropertyException(String propertyName, PropertyIssue propertyIssue) {
        super(String.format(FORMAT, propertyName, propertyIssue.whatItShouldBeInstead()));
    }

    @RequiredArgsConstructor
    public enum PropertyIssue {
        NULL("not null"), EMPTY("not empty"),
        NON_POSITIVE("greater than zero"),
        NOT_MATCHING_REGEX("matching the regular expression");

        private final String whatItShouldBeInstead;

        String whatItShouldBeInstead() {
            return whatItShouldBeInstead;
        }
    }
}
BeatBox
package org.example.demos.sequencer.beatBox;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.example.demos.sequencer.beatBox.exceptions.IllegalPatternException;
import org.example.shared.utilities.MidiUtil;
import org.example.shared.utilities.UIUtil;
import org.example.shared.utilities.Util;

import static java.awt.BorderLayout.*;
import static javax.sound.midi.Sequencer.LOOP_CONTINUOUSLY;
import static javax.sound.midi.ShortMessage.*;
import static javax.swing.BoxLayout.Y_AXIS;
import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;

@Slf4j
public class BeatBox {
    private final Sequencer sequencer;
    private final Sequence sequence = Util.wrapInTryCatchAndGet(() -> new Sequence(Sequence.PPQ, 4));
    private Track track = sequence.createTrack();
    @Getter
    private int numberOfBeats;
    private JPanel mainPanel;
    @Getter(value = AccessLevel.PACKAGE)
    private JComponent labelBox;
    @Getter(value = AccessLevel.PACKAGE)
    private JPanel checkBoxPanel;
    private final ItemListener checkBoxItemListener = new CheckBoxItemListener();
    private final MouseListener checkBoxMouseListener = new CheckBoxMouseListener();
    private JButton saveButton;
    private JButton startButton;
    private JButton stopButton;
    private JButton tempoUpButton;
    private JButton tempoDownButton;
    private JButton clearButton;
    private boolean isMousePressed;
    @Getter
    private final PressedButtonStrategy pressedButtonStrategy;
    @Getter
    private final MissingInstrumentStrategy missingInstrumentStrategy;
    @Getter
    private final MissingBeatsStrategy missingBeatsStrategy;
    @Getter
    private final ExcessBeatsStrategy excessBeatsStrategy;
    @Getter
    private final String separator;
    @Getter
    private final String checkMark;
    @Getter
    private final String blankMark;
    @Getter(value = AccessLevel.PACKAGE)
    private final Map<MidiInstrument, List<JCheckBox>> instrumentMap;
    private static final Rectangle MAXIMUM_WINDOW_BOUNDS = UIUtil.getMaximumWindowBounds();
    private static final int MINIMUM_FRAME_WIDTH = 500;
    private static final int WIDTH_PER_BEAT = 15;
    private static final int MINIMUM_FRAME_HEIGHT = 225;
    private static final int HEIGHT_PER_INSTRUMENT = 20;

    public BeatBox() {
        this(BeatBoxConfiguration.configure());
    }

    BeatBox(BeatBoxConfiguration config) {
        sequencer = Util.wrapInTryCatchAndGet(() -> {
            Sequencer s = MidiSystem.getSequencer();
            s.open();
            s.setTempoInBPM(config.getTempoInBPM());
            return s;
        });
        instrumentMap = config.getInstrumentMap();
        numberOfBeats = config.getNumberOfBeats();
        pressedButtonStrategy = config.getPressedButtonStrategy();
        missingInstrumentStrategy = config.getMissingInstrumentStrategy();
        missingBeatsStrategy = config.getMissingBeatsStrategy();
        excessBeatsStrategy = config.getExcessBeatsStrategy();
        separator = config.getSeparator();
        checkMark = config.getCheckMark();
        blankMark = config.getBlankMark();
    }

    public Collection<MidiInstrument> getInstruments() {
        return instrumentMap.keySet();
    }

    public float getTempo() {
        return sequencer.getTempoInBPM();
    }

    public void launch() {
        UIUtil.getUIBuilder()
                .withTitle("Beat Box")
                .withComponent(CENTER, () -> UIUtil.getComponentBuilder(() -> {
                            mainPanel = new JPanel(new BorderLayout());
                            return mainPanel;
                        })
                        .withBorder(() -> BorderFactory.createEmptyBorder(10, 10, 10, 10))
                        .withComponent(EAST, () -> UIUtil.getComponentBuilder(() -> new Box(Y_AXIS))
                                .withComponent(() -> {
                                    startButton = new JButton("Start");
                                    startButton.setEnabled(false);
                                    startButton.addActionListener(new StartButtonListener());
                                    return startButton;
                                })
                                .withComponent(() -> {
                                    stopButton = new JButton("Stop");
                                    stopButton.setEnabled(false);
                                    stopButton.addActionListener(new StopButtonListener());
                                    return stopButton;
                                })
                                .withComponent(() -> {
                                    tempoUpButton = new JButton("Tempo Up");
                                    tempoUpButton.setEnabled(false);
                                    tempoUpButton.addActionListener(new TempoUpButtonListener());
                                    return tempoUpButton;
                                })
                                .withComponent(() -> {
                                    tempoDownButton = new JButton("Tempo Down");
                                    tempoDownButton.setEnabled(false);
                                    tempoDownButton.addActionListener(new TempoDownButtonListener());
                                    return tempoDownButton;
                                })
                                .withComponent(() -> {
                                    clearButton = new JButton("Clear");
                                    clearButton.setEnabled(false);
                                    clearButton.addActionListener(new ClearButtonListener());
                                    return clearButton;
                                })
                                .withComponent(() -> {
                                    saveButton = new JButton("Save Pattern...");
                                    saveButton.setEnabled(false);
                                    saveButton.addActionListener(new SaveButtonListener());
                                    return saveButton;
                                })
                                .withComponent(() -> {
                                    var loadButton = new JButton("Load Pattern...");
                                    loadButton.addActionListener(new LoadButtonListener());
                                    return loadButton;
                                })
                                .build())
                        .withComponent(WEST, () -> {
                            labelBox = new Box(Y_AXIS);
                            instrumentMap.keySet().forEach(k -> labelBox.add(new Label(k.getStandardName())));
                            return labelBox;
                        })
                        .withComponent(CENTER, () -> {
                            GridLayout gridLayout = new GridLayout(instrumentMap.size(), numberOfBeats);
                            gridLayout.setVgap(1);
                            gridLayout.setHgap(2);
                            checkBoxPanel = new JPanel(gridLayout);
                            checkBoxPanel.addMouseListener(new CheckBoxPanelMouseListener());
                            ensureOneBlankCheckBoxForEachBeat();
                            return checkBoxPanel;
                        })
                        .withVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED)
                        .withHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED)
                        .build())
                .withFrameSize(calculateAppropriateWidth(), calculateAppropriateHeight())
                .visualize();
    }

    private void ensureOneBlankCheckBoxForEachBeat() {
        checkBoxPanel.removeAll();
        instrumentMap.values().forEach(checkBoxList -> {
            checkBoxList.clear();
            checkBoxList.addAll(
                    Stream.generate(this::getNewCheckBox)
                            .peek(checkBox -> checkBoxPanel.add(checkBox))
                            .limit(numberOfBeats)
                            .toList()
            );
        });
        ((GridLayout) checkBoxPanel.getLayout()).setColumns(numberOfBeats);
        adaptFrameSize();
    }

    void addMoreBeats(int numberOfBeatsToAdd) {
        numberOfBeats += numberOfBeatsToAdd;
        ensureOneBlankCheckBoxForEachBeat();
    }

    void removeBeats(int numberOfBeatsToRemove) {
        numberOfBeats -= numberOfBeatsToRemove;
        ensureOneBlankCheckBoxForEachBeat();
    }

    private int calculateAppropriateWidth() {
        return Math.min(MAXIMUM_WINDOW_BOUNDS.width,
                MINIMUM_FRAME_WIDTH + numberOfBeats * WIDTH_PER_BEAT);
    }

    private int calculateAppropriateHeight() {
        return Math.min(MAXIMUM_WINDOW_BOUNDS.height,
                MINIMUM_FRAME_HEIGHT + instrumentMap.size() * HEIGHT_PER_INSTRUMENT);
    }

    private void makeTrackAndStart() {
        refreshTrack();
        addMidiEventsForSelectedCheckBoxes();
        addDummyEventAtLastBeat();
        setSequenceAndStart();
    }

    private void addMidiEventsForSelectedCheckBoxes() {
        for (Map.Entry<MidiInstrument, List<JCheckBox>> mapEntry : instrumentMap.entrySet()) {
            int instrumentFirstByte = mapEntry.getKey().getFirstByte();
            List<JCheckBox> checkBoxList = mapEntry.getValue();
            for (int i = 0; i < checkBoxList.size(); i++) {
                if (checkBoxList.get(i).isSelected()) {
                    track.add(MidiUtil.getMidiEventBuilder()
                            .withCommand(NOTE_ON)
                            .withChannel(9)
                            .withFirstByte(instrumentFirstByte)
                            .withSecondByte(100)
                            .withTick(i)
                            .build());
                    track.add(MidiUtil.getMidiEventBuilder()
                            .withCommand(NOTE_OFF)
                            .withChannel(9)
                            .withFirstByte(instrumentFirstByte)
                            .withSecondByte(100)
                            .withTick(i + 1)
                            .build());
                }
            }
        }
    }

    private void addDummyEventAtLastBeat() {
        /* adding an event at the last beat is important because the Sequencer may not
         * go all the beats otherwise */

        track.add(MidiUtil.getMidiEventBuilder()
                .withCommand(CONTROL_CHANGE) // it could be PROGRAM_CHANGE, it doesn't matter
                .withChannel(0) // any channel between 0 and 15, doesn't matter
                .withFirstByte(0) // the bytes should be between 0 and 127 inclusive
                .withSecondByte(0) // as long as within the range, the byte values don't matter
                .withTick(numberOfBeats) // the last beat
                .build());
    }

    private void refreshTrack() {
        sequence.deleteTrack(track);
        track = sequence.createTrack();
    }

    private void setSequenceAndStart() {
        Util.wrapInTryCatch(() -> {
            sequencer.setSequence(sequence);
            sequencer.setLoopCount(LOOP_CONTINUOUSLY);
            sequencer.start();
        });
    }

    void addNewInstrument(MidiInstrument instrument) {
        var checkBoxes = Stream.generate(this::getNewCheckBox).limit(numberOfBeats).toList();
        instrumentMap.put(instrument, checkBoxes);
        labelBox.add(new Label(instrument.getStandardName()));
        checkBoxes.forEach(checkBoxPanel::add);
        var checkBoxPanelLayout = (GridLayout) checkBoxPanel.getLayout();
        checkBoxPanelLayout.setRows(checkBoxPanelLayout.getRows() + 1);
    }

    private JCheckBox getNewCheckBox() {
        var checkBox = new JCheckBox();
        checkBox.addMouseListener(checkBoxMouseListener);
        checkBox.addItemListener(checkBoxItemListener);
        checkBox.setSelected(false);
        return checkBox;
    }

    private void adaptFrameSize() {
        JFrame frame = getFrame();
        if (frame != null) {
            frame.setSize(calculateAppropriateWidth(), calculateAppropriateHeight());
        }
    }

    private JFrame getFrame() {
        return (JFrame) SwingUtilities.getWindowAncestor(mainPanel);
    }

    private void togglePlaybackControls() {
        boolean isRunning = sequencer.isRunning();
        stopButton.setEnabled(isRunning);
        tempoUpButton.setEnabled(isRunning);
        tempoDownButton.setEnabled(isRunning);
    }

    private class StartButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            makeTrackAndStart();
            togglePlaybackControls();
        }
    }

    private class StopButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            sequencer.stop();
            togglePlaybackControls();
        }
    }

    private class TempoUpButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            float currentTempoFactor = sequencer.getTempoFactor();
            sequencer.setTempoFactor(currentTempoFactor * 1.03f);
        }
    }

    private class TempoDownButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            float currentTempoFactor = sequencer.getTempoFactor();
            sequencer.setTempoFactor(currentTempoFactor * 0.97f);
        }
    }

    private class ClearButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            instrumentMap.values().stream()
                    .flatMap(Collection::stream)
                    .forEach(checkBox -> checkBox.setSelected(false));
        }
    }

    private class SaveButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent event) {
            JFileChooser fileChooser = new JFileChooser();
            fileChooser.showSaveDialog(mainPanel);
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileChooser.getSelectedFile()))) {
                writeManifest(writer);
                String paddedSeparator = " " + separator + " ";
                for (Map.Entry<MidiInstrument, List<JCheckBox>> mapEntry : instrumentMap.entrySet()) {
                    writer.write(mapEntry.getKey().getStandardName() + paddedSeparator +
                            mapEntry.getValue().stream()
                                    .map(checkBox -> checkBox.isSelected() ? checkMark : blankMark)
                                    .collect(Collectors.joining()) + "\n");
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        private void writeManifest(BufferedWriter writer) throws IOException {
            writer.write("SEPARATOR " + separator + "\n");
            writer.write("CHECK MARK " + checkMark + "\n");
            writer.write("BLANK MARK " + blankMark + "\n");
        }
    }

    private class LoadButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent event) {
            JFileChooser fileChooser = new JFileChooser();
            int returnState = fileChooser.showOpenDialog(mainPanel);
            if (returnState == JFileChooser.APPROVE_OPTION) {
                performPatternLoading(fileChooser);
            } else if (returnState == JFileChooser.CANCEL_OPTION) {
                log.info("No pattern is selected – loading is aborted");
            }
        }

        private void performPatternLoading(JFileChooser fileChooser) {
            boolean frameResizingRequired = false;
            try (BufferedReader reader = new BufferedReader(new FileReader(fileChooser.getSelectedFile()))) {
                PatternManifest m = ManifestParser.parseManifest(reader);
                Map<String, String[]> namedBeatRows = readNamedBeatRows(reader, m);
                ensureEqualBeatRowLengths(namedBeatRows, m.blankMark());
                int numberOfBeatsInPattern = namedBeatRows.values().stream().findAny().orElseThrow().length;
                if (numberOfBeats < numberOfBeatsInPattern) {
                    missingBeatsStrategy.handle(new BeatsMismatchContext(
                            BeatBox.this, namedBeatRows.values().stream().toList(), m.checkMark()
                    ));
                } else if (numberOfBeats > numberOfBeatsInPattern) {
                    excessBeatsStrategy.handle(new BeatsMismatchContext(
                            BeatBox.this, namedBeatRows.values().stream().toList(), m.checkMark()
                    ));
                }
                for (Map.Entry<String, String[]> entry : namedBeatRows.entrySet()) {
                    String instrumentName = entry.getKey();
                    String[] instrumentBeats = entry.getValue();
                    var optionalMatchingEntry = findMatchingEntry(instrumentName);
                    if (optionalMatchingEntry.isPresent()) {
                        copyBeatPattern(optionalMatchingEntry.get().getValue(), instrumentBeats, m);
                    } else {
                        boolean isInstrumentAdded = missingInstrumentStrategy.handle(new InstrumentMismatchContext(
                                BeatBox.this, instrumentName,
                                instrumentBeats, m.checkMark()
                        ));
                        if (isInstrumentAdded) {
                            copyBeatPattern(findMatchingEntry(instrumentName).orElseThrow().getValue(), instrumentBeats, m);
                            frameResizingRequired = true;
                        }
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            } catch (IllegalPatternException e) {
                log.error(e.getMessage());
            }
            if (frameResizingRequired) {
                BeatBox.this.adaptFrameSize();
            }
        }

        private void ensureEqualBeatRowLengths(Map<String, String[]> namedBeatRows, String blankMark) {
            int maxBeatsInPattern = getMaxBeatsInPattern(namedBeatRows);
            namedBeatRows.replaceAll((name, beatRow) -> (beatRow.length < maxBeatsInPattern) ?
                    normalizeRow(beatRow, maxBeatsInPattern, blankMark) : beatRow);
        }

        private int getMaxBeatsInPattern(Map<String, String[]> namedBeatRows) {
            return namedBeatRows.values().stream()
                    .mapToInt(row -> row.length)
                    .max().orElseThrow();
        }

        private Map<String, String[]> readNamedBeatRows(BufferedReader reader, PatternManifest m) throws IOException {
            Map<String, String[]> namedBeatRows = new HashMap<>();
            String line;
            while ((line = reader.readLine()) != null) {
                String[] nameBeatsPair = line.split(m.separator());
                String instrumentName = nameBeatsPair[0].trim();
                String[] instrumentBeats = nameBeatsPair[1].trim().split("");
                namedBeatRows.put(instrumentName, instrumentBeats);
            }
            return namedBeatRows;
        }

        private String[] normalizeRow(String[] beatRow, int requiredLength,
                                      String paddingCharacter) {
            return ArrayUtils.addAll(beatRow, paddingCharacter.repeat(requiredLength - beatRow.length).split(""));
        }

        private Optional<Map.Entry<MidiInstrument, List<JCheckBox>>> findMatchingEntry(String instrumentName) {
            return instrumentMap.entrySet().stream()
                    .filter(entry -> entry.getKey().getStandardName().equalsIgnoreCase(instrumentName))
                    .findFirst();
        }

        private void copyBeatPattern(List<JCheckBox> appBeats, String[] patternBeats, PatternManifest m) {
            for (int i = 0; i < appBeats.size(); i++) {
                var currentCheckBox = appBeats.get(i);
                if (i < patternBeats.length) {
                    if (patternBeats[i].equals(m.checkMark())) {
                        currentCheckBox.setSelected(true);
                    } else if (patternBeats[i].equals(m.blankMark())) {
                        currentCheckBox.setSelected(false);
                    }
                } else {
                    currentCheckBox.setSelected(false);
                }
            }
        }
    }

    private class CheckBoxItemListener implements ItemListener {
        @Override
        public void itemStateChanged(ItemEvent e) {
            if (e.getStateChange() == ItemEvent.SELECTED) {
                setPatternControlsTo(true);
            } else if ((e.getStateChange() == ItemEvent.DESELECTED) && noCheckBoxChecked()) {
                setPatternControlsTo(false);
            }
        }

        private void setPatternControlsTo(boolean value) {
            startButton.setEnabled(value);
            saveButton.setEnabled(value);
            clearButton.setEnabled(value);
        }

        private boolean noCheckBoxChecked() {
            return instrumentMap.values().stream()
                    .flatMap(Collection::stream)
                    .noneMatch(AbstractButton::isSelected);
        }
    }

    private class CheckBoxPanelMouseListener extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent e) {
            isMousePressed = true;
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            isMousePressed = false;
        }
    }

    private class CheckBoxMouseListener extends MouseAdapter {
        private JCheckBox originallyPressedCheckBox;
        private JCheckBox lastEnteredCheckBox;

        @Override
        public void mousePressed(MouseEvent e) {
            originallyPressedCheckBox = (JCheckBox) e.getSource();
            isMousePressed = true;
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            originallyPressedCheckBox = null;
            lastEnteredCheckBox = null;
            isMousePressed = false;
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            if (isMousePressed) {
                var enteredCheckBox = (JCheckBox) e.getSource();
                lastEnteredCheckBox = enteredCheckBox;
                pressedButtonStrategy.apply(enteredCheckBox);
            }
        }

        @Override
        public void mouseExited(MouseEvent e) {
            var exitedCheckBox = (JCheckBox) e.getSource();
            if (isMousePressed &&
                    exitedCheckBox == originallyPressedCheckBox &&
                    exitedCheckBox != lastEnteredCheckBox) {
                pressedButtonStrategy.apply(exitedCheckBox);
            }
        }
    }
}
BeatBoxConfiguration
package org.example.demos.sequencer.beatBox;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;

@Slf4j
public class BeatBoxConfiguration {
    @Getter(value = AccessLevel.PACKAGE)
    private Map<MidiInstrument, List<JCheckBox>> instrumentMap =
            instrumentsToInstrumentMap(InstrumentFactory.getKathysInstruments());
    @Getter
    private int numberOfBeats = 16;
    @Getter
    private float tempoInBPM = 120;
    @Getter
    private PressedButtonStrategy pressedButtonStrategy = PressedButtonStrategy.TOGGLE;
    @Getter
    private MissingInstrumentStrategy missingInstrumentStrategy = MissingInstrumentStrategy.ADD_IF_BEATS_NOT_BLANK;
    @Getter
    private MissingBeatsStrategy missingBeatsStrategy = MissingBeatsStrategy.ADD_IF_BEATS_NOT_BLANK;
    @Getter
    private ExcessBeatsStrategy excessBeatsStrategy = ExcessBeatsStrategy.TRIM_ALWAYS;
    @Getter
    private String separator = ":";
    @Getter
    private String checkMark = "X";
    @Getter
    private String blankMark = "O";
    private boolean didUnsuccessfulInstrumentSettingOccur;

    public static BeatBoxConfiguration configure() {
        return new BeatBoxConfiguration();
    }

    public Collection<MidiInstrument> getInstruments() {
        return instrumentMap.keySet();
    }

    public BeatBoxConfiguration setInstruments(@NotNull MidiInstrument... instruments) {
        setInstruments(Arrays.asList(instruments));
        return this;
    }

    public BeatBoxConfiguration setInstruments(@NotNull Collection<MidiInstrument> instruments) {
        BeatBoxUtils.requireNonNull(instruments, "instruments");
        var customInstrumentMap = instrumentsToInstrumentMap(instruments);
        if (!customInstrumentMap.isEmpty()) {
            instrumentMap = customInstrumentMap;
            didUnsuccessfulInstrumentSettingOccur = false;
        } else {
            didUnsuccessfulInstrumentSettingOccur = true;
        }
        return this;
    }

    public BeatBoxConfiguration setNumberOfBeats(int numberOfBeats) {
        BeatBoxUtils.requirePositive(numberOfBeats, "numberOfBeats");
        this.numberOfBeats = numberOfBeats;
        return this;
    }

    public BeatBoxConfiguration setTempoInBPM(float tempoInBPM) {
        BeatBoxUtils.requirePositive(tempoInBPM, "tempoInBPM");
        this.tempoInBPM = tempoInBPM;
        return this;
    }

    public BeatBoxConfiguration setSeparator(@NotNull String separator) {
        BeatBoxUtils.requireManifestComponentValidity(separator, ManifestComponent.SEPARATOR);
        this.separator = separator;
        return this;
    }

    public BeatBoxConfiguration setCheckMark(@NotNull String checkMark) {
        BeatBoxUtils.requireManifestComponentValidity(checkMark, ManifestComponent.CHECK_MARK);
        this.checkMark = checkMark;
        return this;
    }

    public BeatBoxConfiguration setBlankMark(@NotNull String blankMark) {
        BeatBoxUtils.requireManifestComponentValidity(blankMark, ManifestComponent.BLANK_MARK);
        this.blankMark = blankMark;
        return this;
    }

    public BeatBoxConfiguration setMissingInstrumentStrategy(@NotNull MissingInstrumentStrategy strategy) {
        BeatBoxUtils.requireNonNull(strategy, "missingInstrumentStrategy");
        this.missingInstrumentStrategy = strategy;
        return this;
    }

    public BeatBoxConfiguration setMissingBeatsStrategy(@NotNull MissingBeatsStrategy strategy) {
        BeatBoxUtils.requireNonNull(strategy, "missingBeatsStrategy");
        this.missingBeatsStrategy = strategy;
        return this;
    }

    public BeatBoxConfiguration setExcessBeatsStrategy(@NotNull ExcessBeatsStrategy strategy) {
        BeatBoxUtils.requireNonNull(strategy, "excessBeatsStrategy");
        this.excessBeatsStrategy = strategy;
        return this;
    }

    public BeatBoxConfiguration setPressedButtonStrategy(@NotNull PressedButtonStrategy pressedButtonStrategy) {
        BeatBoxUtils.requireNonNull(pressedButtonStrategy, "pressedButtonBehavior");
        this.pressedButtonStrategy = pressedButtonStrategy;
        return this;
    }

    public BeatBox build() {
        if (didUnsuccessfulInstrumentSettingOccur) {
            log.warn("No instruments found. Default instruments are going to be applied");
        }
        return new BeatBox(this);
    }

    private Map<MidiInstrument, List<JCheckBox>> instrumentsToInstrumentMap(Collection<MidiInstrument> instruments) {
        Map<MidiInstrument, List<JCheckBox>> instrumentMap = new LinkedHashMap<>(instruments.size());
        for (MidiInstrument instrument : instruments) {
            instrumentMap.put(instrument, new ArrayList<>(numberOfBeats));
        }
        return instrumentMap;
    }
}
BeatBoxUtils
package org.example.demos.sequencer.beatBox;

import org.example.demos.sequencer.beatBox.exceptions.IllegalPropertyException;
import org.example.demos.sequencer.beatBox.exceptions.IllegalPropertyException.PropertyIssue;

public class BeatBoxUtils {
    public static void requireNonEmpty(Collection<?> collection, String propertyName) {
        requireNonNull(collection, propertyName);
        if (collection.isEmpty()) {
            throw new IllegalPropertyException(propertyName, PropertyIssue.EMPTY);
        }
    }

    public static void requireNonEmpty(String property, String propertyName) {
        requireNonNull(property, propertyName);
        if (property.isEmpty()) {
            throw new IllegalPropertyException(propertyName, PropertyIssue.EMPTY);
        }
    }

    public static void requirePositive(int property, String propertyName) {
        if (property <= 0) {
            throw new IllegalPropertyException(propertyName, PropertyIssue.NON_POSITIVE);
        }
    }

    public static void requirePositive(float property, String propertyName) {
        if (property <= 0) {
            throw new IllegalPropertyException(propertyName, PropertyIssue.NON_POSITIVE);
        }
    }

    public static void requireNonNull(Object property, String propertyName) {
        if (property == null) {
            throw new IllegalPropertyException(propertyName, PropertyIssue.NULL);
        }
    }

    public static void requireManifestComponentValidity(String passedComponent, ManifestComponent componentToMatch) {
        requireNonNull(passedComponent, "passed " + componentToMatch.getManifestName());
        requireNonNull(componentToMatch, "componentToMatch");
        if (!Pattern.matches(componentToMatch.getRegex(), passedComponent)) {
            throw new IllegalPropertyException("passed " + componentToMatch.getManifestName(), PropertyIssue.NOT_MATCHING_REGEX);
        }
    }
}
BeatsMismatchContext
package org.example.demos.sequencer.beatBox;

import java.util.List;

public class BeatsMismatchContext extends MismatchContext {
    private final List<String[]> beats;

    public BeatsMismatchContext(BeatBox beatBox, List<String[]> beats, String checkMark) {
        super(beatBox, checkMark);
        this.beats = beats;
    }

    List<String[]> beats() {
        return beats;
    }
}
BeatsStrategy
package org.example.demos.sequencer.beatBox;

public interface BeatsStrategy extends Strategy {
    String BEAT_MISMATCH_WILL_HAVE_EFFECT_MSG = "The sound won't be able to fully match the pattern";
}
ExcessBeatsStrategy
package org.example.demos.sequencer.beatBox;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public enum ExcessBeatsStrategy implements BeatsStrategy {
    TRIM_ALWAYS {
        @Override
        public void handle(BeatsMismatchContext context) {
            BeatBox beatBox = context.beatBox();
            int lengthInPattern = context.beats().get(0).length;
            int lengthInApp = beatBox.getNumberOfBeats();
            beatBox.removeBeats(lengthInApp - lengthInPattern);
            log.info(String.format(LOG_FORMAT, lengthInPattern, lengthInApp,
                    BEATS_REMOVED_MSG, BEAT_MISMATCH_FIXED_MSG));
        }
    }, TRIM_NEVER {
        @Override
        public void handle(BeatsMismatchContext context) {
            int lengthInPattern = context.beats().get(0).length;
            int lengthInApp = context.beatBox().getNumberOfBeats();
            log.warn(String.format(LOG_FORMAT, lengthInPattern, lengthInApp,
                    NO_ACTION_TAKEN_MSG, BEAT_MISMATCH_WILL_HAVE_EFFECT_MSG));
        }
    };
    // number in pattern; number in instrument map; action; result
    private static final String LOG_FORMAT = "The number of beats per instrument in the pattern (%d) was lower than the number " +
            "of beats per instrument in the current instrument map (%d). %s. %s";
    private static final String BEATS_REMOVED_MSG = "Excess beats were removed";
    private static final String BEAT_MISMATCH_FIXED_MSG = "The number of beats per instrument is now in line with the beat pattern";

    public abstract void handle(BeatsMismatchContext context);
}
InstrumentFactory
package org.example.demos.sequencer.beatBox;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import static org.example.demos.sequencer.beatBox.MidiInstrument.*;

public final class InstrumentFactory {
    public static Collection<MidiInstrument> getKathysInstruments() {
        /*
        Instruments used in Kathy Sierra's book Head First Java
         */
        return List.of(
                ACOUSTIC_BASS_DRUM, CLOSED_HI_HAT, OPEN_HI_HAT, ACOUSTIC_SNARE,
                CRASH_CYMBAL_1, HAND_CLAP, HIGH_TOM, HIGH_BONGO,
                MARACAS, LONG_WHISTLE, LOW_CONGA, COWBELL,
                VIBRASLAP, LOW_MID_TOM, HIGH_AGOGO, OPEN_HIGH_CONGA
        );
    }

    public static Collection<MidiInstrument> getAllInstrumentsInStandardOrder() {
        return Arrays.asList(MidiInstrument.values());
    }
}
InstrumentMismatchContext
package org.example.demos.sequencer.beatBox;

import java.util.Arrays;

public final class InstrumentMismatchContext extends MismatchContext {
    private final String name;
    private final String[] beats;

    public InstrumentMismatchContext(BeatBox beatBox, String name,
                                     String[] beats, String checkMark) {
        super(beatBox, checkMark);
        this.name = name;
        this.beats = beats;
    }

    String name() {
        return name;
    }

    String[] beats() {
        return beats;
    }

    // I don't really need equals(), hashCode(), and toString() in this class. It just used to be a record and then I converted it to a class via "Convert record to class" to allow inheritance. So it was generated for me and I saw no reason to manually remove it

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (InstrumentMismatchContext) obj;
        return this.beatBox().equals(that.beatBox()) &&
                this.name.equals(that.name) &&
                Arrays.equals(this.beats(), that.beats()) &&
                this.checkMark().equals(that.checkMark());
    }

    @Override
    public int hashCode() {
        int result = beatBox().hashCode();
        result = 31 * result + name.hashCode();
        result = 31 * result + Arrays.hashCode(beats());
        result = 31 * result + checkMark().hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "InstrumentMismatchContext[" +
                "beatBox=" + beatBox() + ", " +
                "name=" + name + ", " +
                "beats=" + Arrays.toString(beats()) + ", " +
                "checkMark=" + checkMark() + ']';
    }

}
ManifestComponent
package org.example.demos.sequencer.beatBox;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum ManifestComponent {
    SEPARATOR("SEPARATOR", ".+"), CHECK_MARK("CHECK MARK", ".{1}"),
    BLANK_MARK("BLANK MARK", ".{1}");

    private final String manifestName;
    private final String regex;

    @Override
    public String toString() {
        return manifestName;
    }
} 
ManifestParser
package org.example.demos.sequencer.beatBox;

import org.example.demos.sequencer.beatBox.exceptions.IllegalPatternException;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.String.format;
import static org.example.demos.sequencer.beatBox.ManifestComponent.*;

public class ManifestParser {
    private static BufferedReader reader;

    static PatternManifest parseManifest(BufferedReader reader) throws IllegalPatternException, IOException {
        ManifestParser.reader = reader;
        String separator = parse(SEPARATOR);
        String checkMark = parse(CHECK_MARK);
        String blankMark = parse(BLANK_MARK);
        return new PatternManifest(separator, checkMark, blankMark);
    }

    private static String parse(ManifestComponent mc) throws IllegalPatternException, IOException {
        Matcher matcher = Pattern.compile(format("(?<=%s )%s", mc.getManifestName(), mc.getRegex()))
                .matcher(reader.readLine());
        if (matcher.find()) {
            return matcher.group();
        } else {
            throw new IllegalPatternException(mc);
        }
    }
}
MidiInstrument
package org.example.demos.sequencer.beatBox;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.Arrays;
import java.util.Optional;

@RequiredArgsConstructor
@Getter
public enum MidiInstrument {
    FILTER_SNAP("Filter Snap", 27), SLAP_NOISE("Slap Noise", 28),
    SCRATCH_PUSH("Scratch Push", 29), SCRATCH_PULL("Scratch Pull", 30),
    DRUMSTICKS("Drumsticks", 31), CLICK_BOISE("Click Boise", 32),
    METRONOME_CLICK("Metronome Click", 33), METRONOME_BELL("Metronome Bell", 34),
    ACOUSTIC_BASS_DRUM("Acoustic Bass Drum", 35), ELECTRIC_BASS_DRUM("Electric Bass Drum", 36),
    SIDE_STICK("Side Stick", 37), ACOUSTIC_SNARE("Acoustic Snare", 38),
    HAND_CLAP("Hand Clap", 39), ELECTRIC_SNARE("Electric Snare", 40),
    LOW_FLOOR_TOM("Low Floor Tom", 41), CLOSED_HI_HAT("Closed Hi-hat", 42),
    HIGH_FLORR_TOM("High Floor Tom", 43), PEDAL_HI_HAT("Pedal Hi-hat", 44),
    LOW_TOM("Low Tom", 45), OPEN_HI_HAT("Open Hi-hat", 46),
    LOW_MID_TOM("Low-Mid Tom", 47), HIGH_MID_TOM("High-Mid Tom", 48),
    CRASH_CYMBAL_1("Crash Cymbal 1", 49), HIGH_TOM("High Tom", 50),
    RIDE_CYMBAL_1("Ride Cymbal 1", 51), CHINESE_CYMBAL("Chinese Cymbal", 52),
    RIDE_BELL("Ride Bell", 53), TAMBOURINE("Tambourine", 54),
    SPLASH_CYMBAL("Splash Cymbal", 55), COWBELL("Cowbell", 56),
    CRASH_CYMBAL_2("Crash Cymbal 2", 57), VIBRASLAP("Vibraslap", 58),
    RIDE_CYMBAL_2("Ride Cymbal 2", 59), HIGH_BONGO("High Bongo", 60),
    LOW_BONGO("Low Bongo", 61), MUTE_HIGH_CONGA("Mute High Conga", 62),
    OPEN_HIGH_CONGA("Open High Conga", 63), LOW_CONGA("Low Conga", 64),
    HIGH_TIMBALE("High Timbale", 65), LOW_TIMBALE("Low Timbale", 66),
    HIGH_AGOGO("High Agogo", 67), LOW_AGOGO("Low Agogo", 68),
    CABASA("Cabasa", 69), MARACAS("Maracas", 70),
    SHORT_WHISTLE("Short Whistle", 71), LONG_WHISTLE("Long Whistle", 72),
    SHORT_GUIRO("Short Guiro", 73), LONG_GUIRO("Long Guiro", 74),
    CLAVES("Claves", 75), HIGH_WOODBLOCK("High Woodblock", 76),
    LOW_WOODBLOCK("Low Woodblock", 77), MUTE_CUICA("Mute Cuica", 78),
    OPEN_CUICA("Open Cuica", 79), MUTE_TRIANGLE("Mute Triangle", 80),
    OPEN_TRIANGLE("Open Triangle", 81), SHAKER("Shaker", 82),
    JINGLE_BELL("Jingle Bell", 83), BELL_TREE("Bell Tree", 84),
    CASTANETS("Castanets", 85), MUTED_SURDO("Muted Surdo", 86),
    OPEN_SURDO("Open Surdo", 87);

    private final String standardName;
    private final int firstByte;

    static Optional<MidiInstrument> getForName(String instrumentName) {
        return Arrays.stream(values())
                .filter(instrument -> instrument.standardName.equalsIgnoreCase(instrumentName))
                .findFirst();
    }

    @Override
    public String toString() {
        return standardName;
    }
}
MismatchContext
package org.example.demos.sequencer.beatBox;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public abstract class MismatchContext {
    private final BeatBox beatBox;
    private final String checkMark;

    BeatBox beatBox() {
        return this.beatBox;
    }

    String checkMark() {
        return this.checkMark;
    }
}
MissingBeatsStrategy
package org.example.demos.sequencer.beatBox;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.List;

@Slf4j
public enum MissingBeatsStrategy implements BeatsStrategy {
    ADD_ALWAYS {
        @Override
        public void handle(BeatsMismatchContext context) {
            BeatBox beatBox = context.beatBox();
            int lengthInApp = beatBox.getNumberOfBeats();
            int lengthInPattern = context.beats().get(0).length;
            beatBox.addMoreBeats(lengthInPattern - lengthInApp);
            log.info(String.format(LOG_FORMAT, lengthInApp, lengthInPattern,
                    BEATS_ADDED_MSG, BEAT_MISMATCH_FIXED_MSG));
        }
    },
    ADD_IF_BEATS_NOT_BLANK {
        @Override
        public void handle(BeatsMismatchContext context) {
            var patternBeats = context.beats();
            int lengthInApp = context.beatBox().getNumberOfBeats();
            int lengthInPattern = patternBeats.get(0).length;
            if (areAdditionalBeatsInPatternBlank(patternBeats, context.checkMark(), lengthInApp)) {
                log.info(String.format(LOG_FORMAT, lengthInApp, lengthInPattern,
                        NO_ACTION_TAKEN_MSG, BEAT_MISMATCH_IRRELEVANT_MSG));
            } else {
                context.beatBox().addMoreBeats(lengthInPattern - lengthInApp);
                log.info(String.format(LOG_FORMAT, lengthInApp, lengthInPattern,
                        BEATS_ADDED_MSG, BEAT_MISMATCH_FIXED_MSG));
            }
        }
    },
    ADD_NEVER {
        @Override
        public void handle(BeatsMismatchContext context) {
            List<String[]> patternBeats = context.beats();
            int lengthInApp = context.beatBox().getNumberOfBeats();
            int lengthInPattern = patternBeats.get(0).length;
            if (areAdditionalBeatsInPatternBlank(patternBeats, context.checkMark(), lengthInApp)) {
                log.info(String.format(LOG_FORMAT, lengthInApp, lengthInPattern,
                        NO_ACTION_TAKEN_MSG, BEAT_MISMATCH_IRRELEVANT_MSG));
            } else {
                log.warn(String.format(LOG_FORMAT, lengthInApp, lengthInPattern,
                        NO_ACTION_TAKEN_MSG, BEAT_MISMATCH_WILL_HAVE_EFFECT_MSG));
            }
        }
    };

    // number in instrument map; number in pattern; action; result
    private static final String LOG_FORMAT = "The number of beats per instrument in the current " +
            "instrument map (%d) was lower than the number of beats per instrument in the pattern (%d). %s. %s";
    private static final String BEATS_ADDED_MSG = "Missing beats were added";
    private static final String BEAT_MISMATCH_FIXED_MSG = "Beat pattern will be copied";
    private static final String BEAT_MISMATCH_IRRELEVANT_MSG = "No effect on the sound is expected: additional beats in the pattern were blank";

    public abstract void handle(BeatsMismatchContext context);

    private static boolean areAdditionalBeatsInPatternBlank(List<String[]> patternBeats, String checkMark, int numberOfBeatsInApp) {
        return patternBeats.stream()
                .map(beatRow -> Arrays.copyOfRange(beatRow, numberOfBeatsInApp, beatRow.length))
                .flatMap(Arrays::stream)
                .noneMatch(beat -> beat.equals(checkMark));
    }

}
MissingInstrumentStrategy
package org.example.demos.sequencer.beatBox;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.Optional;

@Slf4j
public enum MissingInstrumentStrategy implements Strategy {
    ADD_ALWAYS {
        @Override
        boolean handle(InstrumentMismatchContext c) {
            var missingInstrumentName = c.name();
            var isInstrumentAdded = tryAndAddMissingInstrument(c);
            if (isInstrumentAdded) {
                log.info(String.format(LOG_FORMAT, missingInstrumentName,
                        INSTRUMENT_SUCCESSFULLY_ADDED_MSG, INSTRUMENT_MISMATCH_FIXED_MSG));
            } else {
                if (areBeatsNonBlank(c.beats(), c.checkMark())) {
                    log.warn(String.format(LOG_FORMAT, missingInstrumentName,
                            INSTRUMENT_COULDNT_BE_ADDED_MSG, INSTRUMENT_MISMATCH_WILL_HAVE_EFFECT_MSG));
                } else {
                    log.info(String.format(LOG_FORMAT, missingInstrumentName,
                            INSTRUMENT_COULDNT_BE_ADDED_MSG, INSTRUMENT_MISMATCH_IRRELEVANT_MSG));
                }
            }
            return isInstrumentAdded;
        }
    }, ADD_IF_BEATS_NOT_BLANK {
        @Override
        boolean handle(InstrumentMismatchContext context) {
            var missingInstrumentName = context.name();
            var beats = context.beats();
            var checkMark = context.checkMark();
            if (areBeatsNonBlank(beats, checkMark)) {
                var isInstrumentAdded = tryAndAddMissingInstrument(context);
                if (isInstrumentAdded) {
                    log.info(String.format(LOG_FORMAT, missingInstrumentName,
                            INSTRUMENT_SUCCESSFULLY_ADDED_MSG, INSTRUMENT_MISMATCH_FIXED_MSG));
                } else {
                    log.warn(String.format(LOG_FORMAT, missingInstrumentName,
                            INSTRUMENT_COULDNT_BE_ADDED_MSG, INSTRUMENT_MISMATCH_WILL_HAVE_EFFECT_MSG));
                }
                return isInstrumentAdded;
            }
            log.info(String.format(LOG_FORMAT, missingInstrumentName,
                    NO_ACTION_TAKEN_MSG, INSTRUMENT_MISMATCH_IRRELEVANT_MSG));
            return false;
        }
    }, ADD_NEVER {
        @Override
        boolean handle(InstrumentMismatchContext context) {
            var missingInstrumentName = context.name();
            var beats = context.beats();
            var checkMark = context.checkMark();
            if (areBeatsNonBlank(beats, checkMark)) {
                log.warn(String.format(LOG_FORMAT, missingInstrumentName, NO_ACTION_TAKEN_MSG,
                        INSTRUMENT_MISMATCH_WILL_HAVE_EFFECT_MSG));
            } else {
                log.info(String.format(LOG_FORMAT, missingInstrumentName, NO_ACTION_TAKEN_MSG,
                        INSTRUMENT_MISMATCH_IRRELEVANT_MSG));
            }
            return false;
        }
    };
    // instrument name; action; result
    static final String LOG_FORMAT = "%s was missing from the current instrument map. %s. %s";
    static final String INSTRUMENT_SUCCESSFULLY_ADDED_MSG = "The missing instrument was successfully added";
    static final String INSTRUMENT_COULDNT_BE_ADDED_MSG = "The missing instrument couldn't be added " +
            "(likely because it's not supported by the MIDI standard)";
    static final String INSTRUMENT_MISMATCH_FIXED_MSG = "The instrument's beats will be copied";
    static final String INSTRUMENT_MISMATCH_WILL_HAVE_EFFECT_MSG = "The beats for the instrument in the pattern " +
            "were not blank – the sound won't be able to fully match the pattern";
    static final String INSTRUMENT_MISMATCH_IRRELEVANT_MSG = "No effect on the sound is expected: the beats for the instrument were blank";

    abstract boolean handle(InstrumentMismatchContext context);

    boolean tryAndAddMissingInstrument(InstrumentMismatchContext context) {
        String missingInstrumentName = context.name();
        var beatBox = context.beatBox();
        Optional<MidiInstrument> optionalInstrument = MidiInstrument.getForName(missingInstrumentName);
        if (optionalInstrument.isPresent()) {
            beatBox.addNewInstrument(optionalInstrument.get());
            return true;
        }
        return false;
    }

    boolean areBeatsNonBlank(String[] beats, String checkMark) {
        return Arrays.asList(beats).contains(checkMark);
    }
}
PatternManifest
package org.example.demos.sequencer.beatBox;

public record PatternManifest(String separator, String checkMark, String blankMark) {
}
PressedButtonStrategy
package org.example.demos.sequencer.beatBox;

import javax.swing.*;

public enum PressedButtonStrategy {
    SELECT {
        @Override
        public void apply(JCheckBox checkBox) {
            checkBox.setSelected(true);
        }
    }, TOGGLE {
        @Override
        public void apply(JCheckBox checkBox) {
            checkBox.setSelected(!checkBox.isSelected());
        }
    }, DO_NOTHING {
        @Override
        public void apply(JCheckBox checkBox) {
        }
    };

    public abstract void apply(JCheckBox checkBox);
}
Strategy
package org.example.demos.sequencer.beatBox;

public interface Strategy {
    String NO_ACTION_TAKEN_MSG = "No action was taken";
}
Util
package org.example.shared.utilities;

import org.example.shared.ThrowingRunnable;

import java.util.concurrent.Callable;

public class Util {
// irrelevant utilities omitted

    public static void requirePositive(int... intValues) {
        if (Arrays.stream(intValues).anyMatch(intValue -> intValue <= 0)) {
            throw new IllegalArgumentException("Values must be positive");
        }
    }

    public static void wrapInTryCatch(ThrowingRunnable codeBlock) {
        try {
            codeBlock.run();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T wrapInTryCatchAndGet(Callable<T> returningCodeBlock) {
        try {
            return returningCodeBlock.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
MidiUtil
package org.example.shared.utilities;

import javax.sound.midi.MidiEvent;
import javax.sound.midi.ShortMessage;

public class MidiUtil {
    public static MidiEventBuilder getMidiEventBuilder() {
        return new MidiEventBuilder();
    }
    public static class MidiEventBuilder {
        private int command;
        private int channel;
        private int firstByte;
        private int secondByte;
        private int tick;

        public MidiEventBuilder withCommand(int command) {
            this.command = command;
            return this;
        }

        public MidiEventBuilder withChannel(int channel) {
            this.channel = channel;
            return this;
        }

        public MidiEventBuilder withFirstByte(int firstByte) {
            this.firstByte = firstByte;
            return this;
        }

        public MidiEventBuilder withSecondByte(int secondByte) {
            this.secondByte = secondByte;
            return this;
        }

        public MidiEventBuilder withTick(int tick) {
            this.tick = tick;
            return this;
        }

        public MidiEvent build() {
            return Util.wrapInTryCatchAndGet(() -> {
                ShortMessage message = new ShortMessage(command, channel, firstByte, secondByte);
                return new MidiEvent(message, tick);
            });
        }
    }
}
UIUtil (you can comment on UIUtil specifically here)
package org.example.shared.utilities;

import lombok.NoArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class UIUtil {
/*
Since MRE rules don't apply on Code Review, I did not trim it thoroughly – only enough to fit into the character limit (which is, as of this writing, 65 536)
*/

    private static Rectangle maximumWindowBounds;

    public static UIBuilder getUIBuilder() {
        return new UIBuilder();
    }

    public static UIBuilder getUIBuilderWithLayout(@NotNull Supplier<LayoutManager> layoutSupplier) {
        Objects.requireNonNull(layoutSupplier);
        return new UIBuilder()
                .withLayout(layoutSupplier);
    }

    public static UIBuilder getUIBuilderWithContentPane(@NotNull Supplier<Container> contentPaneSupplier) {
        Objects.requireNonNull(contentPaneSupplier);
        return new UIBuilder()
                .withContentPane(contentPaneSupplier);
    }

    public static UIBuilder getUIBuilderWithLayoutAndContentPane(@NotNull Supplier<LayoutManager> layoutSupplier,
                                                                 @NotNull Supplier<Container> contentPaneSupplier) {
        Stream.of(layoutSupplier, contentPaneSupplier).forEach(Objects::requireNonNull);
        return new UIBuilder()
                .withLayout(layoutSupplier)
                .withContentPane(contentPaneSupplier);
    }

    public static ComponentBuilder getComponentBuilder(@NotNull Supplier<JComponent> componentSupplier) {
        Objects.requireNonNull(componentSupplier);
        return new ComponentBuilder(componentSupplier);
    }

    public static void requestFocusIfBlank(JTextArea textArea, JTextArea... otherTextAreas) {
        Stream.of(ArrayUtils.add(otherTextAreas, textArea))
                .filter(area -> area.getText().isBlank())
                .forEach(JComponent::requestFocus);
    }

    public static Dimension getScreenSize() {
        return Toolkit.getDefaultToolkit().getScreenSize();
    }

    public static Rectangle getMaximumWindowBounds() {
        if (maximumWindowBounds == null) {
            maximumWindowBounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
        }
        return maximumWindowBounds;
    }

    private static void checkBounds(int width, int height) {
        Stream.of(width, height).forEach(Util::requirePositive);
        checkAgainstScreenSize(width, height);
    }

    private static void checkAgainstScreenSize(int widthOrX, int heightOrY) {
        var bounds = getMaximumWindowBounds();
        if (bounds.width < widthOrX) {
            throw new IllegalArgumentException("Values like width or X coordinate cannot be greater than screen width");
        } else if (bounds.height < heightOrY) {
            throw new IllegalArgumentException("Values like height or Y coordinate cannot be greater than screen height");
        }
    }

    @NoArgsConstructor
    public static class UIBuilder {
        private final JFrame frame;
        private final Rectangle maximumWindowBounds = getMaximumWindowBounds();

        {
            frame = new JFrame();
            this.withFrameSize(300, 300)
                    .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }

        private UIBuilder withLayout(@NotNull Supplier<LayoutManager> layoutSupplier) {
            Objects.requireNonNull(layoutSupplier);
            frame.setLayout(layoutSupplier.get());
            return this;
        }

        private UIBuilder withContentPane(@NotNull Supplier<Container> contentPaneSupplier) {
            Objects.requireNonNull(contentPaneSupplier);
            frame.setContentPane(contentPaneSupplier.get());
            return this;
        }

        public UIBuilder withTitle(@NotNull String title) {
            Objects.requireNonNull(title);
            frame.setTitle(title);
            return this;
        }

        public UIBuilder withComponent(@NotNull Supplier<JComponent> componentSupplier) {
            Objects.requireNonNull(componentSupplier);
            getAndAddToPane(componentSupplier);
            return this;
        }

        public UIBuilder withComponent(@NotNull String position, @NotNull Supplier<JComponent> componentSupplier) {
            Stream.of(position, componentSupplier).forEach(Objects::requireNonNull);
            getAndAddToPane(position, componentSupplier);
            return this;
        }

        public UIBuilder withBackgroundColor(@NotNull Color color) {
            Objects.requireNonNull(color);
            frame.getContentPane().setBackground(color);
            return this;
        }

        public UIBuilder withFrameSize(int width, int height) {
            checkBounds(width, height);
            int x = (maximumWindowBounds.width - width) / 2;
            int y = (maximumWindowBounds.height - height) / 2;
            frame.setBounds(x, y, width, height); // implicit frame centering
            return this;
        }

        private void getAndAddToPane(@NotNull Supplier<JComponent> componentSupplier) {
            JComponent textField = componentSupplier.get();
            frame.getContentPane().add(textField);
        }

        private void getAndAddToPane(@NotNull String position, @NotNull Supplier<JComponent> componentSupplier) {
            JComponent textField = componentSupplier.get();
            frame.getContentPane().add(position, textField);
        }

        public UIBuilder withDefaultCloseOperation(int windowConstant) {
            frame.setDefaultCloseOperation(windowConstant);
            return this;
        }

        public UIBuilder withMenuBar(@NotNull Supplier<JMenuBar> menuBarSupplier) {
            Objects.requireNonNull(menuBarSupplier);
            frame.setJMenuBar(menuBarSupplier.get());
            return this;
        }

        public UIBuilder withResizable(boolean isResizable) {
            frame.setResizable(isResizable);
            return this;
        }

        public UIBuilder withLocationRelativeTo(JComponent component) {
            frame.setLocationRelativeTo(component);
            return this;
        }

        public void visualize() {
            frame.setVisible(true);
        }
    }

    public static class ComponentBuilder {
        private JComponent component;

        public ComponentBuilder(@NotNull Supplier<JComponent> panelSupplier) {
            Objects.requireNonNull(panelSupplier);
            this.component = panelSupplier.get();
        }

        public ComponentBuilder withBorder(@NotNull Supplier<Border> borderSupplier) {
            Objects.requireNonNull(borderSupplier);
            component.setBorder(borderSupplier.get());
            return this;
        }

        public ComponentBuilder withSize(int width, int height) {
            checkBounds(width, height);
            component.setSize(width, height);
            return this;
        }

        public ComponentBuilder withComponent(@NotNull String position, @NotNull Supplier<JComponent> componentSupplier) {
            Stream.of(position, componentSupplier).forEach(Objects::requireNonNull);
            component.add(position, componentSupplier.get());
            return this;
        }

        public ComponentBuilder withComponent(@NotNull Supplier<JComponent> componentSupplier) {
            Objects.requireNonNull(componentSupplier);
            component.add(componentSupplier.get());
            return this;
        }

        public ComponentBuilder withVerticalScrollBarPolicy(int verticalScrollbarPolicy) {
            makeComponentScrollableAsNeeded();
            ((JScrollPane) component).setVerticalScrollBarPolicy(verticalScrollbarPolicy);
            return this;
        }

        public ComponentBuilder withHorizontalScrollBarPolicy(int horizontalScrollbarPolicy) {
            makeComponentScrollableAsNeeded();
            ((JScrollPane) component).setHorizontalScrollBarPolicy(horizontalScrollbarPolicy);
            return this;
        }

        public ComponentBuilder withPreferredSize(@NotNull Dimension preferredSize) {
            Objects.requireNonNull(preferredSize);
            component.setPreferredSize(preferredSize);
            return this;
        }

        public ComponentBuilder withLocation(int x, int y) {
            checkBounds(x, y);
            component.setLocation(x, y);
            return this;
        }

        private void makeComponentScrollableAsNeeded() {
            if (!(component instanceof JScrollPane)) {
                component = new JScrollPane(component);
            }
        }

        public JComponent build() {
            return component;
        }
    }
}
ThrowingRunnable
package org.example.shared;

// it's really not important (you could use Junit's ThrowingRunnable or avoid it altogether), but I included it for completeness

public interface ThrowingRunnable {
    void run() throws Exception;
}
App
package org.example.demos.sequencer.beatBox;

import static org.example.demos.sequencer.beatBox.MidiInstrument.*;

public class App {
    public static void main(String[] args) {
        BeatBox beatBox = BeatBoxConfiguration.configure()
                .setPressedButtonStrategy(PressedButtonStrategy.SELECT)
                .setNumberOfBeats(10)
                .setCheckMark("I") // thanks to manifests, it doesn't have to be in line with read patterns' check marks
                .setInstruments(MARACAS, ACOUSTIC_SNARE, DRUMSTICKS) // or you can remove this line and get default instruments
                .build();
        beatBox.launch();
    }
}

Example pattern you can read (but you don't have to read anything to play):

SEPARATOR :
CHECK MARK X
BLANK MARK O
Acoustic Bass Drum : XOOOXOOOXOOOXOOO
Closed Hi-Hat : OOXOOOXXOOXOOOXX
Open Hi-Hat : OOOOOOOOOOOOOOOO
Acoustic Snare : OOOOOOOOOOOOOOOO
Crash Cymbal 1 : OOOOOOOOOOOOOOOO
Hand Clap : OOOOOOOOOOOOOOOO
High Tom : OOOOOOOOOOOOOOOO
High Bongo : OOOOOOOOOOOOXXXO
Maracas : XOXOXOXOXOXOXOXO
Long Whistle : OOOOOOOOOOOOOOOO
Low Conga : OOOOOOXOOXXOXOOO
Cowbell : OOOOOOOOOOOOOOOO
Vibraslap : OOOOOOOOOOOOOOOO
Low-mid Tom : OOOOOOOOOOOOOOOO
High Agogo : OOOOOOOOOOOOOOOO
Open High Conga : OOOXXXOOOOOOXXXO

What the app may look like with default settings:

enter image description here

What do you think?

\$\endgroup\$
13
  • \$\begingroup\$ I assume that Lombok is a dependency? You haven't made that clear \$\endgroup\$
    – Reinderien
    Aug 31, 2023 at 21:17
  • \$\begingroup\$ @Reinderien sure \$\endgroup\$
    – Sergey
    Aug 31, 2023 at 22:16
  • \$\begingroup\$ Did you include Util? This will not run without it. \$\endgroup\$
    – Reinderien
    Aug 31, 2023 at 23:03
  • \$\begingroup\$ You're missing scores of other imports - some can be guessed, but e.g. log really can't. so this is incomplete. \$\endgroup\$
    – Reinderien
    Aug 31, 2023 at 23:05
  • 1
    \$\begingroup\$ It might be better to post a link to a repository such as on Github, and then in your question you would include only the files that you want review comments on, but post them in their entirety. A link to a repository makes it significantly easier for reviewers to load the code into their IDE. Just be careful not to break the rule that says you have to include the parts of the code to be reviewed in the question itself. \$\endgroup\$ Sep 9, 2023 at 21:52

1 Answer 1

3
\$\begingroup\$

Since you aren't dealing with internationalization (i18n), there is no benefit to externalising strings from the site of use to class members, as is done in IllegalPatternException.FORMAT etc. Just use the string inline.

I find "some literal string".formatted() to be more natural than String.format("some format string", args...).

To illustrate the above, you could have

public class IllegalPatternException extends Exception {
    public IllegalPatternException(ManifestComponent wrongComponent) {
        super("The pattern file doesn't specify a valid %s or does it in an illegal format".formatted(wrongComponent));
    }
}

I'm allergic to Lombok. I find that for the brevity it (sometimes) introduces, it doesn't actually help to improve the code. It encourages declaration (even if it's implicit declaration) of boilerplate accessors, when often those accessors shouldn't exist at all. Stylistically, and in a broad sense, I would recommend taking advantage of modern in-built Java features like records, and if you really don't like the verbosity of Java after that's complete, then... there's always C#, which is better in most ways (yes, I realise this is controversial) and by its design doesn't need the likes of Lombok.

Here you need the non-standard org.jetbrains.annotations.NotNull; in C# this is built in.

Here's an example of a reflexive declaration of a property that doesn't accomplish what it was meant to do. BeatsMismatchContext has a beats that's already final (good), but is private with a public accessor meant to prevent it from being changed. But the member is already final, so if made public still wouldn't allow it to be changed referentially; and the accessor just returns the list directly so outside callers can still change the contents of the list. So it's in an unhappy middle ground. Either weaken it, getting rid of the accessor and declaring the member public; or strengthen it - from the accessor call Collections.unmodifiableList. (If you actually need the list to be returned modifiable, that's usually a design smell.)

Take for another example your IllegalPropertyException. PropertyIssue has a whatItShouldBeInstead which is a String - already immutable! So just make it public:

public class IllegalPropertyException extends RuntimeException {
    public final PropertyIssue issue;

    public IllegalPropertyException(String name, PropertyIssue issue) {
        super("%s value is invalid: should be %s".formatted(name, issue.replacement));
        this.issue = issue;
    }

    public enum PropertyIssue {
        NULL("not null"),
        EMPTY("not empty"),
        NON_POSITIVE("greater than zero"),
        NOT_MATCHING_REGEX("matching the regular expression");

        public final String replacement;

        PropertyIssue(String replacement) {
            this.replacement = replacement;
        }
    }
}

whatItShouldBeInstead is an awkward name; consider instead replacement or needed.

In nearly all cases you should avoid import *. Either use regular import on explicit symbols, or - don't import at all, and use fully-qualified symbol references instead. Your InstrumentFactory can be:

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public abstract class InstrumentFactory {
    public static Collection<MidiInstrument> getKathysInstruments() {
        /*
        Instruments used in Kathy Sierra's book Head First Java
         */
        return List.of(
            MidiInstrument.ACOUSTIC_BASS_DRUM,
            MidiInstrument.CLOSED_HI_HAT,
            MidiInstrument.OPEN_HI_HAT,
            MidiInstrument.ACOUSTIC_SNARE,
            MidiInstrument.CRASH_CYMBAL_1,
            MidiInstrument.HAND_CLAP,
            MidiInstrument.HIGH_TOM,
            MidiInstrument.HIGH_BONGO,
            MidiInstrument.MARACAS,
            MidiInstrument.LONG_WHISTLE,
            MidiInstrument.LOW_CONGA,
            MidiInstrument.COWBELL,
            MidiInstrument.VIBRASLAP,
            MidiInstrument.LOW_MID_TOM,
            MidiInstrument.HIGH_AGOGO,
            MidiInstrument.OPEN_HIGH_CONGA
        );
    }

    public static Collection<MidiInstrument> getAllInstrumentsInStandardOrder() {
        return Arrays.asList(MidiInstrument.values());
    }
}

Note that it's been demoted to abstract since it only has statics. Similarly, ManifestParser can remove its import * and instead write

        String separator = parse(ManifestComponent.SEPARATOR);
        String checkMark = parse(ManifestComponent.CHECK_MARK);
        String blankMark = parse(ManifestComponent.BLANK_MARK);

BeatBox is a clear example of a class with too many responsibilities, and we can see that just from looking at the imports list. This needs to be split into, at the least, a view class having Swing stuff, and a business logic or controller class having midi stuff.

writeManifest, instead of writing out plus-concatenated strings and hard-coded newline codepoints, should use printf("SEPARATOR %s%n"). Of course this means you'll need to substitute BufferedWriter with a more capable interface like PrintStream.

didUnsuccessfulInstrumentSettingOccur is strange. You have an error flag sitting in the configuration class state, that will get set (and overwritten!) on every call to setInstruments, and won't have any effect until build. Since all it does is produce a warning, why not just... log in place when that condition occurs, with no flag?

I don't find Strategy to be a useful interface; it doesn't tell us anything about its implementers.

Having MidiEvent be constructed by a builder is not crazy - in a language where there are no keyword arguments. (That is to say, in better languages, there is less need for builders because there is good support for named-argument construction.)

All of that said, I see promise here, and especially if you successfully split up your view and controller then this application is not crazy.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you! 1. The format is the same in all strategies of the same type, and I didn't want to duplicate. 2. For the most part, I don't manage imports directly, I let IntelliJ manage them for me. If it decides to put import *, I don't see what harm it could do. As for the static imports, I like them since I can avoid spelling out class names each time which brings additional brevity. 3. didUnsuccessfulInstrumentSettingOccur is not as weird as it might seem. If I set instruments wrong but then set them right, what's the point of logging if it ended up fine? \$\endgroup\$
    – Sergey
    Sep 10, 2023 at 9:13
  • \$\begingroup\$ 4. I introduced Strategy just to avoid writing "No action was taken" multiple times. What if I decide to alter the phrasing a little bit? With Strategy, I only need to edit the code in one place \$\endgroup\$
    – Sergey
    Sep 10, 2023 at 9:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.