001//@formatter:off
002/*
003 * Product Manager Server - the server side
004 * Code-Beispiel zum Buch Patterns Kompakt, Verlag Springer Vieweg
005 * Copyright 2014 Karl Eilebrecht
006 * 
007 * Licensed under the Apache License, Version 2.0 (the "License"):
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019//@formatter:on
020package de.calamanari.pk.combinedmethod;
021
022import java.io.IOException;
023import java.rmi.AlreadyBoundException;
024import java.rmi.NotBoundException;
025import java.rmi.RemoteException;
026import java.rmi.registry.LocateRegistry;
027import java.rmi.registry.Registry;
028import java.rmi.server.UnicastRemoteObject;
029import java.util.ArrayList;
030import java.util.List;
031import java.util.Map;
032import java.util.Queue;
033import java.util.concurrent.ConcurrentHashMap;
034import java.util.concurrent.ConcurrentLinkedQueue;
035import java.util.concurrent.atomic.AtomicInteger;
036
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039import org.slf4j.event.Level;
040
041import de.calamanari.pk.util.AbstractConsoleServer;
042import de.calamanari.pk.util.TimeUtils;
043
044/**
045 * Product Manager Server - the server side
046 * 
047 * @author <a href="mailto:Karl.Eilebrecht(a/t)calamanari.de">Karl Eilebrecht</a>
048 */
049public class ProductManagerServer extends AbstractConsoleServer implements ProductManager {
050
051    private static final Logger LOGGER = LoggerFactory.getLogger(ProductManagerServer.class);
052
053    /**
054     * default registry port (our private server, usually this is 1099 for RMI!)
055     */
056    public static final int DEFAULT_REGISTRY_PORT = 8091;
057
058    /**
059     * product-ids will start at {@value} + 1
060     */
061    private static final int START_PRODUCT_ID = 1000;
062
063    /**
064     * Time for RMI-registry to get ready.
065     */
066    private static final long REGISTRY_STARTUP_MILLIS = 3000;
067
068    /**
069     * some milliseconds to simulate expensive request
070     */
071    private static final long REQUEST_DELAY_MILLIS = 500;
072
073    /**
074     * server-side ID-sequence
075     */
076    private final AtomicInteger lastId = new AtomicInteger(START_PRODUCT_ID);
077
078    /**
079     * Unused IDs
080     */
081    private final Queue<String> lostAndFoundIds = new ConcurrentLinkedQueue<>();
082
083    /**
084     * server-side database
085     */
086    private final Map<String, Product> database = new ConcurrentHashMap<>();
087
088    /**
089     * port the RMI server listens
090     */
091    private volatile int registryPort;
092
093    /**
094     * server stub
095     */
096    // Volatile is sufficient as there is no race condition in this scenario
097    @SuppressWarnings("java:S3077")
098    private volatile ProductManager productManagerStub;
099
100    /**
101     * our private RMI-server
102     */
103    // Volatile is sufficient as there is no race condition in this scenario
104    @SuppressWarnings("java:S3077")
105    private volatile Process rmiRegistryProcess;
106
107    /**
108     * flag for testing
109     */
110    private volatile boolean nextProductRegistrationMustFail = false;
111
112    /**
113     * Creates new Product Manager Server
114     */
115    public ProductManagerServer() {
116        super(ProductManagerServer.class.getSimpleName());
117    }
118
119    @Override
120    public Product findProductById(String id) throws RemoteException {
121        LOGGER.debug("findProductById('{}') called", id);
122        return database.get(id);
123    }
124
125    @Override
126    public String acquireProductId() throws RemoteException {
127        LOGGER.debug("acquireProductId() called");
128        return "ID" + lastId.incrementAndGet();
129    }
130
131    @Override
132    public void registerProduct(Product product) throws RemoteException {
133        LOGGER.debug("registerProduct({}) called", product);
134        if (nextProductRegistrationMustFail) {
135            LOGGER.debug("Simulating some error");
136            nextProductRegistrationMustFail = false;
137            throw new ProductManagerServerException("Some error!");
138        }
139        String id = product.getProductId();
140        if (id != null) {
141            database.put(id, product);
142            LOGGER.debug("Product registration successful.");
143        }
144    }
145
146    @Override
147    public void setNextProductRegistrationMustFail() throws RemoteException {
148        this.nextProductRegistrationMustFail = true;
149    }
150
151    @Override
152    public Product combinedCreateAndRegisterProduct(Product product) throws RemoteException {
153        LOGGER.debug("combinedCreateAndRegisterProduct({}) called - The COMBINED METHOD", product);
154        String productId = obtainProductId();
155        LOGGER.debug("Assigning ID='{}' to product", productId);
156        product.setProductId(productId);
157        try {
158            registerProduct(product);
159        }
160        catch (RemoteException | RuntimeException ex) {
161            // "rollback"
162            LOGGER.debug("Handling error during registration, preserving ID='{}' for reuse", productId);
163            lostAndFoundIds.add(productId);
164            throw ex;
165        }
166        return product;
167    }
168
169    /**
170     * This method returns a free product-id, either from lost+found pool or newly acquired
171     * 
172     * @return fresh product-id
173     * @throws RemoteException on remoting error
174     */
175    private String obtainProductId() throws RemoteException {
176        LOGGER.debug("Checking for lost IDs ...");
177        String productId = lostAndFoundIds.poll();
178        if (productId == null) {
179            productId = acquireProductId();
180        }
181        else {
182            LOGGER.debug("Reusing lost ID '{}'", productId);
183        }
184        return productId;
185    }
186
187    @Override
188    public void reset() throws RemoteException {
189        this.lastId.set(START_PRODUCT_ID);
190        this.database.clear();
191        this.lostAndFoundIds.clear();
192    }
193
194    @Override
195    protected void configureInstance(String[] cmdLineArgs) {
196        configureRegistryPort(cmdLineArgs);
197        startRmiRegistry();
198    }
199
200    /**
201     * Starts the RMI-registry according to this instance's settings.
202     */
203    private void startRmiRegistry() {
204        List<String> args = new ArrayList<>();
205        args.add("rmiregistry");
206        args.add("" + this.registryPort);
207        ProcessBuilder pb = new ProcessBuilder(args);
208        pb.environment().put("CLASSPATH", System.getProperties().getProperty("java.class.path", null));
209        try {
210            LOGGER.info("Starting private RMI-Registry on port {} ...", this.registryPort);
211            rmiRegistryProcess = pb.start();
212            // give the registry some time to get ready
213            Thread.sleep(REGISTRY_STARTUP_MILLIS);
214            LOGGER.info("RMI-Registry ready.");
215        }
216        catch (InterruptedException ex) {
217            Thread.currentThread().interrupt();
218            throw new ProductManagerServerException(
219                    String.format("Unexpected interruption exception (could not start RMI-Registry on port %d)!", this.registryPort), ex);
220        }
221        catch (IOException | RuntimeException ex) {
222            throw new ProductManagerServerException(
223                    String.format("Unexpected communication exception (could not start RMI-Registry on port %d)!", this.registryPort), ex);
224        }
225    }
226
227    /**
228     * Parses the registry port from the command line arguments
229     * 
230     * @param cmdLineArgs program arguments
231     */
232    private void configureRegistryPort(String[] cmdLineArgs) {
233        int port = DEFAULT_REGISTRY_PORT;
234        if (cmdLineArgs != null && cmdLineArgs.length > 0) {
235            try {
236                port = Integer.parseInt(cmdLineArgs[0]);
237            }
238            catch (Exception ex) {
239                LOGGER.warn("Error parsing rmi-port='{}', using default={}", cmdLineArgs[0], port, ex);
240            }
241        }
242        this.registryPort = port;
243    }
244
245    @Override
246    protected void prepare() {
247        try {
248            this.productManagerStub = (ProductManager) UnicastRemoteObject.exportObject(this, 0);
249            Registry registry = LocateRegistry.getRegistry(registryPort);
250            registry.bind("ProductManager", this.productManagerStub);
251        }
252        catch (RemoteException | AlreadyBoundException ex) {
253            throw new ProductManagerServerException("Error during preparation!", ex);
254        }
255    }
256
257    @Override
258    protected String createStartupCompletedMessage() {
259        return this.getServerName() + " started!";
260    }
261
262    @Override
263    protected void doRequestProcessing() {
264        while (true) {
265            TimeUtils.sleep(Level.WARN, REQUEST_DELAY_MILLIS);
266            if (getServerState() != ServerState.ONLINE) {
267                break;
268            }
269        }
270    }
271
272    @Override
273    protected void initiateShutdown() {
274        // nothing to do
275
276    }
277
278    @Override
279    protected void cleanUp() {
280        try {
281            if (this.productManagerStub != null) {
282                Registry registry = LocateRegistry.getRegistry(registryPort);
283                registry.unbind("ProductManager");
284                UnicastRemoteObject.unexportObject(this, true);
285            }
286        }
287        catch (RemoteException | NotBoundException | RuntimeException t) {
288            LOGGER.error("Error during clean-up!", t);
289        }
290        try {
291            if (rmiRegistryProcess != null) {
292                rmiRegistryProcess.destroy();
293                LOGGER.info("Private RMI-Registry stopped!");
294            }
295        }
296        catch (RuntimeException t) {
297            LOGGER.error("Error during RMI-shutdown!", t);
298        }
299    }
300
301    /**
302     * Creates stand-alone console server
303     * 
304     * @param args first argument may optionally specify the port
305     */
306    public static void main(String[] args) {
307        try {
308            (new ProductManagerServer()).setupAndStart(args);
309        }
310        catch (RuntimeException ex) {
311            LOGGER.error("Server startup failed!", ex);
312        }
313    }
314
315}