001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    
021    package org.apache.geronimo.samples.daytrader;
022    
023    import java.math.BigDecimal;
024    import java.sql.Timestamp;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    
028    import javax.ejb.CreateException;
029    import javax.ejb.EJBException;
030    import javax.ejb.FinderException;
031    import javax.ejb.SessionBean;
032    import javax.ejb.SessionContext;
033    import javax.jms.Connection;
034    import javax.jms.ConnectionFactory;
035    import javax.jms.MessageProducer;
036    import javax.jms.Queue;
037    import javax.jms.Session;
038    import javax.jms.TextMessage;
039    import javax.jms.Topic;
040    import javax.naming.InitialContext;
041    import javax.naming.NamingException;
042    import javax.persistence.EntityManager;
043    import javax.persistence.Query;
044    
045    import org.apache.geronimo.samples.daytrader.ejb.Trade;
046    import org.apache.geronimo.samples.daytrader.util.FinancialUtils;
047    import org.apache.geronimo.samples.daytrader.util.Log;
048    import org.apache.geronimo.samples.daytrader.util.MDBStats;
049    
050    public class TradeJPA implements SessionBean {
051    
052        private EntityManager entityManager;
053    
054        private SessionContext context = null;
055    
056        private ConnectionFactory qConnFactory = null;
057        private Queue queue = null;
058        private ConnectionFactory tConnFactory = null;
059        private Topic streamerTopic = null;
060    
061        private boolean publishQuotePriceChange = true;
062    
063        private void queueOrderInternal(Integer orderID, boolean twoPhase)
064                throws javax.jms.JMSException {
065            if (Log.doTrace()) Log.trace("TradeBean:queueOrderInternal", orderID);
066    
067            Connection conn = null;
068            Session sess = null;
069            try {
070                conn = qConnFactory.createConnection();
071                sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
072                MessageProducer msgProducer = sess.createProducer(queue);
073                TextMessage message = sess.createTextMessage();
074    
075                message.setStringProperty("command", "neworder");
076                message.setIntProperty("orderID", orderID);
077                message.setBooleanProperty("twoPhase", twoPhase);
078                message.setText("neworder: orderID=" + orderID + " runtimeMode=EJB twoPhase=" + twoPhase);
079                message.setLongProperty("publishTime", System.currentTimeMillis());
080    
081                if (Log.doTrace()) Log.trace("TradeBean:queueOrder Sending message: " + message.getText());
082                msgProducer.send(message);
083            }
084            catch (javax.jms.JMSException e) {
085                throw e; // pass the exception back
086            }
087    
088            finally {
089                if (conn != null)
090                    conn.close();
091                if (sess != null)
092                    sess.close();
093            }
094        }
095    
096        /**
097         * @see org.apache.geronimo.samples.daytrader.TradeServices#queueOrder(Integer, boolean)
098         */
099    
100        public void queueOrder(Integer orderID, boolean twoPhase)
101                throws Exception {
102            if (Log.doTrace()) Log.trace("TradeBean:queueOrder", orderID, twoPhase);
103            if (twoPhase)
104                queueOrderInternal(orderID, true);
105            else {
106                // invoke the queueOrderOnePhase method -- which requires a new transaction
107                // the queueOrder will run in it's own transaction thus not requiring a
108                // 2-phase commit
109                ((Trade) context.getEJBObject()).queueOrderOnePhase(orderID);
110            }
111        }
112    
113    
114        /**
115         * @see Trade#queueOrderOnePhase(Integer)
116         *      Queue the Order identified by orderID to be processed in a One Phase commit
117         *      <p/>
118         *      In short, this method is deployed as TXN REQUIRES NEW to avoid a
119         *      2-phase commit transaction across Entity and MDB access
120         */
121    
122        public void queueOrderOnePhase(Integer orderID) {
123            try {
124                if (Log.doTrace()) Log.trace("TradeBean:queueOrderOnePhase", orderID);
125                queueOrderInternal(orderID, false);
126            } catch (Exception e) {
127                throw new EJBException(e.getMessage(), e);
128            }
129        }
130    
131        class quotePriceComparator implements java.util.Comparator {
132            public int compare(Object quote1, Object quote2) {
133                double change1 = ((QuoteDataBean) quote1).getChange();
134                double change2 = ((QuoteDataBean) quote2).getChange();
135                return new Double(change2).compareTo(change1);
136            }
137        }
138    
139        public MarketSummaryDataBean getMarketSummary()
140                throws Exception {
141    
142    
143            MarketSummaryDataBean marketSummaryData;
144            try {
145                if (Log.doTrace()) {
146                    Log.trace("TradeBean:getMarketSummary -- getting market summary");
147                }
148    
149                //Find Trade Stock Index Quotes (Top 100 quotes)
150                //ordered by their change in value
151                Collection quotes;
152    //            if (orderBySQLSupported) {
153                    Query query = entityManager.createNamedQuery("quotesByChange");
154                    quotes = query.getResultList();
155    //            }
156    //            else
157    //                quotes = quoteHome.findTSIAQuotes();
158    
159                //SORT by price change the collection of stocks if the AppServer
160                //     does not support the "ORDER BY" SQL clause
161    //            if (! orderBySQLSupported) {
162                    //if (Log.doTrace())
163    //                Log.trace("TradeBean:getMarketSummary() -- Sorting TSIA quotes");
164    //                ArrayList sortedQuotes = new ArrayList(quotes);
165    //                java.util.Collections.sort(sortedQuotes, new TradeJPA.quotePriceComparator());
166    //                quotes = sortedQuotes;
167    //            }
168                //SORT END
169                QuoteDataBean[] quoteArray = (QuoteDataBean[]) quotes.toArray(new QuoteDataBean[quotes.size()]);
170                ArrayList<QuoteDataBean> topGainers = new ArrayList<QuoteDataBean>(5);
171                ArrayList<QuoteDataBean> topLosers = new ArrayList<QuoteDataBean>(5);
172                BigDecimal TSIA = FinancialUtils.ZERO;
173                BigDecimal openTSIA = FinancialUtils.ZERO;
174                double totalVolume = 0.0;
175    
176                if (quoteArray.length > 5) {
177                    for (int i = 0; i < 5; i++)
178                        topGainers.add(quoteArray[i]);
179                    for (int i = quoteArray.length - 1; i >= quoteArray.length - 5; i--)
180                        topLosers.add(quoteArray[i]);
181    
182                    for (QuoteDataBean quote : quoteArray) {
183                        BigDecimal price = quote.getPrice();
184                        BigDecimal open = quote.getOpen();
185                        double volume = quote.getVolume();
186                        TSIA = TSIA.add(price);
187                        openTSIA = openTSIA.add(open);
188                        totalVolume += volume;
189                    }
190                    TSIA = TSIA.divide(new BigDecimal(quoteArray.length),
191                            FinancialUtils.ROUND);
192                    openTSIA = openTSIA.divide(new BigDecimal(quoteArray.length),
193                            FinancialUtils.ROUND);
194                }
195                /* This is an alternate approach using ejbSelect methods
196                    *   In this approach an ejbSelect is used to select only the
197                    *   current price and open price values for the TSIA
198                       LocalQuote quote = quoteHome.findOne();
199                       BigDecimal TSIA = quote.getTSIA();
200                       openTSIA = quote.getOpenTSIA();
201                       Collection topGainers = quote.getTopGainers(5);
202                       Collection topLosers = quote.getTopLosers(5);
203                       LocalQuote quote = (LocalQuote)topGainers.iterator().next();
204                         double volume = quote.getTotalVolume();
205                    *
206                    */
207    
208                marketSummaryData = new MarketSummaryDataBean(TSIA, openTSIA, totalVolume, topGainers, topLosers);
209            }
210            catch (Exception e) {
211                Log.error("TradeBean:getMarketSummary", e);
212                throw new EJBException("TradeBean:getMarketSummary -- error ", e);
213            }
214            return marketSummaryData;
215        }
216    
217        public QuoteDataBean createQuote(String symbol, String companyName, BigDecimal price) {
218            try {
219                QuoteDataBean quote = new QuoteDataBean(symbol, companyName, 0, price, price, price, price, 0);
220                entityManager.persist(quote);
221                if (Log.doTrace()) Log.trace("TradeBean:createQuote-->" + quote);
222                return quote;
223            } catch (Exception e) {
224                Log.error("TradeBean:createQuote -- exception creating Quote", e);
225                throw new EJBException(e);
226            }
227        }
228    
229        public QuoteDataBean getQuote(String symbol) {
230    
231            if (Log.doTrace()) Log.trace("TradeBean:getQuote", symbol);
232                return entityManager.find(QuoteDataBean.class, symbol);
233    //        } catch (NamingException e) {
234    //            throw new EJBException(e);
235                //Cannot find quote for given symbol
236    //            Log.error("TradeBean:getQuote--> Symbol: " + symbol + " cannot be found");
237    //            BigDecimal z = new BigDecimal(0.0);
238    //            return new QuoteDataBean("Error: Symbol " + symbol + " not found", "", 0.0, z, z, z, z, 0.0);
239    //        }
240        }
241    
242        public Collection getAllQuotes()
243                throws Exception {
244            if (Log.doTrace()) Log.trace("TradeBean:getAllQuotes");
245    
246                Query query = entityManager.createNamedQuery("allQuotes");
247                return query.getResultList();
248        }
249    
250        public QuoteDataBean updateQuotePriceVolume(String symbol, BigDecimal changeFactor, double sharesTraded)
251                throws Exception {
252    
253            if (!TradeConfig.getUpdateQuotePrices())
254                return new QuoteDataBean();
255    
256            if (Log.doTrace())
257                Log.trace("TradeBean:updateQuote", symbol, changeFactor);
258    
259    //        try {
260                QuoteDataBean quote = entityManager.find(QuoteDataBean.class, symbol);
261                BigDecimal oldPrice = quote.getPrice();
262    
263                if (quote.getPrice().equals(TradeConfig.PENNY_STOCK_PRICE)) {
264                    changeFactor = TradeConfig.PENNY_STOCK_RECOVERY_MIRACLE_MULTIPLIER;
265                }
266    
267                BigDecimal newPrice = changeFactor.multiply(oldPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
268    
269                quote.setPrice(newPrice);
270                quote.setVolume(quote.getVolume() + sharesTraded);
271    //        TODO find out if requires new here is really intended -- it is backwards, change can get published w/o it occurring.
272                ((Trade) context.getEJBObject()).publishQuotePriceChange(quote, oldPrice, changeFactor, sharesTraded);
273    //            publishQuotePriceChange(quote, oldPrice, changeFactor, sharesTraded);
274                return quote;
275    //        } catch (FinderException fe) {
276    //            Cannot find quote for given symbol
277    //            Log.error("TradeBean:updateQuotePriceVolume--> Symbol: " + symbol + " cannot be found");
278    //            quoteData = new QuoteDataBean("Error: Symbol " + symbol + " not found");
279    //        }
280        }
281    
282        public void publishQuotePriceChange(QuoteDataBean quoteData, BigDecimal oldPrice, BigDecimal changeFactor, double sharesTraded) {
283            if (!publishQuotePriceChange)
284                return;
285            if (Log.doTrace())
286                Log.trace("TradeBean:publishQuotePricePublishing -- quoteData = " + quoteData);
287    
288            Connection conn = null;
289            Session sess = null;
290    
291            try {
292                conn = tConnFactory.createConnection();
293                sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
294                MessageProducer msgProducer = sess.createProducer(streamerTopic);
295                TextMessage message = sess.createTextMessage();
296    
297                String command = "updateQuote";
298                message.setStringProperty("command", command);
299                message.setStringProperty("symbol", quoteData.getSymbol());
300                message.setStringProperty("company", quoteData.getCompanyName());
301                message.setStringProperty("price", quoteData.getPrice().toString());
302                message.setStringProperty("oldPrice", oldPrice.toString());
303                message.setStringProperty("open", quoteData.getOpen().toString());
304                message.setStringProperty("low", quoteData.getLow().toString());
305                message.setStringProperty("high", quoteData.getHigh().toString());
306                message.setDoubleProperty("volume", quoteData.getVolume());
307    
308                message.setStringProperty("changeFactor", changeFactor.toString());
309                message.setDoubleProperty("sharesTraded", sharesTraded);
310                message.setLongProperty("publishTime", System.currentTimeMillis());
311                message.setText("Update Stock price for " + quoteData.getSymbol() + " old price = " + oldPrice + " new price = " + quoteData.getPrice());
312    
313                msgProducer.send(message);
314            }
315            catch (Exception e) {
316                throw new EJBException(e.getMessage(), e); // pass the exception back
317            }
318            finally {
319                try {
320                    if (conn != null)
321                        conn.close();
322                    if (sess != null)
323                        sess.close();
324                } catch (Exception e) {
325                    throw new EJBException(e.getMessage(), e); // pass the exception back
326                }
327            }
328        }
329    
330        public OrderDataBean buy(String userID, String symbol, double quantity, int orderProcessingMode)
331                throws Exception {
332    
333            OrderDataBean order;
334            BigDecimal total;
335            try {
336                if (Log.doTrace())
337                    Log.trace("TradeBean:buy", userID, symbol, quantity, orderProcessingMode);
338                /*  The following commented code shows alternative forms of the finder needed for this
339                *  method
340                *  The first alternative requires a 2-table join. Some database cannot allocate an Update
341                *  Lock on a join select.
342                *
343                *  The second alternative shows the finder being executed without allocation an update
344                *  lock on the row. Normally, an update lock would not be necessary, but is required if
345                *  the same user logs in multiple times to avoid a deadlock situation.
346                *
347                *  The third alternative runs the finder and allocates an update lock on the row(s)
348                *
349                   LocalAccount account = accountHome.findByUserIDForUpdate(userID);
350    
351                   LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate();
352                */
353                AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID);
354                AccountDataBean account = profile.getAccount();
355                QuoteDataBean quote = entityManager.find(QuoteDataBean.class, symbol);
356                HoldingDataBean holding = null;  //The holding will be created by this buy order
357    //            Integer orderID = keySequence.getNextID("order");
358    
359                order = createOrder(account, quote, holding, "buy", quantity);
360    
361                //UPDATE - account should be credited during completeOrder
362                BigDecimal price = quote.getPrice();
363                BigDecimal orderFee = order.getOrderFee();
364                BigDecimal balance = account.getBalance();
365                total = (new BigDecimal(quantity).multiply(price)).add(orderFee);
366                account.setBalance(balance.subtract(total));
367    
368                if (orderProcessingMode == TradeConfig.SYNCH)
369                    completeOrderInternal(order.getOrderID());
370                else if (orderProcessingMode == TradeConfig.ASYNCH)
371                    // Invoke the queueOrderOnePhase method w/ TXN requires new attribute
372                    // to side-step a 2-phase commit across DB and JMS access
373                    queueOrder(order.getOrderID(), false);
374                else //TradeConfig.ASYNC_2PHASE
375                    queueOrder(order.getOrderID(), true);
376            }
377            catch (Exception e) {
378                Log.error("TradeBean:buy(" + userID + "," + symbol + "," + quantity + ") --> failed", e);
379                /* On exception - cancel the order */
380                //TODO figure out how to do this with JPA
381    //            if (order != null) order.cancel();
382                throw new EJBException(e);
383            }
384            return order;
385        }
386    
387        public OrderDataBean sell(String userID, Integer holdingID, int orderProcessingMode)
388                throws Exception {
389    
390            OrderDataBean order;
391            BigDecimal total;
392            try {
393                if (Log.doTrace())
394                    Log.trace("TradeBean:sell", userID, holdingID, orderProcessingMode);
395    
396                /* Some databases cannot allocate an update lock on a JOIN
397                    * use the second approach below to acquire update lock
398                   LocalAccount account = accountHome.findByUserIDForUpdate(userID);
399                   */
400    
401                AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID);
402                AccountDataBean account = profile.getAccount();
403                HoldingDataBean holding = entityManager.find(HoldingDataBean.class, holdingID);
404    //            catch (ObjectNotFoundException oe) {
405    //                Log.error("TradeBean:sell User " + userID + " attempted to sell holding " + holdingID + " which has already been sold");
406    //                OrderDataBean orderData = new OrderDataBean();
407    //                orderData.setOrderStatus("cancelled");
408    //                return orderData;
409    //            }
410                QuoteDataBean quote = holding.getQuote();
411                double quantity = holding.getQuantity();
412                order = createOrder(account, quote, holding, "sell", quantity);
413    
414                //UPDATE the holding purchase data to signify this holding is "inflight" to be sold
415                //    -- could add a new holdingStatus attribute to holdingEJB
416                holding.setPurchaseDate(new java.sql.Timestamp(0));
417    
418                //UPDATE - account should be credited during completeOrder
419                BigDecimal price = quote.getPrice();
420                BigDecimal orderFee = order.getOrderFee();
421                BigDecimal balance = account.getBalance();
422                total = (new BigDecimal(quantity).multiply(price)).subtract(orderFee);
423                account.setBalance(balance.add(total));
424    
425                if (orderProcessingMode == TradeConfig.SYNCH)
426                    completeOrderInternal(order.getOrderID());
427                else if (orderProcessingMode == TradeConfig.ASYNCH)
428                    queueOrder(order.getOrderID(), false);
429                else //TradeConfig.ASYNC_2PHASE
430                    queueOrder(order.getOrderID(), true);
431    
432            }
433            catch (Exception e) {
434                Log.error("TradeBean:sell(" + userID + "," + holdingID + ") --> failed", e);
435                //TODO figure out JPA cancel
436    //            if (order != null) order.cancel();
437                //UPDATE - handle all exceptions like:
438                throw new EJBException("TradeBean:sell(" + userID + "," + holdingID + ")", e);
439            }
440            return order;
441        }
442    
443    
444        public Collection getOrders(String userID)
445                throws FinderException, Exception {
446    
447            if (Log.doTrace())
448                Log.trace("TradeBean:getOrders", userID);
449    
450            /*  The following commented code shows alternative forms of the finder needed for this
451               *  method
452               *  The first alternative requires a 2-table join. Some database cannot allocate an Update
453               *  Lock on a join select.
454               *
455               *  The second alternative shows the finder being executed without allocation an update
456               *  lock on the row. Normally, an update lock would not be necessary, but is required if
457               *  the same user logs in multiple times to avoid a deadlock situation.
458               *
459               *  The third alternative runs the finder and allocates an update lock on the row(s)
460               *
461    
462                  Collection orders = accountHome.findByUserID(userID).getOrders();
463    
464                  LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate();
465               */
466            AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID);
467            AccountDataBean account = profile.getAccount();
468            return account.getOrders();
469    
470    //        ArrayList dataBeans = new ArrayList();
471    //        if (orders == null) return dataBeans;
472    //
473    //        Iterator it = orders.iterator();
474            //TODO: return top 5 orders for now -- next version will add a getAllOrders method
475            //      also need to get orders sorted by order id descending
476    //        int i = 0;
477    //        while ((it.hasNext()) && (i++ < 5))
478    //            dataBeans.add(((OrderData) it.next()).getDataBean());
479    //
480    //        return dataBeans;
481        }
482    
483        public Collection getClosedOrders(String userID)
484                throws FinderException, Exception {
485            if (Log.doTrace())
486                Log.trace("TradeBean:getClosedOrders", userID);
487    
488            try {
489    
490                /*  The following commented code shows alternative forms of the finder needed for this
491                *  method
492                *  The first alternative requires a 2-table join. Some database cannot allocate an Update
493                *  Lock on a join select.
494                *
495                *  The second alternative shows the finder being executed without allocation an update
496                *  lock on the row. Normally, an update lock would not be necessary, but is required if
497                *  the same user logs in multiple times to avoid a deadlock situation.
498                *
499                *  The third alternative runs the finder and allocates an update lock on the row(s)
500                *
501                   Collection orders = orderHome.findClosedOrdersForUpdate(userID);
502                *
503                      LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccount();
504                */
505    
506                //Get the primary keys for all the closed Orders for this account.
507                Query query = entityManager.createNamedQuery("closedOrders");
508                query.setParameter("userID", userID);
509                Collection results = query.getResultList();
510                Query updateStatus = entityManager.createNamedQuery("completeClosedOrders");
511                updateStatus.setParameter("userID", userID);
512                updateStatus.executeUpdate();
513                return results;
514    //            if (ordersKeys == null) return dataBeans;
515    //
516    //            Iterator it = ordersKeys.iterator();
517    //            while (it.hasNext()) {
518    //                Integer orderKey = (Integer) it.next();
519    //                LocalOrder order = (LocalOrder) orderHome.findByPrimaryKeyForUpdate(orderKey);
520                    //Complete the order
521    //                order.setOrderStatus("completed");
522    //                dataBeans.add(order.getDataBean());
523    //            }
524    
525            }
526            catch (Exception e) {
527                Log.error("TradeBean.getClosedOrders", e);
528                throw new EJBException("TradeBean.getClosedOrders - error", e);
529            }
530        }
531    
532    
533        public OrderDataBean completeOrder(Integer orderID, boolean twoPhase)
534                throws Exception {
535            if (Log.doTrace()) Log.trace("TradeBean:completeOrder", orderID + " twoPhase=" + twoPhase);
536            if (twoPhase)
537                return completeOrderInternal(orderID);
538            else {
539                // invoke the completeOrderOnePhase -- which requires a new transaction
540                // the completeOrder will run in it's own transaction thus not requiring a
541                // 2-phase commit
542                return ((Trade) context.getEJBObject()).completeOrderOnePhase(orderID);
543            }
544        }
545    
546        //completeOrderOnePhase method is deployed w/ TXN_REQUIRES_NEW
547        //thus the completeOrder call from the MDB should not require a 2-phase commit
548        public OrderDataBean completeOrderOnePhase(Integer orderID) {
549            try {
550                if (Log.doTrace()) Log.trace("TradeBean:completeOrderOnePhase", orderID);
551                return completeOrderInternal(orderID);
552            } catch (Exception e) {
553                throw new EJBException(e.getMessage(), e);
554            }
555        }
556    
557        private OrderDataBean completeOrderInternal(Integer orderID)
558                throws Exception {
559    
560             OrderDataBean order = entityManager.find(OrderDataBean.class, orderID);
561    
562            if (order == null) {
563                Log.error("TradeBean:completeOrderInternal  -- Unable to find Order " + orderID + " FBPK returned " + order);
564                return null;
565            }
566    
567            if (order.isCompleted())
568                throw new EJBException("Error: attempt to complete Order that is already completed\n" + order);
569    
570            AccountDataBean account = order.getAccount();
571            QuoteDataBean quote = order.getQuote();
572            HoldingDataBean holding = order.getHolding();
573            BigDecimal price = order.getPrice();
574            double quantity = order.getQuantity();
575    
576            /*
577               *    getProfile is marked as Pess. Update to get a DB lock
578               * Here we invoke getProfileForRead which is deployed to not
579               * lock the DB (Pess. Read)
580               */
581            String userID = account.getProfile().getUserID();
582    
583            /*
584               * total = (quantity * purchasePrice) + orderFee
585               */
586    
587    
588            if (Log.doTrace()) Log.trace(
589                    "TradeBeanInternal:completeOrder--> Completing Order " + order.getOrderID()
590                            + "\n\t Order info: " + order
591                            + "\n\t Account info: " + account
592                            + "\n\t Quote info: " + quote
593                            + "\n\t Holding info: " + holding);
594    
595    
596            if (order.isBuy()) {
597                /* Complete a Buy operation
598                    *       - create a new Holding for the Account
599                    *       - deduct the Order cost from the Account balance
600                    */
601    
602                HoldingDataBean newHolding = createHolding(account, quote, quantity, price);
603                order.setHolding(newHolding);
604            }
605    
606            if (order.isSell()) {
607                /* Complete a Sell operation
608                    *       - remove the Holding from the Account
609                    *       - deposit the Order proceeds to the Account balance
610                    */
611                if (holding == null) {
612                    Log.error("TradeBean:completeOrderInternal -- Unable to sell order " + order.getOrderID() + " holding already sold");
613                    order.cancel();
614                    return order;
615                } else {
616                    entityManager.remove(holding);
617                    order.setHolding(null);
618    //                holding.remove();
619    //                holding = null;
620                }
621    
622                // This is managed by the container
623                // order.setHolding(null);
624    
625            }
626            order.setOrderStatus("closed");
627    
628            order.setCompletionDate(new java.sql.Timestamp(System.currentTimeMillis()));
629    
630            if (Log.doTrace()) Log.trace(
631                    "TradeBean:completeOrder--> Completed Order " + order.getOrderID()
632                            + "\n\t Order info: " + order
633                            + "\n\t Account info: " + account
634                            + "\n\t Quote info: " + quote
635                            + "\n\t Holding info: " + holding);
636    
637            if (Log.doTrace())
638                Log.trace("Calling TradeAction:orderCompleted from Session EJB using Session Object");
639            //FUTURE All getEJBObjects could be local -- need to add local I/F
640    
641            TradeServices trade = (TradeServices) context.getEJBObject();
642            TradeAction tradeAction = new TradeAction(trade);
643    
644            //signify this order for user userID is complete
645            //TODO figure out wtf is going on, all implementations of this method throw NotSupportedException !!!
646            //Also try to figure out the business reason this should be in a separate tx ?!?!?!?
647            //tradeAction.orderCompleted(userID, orderID);
648            return order;
649        }
650    
651    
652        //These methods are used to provide the 1-phase commit runtime option for TradeDirect
653        // Basically these methods are deployed as txn requires new and invoke TradeDirect methods
654        // There is no mechanism outside of EJB to start a new transaction
655        public OrderDataBean completeOrderOnePhaseDirect(Integer orderID) {
656            try {
657                if (Log.doTrace())
658                    Log.trace("TradeBean:completeOrderOnePhaseDirect -- completing order by calling TradeDirect orderID=" + orderID);
659                return (new org.apache.geronimo.samples.daytrader.direct.TradeDirect()).completeOrderOnePhase(orderID);
660            } catch (Exception e) {
661                throw new EJBException(e.getMessage(), e);
662            }
663        }
664    
665        public void cancelOrderOnePhaseDirect(Integer orderID) {
666            try {
667                if (Log.doTrace())
668                    Log.trace("TradeBean:cancelOrderOnePhaseDirect -- cancelling order by calling TradeDirect orderID=" + orderID);
669                (new org.apache.geronimo.samples.daytrader.direct.TradeDirect()).cancelOrderOnePhase(orderID);
670            } catch (Exception e) {
671                throw new EJBException(e.getMessage(), e);
672            }
673        }
674    
675    
676        public void cancelOrder(Integer orderID, boolean twoPhase)
677                throws Exception {
678            if (Log.doTrace()) Log.trace("TradeBean:cancelOrder", orderID + " twoPhase=" + twoPhase);
679            if (twoPhase)
680                cancelOrderInternal(orderID);
681            else {
682                // invoke the cancelOrderOnePhase -- which requires a new transaction
683                // the completeOrder will run in it's own transaction thus not requiring a
684                // 2-phase commit
685                ((Trade) context.getEJBObject()).cancelOrderOnePhase(orderID);
686            }
687        }
688    
689        //cancelOrderOnePhase method is deployed w/ TXN_REQUIRES_NEW
690        //thus the completeOrder call from the MDB should not require a 2-phase commit
691        public void cancelOrderOnePhase(Integer orderID) {
692            try {
693                if (Log.doTrace()) Log.trace("TradeBean:cancelOrderOnePhase", orderID);
694                cancelOrderInternal(orderID);
695            } catch (Exception e) {
696                throw new EJBException(e.getMessage(), e);
697            }
698        }
699    
700    
701        private void cancelOrderInternal(Integer orderID)
702                throws Exception {
703            OrderDataBean order = entityManager.find(OrderDataBean.class, orderID);
704            order.cancel();
705        }
706    
707    
708        public void orderCompleted(String userID, Integer orderID)
709                throws Exception {
710            throw new UnsupportedOperationException("TradeBean:orderCompleted method not supported");
711        }
712    
713        public HoldingDataBean createHolding(
714                AccountDataBean account,
715                QuoteDataBean quote,
716                double quantity,
717                BigDecimal purchasePrice)
718                throws Exception {
719            HoldingDataBean newHolding = new HoldingDataBean(quantity, purchasePrice, new Timestamp(System.currentTimeMillis()), account, quote);
720            entityManager.persist(newHolding);
721            return newHolding;
722        }
723    
724        public Collection getHoldings(String userID)
725                throws FinderException, Exception {
726            if (Log.doTrace())
727                Log.trace("TradeBean:getHoldings", userID);
728            Query query = entityManager.createNamedQuery("holdingsByUserID");
729            query.setParameter("userID", userID);
730            return query.getResultList();
731        }
732    
733        public HoldingDataBean getHolding(Integer holdingID)
734                throws FinderException, Exception {
735            if (Log.doTrace())
736                Log.trace("TradeBean:getHolding", holdingID);
737            return entityManager.find(HoldingDataBean.class, holdingID);
738        }
739    
740    
741    
742        public OrderDataBean createOrder(
743                AccountDataBean account,
744                QuoteDataBean quote,
745                HoldingDataBean holding,
746                String orderType,
747                double quantity)
748                throws CreateException, Exception {
749    
750            OrderDataBean order;
751    
752            if (Log.doTrace())
753                Log.trace(
754                        "TradeBean:createOrder(orderID="
755                                + " account="
756                                + ((account == null) ? null : account.getAccountID())
757                                + " quote="
758                                + ((quote == null) ? null : quote.getSymbol())
759                                + " orderType="
760                                + orderType
761                                + " quantity="
762                                + quantity);
763            try {
764                assert quote != null;
765                order = new OrderDataBean(orderType, "open", new Timestamp(System.currentTimeMillis()), null, quantity, quote.getPrice().setScale(FinancialUtils.SCALE, FinancialUtils.ROUND), TradeConfig.getOrderFee(orderType), account, quote, holding);
766                entityManager.persist(order);
767            }
768            catch (Exception e) {
769                Log.error("TradeBean:createOrder -- failed to create Order", e);
770                throw new EJBException("TradeBean:createOrder -- failed to create Order", e);
771            }
772            return order;
773        }
774    
775        public AccountDataBean login(String userID, String password)
776                throws FinderException, Exception {
777    
778            /*  The following commented code shows alternative forms of the finder needed for this
779               *  method
780               *  The first alternative requires a 2-table join. Some database cannot allocate an Update
781               *  Lock on a join select.
782               *
783               *  The second alternative shows the finder being executed without allocation an update
784               *  lock on the row. Normally, an update lock would not be necessary, but is required if
785               *  the same user logs in multiple times to avoid a deadlock situation.
786               *
787               *  The third alternative runs the finder and allocates an update lock on the row(s)
788               *
789                  LocalAccount account = accountHome.findByUserIDForUpdate(userID);
790    
791                  LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate();
792               */
793    
794            AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID);
795            if (profile == null) {
796                throw new EJBException("No such user: " + userID);
797            }
798            AccountDataBean account = profile.getAccount();
799    
800            if (Log.doTrace())
801                Log.trace("TradeBean:login", userID, password);
802            account.login(password);
803            if (Log.doTrace())
804                Log.trace("TradeBean:login(" + userID + "," + password + ") success" + account);
805            return account;
806        }
807    
808        public void logout(String userID)
809                throws FinderException, Exception {
810            if (Log.doTrace())
811                Log.trace("TradeBean:logout", userID);
812    
813            /*  The following commented code shows alternative forms of the finder needed for this
814               *  method
815               *  The first alternative requires a 2-table join. Some database cannot allocate an Update
816               *  Lock on a join select.
817               *
818               *  The second alternative shows the finder being executed without allocation an update
819               *  lock on the row. Normally, an update lock would not be necessary, but is required if
820               *  the same user logs in multiple times to avoid a deadlock situation.
821               *
822               *  The third alternative runs the finder and allocates an update lock on the row(s)
823    
824               *    LocalAccount account = accountHome.findByUserIDForUpdate(userID);
825                  LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate();
826               *
827               */
828            AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID);
829            AccountDataBean account = profile.getAccount();
830    
831    
832            if (Log.doTrace()) Log.trace("TradeBean:logout(" + userID + ") success");
833            account.logout();
834        }
835    
836        public AccountDataBean register(
837                String userID,
838                String password,
839                String fullname,
840                String address,
841                String email,
842                String creditcard,
843                BigDecimal openBalance)
844                throws CreateException, Exception {
845    
846            AccountDataBean account = new AccountDataBean(0, 0, null, new Timestamp(System.currentTimeMillis()), openBalance, openBalance, null);
847            AccountProfileDataBean profile = new AccountProfileDataBean(userID, password, fullname, address, email, creditcard);
848            account.setProfile(profile);
849            //are both of these necessary?
850            profile.setAccount(account);
851            //this should save the linked profile as well?
852            entityManager.persist(account);
853            //apparently doesn't??
854            entityManager.persist(profile);
855            return account;
856        }
857    
858        public AccountDataBean getAccountData(String userID) {
859    
860            if (Log.doTrace())
861                Log.trace("TradeBean:getAccountData", userID);
862    
863            /*  The following commented code shows alternative forms of the finder needed for this
864               *  method
865               *  The first alternative requires a 2-table join. Some database cannot allocate an Update
866               *  Lock on a join select.
867               *
868               *  The second alternative shows the finder being executed without allocation an update
869               *  lock on the row. Normally, an update lock would not be necessary, but is required if
870               *  the same user logs in multiple times to avoid a deadlock situation.
871               *
872               *  The third alternative runs the finder and allocates an update lock on the row(s)
873               *
874                  LocalAccount account = accountHome.findByUserID(userID);
875    
876                  LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate();
877               */
878            AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID);
879            return profile.getAccount();
880        }
881    
882        public AccountProfileDataBean getAccountProfileData(String userID)
883                throws FinderException, Exception {
884            if (Log.doTrace())
885                Log.trace("TradeBean:getAccountProfileData", userID);
886    
887            /*  The following commented code shows alternative forms of the finder needed for this
888               *  method
889               *  The first alternative requires a 2-table join. Some database cannot allocate an Update
890               *  Lock on a join select.
891               *
892               *  The second alternative shows the finder being executed without allocation an update
893               *  lock on the row. Normally, an update lock would not be necessary, but is required if
894               *  the same user logs in multiple times to avoid a deadlock situation.
895               *
896               *  The third alternative runs the finder and allocates an update lock on the row(s)
897               *
898                  LocalAccount account = accountHome.findByUserID(userID);
899    
900                  LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate();
901               */
902            return entityManager.find(AccountProfileDataBean.class, userID);
903        }
904    
905        public AccountProfileDataBean updateAccountProfile(AccountProfileDataBean accountProfileData)
906                throws FinderException, Exception {
907    
908            if (Log.doTrace())
909                Log.trace("TradeBean:updateAccountProfileData", accountProfileData);
910    
911            //TODO this might not be correct
912            entityManager.merge(accountProfileData);
913    //        LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKeyForUpdate(accountProfileData.getUserID())).getAccountForUpdate();
914    //
915    //        accountProfileData =
916    //                account.updateAccountProfile(accountProfileData);
917            return accountProfileData;
918        }
919    
920        public RunStatsDataBean resetTrade(boolean deleteAll)
921                throws Exception {
922            if (Log.doTrace())
923                Log.trace("TradeBean:resetTrade", deleteAll);
924    
925            //Clear MDB Statistics
926            MDBStats.getInstance().reset();
927            // Reset Trade
928            return new org.apache.geronimo.samples.daytrader.direct.TradeDirect().resetTrade(deleteAll);
929        }
930    
931        /**
932         * provides a simple session method with no database access to test performance of a simple
933         * path through a stateless session
934         *
935         * @param investment amount
936         * @param NetValue   current value
937         * @return return on investment as a percentage
938         */
939        public double investmentReturn(double investment, double NetValue) {
940            if (Log.doTrace())
941                Log.trace("TradeBean:investmentReturn");
942    
943            double diff = NetValue - investment;
944            return diff / investment;
945    
946        }
947    
948        /**
949         * This method provides a ping test for a 2-phase commit operation
950         *
951         * @param symbol to lookup
952         * @return quoteData after sending JMS message
953         */
954        public QuoteDataBean pingTwoPhase(String symbol) {
955            try {
956                if (Log.doTrace()) Log.trace("TradeBean:pingTwoPhase", symbol);
957                Connection conn = null;
958                Session sess = null;
959                try {
960    
961                    //Get a Quote and send a JMS message in a 2-phase commit
962                    QuoteDataBean quote = entityManager.find(QuoteDataBean.class, symbol);
963    
964                    conn = qConnFactory.createConnection();
965                    sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
966                    MessageProducer msgProducer = sess.createProducer(queue);
967                    TextMessage message = sess.createTextMessage();
968    
969                    String command = "ping";
970                    message.setStringProperty("command", command);
971                    message.setLongProperty("publishTime", System.currentTimeMillis());
972                    message.setText("Ping message for queue java:comp/env/jms/TradeBrokerQueue sent from TradeSessionEJB:pingTwoPhase at " + new java.util.Date());
973    
974                    msgProducer.send(message);
975                    return quote;
976                } finally {
977                    if (conn != null)
978                        conn.close();
979                    if (sess != null)
980                        sess.close();
981                }
982    
983            } catch (Exception e) {
984                throw new EJBException(e.getMessage(), e);
985            }
986        }
987    
988        /* Required javax.ejb.SessionBean interface methods */
989    
990        public TradeJPA() {
991        }
992    
993        private static boolean warnJMSFailure = true;
994    
995        public void ejbCreate() throws CreateException {
996            try {
997    
998                if (Log.doTrace())
999                    Log.trace("TradeBean:ejbCreate  -- JNDI lookups of EJB and JMS resources");
1000    
1001                InitialContext ic = new InitialContext();
1002    
1003                publishQuotePriceChange = (Boolean) ic.lookup("java:comp/env/publishQuotePriceChange");
1004                boolean updateQuotePrices = (Boolean) ic.lookup("java:comp/env/updateQuotePrices");
1005                TradeConfig.setUpdateQuotePrices(updateQuotePrices);
1006    
1007                try {
1008                    qConnFactory = (ConnectionFactory) ic.lookup("java:comp/env/jms/QueueConnectionFactory");
1009                    tConnFactory = (ConnectionFactory) ic.lookup("java:comp/env/jms/TopicConnectionFactory");
1010                    streamerTopic = (Topic) ic.lookup("java:comp/env/jms/TradeStreamerTopic");
1011                    queue = (Queue) ic.lookup("java:comp/env/jms/TradeBrokerQueue");
1012                }
1013                catch (Exception e) {
1014                    if (TradeJPA.warnJMSFailure) {
1015                        TradeJPA.warnJMSFailure = false;
1016                        Log.error("TradeBean:ejbCreate  Unable to lookup JMS Resources\n\t -- Asynchronous mode will not work correctly and Quote Price change publishing will be disabled", e);
1017                    }
1018                    publishQuotePriceChange = false;
1019                }
1020    
1021            } catch (Exception e) {
1022                Log.error("TradeBean:ejbCreate: Lookup of Local Entity Homes Failed\n" + e);
1023                e.printStackTrace();
1024                //UPDATE
1025                //throw new CreateException(e.toString());
1026            }
1027    
1028        }
1029    
1030        public void ejbRemove() {
1031            try {
1032                if (Log.doTrace())
1033                    Log.trace("TradeBean:ejbRemove");
1034            }
1035            catch (Exception e) {
1036                Log.error(e, "Unable to close Queue or Topic connection on Session EJB remove");
1037            }
1038        }
1039    
1040        public void ejbActivate() {
1041        }
1042    
1043        public void ejbPassivate() {
1044        }
1045    
1046        public void setSessionContext(SessionContext sc) {
1047            context = sc;
1048            if (sc != null) {
1049            try {
1050                entityManager = (EntityManager) new InitialContext().lookup("java:comp/env/jpa/daytrader");
1051            } catch (NamingException e) {
1052                throw new EJBException("could not get Naming Context", e);
1053            }
1054            } else {
1055                entityManager = null;
1056            }
1057    
1058        }
1059    }