diff --git a/projects/robodj/src/java/robodj/chooser/Chooser.java b/projects/robodj/src/java/robodj/chooser/Chooser.java index 2e72cec2..1f2c82d7 100644 --- a/projects/robodj/src/java/robodj/chooser/Chooser.java +++ b/projects/robodj/src/java/robodj/chooser/Chooser.java @@ -1,5 +1,5 @@ // -// $Id: Chooser.java,v 1.1 2001/06/05 16:42:38 mdb Exp $ +// $Id: Chooser.java,v 1.2 2001/06/07 08:37:47 mdb Exp $ package robodj.chooser; @@ -12,6 +12,7 @@ import java.util.Properties; import com.samskivert.util.PropertiesUtil; import robodj.Log; +import robodj.repository.Model; import robodj.repository.Repository; import robodj.util.ServerControl; @@ -25,6 +26,8 @@ public class Chooser public static Repository repository; + public static Model model; + public static ServerControl scontrol; public static void main (String[] args) @@ -43,6 +46,8 @@ public class Chooser Properties dbprops = PropertiesUtil.getSubProperties(config, "repository.db"); repository = new Repository(dbprops); + model = new Model(repository); + } catch (SQLException sqe) { Log.warning("Unable to establish communication with music " + "database: " + sqe); diff --git a/projects/robodj/src/java/robodj/chooser/ChooserFrame.java b/projects/robodj/src/java/robodj/chooser/ChooserFrame.java index c53b3d68..7e41a14c 100644 --- a/projects/robodj/src/java/robodj/chooser/ChooserFrame.java +++ b/projects/robodj/src/java/robodj/chooser/ChooserFrame.java @@ -1,5 +1,5 @@ // -// $Id: ChooserFrame.java,v 1.1 2001/06/05 16:42:38 mdb Exp $ +// $Id: ChooserFrame.java,v 1.2 2001/06/07 08:37:47 mdb Exp $ package robodj.chooser; @@ -9,6 +9,7 @@ import java.awt.event.ActionListener; import javax.swing.*; import com.samskivert.swing.*; +import robodj.repository.*; public class ChooserFrame extends JFrame @@ -29,8 +30,21 @@ public class ChooserFrame // the top of the UI is the playlist manager section JTabbedPane tpane = new JTabbedPane(); - Component elist = new EntryList(); - tpane.addTab("Browse", null, elist, "Browse music database."); + EntryList elist; + Category[] cats = Chooser.model.getCategories(); + + // create a tab for each category + for (int i = 0; i < cats.length; i++) { + elist = new EntryList(cats[i].categoryid); + String tip = "Browse entries in '" + cats[i].name + "' category."; + tpane.addTab(cats[i].name, null, elist, tip); + } + + // and add one for uncategorized entries + elist = new EntryList(-1); + tpane.addTab("Uncategorized", null, elist, + "Browse uncategorized entries."); + tpane.setSelectedIndex(0); top.add(tpane); diff --git a/projects/robodj/src/java/robodj/chooser/EntryList.java b/projects/robodj/src/java/robodj/chooser/EntryList.java index 4c30175f..9de25201 100644 --- a/projects/robodj/src/java/robodj/chooser/EntryList.java +++ b/projects/robodj/src/java/robodj/chooser/EntryList.java @@ -1,13 +1,15 @@ // -// $Id: EntryList.java,v 1.1 2001/06/05 16:42:38 mdb Exp $ +// $Id: EntryList.java,v 1.2 2001/06/07 08:37:47 mdb Exp $ package robodj.chooser; +import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.sql.SQLException; import java.util.*; import javax.swing.*; +import javax.swing.event.*; import com.samskivert.swing.*; import com.samskivert.swing.util.*; @@ -17,9 +19,9 @@ import robodj.repository.*; public class EntryList extends JPanel - implements TaskObserver, ActionListener + implements TaskObserver, ActionListener, AncestorListener { - public EntryList () + public EntryList (int categoryid) { GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); gl.setOffAxisPolicy(GroupLayout.STRETCH); @@ -27,7 +29,7 @@ public class EntryList // create the pane that will hold the buttons gl = new VGroupLayout(GroupLayout.NONE); - gl.setOffAxisPolicy(GroupLayout.EQUALIZE); + gl.setJustification(GroupLayout.TOP); _bpanel = new JPanel(gl); // give ourselves a wee bit of a border @@ -43,8 +45,15 @@ public class EntryList _upbut.addActionListener(this); add(_upbut, GroupLayout.FIXED); - // start up the task that reads the CD info from the database - TaskMaster.invokeMethodTask("readEntries", this, this); + // use a special font for our name buttons + _nameFont = new Font("Helvetica", Font.PLAIN, 10); + + // keep track of the query that we'll use to populate our entries + // display + _catid = categoryid; + + // listen to ancestor events + addAncestorListener(this); } public void taskCompleted (String name, Object result) @@ -84,7 +93,7 @@ public class EntryList public Entry[] readEntries () throws SQLException { - return Chooser.repository.getEntries(""); + return Chooser.model.getEntries(_catid); } /** @@ -105,6 +114,15 @@ public class EntryList Chooser.repository.populateSongs(_entry); } + /** + * Updates the category for the displayed entry. + */ + public void recategorizeEntry () + throws SQLException + { + Chooser.model.recategorize(_entry, _newcatid); + } + /** * Creates the proper buttons, etc. for each entry. */ @@ -116,6 +134,11 @@ public class EntryList // clear out any existing children _bpanel.removeAll(); + // adjust our layout policy + GroupLayout gl = (GroupLayout)_bpanel.getLayout(); + gl.setOffAxisPolicy(GroupLayout.EQUALIZE); + gl.setOffAxisJustification(GroupLayout.LEFT); + // sort our entries Comparator ecomp = new Comparator() { public int compare (Object o1, Object o2) { @@ -140,12 +163,18 @@ public class EntryList // create a browse and a play button JPanel hpanel = new JPanel(); - GroupLayout gl = new HGroupLayout(GroupLayout.NONE); + gl = new HGroupLayout(GroupLayout.NONE); gl.setOffAxisPolicy(GroupLayout.STRETCH); gl.setJustification(GroupLayout.LEFT); hpanel.setLayout(gl); JButton button; + button = new JButton(entries[i].title); + button.setFont(_nameFont); + button.setActionCommand("browse"); + button.addActionListener(this); + button.putClientProperty("entry", entries[i]); + hpanel.add(button); button = new JButton("Play"); button.setActionCommand("playall"); @@ -153,14 +182,13 @@ public class EntryList button.putClientProperty("entry", entries[i]); hpanel.add(button); - button = new JButton(entries[i].title); - button.setActionCommand("browse"); - button.addActionListener(this); - button.putClientProperty("entry", entries[i]); - hpanel.add(button); - _bpanel.add(hpanel); } + + // if there were no entries, stick a label in to that effect + if (_entries.length == 0) { + _bpanel.add(new JLabel("No entries in this category.")); + } } protected void populateSong (Entry entry) @@ -171,6 +199,20 @@ public class EntryList // clear out any existing children _bpanel.removeAll(); + // adjust our layout policy + ((GroupLayout)_bpanel.getLayout()).setOffAxisPolicy( + GroupLayout.NONE); + + // create a combo box for the categories + JComboBox catcombo = + new JComboBox(ModelUtil.catBoxNames(Chooser.model)); + catcombo.addActionListener(this); + catcombo.setActionCommand("categorize"); + int catid = Chooser.model.getCategory(entry.entryid); + int catidx = ModelUtil.getCategoryIndex(Chooser.model, catid); + catcombo.setSelectedIndex(catidx+1); + _bpanel.add(catcombo); + // add a label for the title JLabel label = new JLabel(entry.title); _bpanel.add(label); @@ -190,8 +232,10 @@ public class EntryList // and add buttons for every song for (int i = 0; i < entry.songs.length; i++) { JButton button = new JButton(entry.songs[i].title); + button.setFont(_nameFont); button.setActionCommand("play"); button.addActionListener(this); + button.setHorizontalAlignment(JButton.LEFT); button.putClientProperty("song", entry.songs[i]); _bpanel.add(button); } @@ -218,16 +262,51 @@ public class EntryList Chooser.scontrol.append(song.location); } else if (cmd.equals("up")) { - populateEntries(_entries); + // re-read the category beacuse this entry may have been + // recategorized + TaskMaster.invokeMethodTask("readEntries", this, this); + + } else if (cmd.equals("categorize")) { + JComboBox cb = (JComboBox)e.getSource(); + String catname = (String)cb.getSelectedItem(); + _newcatid = Chooser.model.getCategoryId(catname); + // do the recategorization in a separate task + TaskMaster.invokeMethodTask("recategorizeEntry", this, this); } else { System.out.println("Unknown action event: " + cmd); } } + public void ancestorAdded (AncestorEvent e) + { + // stick a "loading" label in the list to let the user know + // what's up + _bpanel.add(new JLabel("Loading...")); + + // start up the task that reads the CD info from the database + TaskMaster.invokeMethodTask("readEntries", this, this); + } + + public void ancestorRemoved (AncestorEvent e) + { + // clear out our entry ui elements + _bpanel.removeAll(); + } + + public void ancestorMoved (AncestorEvent e) + { + // nothing doing + } + protected JPanel _bpanel; protected JButton _upbut; + protected int _catid; protected Entry[] _entries; + protected Entry _entry; + protected int _newcatid; + + protected Font _nameFont; } diff --git a/projects/robodj/src/java/robodj/importer/FinishedPanel.java b/projects/robodj/src/java/robodj/importer/FinishedPanel.java index 3a57c034..2ae1830e 100644 --- a/projects/robodj/src/java/robodj/importer/FinishedPanel.java +++ b/projects/robodj/src/java/robodj/importer/FinishedPanel.java @@ -1,5 +1,5 @@ // -// $Id: FinishedPanel.java,v 1.1 2001/03/21 00:41:03 mdb Exp $ +// $Id: FinishedPanel.java,v 1.2 2001/06/07 08:37:47 mdb Exp $ package robodj.importer; @@ -54,6 +54,7 @@ public class FinishedPanel public void wasAddedToFrame (ImporterFrame frame) { + frame.addControlButton("Another", "import", this); frame.addControlButton("Exit", "exit", this); // keep a handle on this for later diff --git a/projects/robodj/src/java/robodj/repository/Entry.java b/projects/robodj/src/java/robodj/repository/Entry.java index 0309938b..2250838d 100644 --- a/projects/robodj/src/java/robodj/repository/Entry.java +++ b/projects/robodj/src/java/robodj/repository/Entry.java @@ -1,5 +1,5 @@ // -// $Id: Entry.java,v 1.1 2000/11/08 06:42:57 mdb Exp $ +// $Id: Entry.java,v 1.2 2001/06/07 08:37:47 mdb Exp $ package robodj.repository; @@ -20,8 +20,8 @@ public class Entry public String title; /** - * The artist that created the music for this entry or "Various" if it - * is not a single-artist work. + * The artist that created the music for this entry or "Various + * Artists" if it is not a single-artist work. */ public String artist; diff --git a/projects/robodj/src/java/robodj/repository/Model.java b/projects/robodj/src/java/robodj/repository/Model.java new file mode 100644 index 00000000..5eeeabf2 --- /dev/null +++ b/projects/robodj/src/java/robodj/repository/Model.java @@ -0,0 +1,235 @@ +// +// $Id: Model.java,v 1.1 2001/06/07 08:37:47 mdb Exp $ + +package robodj.repository; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; + +import com.samskivert.util.IntMap; +import com.samskivert.util.IntIntMap; + +/** + * The model provides an interface to the contents of the repository which + * efficiently accesses the database, caches information and preserves a + * consistent view of the repository in light of modifications made by the + * user (through the model). It is structured so that the client can + * maintain no in-memory state and instead fetch content via the model + * every time it needs to make use of it. + */ +public class Model +{ + /** + * Constructs a new model instance from the supplied repository. + */ + public Model (Repository rep) + throws SQLException + { + _rep = rep; + + // load up our category information + _cats = _rep.getCategories(); + + // sort them into alphabetical order + Arrays.sort(_cats, new Comparator() { + public int compare (Object o1, Object o2) + { + return ((Category)o1).name.compareTo(((Category)o2).name); + } + + public boolean equals (Object other) + { + return other == this; + } + }); + } + + /** + * Returns the categories. + */ + public Category[] getCategories () + { + return _cats; + } + + /** + * Looks up the category id of the category with the specified name. + * + * @return category id of the matching category or -1 if no category + * matched. + */ + public int getCategoryId (String catname) + { + for (int i = 0; i < _cats.length; i++) { + if (_cats[i].name.equals(catname)) { + return _cats[i].categoryid; + } + } + return -1; + } + + /** + * Returns a reference to the specified entry id. This call blocks + * if database access takes place. + */ + public Entry getEntry (int entryid) + throws SQLException + { + Entry entry = (Entry)_entries.get(entryid); + if (entry == null) { + _entries.put(entryid, entry = _rep.getEntry(entryid)); + } + return entry; + } + + /** + * Returns an array containing of the entries in the specified + * category. This call blocks if database access takes place. + */ + public Entry[] getEntries (int categoryid) + throws SQLException + { + ArrayList catlist = getCatList(categoryid); + Entry[] ents = new Entry[catlist.size()]; + catlist.toArray(ents); + return ents; + } + + /** + * Associates the specified entry with the specified category. An + * entry can be mapped into multiple categories. + */ + public void associateEntry (Entry entry, int categoryid) + throws SQLException + { + // keep our category mapping up to date + ArrayList catlist = getCatList(categoryid); + // if the entry's not already in the list, stick it in + if (!catlist.contains(entry)) { + catlist.add(entry); + // update the entry to category map + _entmap.put(entry.entryid, categoryid); + // and update the database + _rep.associateEntry(entry, categoryid); + } + } + + /** + * Disassociates the specified entry from the specified category. + */ + public void disassociateEntry (Entry entry, int categoryid) + throws SQLException + { + // keep our category mapping up to date + ArrayList catlist = getCatList(categoryid); + // if the entry's already in the list, take it out + if (catlist.contains(entry)) { + catlist.remove(entry); + // update the entry to category map + _entmap.remove(entry.entryid); + // and update the database + _rep.disassociateEntry(entry, categoryid); + } + } + + /** + * Returns the category to which this entry is mapped or -1 if it's + * mapped to no category. + */ + public int getCategory (int entryid) + { + return _entmap.get(entryid); + } + + /** + * Removes the entry from whatever category it is currently occupying + * and adds it to the specified category. If the entry is already in + * the specified category, this function does nothing. This is + * temporary until we fully implement multi-category support. + */ + public void recategorize (Entry entry, int categoryid) + throws SQLException + { + // see if we need to change categories + int curcatid = _entmap.get(entry.entryid); + if (curcatid != categoryid) { + disassociateEntry(entry, curcatid); + associateEntry(entry, categoryid); + } + } + + /** + * Ensures that the entry's songs array is populated. This call blocks + * if database access takes place. + */ + public void populateSongs (Entry entry) + throws SQLException + { + _rep.populateSongs(entry); + } + + protected ArrayList getCatList (int categoryid) + throws SQLException + { + ArrayList catlist = (ArrayList)_catmap.get(categoryid); + + if (catlist == null) { + // create a new category list and stick it into the table + catlist = new ArrayList(); + _catmap.put(categoryid, catlist); + + // figure out which entries are in this category + int[] entids = _rep.getEntryIds(categoryid); + + // create a select string with the eids that aren't already + // loaded + StringBuffer query = new StringBuffer(); + for (int i = 0; i < entids.length; i++) { + // make sure the entry to category id mapping table + // contains this information + _entmap.put(entids[i], categoryid); + + // see if we've cached the entry + Entry entry = (Entry)_entries.get(entids[i]); + if (entry != null) { + // if we have already loaded this entry, stick it into + // the category list + catlist.add(entry); + + } else { + // otherwise, append it to the query to be loaded + if (query.length() > 0) { + query.append(", "); + } + query.append(entids[i]); + } + } + + // load up these entries if we have any to load + if (query.length() > 0) { + query.insert(0, "where entryid in ("); + query.append(")"); + + // now load up these entries + Entry[] ents = _rep.getEntries(query.toString()); + for (int i = 0; i < ents.length; i++) { + // cache 'em + _entries.put(ents[i].entryid, ents[i]); + // and stick 'em in the category list + catlist.add(ents[i]); + } + } + } + + return catlist; + } + + protected Repository _rep; + protected Category[] _cats; + + protected IntMap _entries = new IntMap(); + protected IntMap _catmap = new IntMap(); + protected IntIntMap _entmap = new IntIntMap(); +} diff --git a/projects/robodj/src/java/robodj/repository/ModelUtil.java b/projects/robodj/src/java/robodj/repository/ModelUtil.java new file mode 100644 index 00000000..9c7c2f74 --- /dev/null +++ b/projects/robodj/src/java/robodj/repository/ModelUtil.java @@ -0,0 +1,45 @@ +// +// $Id: ModelUtil.java,v 1.1 2001/06/07 08:37:47 mdb Exp $ + +package robodj.chooser; + +import robodj.repository.*; + +/** + * Some utility functions. + */ +public class ModelUtil +{ + /** + * Generates a string array suitable for sticking into a combo box + * with the category names. + */ + public static String[] catBoxNames (Model model) + { + Category[] cats = model.getCategories(); + String[] names = new String[cats.length+1]; + names[0] = ""; + for (int i = 0; i < cats.length; i++) { + names[i+1] = cats[i].name; + } + return names; + } + + /** + * Returns the index of the specified category in the category array. + * Useful for activating a particular category in a combo box, for + * example. -1 is returned if no category with the specified id exists + * in the category list (but if that's the case, something really + * wacky is going on). + */ + public static int getCategoryIndex (Model model, int categoryid) + { + Category[] cats = model.getCategories(); + for (int i = 0; i < cats.length; i++) { + if (cats[i].categoryid == categoryid) { + return i; + } + } + return -1; + } +} diff --git a/projects/robodj/src/java/robodj/repository/Repository.java b/projects/robodj/src/java/robodj/repository/Repository.java index 80dabf11..f5cafe67 100644 --- a/projects/robodj/src/java/robodj/repository/Repository.java +++ b/projects/robodj/src/java/robodj/repository/Repository.java @@ -1,13 +1,15 @@ // -// $Id: Repository.java,v 1.4 2001/06/02 02:22:55 mdb Exp $ +// $Id: Repository.java,v 1.5 2001/06/07 08:37:47 mdb Exp $ package robodj.repository; import java.sql.*; +import java.util.Enumeration; import java.util.List; import java.util.Properties; import com.samskivert.jdbc.MySQLRepository; +import com.samskivert.jdbc.JDBCUtil; import com.samskivert.jdbc.jora.*; import com.samskivert.util.*; @@ -50,8 +52,8 @@ public class Repository extends MySQLRepository "entryid"); _stable = new Table(Song.class.getName(), "songs", _session, "songid"); - _ctable = new Table(Category.class.getName(), "categories", _session, - "categoryid"); + _ctable = new Table(Category.class.getName(), "category_names", + _session, "categoryid"); _cmtable = new Table(CategoryMapping.class.getName(), "category_map", _session, new String[] {"categoryid", "entryid"}); } @@ -98,7 +100,7 @@ public class Repository extends MySQLRepository public Entry[] getEntries (final String query) throws SQLException { - return (Entry[])execute(new Operation () { + return (Entry[])execute(new Operation() { public Object invoke () throws SQLException { // look up the entry @@ -123,7 +125,7 @@ public class Repository extends MySQLRepository throws SQLException { if (entry.songs == null) { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { // load up the songs for this entry @@ -149,7 +151,7 @@ public class Repository extends MySQLRepository public void insertEntry (final Entry entry) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { // insert the entry into the entry table @@ -187,7 +189,7 @@ public class Repository extends MySQLRepository public void updateEntry (final Entry entry) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { // update the entry @@ -211,7 +213,7 @@ public class Repository extends MySQLRepository public void deleteEntry (final Entry entry) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { // remove the entry from the entry table and the foreign @@ -238,7 +240,7 @@ public class Repository extends MySQLRepository public void addSongToEntry (final Entry entry, final Song song) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { // fill in the appropriate entry id value @@ -261,7 +263,7 @@ public class Repository extends MySQLRepository public void updateSong (final Song song) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { _stable.update(song); @@ -277,7 +279,7 @@ public class Repository extends MySQLRepository public int createCategory (final String name) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { Category cat = new Category(); @@ -297,7 +299,7 @@ public class Repository extends MySQLRepository public Category[] getCategories () throws SQLException { - return (Category[])execute(new Operation () { + return (Category[])execute(new Operation() { public Object invoke () throws SQLException { Cursor ccur = _ctable.select(""); @@ -309,6 +311,64 @@ public class Repository extends MySQLRepository }); } + /** + * Fetches all of the entry ids that are associated with this category + * and returns them. If categoryid is -1, it instead + * returns all of the entry ids that are not in any category. + */ + public int[] getEntryIds (final int categoryid) + throws SQLException + { + return (int[])execute(new Operation() { + public Object invoke () throws SQLException + { + if (categoryid >= 0) { + String query = "where categoryid=" + categoryid; + Cursor cmcur = _cmtable.select(query); + List cmlist = cmcur.toArrayList(); + int[] eids = new int[cmlist.size()]; + for (int i = 0; i < cmlist.size(); i++) { + eids[i] = ((CategoryMapping)cmlist.get(i)).entryid; + } + return eids; + + } else { + // i wish i knew how to do this sort of jockeying in + // SQL, but I can't be bothered to sort it out for a + // data set that isn't going to be so big that it + // can't be done in Java + IntMap ids = new IntMap(); + Statement stmt = _session.connection.createStatement(); + try { + // first add all the entryids in the repository + String query = "select entryid from entries"; + ResultSet rs = stmt.executeQuery(query); + while (rs.next()) { + ids.put(rs.getInt(1), ""); + } + + // now remove those that are mapped to a category + query = "select entryid from category_map"; + rs = stmt.executeQuery(query); + while (rs.next()) { + ids.remove(rs.getInt(1)); + } + + } finally { + JDBCUtil.close(stmt); + } + + int[] eids = new int[ids.size()]; + Enumeration keys = ids.keys(); + for (int i = 0; keys.hasMoreElements(); i++) { + eids[i] = ((Integer)keys.nextElement()).intValue(); + } + return eids; + } + } + }); + } + /** * Associates the specified entry with the specified category. An * entry can be mapped into multiple categories. @@ -316,7 +376,7 @@ public class Repository extends MySQLRepository public void associateEntry (final Entry entry, final int categoryid) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { CategoryMapping map = @@ -333,7 +393,7 @@ public class Repository extends MySQLRepository public void disassociateEntry (final Entry entry, final int categoryid) throws SQLException { - execute(new Operation () { + execute(new Operation() { public Object invoke () throws SQLException { CategoryMapping map =