package sale; import java.util.*; import java.io.IOException; import java.awt.Rectangle; import javax.swing.*; import javax.swing.event.*; import users.*; import log.*; import sale.events.*; import data.DataBasket; import data.Stock; import data.Catalog; import util.*; /** * A single point of sale in a shop. * * <p>SalesPoints represent single points of sale in a {@link Shop} at which user interaction occurs. * Examples for such SalesPoints are cash counters, box offices, ticket vending machines etc.</p> * * <p>Normally, you will have at least one SalesPoint in your application.</p> * * <p>Services available at SalesPoints are implemented as {@link SaleProcess processes}. There can be at most * one process running at a SalesPoint at any given point of time.</p> * * <p>SalesPoints are {@link ProcessContext process contexts} to the processes running at them. They provide * data and log access, as well as a display and current user. When a SalesPoint is created, a * {@link Display display] is attached to it, which will be used by the process.</p> * * <p>A {@link User user} can be attached to the SalesPoint. Its capabilities will determine what can and cannot * be done at the SalesPoint.</p> * * @author Steffen Zschaler * @version 2.0 15/07/1999 * @since v1.0 */ public class SalesPoint extends Object implements ProcessContext, FormSheetListener, SerializableListener { /** * The process currently running on this SalesPoint, if any. * * @serial */ protected SaleProcess m_pCurProcess; /** * The monitor synchronizing process access. */ private transient Object m_oProcessLock; /** * Return the monitor synchronizing process access. * * @override Never */ protected final Object getProcessLock() { if (m_oProcessLock == null) { m_oProcessLock = new Object(); } return m_oProcessLock; } /** * The name of this SalesPoint. * * @serial */ private String m_sName; /** * The display currently attached to this SalesPoint, if any. This will be saved/restored by the Shop. */ private transient Display m_dDisplay; /** * The monitor synchronizing access to the display. */ private transient Object m_oDisplayLock; /** * Return the monitor synchronizing access to the display. * * @override Never */ private final Object getDisplayLock() { if (m_oDisplayLock == null) { m_oDisplayLock = new Object(); } return m_oDisplayLock; } /** * The status display currently attached to this SalesPoint, if any. * * @serial */ private Display m_dStatus; /** * The monitor synchronizing access to the status display. */ private transient Object m_oStatusDisplayLock; /** * Return the monitor synchronizing access to the status display. * * @override Never */ private final Object getStatusDisplayLock() { if (m_oStatusDisplayLock == null) { m_oStatusDisplayLock = new Object(); } return m_oStatusDisplayLock; } /** * Flag indicating whether or not the SalesPoint is currently suspended. * * @serial */ private boolean m_fSuspended = false; /** * The DataBasket currently attached to this SalesPoint, if any. * * @serial */ private DataBasket m_dbBasket; /** * The monitor synchronizing access to the DataBasket. */ private transient Object m_oBasketLock; /** * Return the monitor synchronizing access to the DataBasket. * * @override Never */ private final Object getBasketLock() { if (m_oBasketLock == null) { m_oBasketLock = new Object(); } return m_oBasketLock; } /** * The User currently attached to this SalesPoint, if any. * * @serial */ private User m_usrUser; /** * The monitor synchronizing access to the User. */ private transient Object m_oUserLock; /** * The SalesPoints Framebounds. */ private Rectangle m_rSalesPointFrameBounds = null; /** * Return the monitor synchronizing access to the User. * * @override Never */ private final Object getUserLock() { if (m_oUserLock == null) { m_oUserLock = new Object(); } return m_oUserLock; } /** * SalesPoint store no data except the default serializable fields. This method exists only for debugging * purposes. */ private void writeObject (java.io.ObjectOutputStream oos) throws java.io.IOException { util.Debug.print ("Writing SalesPoint: \"" + getName() + "\".", -1); oos.defaultWriteObject(); util.Debug.print ("Finished writing SalesPoint: \"" + getName() + "\".", -1); } /** * Create a new SalesPoint. * * @param sName the name of the SalesPoint. */ public SalesPoint (String sName) { super(); m_sName = sName; } /** * Return the name of this SalesPoint. * * @override Never */ public String getName() { return m_sName; } /** * Check whether this SalesPoint can be closed. Unless the SalesPoint has been * {@link #suspend suspended} before calling <code>canQuit</code> the result of this method may not * be reliable. * * <p>By default, if a process runs on the SalesPoint its {@link SaleProcess#canQuit canQuit} * method is called. If no process is running, and <code>fNoPersistence</code> is <code>true</code>, * {@link #onCanQuit} is called which opens a MsgForm to ask the user whether he/she really wants to close * the SalesPoint. If <code>fNoPersistence</code> is <code>false</code> and no process is running on the * SalesPoint, the default implementation always returns <code>true</code>. * * <p>This method is usually not called directly.</p> * * @override Sometimes See above for an description of the default implementation. * * @param fNoPersistence <code>true</code> if the call to <code>canQuit</code> resulted from an * explicit call to {@link #quit} or from a call to {@link Shop#shutdown Shop.shutdown (false)}. If * <code>fNoPersistence == false</code> you can assume, the state of the SalesPoint will be made persistent * before it is closed. */ protected boolean canQuit (boolean fNoPersistence) { synchronized (getProcessLock()) { if (m_pCurProcess != null) { return m_pCurProcess.canQuit (fNoPersistence); } else { if (fNoPersistence) { return onCanQuit(); } else { return true; } } } } /** * Hook method called to determine whether a SalesPoint with no process running on it can be closed by an * explicit quit call. In this method you can assume that the state of the SalesPoint will not be saved and * therefore will not be restoreable. * * @override Sometimes The default implementation opens a {@link sale.stdforms.MsgForm} asking the user if * they really want to close the SalesPoint. * * @return true if the SalesPoint can be closed, false otherwise. */ protected boolean onCanQuit() { JDisplayDialog jddConfirmer = new JDisplayDialog(); final boolean[] abResult = {true}; final sale.stdforms.MsgForm mf = new sale.stdforms.MsgForm ("Closing \"" + getName() + "\"", "Are you sure, you want to close this SalesPoint?"); mf.removeAllButtons(); mf.addButton ("Yes", 0, new sale.Action() { public void doAction (SaleProcess p, SalesPoint sp) { mf.close(); } }); mf.addButton ("No", 1, new sale.Action() { public void doAction (SaleProcess p, SalesPoint sp) { abResult[0] = false; mf.close(); } }); jddConfirmer.setVisible (true); try { jddConfirmer.setFormSheet (mf); } catch (InterruptedException ie) { return false; } return abResult[0]; } /** * Close the SalesPoint. * * <p>First {@link #suspend suspends} the SalesPoint, then calls {@link #canQuit}. If that returns * <code>false</code>, <code>quit</code> will {@link #resume} the SalesPoint and return. Otherwise, * it {@link SaleProcess#quit quits} the current process, if any, and * {@link Shop#removeSalesPoint removes the SalesPoint from the Shop}.</p> * * @override Never */ public void quit() { SaleProcess p = null; synchronized (getProcessLock()) { try { suspend(); } catch (InterruptedException e) {} if (!canQuit (true)) { resume(); return; } p = m_pCurProcess; } if (p != null) { try { // This will resume the process and block until its finished. p.quit (true); } catch (InterruptedException e) {} } Shop.getTheShop().removeSalesPoint (this); } // logging /** * Hook method called when the SalesPoint is added to a Shop. You can write a log entry here. * * @override Sometimes The default implementation does nothing. * * @see Shop#addSalesPoint * @see Log * @see LogEntry */ protected void logSalesPointOpened() {} /** * Hook method called when the SalesPoint is removed from a Shop. You can write a log entry here. * * @override Sometimes The default implementation does nothing. * * @see Shop#removeSalesPoint * @see Log * @see LogEntry */ protected void logSalesPointClosed() {} // Process management /** * Start a process on this SalesPoint. * * <p>The process will run in the context of this SalesPoint, and with the * DataBasket attached to this SalesPoint.</p> * * <p>Although there can only be one process at a time running on this * SalesPoint, no exception is thrown if a process already runs. Instead, * the new process is silently ignored.</p> * * @override Never * * @param p the process to be run. */ public final void runProcess (SaleProcess p) { synchronized (getProcessLock()) { if ((m_pCurProcess == null) && (!m_fSuspended)) { m_pCurProcess = p; p.attach ((ProcessContext) this); p.attach (getBasket()); p.start(); } } } /** * Get the process currently running on this SalesPoint, if any. * * @override Never */ public final SaleProcess getCurrentProcess() { synchronized (getProcessLock()) { return m_pCurProcess; } } /** * Suspend the SalesPoint. * * <p>If a process is running on the SalesPoint, it is {@link SaleProcess#suspend suspended}. * The method will only return when the SalesPoint has been properly suspended.</p> * * @override Never * * @exception InterruptedException if an interrupt occurred while waiting for the SalesPoint to be * suspended. */ public void suspend() throws InterruptedException { synchronized (getProcessLock()) { m_fSuspended = true; if (m_pCurProcess != null) { m_pCurProcess.suspend(); } } } /** * Resume the SalesPoint. * * <p>If a process is running on the SalesPoint, it is {@link SaleProcess#resume resumed}.</p> * * @override Never */ public void resume() { synchronized (getProcessLock()) { if (m_pCurProcess != null) { m_pCurProcess.resume(); } m_fSuspended = false; } } // User management /** * Attach a user to this SalesPoint. * * <p>The user attached to a SalesPoint can be accessed by processes running * on the SalesPoint an can be used to determine capabilities etc.</p> * * @override Never * * @param usr the user to be attached. * @return the user that was previously attached to this SalesPoint, if any. */ public User attach (User usr) { synchronized (getUserLock()) { User usrOld = m_usrUser; m_usrUser = usr; return usrOld; } } /** * Detach any user currently attached to this SalesPoint. * * @override Never * * @return the user that was previously attached to this SalesPoint, if any. */ public User detachUser() { return attach ((User) null); } /** * Get the user currently attached to this SalesPoint. * * @override Never * * @return the user currently attached to this SalesPoint, if any. */ public User getUser() { synchronized (getUserLock()) { return m_usrUser; } } // DataBasket management /** * Attach a DataBasket to this SalesPoint. * * @override Never * * @param db the DataBasket to be attached. * @return the DataBasket that was previously attached to this SalesPoint, if any. */ public DataBasket attach (DataBasket db) { synchronized (getBasketLock()) { DataBasket dbOld = m_dbBasket; m_dbBasket = db; return dbOld; } } /** * Detach any DataBasket currently attached to this SalesPoint. * * @override Never * * @return the DataBasket that was previously attached to this SalesPoint, if any. */ public DataBasket detachBasket() { return attach ((DataBasket) null); } /** * Get the DataBasket currently attached to this SalesPoint. * * @override Never * * @return the DataBasket currently attached to this SalesPoint, if any. */ public DataBasket getBasket() { synchronized (getBasketLock()) { return m_dbBasket; } } // display management /** * Attach a status display to the SalesPoint. * * <p>This display can be used to give status information about the SalesPoint. It can also * be used to trigger background processes for the SalesPoint. It should not be used to trigger * processes on the SalesPoint.</p> * * <p>If the given display is not <code>null</code>, it must be {@link Display#isUseableDisplay useable}.</p> * * @override Never * * @param dStatus the new status display for this SalesPoint. * * @return the previous status display, if any. */ protected Display attachStatusDisplay (Display dStatus) { synchronized (getStatusDisplayLock()) { if (m_dStatus != null) { m_dStatus.closeFormSheet(); m_dStatus.setMenuSheet (null); } Display dReturn = m_dStatus; m_dStatus = dStatus; setStatusFormSheet (getDefaultStatusFormSheet()); setStatusMenuSheet (getDefaultStatusMenuSheet()); return dReturn; } } /** * Detach the current status display. * * @override Never * * @return the previous status display, if any. */ protected Display detachStatusDisplay() { return attachStatusDisplay (null); } /** * Set a FormSheet in the SalesPoint's status display. * * <p>Status display FormSheet's are always nonmodal, which is why this method returns * immediately after setting the FormSheet and can never throw an InterruptedException.</p> * * @override Never * * @param fs the FormSheet to be set. */ protected void setStatusFormSheet (FormSheet fs) { synchronized (getStatusDisplayLock()) { if (m_dStatus == null) { return; } if (fs != null) { fs.setWaitResponse (false); fs.attach (this); } try { m_dStatus.setFormSheet (fs); } catch (InterruptedException e) {} } } /** * Set a MenuSheet in the SalesPoint's status display. * * @override Never * * @param ms the MenuSheet to be set. */ protected void setStatusMenuSheet (MenuSheet ms) { synchronized (getStatusDisplayLock()) { if (m_dStatus == null) { return; } if (ms != null) { ms.attach (this); } m_dStatus.setMenuSheet (ms); } } /** * Get the default status MenuSheet for this SalesPoint. * * <p>Unless you specify differently through an explicit call to {@link #setStatusMenuSheet}, the Framework * will use this MenuSheet for the SalesPoint's status display.</p> * * @override Sometimes The default implementation returns <code>null</code> to indicate no MenuSheet. * * @see #attachStatusDisplay */ protected MenuSheet getDefaultStatusMenuSheet() { return null; } /** * Get the default status FormSheet for this SalesPoint. * * <p>Unless you specify differently through an explicit call to {@link #setStatusFormSheet}, the Framework * will use this FormSheet for the SalesPoint's status display.</p> * * @override Sometimes The default implementation returns an empty FormSheet. * * @see #attachStatusDisplay */ protected FormSheet getDefaultStatusFormSheet() { FormSheetContentCreator fscc = new FormSheetContentCreator() { protected void createFormSheetContent (final FormSheet fs) { fs.setComponent (new JPanel()); fs.removeAllButtons(); } }; return new FormSheet (getName(), fscc, false); } /** * Internal communication method called by Shop to attach a display during restoration of the Shop * from a stream. * * @override Never * * @param d the display just loaded. */ void attachLoadedDisplay (Display d) { synchronized (getDisplayLock()) { if (hasUseableDisplay (null)) { detachDisplay(); } m_dDisplay = d; } } /** * Attach a new display to the SalesPoint. * * <p>Any Form- or MenuSheets displayed on the current display will be removed, and the SalesPoint's * default sheets will be set on the new display.</p> * * @override Never * * @param d the new display * * @return the previously attached display, if any. * * @see #getDefaultFormSheet * @see #getDefaultMenuSheet */ public Display attach (Display d) { synchronized (getDisplayLock()) { if (hasUseableDisplay (null)) { m_dDisplay.removeFormSheetListener (this); try { m_dDisplay.setFormSheet (null); } catch (InterruptedException e) {} m_dDisplay.setMenuSheet (null); } Display dOld = m_dDisplay; m_dDisplay = d; // We can't use the previous FormSheet on the new display, as it will have been cancelled by the // setFormSheet call. if (hasUseableDisplay (null)) { m_dDisplay.addFormSheetListener (this); setDefaultSheets(); } return dOld; } } /** * Detach the current display. * * <p>Any Form- or MenuSheet on the current display will be removed before detaching the display.</p> * * @override Never * * @return the detached display, if any. */ public Display detachDisplay() { return attach ((Display) null); } /** * Return the display of this SalesPoint. * * @return the display of this SalesPoint. */ public Display getDisplay() { return m_dDisplay; } /** * Set the default Form- and MenuSheet on the currently attached display. * * @override Never * * @see #getDefaultMenuSheet * @see #getDefaultFormSheet */ private void setDefaultSheets() { synchronized (getDisplayLock()) { if (hasUseableDisplay (null)) { FormSheet fs = getDefaultFormSheet(); if (fs != null) { fs.setWaitResponse (false); fs.attach (this); } try { m_dDisplay.setFormSheet (fs); } catch (InterruptedException e) {} MenuSheet ms = getDefaultMenuSheet(); if (ms != null) { ms.attach (this); } m_dDisplay.setMenuSheet (ms); } } } /** * Get the default FormSheet for this SalesPoint. * * <p>The default FormSheet will be displayed whenever there is a current user (and, thus, a display), * but no process is running and no other FormSheet is being displayed.</p> * * @override Always The default implementation returns a FormSheet that simply states the name of the * SalesPoint. * * @return the default FormSheet, if any. */ protected FormSheet getDefaultFormSheet() { return new sale.stdforms.MsgForm (getName(), "This is the default FormSheet of SalesPoint " + getName()); } /** * Get the default MenuSheet for this SalesPoint. * * <p>The default MenuSheet will be displayed whenever there is a current user (and, thus, a display), * but no process is running.</p> * * @override Always The default implementation returns <code>null</code> indicating no MenuSheet. * * @return the default MenuSheet, if any. */ protected MenuSheet getDefaultMenuSheet() { return null; } // ProcessContext methods /** * Allow a process to set a FormSheet on the SalesPoint's current display. * * <p>The process launching the FormSheet as well as this SalesPoint will be attached to the * FormSheet prior to displaying it. Thus, Actions triggered by the FormSheet will run in the * correct context and will be able to access the process and the SalesPoint.</p> * * <p>If the FormSheet requests that the Framework wait for it being closed, * <code>setFormSheet()</code> will block until the FormSheet was closed or an interrupt occured.</p> * * @override Never * * @param p the process that wants to display the FormSheet. * @param fs the FormSheet to be displayed. * * @exception InterruptedException if an interrupt occurred while waiting for the FormSheet to be * closed. * * @see sale.Action * @see FormSheet#waitResponse */ public void setFormSheet (SaleProcess p, FormSheet fs) throws InterruptedException { if (fs != null) { fs.attach (p); fs.attach (this); } Display d; synchronized (getDisplayLock()) { d = m_dDisplay; } // not in synchronized, as this call might block, and we don't want dead locks. d.setFormSheet (fs); } /** * Sets the Framebounds of the SalesPoints assoziated Display (JDisplayFrame). * * <p>Example:<p> * <code>sp.setSalesPointFrameBounds (new Rectangle (10,10,200,200));<code> * * This moves the SalesPointFrame to Position (10,10) with a size of (200,200). * * @override Sometimes */ public void setSalesPointFrameBounds (Rectangle r) { if (getDisplay() == null) { m_rSalesPointFrameBounds = r; } else { m_rSalesPointFrameBounds = r; if (Shop.getTheShop() != null) { if (Shop.getTheShop().getShopState() == Shop.RUNNING) { // nicht sehr schön - vielmehr sollte hier die Schnittstelle von Display // erweitert werden, um unabhängig von der Art des Displays zu sein. // NOCH ZU ÄNDERN !!! - NICHT VERGESSEN ((JDisplayFrame)getDisplay()).setBounds (getSalesPointFrameBounds()); ((JDisplayFrame)getDisplay()).hide(); ((JDisplayFrame)getDisplay()).show(); } } } } /** * Returns the Framebounds of the SalesPoints assoziated Display(JDisplayFrame). * * @override Sometimes */ public Rectangle getSalesPointFrameBounds () { return m_rSalesPointFrameBounds; } /** * Allow a process to pop up a FormSheet on the SalesPoint's current display. * * <p>The process launching the FormSheet as well as this SalesPoint will be attached to the * FormSheet prior to displaying it. Thus, Actions triggered by the FormSheet will run in the * correct context and will be able to access the process and the SalesPoint.</p> * * <p>If the FormSheet requests that the Framework wait for it being closed, * <code>popUpFormSheet</code> will block until the FormSheet was closed or an interrupt occured.</p> * * @override Never * * @param p the process that wants to display the FormSheet. * @param fs the FormSheet to be displayed. * * @exception InterruptedException if an interrupt occurred while waiting for the FormSheet to be * closed. * * @see sale.Action * @see FormSheet#waitResponse */ public void popUpFormSheet (SaleProcess p, FormSheet fs) throws InterruptedException { if (fs != null) { fs.attach (p); fs.attach (this); } Display d; synchronized (getDisplayLock()) { d = m_dDisplay; } d.popUpFormSheet (fs); } /** * Allow a process to set a MenuSheet on the SalesPoint's current display. * * <p>The process setting the MenuSheet as well as this SalesPoint will be attached to the * MenuSheet prior to displaying it. Thus, Actions triggered by the MenuSheet will run in the * correct context and will be able to access the process and the SalesPoint.</p> * * @override Never * * @param p the process that wants to display the MenuSheet. * @param ms the MenuSheet to be displayed. * * @see sale.Action */ public void setMenuSheet (SaleProcess p, MenuSheet ms) { if (ms != null) { ms.attach (p); ms.attach (this); } synchronized (getDisplayLock()) { m_dDisplay.setMenuSheet (ms); } } /** * True, if the SalesPoint currently has a display and this display is useable. * * @override Never * * @param p the process querying, unused. * * @see Display#isUseableDisplay */ public boolean hasUseableDisplay (SaleProcess p) { synchronized (getDisplayLock()) { return ((m_dDisplay != null) && (m_dDisplay.isUseableDisplay())); } } /** * Log the given Loggable. * * <p>The given loggable object will be logged into the global log file unless you override this method.</p> * * @override Sometimes * * @exception IOException if an error occurs while trying to write the log data. * * @param p the SalesProcess demanding logging, unused. * @param la the object to be logged. */ public void log (SaleProcess p, Loggable la) throws IOException { Shop.getTheShop().log (la); } /** * Get the current user for the given process. This is the user, if any, * previously attached with the {@link #attach(User)} method. * * @override Never * * @param p the process querying, unused. * * @return the current user * * @see #getUser */ public final User getCurrentUser (SaleProcess p) { return getUser(); } /** * Return a Stock for a given name. * * @override Sometimes * * @param sName the name of the Stock. */ public Stock getStock (String sName) { return Shop.getTheShop().getStock (sName); } /** * Return a Catalog for a given name. * * @override Sometimes * * @param sName the name of the Catalog. */ public Catalog getCatalog (String sName) { return Shop.getTheShop().getCatalog (sName); } /** * Notification that a process started on a SalesPoint. * * <p>This will remove all SalesPoint owned Form- and MenuSheets from the display, as to * make room for the process' sheets.</p> * * @override Never * * @param p the process that was just launched. */ public void processStarted (SaleProcess p) { synchronized (getDisplayLock()) { if (hasUseableDisplay (null)) { m_dDisplay.removeFormSheetListener (this); try { m_dDisplay.setFormSheet (null); } catch (InterruptedException e) {} m_dDisplay.setMenuSheet (null); } } } /** * Notification that a process finished running on this SalesPoint. * * <p>This will restore the SalesPoint's default sheets.</p> * * @override Never */ public void processFinished (SaleProcess p) { synchronized (getProcessLock()) { m_pCurProcess = null; } synchronized (getDisplayLock()) { if (hasUseableDisplay (null)) { m_dDisplay.addFormSheetListener (this); setDefaultSheets(); } } } // FormSheetListener interface methods /** * Dummy interface to conform by the FormSheetListener interface. * * @override Never */ public void formSheetSet (FormSheetEvent e) {} /** * Implemented to make sure there always is a FormSheet. * * @override Never */ public void formSheetRemoved (FormSheetEvent e) { if (e.isExplicit()) { synchronized (getDisplayLock()) { if (hasUseableDisplay (null)) { FormSheet fs = getDefaultFormSheet(); if (fs != null) { fs.setWaitResponse (false); fs.attach (this); } try { m_dDisplay.setFormSheet (fs); } catch (InterruptedException ex) {} } } } } /** * Return a String description of this SalesPoint: the name. * * @override Sometimes */ public String toString() { return getName(); } }