Andrew Cooke | Contents | Latest | RSS | Previous | Next

C[omp]ute

Welcome to my blog, which was once a mailing list of the same name and is still generated by mail. Please reply via the "comment" links.

Always interested in offers/projects/new ideas. Eclectic experience in fields like: numerical computing; Python web; Java enterprise; functional languages; GPGPU; SQL databases; etc. Based in Santiago, Chile; telecommute worldwide. CV; email.

Personal Projects

Choochoo Training Diary

Last 100 entries

[Programming] React Leaflet; AliExpress Independent Sellers; Applebaum - Twilight of Democracy; [Politics] Back + US Elections; [Programming,Exercise] Simple Timer Script; [News] 2019: The year revolt went global; [Politics] The world's most-surveilled cities; [Bike] Hope Freehub; [Restaurant] Mama Chau's (Chinese, Providencia); [Politics] Brexit Podcast; [Diary] Pneumonia; [Politics] Britain's Reichstag Fire moment; install cairo; [Programming] GCC Sanitizer Flags; [GPU, Programming] Per-Thread Program Counters; My Bike Accident - Looking Back One Year; [Python] Geographic heights are incredibly easy!; [Cooking] Cookie Recipe; Efficient, Simple, Directed Maximisation of Noisy Function; And for argparse; Bash Completion in Python; [Computing] Configuring Github Jekyll Locally; [Maths, Link] The Napkin Project; You can Masquerade in Firewalld; [Bike] Servicing Budget (Spring) Forks; [Crypto] CIA Internet Comms Failure; [Python] Cute Rate Limiting API; [Causality] Judea Pearl Lecture; [Security, Computing] Chinese Hardware Hack Of Supermicro Boards; SQLAlchemy Joined Table Inheritance and Delete Cascade; [Translation] The Club; [Computing] Super Potato Bruh; [Computing] Extending Jupyter; Further HRM Details; [Computing, Bike] Activities in ch2; [Books, Link] Modern Japanese Lit; What ended up there; [Link, Book] Logic Book; Update - Garmin Express / Connect; Garmin Forerunner 35 v 230; [Link, Politics, Internet] Government Trolls; [Link, Politics] Why identity politics benefits the right more than the left; SSH Forwarding; A Specification For Repeating Events; A Fight for the Soul of Science; [Science, Book, Link] Lost In Math; OpenSuse Leap 15 Network Fixes; Update; [Book] Galileo's Middle Finger; [Bike] Chinese Carbon Rims; [Bike] Servicing Shimano XT Front Hub HB-M8010; [Bike] Aliexpress Cycling Tops; [Computing] Change to ssh handling of multiple identities?; [Bike] Endura Hummvee Lite II; [Computing] Marble Based Logic; [Link, Politics] Sanity Check For Nuclear Launch; [Link, Science] Entropy and Life; [Link, Bike] Cheap Cycling Jerseys; [Link, Music] Music To Steal 2017; [Link, Future] Simulated Brain Drives Robot; [Link, Computing] Learned Index Structures; Solo Air Equalization; Update: Higher Pressures; Psychology; [Bike] Exercise And Fuel; Continental Race King 2.2; Removing Lowers; Mnesiacs; [Maths, Link] Dividing By Zero; [Book, Review] Ray Monk - Ludwig Wittgenstein: The Duty Of Genius; [Link, Bike, Computing] Evolving Lacing Patterns; [Jam] Strawberry and Orange Jam; [Chile, Privacy] Biometric Check During Mail Delivery; [Link, Chile, Spanish] Article on the Chilean Drought; [Bike] Extended Gear Ratios, Shimano XT M8000 (24/36 Chainring); [Link, Politics, USA] The Future Of American Democracy; Mass Hysteria; [Review, Books, Links] Kazuo Ishiguro - Never Let Me Go; [Link, Books] David Mitchell's Favourite Japanese Fiction; [Link, Bike] Rear Suspension Geometry; [Link, Cycling, Art] Strava Artwork; [Link, Computing] Useful gcc flags; [Link] Voynich Manuscript Decoded; [Bike] Notes on Servicing Suspension Forks; [Links, Computing] Snap, Flatpack, Appimage; [Link, Computing] Oracle is leaving Java (to die); [Link, Politics] Cubans + Ultrasonics; [Book, Link] Laurent Binet; VirtualBox; [Book, Link] No One's Ways; [Link] The Biggest Problem For Cyclists Is Bad Driving; [Computing] Doxygen, Sphinx, Breathe; [Admin] Brokw Recent Permalinks; [Bike, Chile] Buying Bearings in Santiago; [Computing, Opensuse] Upgrading to 42.3; [Link, Physics] First Support for a Physics Theory of Life; [Link, Bike] Peruvian Frame Maker; [Link] Awesome Game Theory Tit-For-Tat Thing; [Food, Review] La Fabbrica - Good Italian Food In Santiago; [Link, Programming] MySQL UTF8 Broken; [Link, Books] Latin American Authors

© 2006-2017 Andrew Cooke (site) / post authors (content).

MVC via Beans/Reflection/Spring with Java

From: "andrew cooke" <andrew@...>

Date: Sat, 25 Feb 2006 23:04:26 -0300 (CLST)

I've been reworking the form handling in the ol' Secret Project.  Below is
the handler for a page with two states ("simple" and "editing").  In the
"editing" state you can alter various fields.  The form fields are a
values in a bean (the Model), the JSP page contains the necessary to logic
to display the fields appropriately (the View), and below is the
Controller.

Obviously a lot of work is handled by sub-classes.  What's nice is that
it's generic.  Only the logic below is specific to this form:
 - reading / saving to the database
 - restricting editing to the "editing" state

The switching between states is via submit buttons, with names of the form
"action:xxx:path".  The xxx results in a call to setXxx(model) in the
action bean, where "path" is available in the action bean's scope and
identifies the appropriate information in the model.

So, for example, "action:delete:user.name" would call setDelete(model)
which would delete the user's name from the underlying model (via Java
Bean conventions / reflection).

public class IndexController extends BaseController {

  /**
   * Available bean modes
   */
  private static final String SIMPLE = "simple";
  private static final String EDITING = "editing";
  private static final SetEnum modes = new SetEnum().
  register(SIMPLE).
  register(EDITING);

  private InfoDao infoDao;

  public void setInfoDao(InfoDao infoDao) {
    this.infoDao = infoDao;
  }

  public InfoDao getInfoDao() {
    return infoDao;
  }

  @Override
  protected FormDisplayable newFormBean(HttpServletRequest req) {
    Info info = getInfoDao().getInfo(getViewer(), getOwner(req));
    info._setMembers(modes);
    info._setMode(SIMPLE);
    info.getDescription()._setMode(
        info.isMutable() ?
            FieldDisplayMode.EDITABLE
            : FieldDisplayMode.READONLY);
    logger.debug("new form bean: " + info);
    return info;
  }

  @Override
  protected Object newActionBean(HttpServletRequest req) {
    return new ActionBean(req);
  }

  public class ActionBean extends BaseActionBean {

    public ActionBean(HttpServletRequest req) {super(req);}

    @Override
    public void setEdit(FormDisplayable bean)
    throws Exception {
      super.setEdit(bean);
      bean._setMode(EDITING);
    }

    public void setSave(FormDisplayable bean)
    throws Exception {
      check(bean.isMode(EDITING),
          "can only save edited beans");
      getInfoDao().save((Info)bean);
      setInit(bean);
    }

  }

}

Andrew

A More Complex Example

From: "andrew cooke" <andrew@...>

Date: Mon, 27 Feb 2006 22:24:03 -0300 (CLST)

Here's a more complex example - a page with three different states, and a
variety of paths with slightly different business logic.

public class ContactsController extends BaseController {

  // define the different states for the page
  private static final String SIMPLE = "simple";
  private static final String EDITING = "editing";
  private static final String ALL = "showing-all";
  private static final SetEnum modes = new SetEnum()
  .register(SIMPLE)
  .register(EDITING)
  .register(ALL);

  // and associate the different bean paths with different buisness
  // rules (by tagging with string)
  private static final String MAX_ONE = "max-one";
  private static final String MIN_ONE = "min-one";
  private static final PathIndex paths = new PathIndex()
  .register("names", Name.class, MAX_ONE)
  .register("emails", Email.class, MIN_ONE)
  .register("addresses", Address.class)
  .register("nicks", Nick.class);

  private static String COUNTRIES = "countries";

  // the standard spring stuff for binding values to complex
  // data; in this case the country is selected from a list so
  // we need to convert from string to database key
  @Override
  protected void initBinder(HttpServletRequest req,
      ServletRequestDataBinder binder) {
    CountryPropertyEditor editor = new CountryPropertyEditor();
    editor.setContactsDao(getContactsDao());
    binder.registerCustomEditor(Country.class, "addresses.country", editor);
  }

  // add the list of countries to the model so we can generate
  // the list to select from
  @Override
  protected Map buildModel(HttpServletRequest req,
      FormDisplayable formBean) {
    addExtra(COUNTRIES, getContactsDao().getCountries());
    return super.buildModel(req, formBean);
  }

  // generate and initialise the model bean
  @Override
  protected FormDisplayable newFormBean(HttpServletRequest req) {
    Contacts contacts = getContactsDao().getContacts(getViewer(),
        getOwner(req));
    initialNameState(contacts.getNames());
    initialState(contacts.getAddresses());
    initialState(contacts.getEmails());
    initialState(contacts.getNicks());
    contacts._setMembers(modes);
    contacts._setMode(SIMPLE);
    return contacts;
  }

  // set view information based on model values
  private void initialNameState(List<Name> names) {
    for (Name name : names) {
      switch (name.getState()) {
      case CLOSED:
        name._setMode(FieldDisplayMode.EXPIRED);
        break;
      case OPEN:
        name._setMode(FieldDisplayMode.REPLACEABLE);
        break;
      case READONLY:
        name._setMode(FieldDisplayMode.READONLY);
        break;
      default:
        // error!
        name._setMode(FieldDisplayMode.READONLY);
      }
      logger.debug(name + ": " + name.getMode());
    }
  }

  private void initialState(List<? extends LifetimeAccessControl> items) {
    for (LifetimeAccessControl item : items) {
      switch (item.getState()) {
      case CLOSED:
        item._setMode(FieldDisplayMode.EXPIRED);
        break;
      case OPEN:
        item._setMode(FieldDisplayMode.DELETEABLE);
        break;
      case READONLY:
        item._setMode(FieldDisplayMode.READONLY);
        break;
      default:
        // error!
        item._setMode(FieldDisplayMode.READONLY);
      }
      logger.debug(item + ": " + item.getMode());
    }
  }

  // the action bean, defined below, is used to modify the model
  @Override
  protected Object newActionBean(HttpServletRequest req) {
    return new ActionBean(req);
  }

  public class ActionBean extends LifetimeActionBean {

    public ActionBean(HttpServletRequest req) {
      super(req);
    }

    // save modified data
    public void setSave(FormDisplayable bean) throws Exception {
      logger.debug("saving");
      Contacts contacts = (Contacts) bean;
      check(contacts.isMode(EDITING), "can only save mutable beans");
      Path parent = Bk.parse(path).dropChild();
      Contacts prev = getContactsDao().getContacts(contacts);
      for (Object child : Bk.getChanged(prev, contacts, parent)) {
        getContactsDao().save(contacts, (LifetimeAccessControl) child);
      }
      setInit(bean);
      return;
    }

    // delete a particular path
    // note how we check whether a path is associated with a
    // particular rule (minimum of one value in the collection)
    @Override
    public void setDelete(FormDisplayable bean) throws Exception {
      Path parent = new PathParser(path).getPath().dropChild();
      int children = Bk.getChildren(bean, parent).size();
      logger.debug("deleting at " + path + " (" + children + " children)");
      if (children < 2)
        paths.checkNot(parent.toString(), MIN_ONE);
      check(((ViewedResource) bean).isMutable(),
          "can only delete items from a mutable bean");
      check(bean.isMode(SIMPLE), "can only delete in simple mode");
      super.setDelete(bean);
      bean._setMode(EDITING);
    }

    // replace an exising value with a new one
    @Override
    public void setReplace(FormDisplayable bean) throws Exception {
      logger.debug("replacing at " + path);
      check(bean.isMode(SIMPLE), "can only replace in simple mode");
      super.setReplace(bean);
      bean._setMode(EDITING);
    }

    public void setAll(FormDisplayable bean) {
      logger.debug("showing all");
      check(bean.isMode(SIMPLE), "can only show all in simple mode");
      bean._setMode(ALL);
      return;
    }

    // add a new value.  again, check for path/rule
    @SuppressWarnings("unchecked")
    public void setAdd(FormDisplayable bean) throws Exception {
      int children =
        Bk.getChildren(bean, new PathParser(path).getPath()).size();
      logger.debug("adding at " + path + " (" + children + " children)");
      if (children > 0)
        paths.checkNot(path, MAX_ONE);
      check(((ViewedResource) bean).isMutable(),
          "can only add items to mutable bean");
      check(bean.isMode(SIMPLE), "can only add in simple mode");
      FieldDisplayable item = (FieldDisplayable) Bk.newInstance(paths
          .getClass(path));
      ((List) Bk.getProperty(bean, path)).add(item);
      bean._setMode(EDITING);
      return;
    }

  }

}

Modes as Enums

From: "andrew cooke" <andrew@...>

Date: Wed, 1 Mar 2006 09:48:53 -0300 (CLST)

Looking at the code above, the use of Strings as mode names is ugly -
they're an obvious candidate for enums.  However, the relevant methods are
added to data transport classes with AspectJ Mixins, which don't support
generics.  So it wasn't clear how to do this in a type-safe way.

Anyway, yesterday I found the EnumSet class, which lets me write simple
dynamic code that is pretty type-safe.  Here's the mixin:

public aspect FormDisplayableMixin {

  declare parents : ViewedResource
  implements FormDisplayable;

  private FormDisplayMode FormDisplayable.mode =
    new FormDisplayMode();

  public void FormDisplayable._setMode(Enum mode) {
    this.mode._setMode(mode);
  }

  public void FormDisplayable._setMembers(Class mode) {
    this.mode._setMembers(mode);
  }

  public boolean FormDisplayable.isMode(Enum mode) {
    return this.mode.isMode(mode);
  }

  public int FormDisplayable.modeHashCode() {
    return this.mode.hashCode();
  }

  public String FormDisplayable.getMode() {
    return this.mode.toString();
  }

}

And here's the underlying utility class that manages the mode:

public class FormDisplayMode {

  private EnumSet members;
  private Enum mode;

  @SuppressWarnings("unchecked")
  public void _setMembers(Class mode) {
    this.members = EnumSet.allOf(mode);
    mode = null;
  }

  public void _setMode(Enum mode) {
    check(mode);
    this.mode = mode;
  }

  public void check(Enum mode) {
    if (! members.contains(mode))
      throw new IllegalArgumentException("bad mode");
  }

  public boolean isMode(Enum mode) {
    check(mode);
    return mode.equals(this.mode);
  }

  public String toString() {
    return mode.toString();
  }

  public int hashCode() {
    return Tk.hashCode(mode);
  }

}

See how the "check" method checks that the type is correct?  In
retrospect, I could also have saved the class directly and done an
"instanceof", but I prefer this approach because I can add an extra
setMembers method that takes a set explicitly and which allows a model to
be used with only a subset of states (which is going to be useful for
models shared across several views, I think).

Rules as Enums

From: "andrew cooke" <andrew@...>

Date: Wed, 1 Mar 2006 10:34:29 -0300 (CLST)

And here's the same thing for the rules.  In this case I can use generics,
and it all works out quite nicely:

/**
 * Utility class that associates paths to bean members with classes and
 * rule sets.  The two uses (classes and rule sets) are unconnected and
 * should perhaps be separated.
 */
public class PathIndex<E extends Enum<E>> {

  private Log logger = LogFactory.getLog(getClass());

  private Map<String, Class> classes = new HashMap<String, Class>();
  private Map<Enum<E>, Set<String>> rules =
    new HashMap<Enum<E>, Set<String>>();

  public PathIndex<E>     register(String path, Class clazz) {
    // cannot work out how to use EnumSet.noneOf with generics
    // EnumSet.noneOf(E) fails to compile
    return register(path, clazz, EnumSet.copyOf(new HashSet<E>()));
  }

  public PathIndex<E>     register(String path, Class clazz, E rule) {
    return register(path, clazz, EnumSet.of(rule));
  }

  public PathIndex<E> register(String path, Class clazz, EnumSet<E> all) {
    if (classes.containsKey(path))
      throw new
      IllegalArgumentException("duplicate path: " + path);
    classes.put(path, clazz);
    for (Enum<E> rule: all) {
      if (! rules.containsKey(rule))
        rules.put(rule, new HashSet<String>());
      rules.get(rule).add(path);
      logger.debug("registered path " + path + " as " + rule);
    }
    return this;
  }

  public String check(String path) {
    if (! classes.containsKey(path))
      throw new AccessControlException("bad path: " + path);
    return path;
  }

  public String check(String path, Enum<E> rule) {
    check(path);
    if (! rules.get(rule).contains(path))
      throw new AccessControlException("bad rule: " + rule);
    return path;
  }

  public String checkNot(String path, Enum<E> rule) {
    check(path);
    if (rules.get(rule).contains(path))
      throw new AccessControlException("bad rule: " + rule);
    return path;
  }

  public Class getClass(String path) {
    return classes.get(path);
  }

}

Empty EnumSet

From: "andrew cooke" <andrew@...>

Date: Wed, 1 Mar 2006 10:41:26 -0300 (CLST)

The code above (generating EnumSet<E> from an empty collection) gives a
runtime error.  I guess it's because of type erasure, but then how does
noneOf work?

I've replaced it with null and an test in the main register method.

Anyway, here's the cleaned-up controller:

public class ContactsController extends BaseController {

  private static enum Mode {SIMPLE, EDITING, SHOWING_ALL};

  private static enum Rule {MAX_ONE, MIN_ONE};
  private static final PathIndex<Rule> paths = new PathIndex<Rule>()
  .register("names", Name.class, Rule.MAX_ONE)
  .register("emails", Email.class, Rule.MIN_ONE)
  .register("addresses", Address.class)
  .register("nicks", Nick.class);

  private static String COUNTRIES = "countries";

  @Override
  protected void initBinder(HttpServletRequest req,
      ServletRequestDataBinder binder) {
    CountryPropertyEditor editor = new CountryPropertyEditor();
    editor.setContactsDao(getContactsDao());
    binder.registerCustomEditor(Country.class, "addresses.country", editor);
  }

  @Override
  protected Map buildModel(HttpServletRequest req, FormDisplayable
formBean) {
    addExtra(COUNTRIES, getContactsDao().getCountries());
    return super.buildModel(req, formBean);
  }

  @Override
  protected FormDisplayable newFormBean(HttpServletRequest req) {
    Contacts contacts = getContactsDao().getContacts(getViewer(),
        getOwner(req));
    initialState(contacts.getNames());
    initialState(contacts.getAddresses());
    initialState(contacts.getEmails());
    initialState(contacts.getNicks());
    contacts._setMembers(Mode.class);
    contacts._setMode(Mode.SIMPLE);
    return contacts;
  }

  private void initialState(List<? extends LifetimeAccessControl> items) {
    for (LifetimeAccessControl item : items) {
      switch (item.getState()) {
      case CLOSED:
        item._setMode(FieldDisplayMode.EXPIRED);
        break;
      case OPEN:
        item._setMode(FieldDisplayMode.DELETEABLE);
        break;
      case READONLY:
        item._setMode(FieldDisplayMode.READONLY);
        break;
      default:
        // error!
        item._setMode(FieldDisplayMode.READONLY);
      }
      logger.debug(item + ": " + item.getMode());
    }
  }

  @Override
  protected Object newActionBean(HttpServletRequest req) {
    return new ActionBean(req);
  }

  public class ActionBean extends LifetimeActionBean {

    public ActionBean(HttpServletRequest req) {
      super(req);
    }

    public void setSave(FormDisplayable bean) throws Exception {
      logger.debug("saving");
      Contacts contacts = (Contacts) bean;
      check(contacts.isMode(Mode.EDITING), "can only save mutable beans");
      Path parent = Bk.parse(path).dropChild();
      Contacts prev = getContactsDao().getContacts(contacts);
      for (Object child : Bk.getChanged(prev, contacts, parent)) {
        getContactsDao().save(contacts, (LifetimeAccessControl) child);
      }
      setInit(bean);
      return;
    }

    @Override
    public void setDelete(FormDisplayable bean) throws Exception {
      Path parent = new PathParser(path).getPath().dropChild();
      int children = Bk.getChildren(bean, parent).size();
      logger.debug("deleting at " + path + " (" + children + " children)");
      if (children < 2) paths.checkNot(parent.toString(), Rule.MIN_ONE);
      check(bean.isMode(Mode.SIMPLE), "can only delete in simple mode");
      super.setDelete(bean);
      bean._setMode(Mode.EDITING);
    }

    @Override
    public void setReplace(FormDisplayable bean) throws Exception {
      Path parent = new PathParser(path).getPath().dropChild();
      int children = Bk.getChildren(bean, parent).size();
      logger.debug("replacing at " + path + " (" + children + " children)");
      if (children > 1) paths.checkNot(parent.toString(), Rule.MAX_ONE);
      check(bean.isMode(Mode.SIMPLE), "can only replace in simple mode");
      super.setReplace(bean);
      bean._setMode(Mode.EDITING);
    }

    public void setAll(FormDisplayable bean) {
      logger.debug("showing all");
      check(bean.isMode(Mode.SIMPLE), "can only show all in simple mode");
      bean._setMode(Mode.SHOWING_ALL);
      return;
    }

    @SuppressWarnings("unchecked")
    public void setAdd(FormDisplayable bean) throws Exception {
      int children =
        Bk.getChildren(bean, new PathParser(path).getPath()).size();
      logger.debug("adding at " + path + " (" + children + " children)");
      if (children > 0) paths.checkNot(path, Rule.MAX_ONE);
      check(((ViewedResource) bean).isMutable(),
          "can only add items to mutable bean");
      check(bean.isMode(Mode.SIMPLE), "can only add in simple mode");
      FieldDisplayable item = (FieldDisplayable) Bk.newInstance(paths
          .getClass(path));
      ((List)Bk.getProperty(bean, path)).add(item);
      bean._setMode(Mode.EDITING);
      return;
    }

  }

}

Comment on this post