diff --git a/chapter05/build.xml b/chapter05/build.xml
new file mode 100644
index 0000000..e5ab7dc
--- /dev/null
+++ b/chapter05/build.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/chapter05/paint-example/build.xml b/chapter05/paint-example/build.xml
new file mode 100644
index 0000000..0734a00
--- /dev/null
+++ b/chapter05/paint-example/build.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.paint/build.properties b/chapter05/paint-example/org.foo.paint/build.properties
new file mode 100644
index 0000000..d86d2e9
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/build.properties
@@ -0,0 +1,18 @@
+#-------------------------------------------------
+title=Paint Example - Paint Frame
+#-------------------------------------------------
+
+module=org.foo.paint
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package: \
+ ${module}
+
+Import-Package: \
+ org.foo.shape;version="[5.0,6.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.util.tracker;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter05/paint-example/org.foo.paint/build.xml b/chapter05/paint-example/org.foo.paint/build.xml
new file mode 100644
index 0000000..aafd423
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/build.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.paint/src/org/foo/paint/Activator.java b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/Activator.java
new file mode 100644
index 0000000..608f3b7
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/Activator.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+import org.osgi.framework.*;
+
+/**
+ * The activator of the host application bundle. The activator creates the main
+ * application JFrame and starts tracking SimpleShape
+ * services. All activity is performed on the Swing event thread to avoid
+ * synchronization and repainting issues. Closing the application window will
+ * result in Bundle.stop() being called on the system bundle, which
+ * will cause the framework to shutdown and the JVM to exit.
+ **/
+public class Activator implements BundleActivator, Runnable {
+ private BundleContext m_context = null;
+ private PaintFrame m_frame = null;
+ private ShapeTracker m_shapetracker = null;
+
+ /**
+ * Displays the applications window and starts service tracking; everything is
+ * done on the Swing event thread to avoid synchronization and repainting
+ * issues.
+ *
+ * @param context The context of the bundle.
+ **/
+ public void start(BundleContext context) {
+ m_context = context;
+ if (SwingUtilities.isEventDispatchThread()) {
+ run();
+ } else {
+ try {
+ javax.swing.SwingUtilities.invokeAndWait(this);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Stops service tracking and disposes of the application window.
+ *
+ * @param context The context of the bundle.
+ **/
+ public void stop(BundleContext context) {
+ m_shapetracker.close();
+ final PaintFrame frame = m_frame;
+ javax.swing.SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ frame.setVisible(false);
+ frame.dispose();
+ }
+ });
+ }
+
+ /**
+ * This method actually performs the creation of the application window. It is
+ * intended to be called by the Swing event thread and should not be called
+ * directly.
+ **/
+ public void run() {
+ m_frame = new PaintFrame();
+
+ m_frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+ m_frame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent evt) {
+ try {
+ m_context.getBundle(0).stop();
+ } catch (BundleException ex) {
+ ex.printStackTrace();
+ }
+ }
+ });
+
+ m_frame.setVisible(true);
+
+ m_shapetracker = new ShapeTracker(m_context, m_frame);
+ m_shapetracker.open();
+ }
+}
diff --git a/chapter05/paint-example/org.foo.paint/src/org/foo/paint/DefaultShape.java b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/DefaultShape.java
new file mode 100644
index 0000000..580b437
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/DefaultShape.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.*;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * This class is used as a proxy to defer object creation from shape provider
+ * bundles and also as a placeholder shape when previously used shapes are no
+ * longer available. These two purposes are actually orthogonal, but were
+ * combined into a single class to reduce the number of classes in the
+ * application. The proxy-related functionality is introduced as a way to lazily
+ * create shape objects in an effort to improve performance; this level of
+ * indirection could be removed if eager creation of objects is not a concern.
+ * Since this application uses the service-based extension appraoch, lazy shape
+ * creation will only come into effect if service providers register service
+ * factories instead of directly registering SimpleShape or if they use
+ * a technology like Declarative Services or iPOJO to register services. Since
+ * the example providers register services instances directly there is no
+ * laziness in the example, but the proxy approach is still used to demonstrate
+ * how to make laziness possible and to keep it similar to the extender-based
+ * approach.
+ **/
+class DefaultShape implements SimpleShape {
+ private SimpleShape m_shape;
+ private ImageIcon m_icon;
+ private BundleContext m_context;
+ private ServiceReference m_ref;
+
+ /**
+ * This constructs a placeholder shape that draws a default icon. It is used
+ * when a previously drawn shape is no longer available.
+ **/
+ public DefaultShape() {
+ // Do nothing.
+ }
+
+ /**
+ * This constructs a proxy shape that lazily gets the shape service.
+ *
+ * @param context The bundle context to use for retrieving the shape service.
+ * @param ref The service reference of the service.
+ **/
+ public DefaultShape(BundleContext context, ServiceReference ref) {
+ m_context = context;
+ m_ref = ref;
+ }
+
+ /**
+ * This method tells the proxy to dispose of its service object; this is
+ * called when the underlying service goes away.
+ **/
+ public void dispose() {
+ if (m_shape != null) {
+ m_context.ungetService(m_ref);
+ m_context = null;
+ m_ref = null;
+ m_shape = null;
+ }
+ }
+
+ /**
+ * Implements the SimpleShape interface method. When acting as a
+ * proxy, this method gets the shape service and then uses it to draw the
+ * shape. When acting as a placeholder shape, this method draws the default
+ * icon.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ // If this is a proxy shape, instantiate the shape class
+ // and use it to draw the shape.
+ if (m_context != null) {
+ try {
+ if (m_shape == null) {
+ // Get the shape service.
+ m_shape = (SimpleShape) m_context.getService(m_ref);
+ }
+ // Draw the shape.
+ m_shape.draw(g2, p);
+ // If everything was successful, then simply return.
+ return;
+ } catch (Exception ex) {
+ // This generally should not happen, but if it does then
+ // we can just fall through and paint the default icon.
+ }
+ }
+
+ // If the proxied shape could not be drawn for any reason or if
+ // this shape is simply a placeholder, then draw the default icon.
+ if (m_icon == null) {
+ try {
+ m_icon = new ImageIcon(this.getClass().getResource("underc.png"));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ g2.setColor(Color.red);
+ g2.fillRect(0, 0, 60, 60);
+ return;
+ }
+ }
+ g2.drawImage(m_icon.getImage(), 0, 0, null);
+ }
+}
diff --git a/chapter05/paint-example/org.foo.paint/src/org/foo/paint/PaintFrame.java b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/PaintFrame.java
new file mode 100644
index 0000000..bc81f45
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/PaintFrame.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.HashMap;
+import java.util.Map;
+import javax.swing.*;
+import org.foo.shape.SimpleShape;
+
+/**
+ * This class represents the main application class, which is a JFrame subclass
+ * that manages a toolbar of shapes and a drawing canvas. This class does not
+ * directly interact with the underlying OSGi framework; instead, it is injected
+ * with the available SimpleShape instances to eliminate any
+ * dependencies on the OSGi application programming interfaces.
+ **/
+public class PaintFrame extends JFrame implements MouseListener, MouseMotionListener {
+ private static final long serialVersionUID = 1L;
+ private static final int BOX = 54;
+ private JToolBar m_toolbar;
+ private String m_selected;
+ private JPanel m_panel;
+ private ShapeComponent m_selectedComponent;
+ private Map m_shapes = new HashMap();
+ private ActionListener m_reusableActionListener = new ShapeActionListener();
+ private SimpleShape m_defaultShape = new DefaultShape();
+
+ /**
+ * Default constructor that populates the main window.
+ **/
+ public PaintFrame() {
+ super("PaintFrame");
+
+ m_toolbar = new JToolBar("Toolbar");
+ m_panel = new JPanel();
+ m_panel.setBackground(Color.WHITE);
+ m_panel.setLayout(null);
+ m_panel.setMinimumSize(new Dimension(400, 400));
+ m_panel.addMouseListener(this);
+ getContentPane().setLayout(new BorderLayout());
+ getContentPane().add(m_toolbar, BorderLayout.NORTH);
+ getContentPane().add(m_panel, BorderLayout.CENTER);
+ setSize(400, 400);
+ }
+
+ /**
+ * This method sets the currently selected shape to be used for drawing on the
+ * canvas.
+ *
+ * @param name The name of the shape to use for drawing on the canvas.
+ **/
+ public void selectShape(String name) {
+ m_selected = name;
+ }
+
+ /**
+ * Retrieves the available SimpleShape associated with the given
+ * name.
+ *
+ * @param name The name of the SimpleShape to retrieve.
+ * @return The corresponding SimpleShape instance if available or
+ * null.
+ **/
+ public SimpleShape getShape(String name) {
+ ShapeInfo info = (ShapeInfo) m_shapes.get(name);
+ if (info == null) {
+ return m_defaultShape;
+ } else {
+ return info.m_shape;
+ }
+ }
+
+ /**
+ * Injects an available SimpleShape into the drawing frame.
+ *
+ * @param name The name of the injected SimpleShape.
+ * @param icon The icon associated with the injected SimpleShape.
+ * @param shape The injected SimpleShape instance.
+ **/
+ public void addShape(String name, Icon icon, SimpleShape shape) {
+ m_shapes.put(name, new ShapeInfo(name, icon, shape));
+ JButton button = new JButton(icon);
+ button.setActionCommand(name);
+ button.setToolTipText(name);
+ button.addActionListener(m_reusableActionListener);
+
+ if (m_selected == null) {
+ button.doClick();
+ }
+
+ m_toolbar.add(button);
+ m_toolbar.validate();
+ repaint();
+ }
+
+ /**
+ * Removes a no longer available SimpleShape from the drawing frame.
+ *
+ * @param name The name of the SimpleShape to remove.
+ **/
+ public void removeShape(String name) {
+ m_shapes.remove(name);
+
+ if ((m_selected != null) && m_selected.equals(name)) {
+ m_selected = null;
+ }
+
+ for (int i = 0; i < m_toolbar.getComponentCount(); i++) {
+ JButton sb = (JButton) m_toolbar.getComponent(i);
+ if (sb.getActionCommand().equals(name)) {
+ m_toolbar.remove(i);
+ m_toolbar.invalidate();
+ validate();
+ repaint();
+ break;
+ }
+ }
+
+ if ((m_selected == null) && (m_toolbar.getComponentCount() > 0)) {
+ ((JButton) m_toolbar.getComponent(0)).doClick();
+ }
+ }
+
+ /**
+ * Implements method for the MouseListener interface to draw the
+ * selected shape into the drawing canvas.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseClicked(MouseEvent evt) {
+ if (m_selected == null) {
+ return;
+ }
+
+ if (m_panel.contains(evt.getX(), evt.getY())) {
+ ShapeComponent sc = new ShapeComponent(this, m_selected);
+ sc.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX);
+ m_panel.add(sc, 0);
+ m_panel.validate();
+ m_panel.repaint(sc.getBounds());
+ }
+ }
+
+ /**
+ * Implements an empty method for the MouseListener interface.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseEntered(MouseEvent evt) {}
+
+ /**
+ * Implements an empty method for the MouseListener interface.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseExited(MouseEvent evt) {}
+
+ /**
+ * Implements method for the MouseListener interface to initiate
+ * shape dragging.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mousePressed(MouseEvent evt) {
+ Component c = m_panel.getComponentAt(evt.getPoint());
+ if (c instanceof ShapeComponent) {
+ m_selectedComponent = (ShapeComponent) c;
+ m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+ m_panel.addMouseMotionListener(this);
+ m_selectedComponent.repaint();
+ }
+ }
+
+ /**
+ * Implements method for the MouseListener interface to complete
+ * shape dragging.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseReleased(MouseEvent evt) {
+ if (m_selectedComponent != null) {
+ m_panel.removeMouseMotionListener(this);
+ m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ m_selectedComponent.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX);
+ m_selectedComponent.repaint();
+ m_selectedComponent = null;
+ }
+ }
+
+ /**
+ * Implements method for the MouseMotionListener interface to move a
+ * dragged shape.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseDragged(MouseEvent evt) {
+ m_selectedComponent.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX);
+ }
+
+ /**
+ * Implements an empty method for the MouseMotionListener interface.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseMoved(MouseEvent evt) {}
+
+ /**
+ * Simple action listener for shape tool bar buttons that sets the drawing
+ * frame's currently selected shape when receiving an action event.
+ **/
+ private class ShapeActionListener implements ActionListener {
+ public void actionPerformed(ActionEvent evt) {
+ selectShape(evt.getActionCommand());
+ }
+ }
+
+ /**
+ * This class is used to record the various information pertaining to an
+ * available shape.
+ **/
+ private static class ShapeInfo {
+ public String m_name;
+ public Icon m_icon;
+ public SimpleShape m_shape;
+
+ public ShapeInfo(String name, Icon icon, SimpleShape shape) {
+ m_name = name;
+ m_icon = icon;
+ m_shape = shape;
+ }
+ }
+}
diff --git a/chapter05/paint-example/org.foo.paint/src/org/foo/paint/ShapeComponent.java b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/ShapeComponent.java
new file mode 100644
index 0000000..9a65d5e
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/ShapeComponent.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.*;
+import javax.swing.JComponent;
+import org.foo.shape.SimpleShape;
+
+/**
+ * Simple component class used to represent a drawn shape. This component uses a
+ * SimpleShape to paint its contents.
+ **/
+public class ShapeComponent extends JComponent {
+ private static final long serialVersionUID = 1L;
+ private PaintFrame m_frame;
+ private String m_shapeName;
+
+ /**
+ * Construct a component for the specified drawing frame with the specified
+ * named shape. The component acquires the named shape from the drawing frame
+ * at the time of painting, which helps it account for dynamism.
+ *
+ * @param frame The drawing frame associated with the component.
+ * @param shapeName The name of the shape to draw.
+ **/
+ public ShapeComponent(PaintFrame frame, String shapeName) {
+ m_frame = frame;
+ m_shapeName = shapeName;
+ }
+
+ /**
+ * Paints the contents of the component. The component acquires the named
+ * shape from the drawing frame at the time of painting, which helps it
+ * account for dynamism.
+ *
+ * @param g The graphics object to use for painting.
+ **/
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D) g;
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ SimpleShape shape = m_frame.getShape(m_shapeName);
+ shape.draw(g2, new Point(getWidth() / 2, getHeight() / 2));
+ }
+}
diff --git a/chapter05/paint-example/org.foo.paint/src/org/foo/paint/ShapeTracker.java b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/ShapeTracker.java
new file mode 100644
index 0000000..a1fb7a1
--- /dev/null
+++ b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/ShapeTracker.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import javax.swing.Icon;
+import javax.swing.SwingUtilities;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.*;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Extends the ServiceTracker to create a tracker for
+ * SimpleShape services. The tracker is responsible for listener for
+ * the arrival/departure of SimpleShape services and informing the
+ * application about the availability of shapes. This tracker forces all
+ * notifications to be processed on the Swing event thread to avoid
+ * synchronization and redraw issues.
+ **/
+public class ShapeTracker extends ServiceTracker {
+ // Flag indicating an added shape.
+ private static final int ADDED = 1;
+ // Flag indicating a modified shape.
+ private static final int MODIFIED = 2;
+ // Flag indicating a removed shape.
+ private static final int REMOVED = 3;
+ // The bundle context used for tracking.
+ private BundleContext m_context;
+ // The application object to notify.
+ private PaintFrame m_frame;
+
+ /**
+ * Constructs a tracker that uses the specified bundle context to track
+ * services and notifies the specified application object about changes.
+ *
+ * @param context The bundle context to be used by the tracker.
+ * @param frame The application object to notify about service changes.
+ **/
+ public ShapeTracker(BundleContext context, PaintFrame frame) {
+ super(context, SimpleShape.class.getName(), null);
+ m_context = context;
+ m_frame = frame;
+ }
+
+ /**
+ * Overrides the ServiceTracker functionality to inform the
+ * application object about the added service.
+ *
+ * @param ref The service reference of the added service.
+ * @return The service object to be used by the tracker.
+ **/
+ public Object addingService(ServiceReference ref) {
+ SimpleShape shape = new DefaultShape(m_context, ref);
+ processShapeOnEventThread(ADDED, ref, shape);
+ return shape;
+ }
+
+ /**
+ * Overrides the ServiceTracker functionality to inform the
+ * application object about the modified service.
+ *
+ * @param ref The service reference of the modified service.
+ * @param svc The service object of the modified service.
+ **/
+ public void modifiedService(ServiceReference ref, Object svc) {
+ processShapeOnEventThread(MODIFIED, ref, (SimpleShape) svc);
+ }
+
+ /**
+ * Overrides the ServiceTracker functionality to inform the
+ * application object about the removed service.
+ *
+ * @param ref The service reference of the removed service.
+ * @param svc The service object of the removed service.
+ **/
+ public void removedService(ServiceReference ref, Object svc) {
+ processShapeOnEventThread(REMOVED, ref, (SimpleShape) svc);
+ ((DefaultShape) svc).dispose();
+ }
+
+ /**
+ * Processes a received service notification from the ServiceTracker,
+ * forcing the processing of the notification onto the Swing event thread if
+ * it is not already on it.
+ *
+ * @param action The type of action associated with the notification.
+ * @param ref The service reference of the corresponding service.
+ * @param shape The service object of the corresponding service.
+ **/
+ private void processShapeOnEventThread(int action, ServiceReference ref, SimpleShape shape) {
+ if ((m_context.getBundle(0).getState() & (Bundle.STARTING | Bundle.ACTIVE)) == 0) {
+ return;
+ }
+
+ try {
+ if (SwingUtilities.isEventDispatchThread()) {
+ processShape(action, ref, shape);
+ } else {
+ SwingUtilities.invokeAndWait(new ShapeRunnable(action, ref, shape));
+ }
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ /**
+ * Actually performs the processing of the service notification. Invokes the
+ * appropriate callback method on the application object depending on the
+ * action type of the notification.
+ *
+ * @param action The type of action associated with the notification.
+ * @param ref The service reference of the corresponding service.
+ * @param shape The service object of the corresponding service.
+ **/
+ private void processShape(int action, ServiceReference ref, SimpleShape shape) {
+ String name = (String) ref.getProperty(SimpleShape.NAME_PROPERTY);
+
+ switch (action) {
+ case MODIFIED:
+ m_frame.removeShape(name);
+ // Purposely let this fall through to the 'add' case to
+ // reload the service.
+
+ case ADDED:
+ Icon icon = (Icon) ref.getProperty(SimpleShape.ICON_PROPERTY);
+ m_frame.addShape(name, icon, shape);
+ break;
+
+ case REMOVED:
+ m_frame.removeShape(name);
+ break;
+ }
+ }
+
+ /**
+ * Simple class used to process service notification handling on the Swing
+ * event thread.
+ **/
+ private class ShapeRunnable implements Runnable {
+ private int m_action;
+ private ServiceReference m_ref;
+ private SimpleShape m_shape;
+
+ /**
+ * Constructs an object with the specified action, service reference, and
+ * service object for processing on the Swing event thread.
+ *
+ * @param action The type of action associated with the notification.
+ * @param ref The service reference of the corresponding service.
+ * @param shape The service object of the corresponding service.
+ **/
+ public ShapeRunnable(int action, ServiceReference ref, SimpleShape shape) {
+ m_action = action;
+ m_ref = ref;
+ m_shape = shape;
+ }
+
+ /**
+ * Calls the processShape() method.
+ **/
+ public void run() {
+ processShape(m_action, m_ref, m_shape);
+ }
+ }
+}
diff --git a/chapter05/paint-example/org.foo.paint/src/org/foo/paint/underc.png b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/underc.png
new file mode 100644
index 0000000..425cdb9
Binary files /dev/null and b/chapter05/paint-example/org.foo.paint/src/org/foo/paint/underc.png differ
diff --git a/chapter05/paint-example/org.foo.shape.circle.resource-de/build.properties b/chapter05/paint-example/org.foo.shape.circle.resource-de/build.properties
new file mode 100644
index 0000000..5489cbc
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle.resource-de/build.properties
@@ -0,0 +1,11 @@
+#-------------------------------------------------
+title=Paint Example - Circle Shape Localization
+#-------------------------------------------------
+
+module=org.foo.shape.circle.resource-de
+custom=true
+
+Private-Package:\
+ org.foo.shape.circle.resource
+
+Fragment-Host: org.foo.shape.circle; bundle-version="[5.0,6.0)"
diff --git a/chapter05/paint-example/org.foo.shape.circle.resource-de/build.xml b/chapter05/paint-example/org.foo.shape.circle.resource-de/build.xml
new file mode 100644
index 0000000..8fa7ee7
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle.resource-de/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape.circle.resource-de/src/org/foo/shape/circle/resource/Localize_de.properties b/chapter05/paint-example/org.foo.shape.circle.resource-de/src/org/foo/shape/circle/resource/Localize_de.properties
new file mode 100644
index 0000000..3f55342
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle.resource-de/src/org/foo/shape/circle/resource/Localize_de.properties
@@ -0,0 +1 @@
+CIRCLE_NAME=Kreis
diff --git a/chapter05/paint-example/org.foo.shape.circle/build.properties b/chapter05/paint-example/org.foo.shape.circle/build.properties
new file mode 100644
index 0000000..f49e718
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Paint Example - Circle Shape
+#-------------------------------------------------
+
+module=org.foo.shape.circle
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}.*
+
+Import-Package: \
+ org.foo.shape;version="[5.0,6.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter05/paint-example/org.foo.shape.circle/build.xml b/chapter05/paint-example/org.foo.shape.circle/build.xml
new file mode 100644
index 0000000..1f9cb3b
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/Activator.java b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/Activator.java
new file mode 100644
index 0000000..96f662e
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/Activator.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.circle;
+
+import java.util.Hashtable;
+import java.util.ResourceBundle;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * This class implements a simple bundle activator for the circle
+ * SimpleShape service. This activator simply creates an instance of
+ * the circle service object and registers it with the service registry along
+ * with the service properties indicating the service's name and icon.
+ **/
+public class Activator implements BundleActivator {
+ public static final String CIRCLE_NAME = "CIRCLE_NAME";
+
+ /**
+ * Implements the BundleActivator.start() method, which registers the
+ * circle SimpleShape service.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void start(BundleContext context) {
+ ResourceBundle rb = ResourceBundle.getBundle(
+ "org.foo.shape.circle.resource.Localize");
+ Hashtable dict = new Hashtable();
+ dict.put(SimpleShape.NAME_PROPERTY, rb.getString(CIRCLE_NAME));
+ dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass().getResource("circle.png")));
+ context.registerService(SimpleShape.class.getName(), new Circle(), dict);
+ }
+
+ /**
+ * Implements the BundleActivator.start() method, which does nothing.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/Circle.java b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/Circle.java
new file mode 100644
index 0000000..3c7cd7a
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/Circle.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.circle;
+
+import java.awt.*;
+import java.awt.geom.Ellipse2D;
+import org.foo.shape.SimpleShape;
+
+public class Circle implements SimpleShape {
+
+ /**
+ * Implements the SimpleShape.draw() method for painting the shape.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ int x = p.x - 25;
+ int y = p.y - 25;
+ GradientPaint gradient = new GradientPaint(x, y, Color.RED, x + 50, y, Color.WHITE);
+ g2.setPaint(gradient);
+ g2.fill(new Ellipse2D.Double(x, y, 50, 50));
+ BasicStroke wideStroke = new BasicStroke(2.0f);
+ g2.setColor(Color.black);
+ g2.setStroke(wideStroke);
+ g2.draw(new Ellipse2D.Double(x, y, 50, 50));
+ }
+}
diff --git a/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/circle.png b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/circle.png
new file mode 100644
index 0000000..3d4887e
Binary files /dev/null and b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/circle.png differ
diff --git a/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/resource/Localize.properties b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/resource/Localize.properties
new file mode 100644
index 0000000..3add0dd
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.circle/src/org/foo/shape/circle/resource/Localize.properties
@@ -0,0 +1 @@
+CIRCLE_NAME=Circle
diff --git a/chapter05/paint-example/org.foo.shape.square.resource-de/build.properties b/chapter05/paint-example/org.foo.shape.square.resource-de/build.properties
new file mode 100644
index 0000000..e8e126e
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square.resource-de/build.properties
@@ -0,0 +1,11 @@
+#-------------------------------------------------
+title=Paint Example - Square Shape Localization
+#-------------------------------------------------
+
+module=org.foo.shape.square.resource-de
+custom=true
+
+Private-Package:\
+ org.foo.shape.square.resource
+
+Fragment-Host: org.foo.shape.square
diff --git a/chapter05/paint-example/org.foo.shape.square.resource-de/build.xml b/chapter05/paint-example/org.foo.shape.square.resource-de/build.xml
new file mode 100644
index 0000000..0314f67
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square.resource-de/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape.square.resource-de/src/org/foo/shape/square/resource/Localize_de.properties b/chapter05/paint-example/org.foo.shape.square.resource-de/src/org/foo/shape/square/resource/Localize_de.properties
new file mode 100644
index 0000000..866aa4a
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square.resource-de/src/org/foo/shape/square/resource/Localize_de.properties
@@ -0,0 +1 @@
+SQUARE_NAME=Quadrat
diff --git a/chapter05/paint-example/org.foo.shape.square/build.properties b/chapter05/paint-example/org.foo.shape.square/build.properties
new file mode 100644
index 0000000..7c52233
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Paint Example - Square Shape
+#-------------------------------------------------
+
+module=org.foo.shape.square
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}.*
+
+Import-Package: \
+ org.foo.shape;version="[5.0,6.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter05/paint-example/org.foo.shape.square/build.xml b/chapter05/paint-example/org.foo.shape.square/build.xml
new file mode 100644
index 0000000..25bf2d6
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/Activator.java b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/Activator.java
new file mode 100644
index 0000000..b9245a3
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/Activator.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.square;
+
+import java.util.Hashtable;
+import java.util.ResourceBundle;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * This class implements a simple bundle activator for the square
+ * SimpleShape service. This activator simply creates an instance of
+ * the square service object and registers it with the service registry along
+ * with the service properties indicating the service's name and icon.
+ **/
+public class Activator implements BundleActivator {
+ public static final String SQUARE_NAME = "SQUARE_NAME";
+
+ /**
+ * Implements the BundleActivator.start() method, which registers the
+ * square SimpleShape service.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void start(BundleContext context) {
+ ResourceBundle rb = ResourceBundle.getBundle(
+ "org.foo.shape.square.resource.Localize");
+ Hashtable dict = new Hashtable();
+ dict.put(SimpleShape.NAME_PROPERTY, rb.getString(SQUARE_NAME));
+ dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass().getResource("square.png")));
+ context.registerService(SimpleShape.class.getName(), new Square(), dict);
+ }
+
+ /**
+ * Implements the BundleActivator.start() method, which does nothing.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/Square.java b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/Square.java
new file mode 100644
index 0000000..cbca751
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/Square.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.square;
+
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+import org.foo.shape.SimpleShape;
+
+public class Square implements SimpleShape {
+
+ /**
+ * Implements the SimpleShape.draw() method for painting the shape.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ int x = p.x - 25;
+ int y = p.y - 25;
+ GradientPaint gradient = new GradientPaint(x, y, Color.BLUE, x + 50, y, Color.WHITE);
+ g2.setPaint(gradient);
+ g2.fill(new Rectangle2D.Double(x, y, 50, 50));
+ BasicStroke wideStroke = new BasicStroke(2.0f);
+ g2.setColor(Color.black);
+ g2.setStroke(wideStroke);
+ g2.draw(new Rectangle2D.Double(x, y, 50, 50));
+ }
+}
diff --git a/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/resource/Localize.properties b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/resource/Localize.properties
new file mode 100644
index 0000000..2a7a82c
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/resource/Localize.properties
@@ -0,0 +1 @@
+SQUARE_NAME=Square
diff --git a/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/square.png b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/square.png
new file mode 100644
index 0000000..3f24cfc
Binary files /dev/null and b/chapter05/paint-example/org.foo.shape.square/src/org/foo/shape/square/square.png differ
diff --git a/chapter05/paint-example/org.foo.shape.triangle.resource-de/build.properties b/chapter05/paint-example/org.foo.shape.triangle.resource-de/build.properties
new file mode 100644
index 0000000..bc3bb1b
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle.resource-de/build.properties
@@ -0,0 +1,11 @@
+#-------------------------------------------------
+title=Paint Example - Triangle Shape Localization
+#-------------------------------------------------
+
+module=org.foo.shape.triangle.resource-de
+custom=true
+
+Private-Package:\
+ org.foo.shape.triangle.resource
+
+Fragment-Host: org.foo.shape.triangle
diff --git a/chapter05/paint-example/org.foo.shape.triangle.resource-de/build.xml b/chapter05/paint-example/org.foo.shape.triangle.resource-de/build.xml
new file mode 100644
index 0000000..ea90f99
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle.resource-de/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape.triangle.resource-de/src/org/foo/shape/triangle/resource/Localize_de.properties b/chapter05/paint-example/org.foo.shape.triangle.resource-de/src/org/foo/shape/triangle/resource/Localize_de.properties
new file mode 100644
index 0000000..2259771
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle.resource-de/src/org/foo/shape/triangle/resource/Localize_de.properties
@@ -0,0 +1 @@
+TRIANGLE_NAME=Dreieck
diff --git a/chapter05/paint-example/org.foo.shape.triangle/build.properties b/chapter05/paint-example/org.foo.shape.triangle/build.properties
new file mode 100644
index 0000000..c5d398b
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Paint Example - Triangle Shape
+#-------------------------------------------------
+
+module=org.foo.shape.triangle
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}.*
+
+Import-Package: \
+ org.foo.shape;version="[5.0,6.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter05/paint-example/org.foo.shape.triangle/build.xml b/chapter05/paint-example/org.foo.shape.triangle/build.xml
new file mode 100644
index 0000000..7b3faf8
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/Activator.java b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/Activator.java
new file mode 100644
index 0000000..08d26dd
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/Activator.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.triangle;
+
+import java.util.Hashtable;
+import java.util.ResourceBundle;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * This class implements a simple bundle activator for the triangle
+ * SimpleShape service. This activator simply creates an instance of
+ * the triangle service object and registers it with the service registry along
+ * with the service properties indicating the service's name and icon.
+ **/
+public class Activator implements BundleActivator {
+ public static final String TRIANGLE_NAME = "TRIANGLE_NAME";
+
+ /**
+ * Implements the BundleActivator.start() method, which registers the
+ * triangle SimpleShape service.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void start(BundleContext context) {
+ ResourceBundle rb = ResourceBundle.getBundle(
+ "org.foo.shape.triangle.resource.Localize");
+ Hashtable dict = new Hashtable();
+ dict.put(SimpleShape.NAME_PROPERTY, rb.getString(TRIANGLE_NAME));
+ dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass().getResource("triangle.png")));
+ context.registerService(SimpleShape.class.getName(), new Triangle(), dict);
+ }
+
+ /**
+ * Implements the BundleActivator.start() method, which does nothing.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/Triangle.java b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/Triangle.java
new file mode 100644
index 0000000..081778f
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/Triangle.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.triangle;
+
+import java.awt.*;
+import java.awt.geom.GeneralPath;
+import org.foo.shape.SimpleShape;
+
+public class Triangle implements SimpleShape {
+
+ /**
+ * Implements the SimpleShape.draw() method for painting the shape.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ int x = p.x - 25;
+ int y = p.y - 25;
+ GradientPaint gradient = new GradientPaint(x, y, Color.GREEN, x + 50, y, Color.WHITE);
+ g2.setPaint(gradient);
+ int[] xcoords = { x + 25, x, x + 50 };
+ int[] ycoords = { y, y + 50, y + 50 };
+ GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, xcoords.length);
+ polygon.moveTo(x + 25, y);
+ for (int i = 0; i < xcoords.length; i++) {
+ polygon.lineTo(xcoords[i], ycoords[i]);
+ }
+ polygon.closePath();
+ g2.fill(polygon);
+ BasicStroke wideStroke = new BasicStroke(2.0f);
+ g2.setColor(Color.black);
+ g2.setStroke(wideStroke);
+ g2.draw(polygon);
+ }
+}
diff --git a/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/resource/Localize.properties b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/resource/Localize.properties
new file mode 100644
index 0000000..85c12de
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/resource/Localize.properties
@@ -0,0 +1 @@
+TRIANGLE_NAME=Triangle
diff --git a/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/triangle.png b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/triangle.png
new file mode 100644
index 0000000..46a5288
Binary files /dev/null and b/chapter05/paint-example/org.foo.shape.triangle/src/org/foo/shape/triangle/triangle.png differ
diff --git a/chapter05/paint-example/org.foo.shape/build.properties b/chapter05/paint-example/org.foo.shape/build.properties
new file mode 100644
index 0000000..0addc31
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape/build.properties
@@ -0,0 +1,13 @@
+#-------------------------------------------------
+title=Paint Example - Shape API
+#-------------------------------------------------
+
+module=org.foo.shape
+custom=true
+
+Export-Package: \
+ ${module};version="5.0"
+
+Import-Package: \
+ ${module};version="[5.0,6.0)"
+
diff --git a/chapter05/paint-example/org.foo.shape/build.xml b/chapter05/paint-example/org.foo.shape/build.xml
new file mode 100644
index 0000000..8d0de0a
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter05/paint-example/org.foo.shape/src/org/foo/shape/SimpleShape.java b/chapter05/paint-example/org.foo.shape/src/org/foo/shape/SimpleShape.java
new file mode 100644
index 0000000..fe2bdd1
--- /dev/null
+++ b/chapter05/paint-example/org.foo.shape/src/org/foo/shape/SimpleShape.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape;
+
+import java.awt.Graphics2D;
+import java.awt.Point;
+
+/**
+ * This interface defines the SimpleShape service. This service is used
+ * to draw shapes. It has two service properties:
+ *
+ * - simple.shape.name - A String name for the shape.
+ * - simple.shape.icon - An Icon for the shape.
+ *
+ **/
+public interface SimpleShape {
+
+ /**
+ * A service property for the name of the shape.
+ **/
+ public static final String NAME_PROPERTY = "simple.shape.name";
+
+ /**
+ * A service property for the icon of the shape.
+ **/
+ public static final String ICON_PROPERTY = "simple.shape.icon";
+
+ /**
+ * Draw this shape at the given position.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the shape.
+ **/
+ public void draw(Graphics2D g2, Point p);
+}
diff --git a/chapter06/BeanUtils-example/beanutils.bnd b/chapter06/BeanUtils-example/beanutils.bnd
new file mode 100644
index 0000000..5835f10
--- /dev/null
+++ b/chapter06/BeanUtils-example/beanutils.bnd
@@ -0,0 +1,7 @@
+Export-Package: org.apache.commons.beanutils.*
+Private-Package: !org.apache.commons.collections.*, *
+Import-Package: *
+
+# to avoid automatically generating imports for all exports use:
+#---------------------------------------------------------------
+# Export-Package: org.apache.commons.beanutils.*;-noimport:=true
diff --git a/chapter06/BeanUtils-example/commons-beanutils-1.8.0.jar b/chapter06/BeanUtils-example/commons-beanutils-1.8.0.jar
new file mode 100644
index 0000000..caf7ae3
Binary files /dev/null and b/chapter06/BeanUtils-example/commons-beanutils-1.8.0.jar differ
diff --git a/chapter06/HttpClient-example/build.xml b/chapter06/HttpClient-example/build.xml
new file mode 100644
index 0000000..8e482b7
--- /dev/null
+++ b/chapter06/HttpClient-example/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/build.properties b/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/build.properties
new file mode 100644
index 0000000..3ad9621
--- /dev/null
+++ b/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/build.properties
@@ -0,0 +1,25 @@
+#-------------------------------------------------
+title=HttpClient Example - wrapper bundle
+#-------------------------------------------------
+
+module=org.apache.commons.httpclient
+version=3.1
+custom=true
+
+Bundle-Activator: ${module}.internal.Activator
+
+Export-Package: \
+ ${module}.*;version="3.1"
+
+Private-Package: \
+ org.apache.commons.codec.*, \
+ ${module}.internal
+
+Import-Package: \
+ ${module}.*;version="[3.1,4.0)", \
+ org.osgi.framework;version="[1.3,2)", \
+ javax.net.*, javax.crypto.*
+
+DynamicImport-Package: \
+ org.apache.commons.logging
+
diff --git a/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/build.xml b/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/build.xml
new file mode 100644
index 0000000..362df05
--- /dev/null
+++ b/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/build.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/src/org/apache/commons/httpclient/internal/Activator.java b/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/src/org/apache/commons/httpclient/internal/Activator.java
new file mode 100644
index 0000000..e1cc0f1
--- /dev/null
+++ b/chapter06/HttpClient-example/commons-httpclient-3.1-osgi/src/org/apache/commons/httpclient/internal/Activator.java
@@ -0,0 +1,12 @@
+package org.apache.commons.httpclient.internal;
+
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.osgi.framework.*;
+
+public class Activator implements BundleActivator {
+ public void start(BundleContext ctx) {}
+
+ public void stop(BundleContext ctx) {
+ MultiThreadedHttpConnectionManager.shutdownAll();
+ }
+}
diff --git a/chapter06/HttpClient-example/org.foo.httpclient.test/build.properties b/chapter06/HttpClient-example/org.foo.httpclient.test/build.properties
new file mode 100644
index 0000000..097cf13
--- /dev/null
+++ b/chapter06/HttpClient-example/org.foo.httpclient.test/build.properties
@@ -0,0 +1,11 @@
+#-------------------------------------------------
+title=HttpClient Example - test bundle
+#-------------------------------------------------
+
+module=org.foo.httpclient.test
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package: ${module}
+
diff --git a/chapter06/HttpClient-example/org.foo.httpclient.test/build.xml b/chapter06/HttpClient-example/org.foo.httpclient.test/build.xml
new file mode 100644
index 0000000..ef505e6
--- /dev/null
+++ b/chapter06/HttpClient-example/org.foo.httpclient.test/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter06/HttpClient-example/org.foo.httpclient.test/src/org/foo/httpclient/test/Activator.java b/chapter06/HttpClient-example/org.foo.httpclient.test/src/org/foo/httpclient/test/Activator.java
new file mode 100644
index 0000000..909a4ba
--- /dev/null
+++ b/chapter06/HttpClient-example/org.foo.httpclient.test/src/org/foo/httpclient/test/Activator.java
@@ -0,0 +1,35 @@
+package org.foo.httpclient.test;
+
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.osgi.framework.*;
+
+public class Activator implements BundleActivator {
+
+ public void start(BundleContext ctx) {
+ new Thread(new Runnable() {
+ public void run() {
+ ping(); // query google.com when the bundle starts
+ }
+ }).start();
+ }
+
+ public void stop(BundleContext ctx) {}
+
+ void ping() {
+ HttpClient client = new HttpClient(new MultiThreadedHttpConnectionManager());
+ HttpMethod get = new GetMethod("http://www.google.com");
+ try {
+
+ System.out.println("GET " + get.getURI());
+ client.executeMethod(get);
+ byte[] buf = get.getResponseBody();
+ System.out.println("GOT " + buf.length + " bytes");
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ get.releaseConnection();
+ }
+ }
+}
diff --git a/chapter06/build.xml b/chapter06/build.xml
new file mode 100644
index 0000000..0d38cb6
--- /dev/null
+++ b/chapter06/build.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/chapter06/jEdit-example/build.xml b/chapter06/jEdit-example/build.xml
new file mode 100644
index 0000000..ad584ac
--- /dev/null
+++ b/chapter06/jEdit-example/build.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter06/jEdit-example/org.foo.jedit.extender/build.properties b/chapter06/jEdit-example/org.foo.jedit.extender/build.properties
new file mode 100644
index 0000000..b42a0e1
--- /dev/null
+++ b/chapter06/jEdit-example/org.foo.jedit.extender/build.properties
@@ -0,0 +1,16 @@
+#-------------------------------------------------
+title=jEdit Example - plugin extender bundle
+#-------------------------------------------------
+
+module=org.foo.jedit.extender
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.osgi.framework;version="[1.3,2)", \
+ org.gjt.sp.jedit;version="[4.2,5)"
+
diff --git a/chapter06/jEdit-example/org.foo.jedit.extender/build.xml b/chapter06/jEdit-example/org.foo.jedit.extender/build.xml
new file mode 100644
index 0000000..0a800c9
--- /dev/null
+++ b/chapter06/jEdit-example/org.foo.jedit.extender/build.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter06/jEdit-example/org.foo.jedit.extender/src/org/foo/jedit/extender/Activator.java b/chapter06/jEdit-example/org.foo.jedit.extender/src/org/foo/jedit/extender/Activator.java
new file mode 100644
index 0000000..dd8217d
--- /dev/null
+++ b/chapter06/jEdit-example/org.foo.jedit.extender/src/org/foo/jedit/extender/Activator.java
@@ -0,0 +1,61 @@
+package org.foo.jedit.extender;
+
+import java.io.File;
+import org.gjt.sp.jedit.*;
+import org.osgi.framework.*;
+
+public class Activator implements BundleActivator {
+
+ BundleTracker pluginTracker;
+
+ public void start(final BundleContext ctx) {
+ pluginTracker = new BundleTracker(ctx) {
+
+ public void addedBundle(Bundle bundle) {
+ String path = getBundlePath(bundle);
+ if (path != null && bundle.getResource("actions.xml") != null) {
+ jEdit.addPluginJAR(path);
+ }
+ }
+
+ public void removedBundle(Bundle bundle) {
+ String path = getBundlePath(bundle);
+ if (path != null) {
+ PluginJAR jar = jEdit.getPluginJAR(path);
+ if (jar != null) {
+ jEdit.removePluginJAR(jar, false);
+ }
+ }
+ }
+ };
+
+ EditBus.addToBus(new EBComponent() {
+ public void handleMessage(EBMessage message) {
+ EditBus.removeFromBus(this);
+ pluginTracker.open();
+ }
+ });
+ }
+
+ public void stop(BundleContext ctx) {
+ pluginTracker.close();
+ pluginTracker = null;
+ }
+
+ static String getBundlePath(Bundle bundle) {
+ String location = bundle.getLocation().trim();
+
+ File jar;
+ if (location.startsWith("file:")) {
+ jar = new File(location.substring(5));
+ } else {
+ jar = new File(location);
+ }
+
+ if (jar.isFile()) {
+ return jar.getAbsolutePath();
+ }
+
+ return null;
+ }
+}
diff --git a/chapter06/jEdit-example/org.foo.jedit.extender/src/org/foo/jedit/extender/BundleTracker.java b/chapter06/jEdit-example/org.foo.jedit.extender/src/org/foo/jedit/extender/BundleTracker.java
new file mode 100644
index 0000000..e030149
--- /dev/null
+++ b/chapter06/jEdit-example/org.foo.jedit.extender/src/org/foo/jedit/extender/BundleTracker.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.jedit.extender;
+
+import java.util.*;
+import org.osgi.framework.*;
+
+/**
+ * This is a very simple bundle tracker utility class that tracks active
+ * bundles. The tracker must be given a bundle context upon creation, which it
+ * uses to listen for bundle events. The bundle tracker must be opened to track
+ * objects and closed when it is no longer needed. This class is abstract, which
+ * means in order to use it you must create a subclass of it. Subclasses must
+ * implement the addedBundle() and removedBundle() methods,
+ * which can be used to perform some custom action upon the activation or
+ * deactivation of bundles. Since this tracker is quite simple, its concurrency
+ * control approach is also simplistic. This means that subclasses should take
+ * great care to ensure that their addedBundle() and
+ * removedBundle() methods are very simple and do not do anything to
+ * change the state of any bundles.
+ **/
+public abstract class BundleTracker {
+ final Set m_bundleSet = new HashSet();
+ final BundleContext m_context;
+ final SynchronousBundleListener m_listener;
+ boolean m_open;
+
+ /**
+ * Constructs a bundle tracker object that will use the specified bundle
+ * context.
+ *
+ * @param context The bundle context to use to track bundles.
+ **/
+ public BundleTracker(BundleContext context) {
+ m_context = context;
+ m_listener = new SynchronousBundleListener() {
+ public void bundleChanged(BundleEvent evt) {
+ synchronized (BundleTracker.this) {
+ if (!m_open) {
+ return;
+ }
+
+ if (evt.getType() == BundleEvent.STARTED) {
+ if (!m_bundleSet.contains(evt.getBundle())) {
+ m_bundleSet.add(evt.getBundle());
+ addedBundle(evt.getBundle());
+ }
+ } else if (evt.getType() == BundleEvent.STOPPED) {
+ if (m_bundleSet.contains(evt.getBundle())) {
+ m_bundleSet.remove(evt.getBundle());
+ removedBundle(evt.getBundle());
+ }
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns the current set of active bundles.
+ *
+ * @return The current set of active bundles.
+ **/
+ public synchronized Bundle[] getBundles() {
+ return (Bundle[]) m_bundleSet.toArray(new Bundle[m_bundleSet.size()]);
+ }
+
+ /**
+ * Call this method to start the tracking of active bundles.
+ **/
+ public synchronized void open() {
+ if (!m_open) {
+ m_open = true;
+
+ m_context.addBundleListener(m_listener);
+
+ Bundle[] bundles = m_context.getBundles();
+ for (int i = 0; i < bundles.length; i++) {
+ if (bundles[i].getState() == Bundle.ACTIVE) {
+ m_bundleSet.add(bundles[i]);
+ addedBundle(bundles[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Call this method to stop the tracking of active bundles.
+ **/
+ public synchronized void close() {
+ if (m_open) {
+ m_open = false;
+
+ m_context.removeBundleListener(m_listener);
+
+ Bundle[] bundles = (Bundle[]) m_bundleSet.toArray(new Bundle[m_bundleSet.size()]);
+ for (int i = 0; i < bundles.length; i++) {
+ if (m_bundleSet.remove(bundles[i])) {
+ removedBundle(bundles[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Subclasses must implement this method; it can be used to perform actions
+ * upon the activation of a bundle. Subclasses should keep this method
+ * implementation as simple as possible and should not cause the change in any
+ * bundle state to avoid concurrency issues.
+ *
+ * @param bundle The bundle being added to the active set.
+ **/
+ protected abstract void addedBundle(Bundle bundle);
+
+ /**
+ * Subclasses must implement this method; it can be used to perform actions
+ * upon the deactivation of a bundle. Subclasses should keep this method
+ * implementation as simple as possible and should not cause the change in any
+ * bundle state to avoid concurrency issues.
+ *
+ * @param bundle The bundle being removed from the active set.
+ **/
+ protected abstract void removedBundle(Bundle bundle);
+}
diff --git a/chapter06/jEdit-example/patch/jEdit/build.xml b/chapter06/jEdit-example/patch/jEdit/build.xml
new file mode 100644
index 0000000..d81ded3
--- /dev/null
+++ b/chapter06/jEdit-example/patch/jEdit/build.xml
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter06/jEdit-example/patch/jEdit/jedit-engine.bnd b/chapter06/jEdit-example/patch/jEdit/jedit-engine.bnd
new file mode 100644
index 0000000..34ef1b0
--- /dev/null
+++ b/chapter06/jEdit-example/patch/jEdit/jedit-engine.bnd
@@ -0,0 +1,14 @@
+
+-output: jedit-engine.jar
+
+Bundle-Name: jEdit Engine
+Bundle-SymbolicName: org.gjt.sp.jedit.engine
+Bundle-Version: 4.2
+
+Export-Package:\
+ !org.gjt.sp.jedit,\
+ !org.gjt.sp.jedit.proto.*,\
+ org.gjt.sp.*;version="4.2"
+
+-versionpolicy: [${version;==;${@}},${version;+;${@}})
+
diff --git a/chapter06/jEdit-example/patch/jEdit/jedit-main.bnd b/chapter06/jEdit-example/patch/jEdit/jedit-main.bnd
new file mode 100644
index 0000000..439345d
--- /dev/null
+++ b/chapter06/jEdit-example/patch/jEdit/jedit-main.bnd
@@ -0,0 +1,23 @@
+
+-output: jedit.jar
+
+-include: org/gjt/sp/jedit/jedit.manifest
+Class-Path: jedit-thirdparty.jar jedit-engine.jar
+
+Bundle-Name: jEdit
+Bundle-SymbolicName: org.gjt.sp.jedit
+Bundle-Version: 4.2
+
+Export-Package:\
+ org.gjt.sp.jedit;version="4.2"
+
+Private-Package:\
+ org.gjt.sp.jedit.proto.*
+
+# ...see section 6.2.4...
+# DynamicImport-Package: *
+
+-versionpolicy: [${version;==;${@}},${version;+;${@}})
+
+Bundle-Activator: org.gjt.sp.jedit.Activator
+
diff --git a/chapter06/jEdit-example/patch/jEdit/jedit-mega.bnd b/chapter06/jEdit-example/patch/jEdit/jedit-mega.bnd
new file mode 100644
index 0000000..45b898b
--- /dev/null
+++ b/chapter06/jEdit-example/patch/jEdit/jedit-mega.bnd
@@ -0,0 +1,13 @@
+
+-output: jedit.jar
+
+-include: org/gjt/sp/jedit/jedit.manifest
+
+Bundle-Name: jEdit
+Bundle-SymbolicName: org.gjt.sp.jedit
+Bundle-Version: 4.2
+
+Private-Package: *
+
+Bundle-Activator: org.gjt.sp.jedit.Activator
+
diff --git a/chapter06/jEdit-example/patch/jEdit/jedit-thirdparty.bnd b/chapter06/jEdit-example/patch/jEdit/jedit-thirdparty.bnd
new file mode 100644
index 0000000..0e75643
--- /dev/null
+++ b/chapter06/jEdit-example/patch/jEdit/jedit-thirdparty.bnd
@@ -0,0 +1,12 @@
+
+-output: jedit-thirdparty.jar
+
+Bundle-Name: jEdit Third-party Libraries
+Bundle-SymbolicName: org.gjt.sp.jedit.libs
+Bundle-Version: 4.2
+
+Export-Package: bsh, com.microstar.xml, gnu.regexp
+Private-Package: !org.gjt.sp.*, !installer.*, *
+
+-versionpolicy: [${version;==;${@}},${version;+;${@}})
+
diff --git a/chapter06/jEdit-example/patch/jEdit/org/gjt/sp/jedit/Activator.java b/chapter06/jEdit-example/patch/jEdit/org/gjt/sp/jedit/Activator.java
new file mode 100644
index 0000000..1c06182
--- /dev/null
+++ b/chapter06/jEdit-example/patch/jEdit/org/gjt/sp/jedit/Activator.java
@@ -0,0 +1,35 @@
+package org.gjt.sp.jedit;
+
+import java.io.IOException;
+import java.net.*;
+import java.util.Properties;
+
+import org.osgi.framework.*;
+import org.osgi.service.url.*;
+
+import org.gjt.sp.jedit.proto.jeditresource.Handler;
+
+public class Activator implements BundleActivator {
+ private static class JEditResourceHandlerService
+ extends AbstractURLStreamHandlerService {
+ private Handler jEditResourceHandler = new Handler();
+
+ public URLConnection openConnection(URL url)
+ throws IOException {
+ return jEditResourceHandler.openConnection(url);
+ }
+ }
+
+ public void start(BundleContext context) {
+ Properties properties = new Properties();
+ properties.setProperty(URLConstants.URL_HANDLER_PROTOCOL,
+ "jeditresource");
+
+ context.registerService(
+ URLStreamHandlerService.class.getName(),
+ new JEditResourceHandlerService(),
+ properties);
+ }
+
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter06/jEdit-example/patch/jedit42source.tar.gz b/chapter06/jEdit-example/patch/jedit42source.tar.gz
new file mode 100644
index 0000000..fa582f8
Binary files /dev/null and b/chapter06/jEdit-example/patch/jedit42source.tar.gz differ
diff --git a/chapter06/jEdit-example/test/Calculator.jar b/chapter06/jEdit-example/test/Calculator.jar
new file mode 100644
index 0000000..684855d
Binary files /dev/null and b/chapter06/jEdit-example/test/Calculator.jar differ
diff --git a/chapter07/build.xml b/chapter07/build.xml
new file mode 100644
index 0000000..0f735be
--- /dev/null
+++ b/chapter07/build.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/chapter07/migration-example/build.xml b/chapter07/migration-example/build.xml
new file mode 100644
index 0000000..794d6fc
--- /dev/null
+++ b/chapter07/migration-example/build.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/migration-example/cobertura.osgi/build.xml b/chapter07/migration-example/cobertura.osgi/build.xml
new file mode 100644
index 0000000..4b088cd
--- /dev/null
+++ b/chapter07/migration-example/cobertura.osgi/build.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/migration-example/cobertura.osgi/cobertura-1.9.3-bin.zip b/chapter07/migration-example/cobertura.osgi/cobertura-1.9.3-bin.zip
new file mode 100644
index 0000000..5ecacc5
Binary files /dev/null and b/chapter07/migration-example/cobertura.osgi/cobertura-1.9.3-bin.zip differ
diff --git a/chapter07/migration-example/commons-pool-1.5.3-src.zip b/chapter07/migration-example/commons-pool-1.5.3-src.zip
new file mode 100644
index 0000000..9e04507
Binary files /dev/null and b/chapter07/migration-example/commons-pool-1.5.3-src.zip differ
diff --git a/chapter07/migration-example/junit.osgi/build.xml b/chapter07/migration-example/junit.osgi/build.xml
new file mode 100644
index 0000000..c1ac2f9
--- /dev/null
+++ b/chapter07/migration-example/junit.osgi/build.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/migration-example/main/build.properties b/chapter07/migration-example/main/build.properties
new file mode 100644
index 0000000..cf2c4b7
--- /dev/null
+++ b/chapter07/migration-example/main/build.properties
@@ -0,0 +1,13 @@
+#---------------------------------------
+title=Apache Commons - Pool - Main
+#---------------------------------------
+
+module=org.apache.commons.pool
+custom=true
+
+Export-Package:\
+ ${module}
+
+Private-Package:\
+ ${module}.impl
+
diff --git a/chapter07/migration-example/main/build.xml b/chapter07/migration-example/main/build.xml
new file mode 100644
index 0000000..66b5877
--- /dev/null
+++ b/chapter07/migration-example/main/build.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/migration-example/test/build.properties b/chapter07/migration-example/test/build.properties
new file mode 100644
index 0000000..1818874
--- /dev/null
+++ b/chapter07/migration-example/test/build.properties
@@ -0,0 +1,17 @@
+#---------------------------------------
+title=Apache Commons - Pool - Test
+#---------------------------------------
+
+module=org.apache.commons.pool
+custom=true
+
+Bundle-SymbolicName: ${module}-test
+
+Export-Package:\
+ ${module}
+
+Private-Package:\
+ ${module}.impl
+
+# Fragment-Host: ${module}
+
diff --git a/chapter07/migration-example/test/build.xml b/chapter07/migration-example/test/build.xml
new file mode 100644
index 0000000..db52f37
--- /dev/null
+++ b/chapter07/migration-example/test/build.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/mocking-example/build.xml b/chapter07/mocking-example/build.xml
new file mode 100644
index 0000000..db4ef51
--- /dev/null
+++ b/chapter07/mocking-example/build.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/mocking-example/log_client/.classpath b/chapter07/mocking-example/log_client/.classpath
new file mode 100644
index 0000000..6047d2e
--- /dev/null
+++ b/chapter07/mocking-example/log_client/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter07/mocking-example/log_client/.project b/chapter07/mocking-example/log_client/.project
new file mode 100644
index 0000000..da721be
--- /dev/null
+++ b/chapter07/mocking-example/log_client/.project
@@ -0,0 +1,28 @@
+
+
+ chapter07.org.foo.log-1.0
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.pde.PluginNature
+
+
diff --git a/chapter07/mocking-example/log_client/META-INF/MANIFEST.MF b/chapter07/mocking-example/log_client/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..48ac037
--- /dev/null
+++ b/chapter07/mocking-example/log_client/META-INF/MANIFEST.MF
@@ -0,0 +1,15 @@
+Manifest-Version: 1.0
+Tool: Bnd-0.0.372
+Bundle-Name: log_client
+Created-By: 1.6.0_13 (Sun Microsystems Inc.)
+Bundle-RequiredExecutionEnvironment: J2SE-1.5,JavaSE-1.6
+Bundle-Version: 1.0
+Bnd-LastModified: 1257084267049
+Bundle-ManifestVersion: 2
+Bundle-Activator: org.foo.log.Activator
+Bundle-License: http://www.apache.org/licenses/LICENSE-2.0
+Import-Package: org.osgi.framework;version="[1.3,2.0)",org.osgi.servic
+ e.log;version="[1.3,2.0)"
+Bundle-SymbolicName: org.foo.log
+Bundle-DocURL: http://code.google.com/p/osgi-in-action/
+
diff --git a/chapter07/mocking-example/log_client/build.properties b/chapter07/mocking-example/log_client/build.properties
new file mode 100644
index 0000000..1f7a371
--- /dev/null
+++ b/chapter07/mocking-example/log_client/build.properties
@@ -0,0 +1,16 @@
+#---------------------------------------
+title=Broken Log Client Example
+#---------------------------------------
+
+module=org.foo.log
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package: \
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.service.log;version="[1.3,2.0)"
+
diff --git a/chapter07/mocking-example/log_client/build.xml b/chapter07/mocking-example/log_client/build.xml
new file mode 100644
index 0000000..f030258
--- /dev/null
+++ b/chapter07/mocking-example/log_client/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter07/mocking-example/log_client/src/org/foo/log/Activator.java b/chapter07/mocking-example/log_client/src/org/foo/log/Activator.java
new file mode 100644
index 0000000..5fac395
--- /dev/null
+++ b/chapter07/mocking-example/log_client/src/org/foo/log/Activator.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.log;
+
+import org.osgi.framework.*;
+import org.osgi.service.log.LogService;
+
+/**
+ * Broken code example showing lookup of a service just before it will be used.
+ * The client can tell when the service is removed, and automatically switches
+ * to the current top-ranked LogService implementation, but it doesn't account
+ * for certain scenarios which could cause spurious exceptions.
+ **/
+public class Activator implements BundleActivator {
+
+ BundleContext m_context;
+
+ /**
+ * START LOG TEST
+ **/
+ public void start(BundleContext context) {
+
+ // this time we just store the current bundle context in the shared field
+ m_context = context;
+
+ // start new thread to test LogService - remember to keep bundle activator methods short!
+ startTestThread();
+ }
+
+ /**
+ * STOP LOG TEST
+ **/
+ public void stop(BundleContext context) {
+
+ stopTestThread();
+ }
+
+ // Test LogService by periodically sending a message
+ class LogServiceTest implements Runnable {
+ public void run() {
+
+ while (Thread.currentThread() == m_logTestThread) {
+
+ // lookup the current "best" LogService each time, just before we need to use it
+ ServiceReference logServiceRef = m_context.getServiceReference(LogService.class.getName());
+
+ // if the service reference is null then we know there's no log service available
+ if (logServiceRef != null) {
+ // because we have a valid reference we know the service is there... or do we?
+ ((LogService) m_context.getService(logServiceRef)).log(LogService.LOG_INFO, "ping");
+ } else {
+ alternativeLog("LogService has gone");
+ }
+
+ pauseTestThread();
+ }
+ }
+ }
+
+ //------------------------------------------------------------------------------------------
+ // The rest of this is just support code, not meant to show any particular best practices
+ //------------------------------------------------------------------------------------------
+
+ volatile Thread m_logTestThread;
+
+ void startTestThread() {
+ // start separate worker thread to run the actual tests, managed by the bundle lifecycle
+ m_logTestThread = new Thread(new LogServiceTest(), "LogService Tester");
+ m_logTestThread.setDaemon(true);
+ m_logTestThread.start();
+ }
+
+ void stopTestThread() {
+ // thread should cooperatively shutdown on the next iteration, because field is now null
+ Thread testThread = m_logTestThread;
+ m_logTestThread = null;
+ if (testThread != null) {
+ testThread.interrupt();
+ try {testThread.join();} catch (InterruptedException e) {}
+ }
+ }
+
+ protected void pauseTestThread() {
+ try {
+ // sleep for a bit
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {}
+ }
+
+ void alternativeLog(String message) {
+ // this provides similar style debug logging output for when the LogService disappears
+ String tid = "thread=\"" + Thread.currentThread().getName() + "\"";
+ String bid = "bundle=" + m_context.getBundle().getBundleId();
+ System.out.println("<--> " + tid + ", " + bid + " : " + message);
+ }
+}
diff --git a/chapter07/mocking-example/mock_test/.classpath b/chapter07/mocking-example/mock_test/.classpath
new file mode 100644
index 0000000..4034cea
--- /dev/null
+++ b/chapter07/mocking-example/mock_test/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/mocking-example/mock_test/.project b/chapter07/mocking-example/mock_test/.project
new file mode 100644
index 0000000..5d9f4d2
--- /dev/null
+++ b/chapter07/mocking-example/mock_test/.project
@@ -0,0 +1,17 @@
+
+
+ chapter07.org.foo.mock-1.0
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/chapter07/mocking-example/mock_test/build.properties b/chapter07/mocking-example/mock_test/build.properties
new file mode 100644
index 0000000..d5a5830
--- /dev/null
+++ b/chapter07/mocking-example/mock_test/build.properties
@@ -0,0 +1,10 @@
+#---------------------------------------
+title=Simple OSGi Mockout Example
+#---------------------------------------
+
+module=org.foo.mock
+custom=true
+
+Private-Package:\
+ ${module}
+
diff --git a/chapter07/mocking-example/mock_test/build.xml b/chapter07/mocking-example/mock_test/build.xml
new file mode 100644
index 0000000..14d1760
--- /dev/null
+++ b/chapter07/mocking-example/mock_test/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter07/mocking-example/mock_test/lib/easymock-2.5.2.jar b/chapter07/mocking-example/mock_test/lib/easymock-2.5.2.jar
new file mode 100644
index 0000000..97c8bf0
Binary files /dev/null and b/chapter07/mocking-example/mock_test/lib/easymock-2.5.2.jar differ
diff --git a/chapter07/mocking-example/mock_test/src/org/foo/mock/LogClientTests.java b/chapter07/mocking-example/mock_test/src/org/foo/mock/LogClientTests.java
new file mode 100644
index 0000000..7645840
--- /dev/null
+++ b/chapter07/mocking-example/mock_test/src/org/foo/mock/LogClientTests.java
@@ -0,0 +1,33 @@
+package org.foo.mock;
+
+import static org.easymock.EasyMock.*;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.LockSupport;
+import junit.framework.TestCase;
+import org.foo.log.Activator;
+import org.osgi.framework.*;
+import org.osgi.service.log.LogService;
+
+public class LogClientTests
+ extends TestCase {
+
+ public void testLogClientBehaviour()
+ throws Exception {
+
+ // MOCK - create prototype mock objects
+ // ====================================
+
+ // EXPECT - script the expected behavior
+ // =====================================
+
+ // REPLAY - prepare the mock objects
+ // =================================
+
+ // TEST - run code using the mock objects
+ // ======================================
+
+ // VERIFY - check the behavior matches
+ // ===================================
+
+ }
+}
diff --git a/chapter07/mocking-example/solution/LogClientTests.java b/chapter07/mocking-example/solution/LogClientTests.java
new file mode 100644
index 0000000..ee24f0b
--- /dev/null
+++ b/chapter07/mocking-example/solution/LogClientTests.java
@@ -0,0 +1,93 @@
+package org.foo.mock;
+
+import static org.easymock.EasyMock.*;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.LockSupport;
+import junit.framework.TestCase;
+import org.foo.log.Activator;
+import org.osgi.framework.*;
+import org.osgi.service.log.LogService;
+
+public class LogClientTests
+ extends TestCase {
+
+ public void testLogClientBehaviour()
+ throws Exception {
+
+ // MOCK - create prototype mock objects
+ // ====================================
+
+ // we want a strict mock context so we can test the ordering
+ BundleContext context = createStrictMock(BundleContext.class);
+ ServiceReference serviceRef = createMock(ServiceReference.class);
+ LogService logService = createMock(LogService.class);
+
+ // nice mocks return reasonable defaults
+ Bundle bundle = createNiceMock(Bundle.class);
+
+ // EXPECT - script the expected behavior
+ // =====================================
+
+ // expected behaviour when log service is available
+ expect(context.getServiceReference(LogService.class.getName()))
+ .andReturn(serviceRef);
+ expect(context.getService(serviceRef))
+ .andReturn(logService);
+ logService.log(
+ and(geq(LogService.LOG_ERROR), leq(LogService.LOG_DEBUG)),
+ isA(String.class));
+
+ // expected behaviour when log service is not available
+ expect(context.getServiceReference(LogService.class.getName()))
+ .andReturn(null);
+ expect(context.getBundle())
+ .andReturn(bundle).anyTimes();
+
+ // race condition: log service is available but immediately goes away
+ expect(context.getServiceReference(LogService.class.getName()))
+ .andReturn(serviceRef);
+ expect(context.getService(serviceRef))
+ .andReturn(null);
+ expect(context.getBundle())
+ .andReturn(bundle).anyTimes();
+
+ // REPLAY - prepare the mock objects
+ // =================================
+
+ replay(context, serviceRef, logService, bundle);
+
+ // TEST - run code using the mock objects
+ // ======================================
+
+ // this latch limits the calls to the log service
+ final CountDownLatch latch = new CountDownLatch(3);
+
+ // override pause method to allow test synchronization
+ BundleActivator logClientActivator = new Activator() {
+ @Override protected void pauseTestThread() {
+
+ // report log call
+ latch.countDown();
+
+ // nothing else left to do?
+ if (latch.getCount() == 0) {
+ LockSupport.park();
+ }
+ }
+ };
+
+ logClientActivator.start(context);
+
+ // timeout in case test deadlocks
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("Still expecting" + latch.getCount() + " calls");
+ }
+
+ logClientActivator.stop(context);
+
+ // VERIFY - check the behavior matches
+ // ===================================
+
+ verify(context, serviceRef, logService);
+ }
+}
diff --git a/chapter07/testing-example/build.xml b/chapter07/testing-example/build.xml
new file mode 100644
index 0000000..171fb68
--- /dev/null
+++ b/chapter07/testing-example/build.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/testing-example/fw/build.xml b/chapter07/testing-example/fw/build.xml
new file mode 100644
index 0000000..17f6455
--- /dev/null
+++ b/chapter07/testing-example/fw/build.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/chapter07/testing-example/fw/container/build.properties b/chapter07/testing-example/fw/container/build.properties
new file mode 100644
index 0000000..cf4c882
--- /dev/null
+++ b/chapter07/testing-example/fw/container/build.properties
@@ -0,0 +1,10 @@
+#-------------------------------------------------
+title=Testing Example - Container Tests
+#-------------------------------------------------
+
+module=org.foo.test
+custom=true
+
+Private-Package:\
+ ${module}
+
diff --git a/chapter07/testing-example/fw/container/build.xml b/chapter07/testing-example/fw/container/build.xml
new file mode 100644
index 0000000..f4543ed
--- /dev/null
+++ b/chapter07/testing-example/fw/container/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter07/testing-example/fw/container/src/org/foo/test/ContainerTest.java b/chapter07/testing-example/fw/container/src/org/foo/test/ContainerTest.java
new file mode 100644
index 0000000..c5ff644
--- /dev/null
+++ b/chapter07/testing-example/fw/container/src/org/foo/test/ContainerTest.java
@@ -0,0 +1,43 @@
+package org.foo.test;
+
+import static org.ops4j.pax.exam.CoreOptions.*;
+import static org.osgi.framework.Constants.*;
+
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+import org.ops4j.pax.exam.junit.*;
+import org.ops4j.pax.exam.Option;
+import org.osgi.framework.*;
+
+@RunWith(JUnit4TestRunner.class)
+public class ContainerTest {
+
+ @Configuration
+ public static Option[] configure() {
+ return options(
+ mavenBundle("org.osgi", "org.osgi.compendium", "4.2.0")
+ );
+ }
+
+ @Test
+ public void testContainer(BundleContext ctx) {
+ System.out.println(
+ format(ctx, FRAMEWORK_VENDOR) +
+ format(ctx, FRAMEWORK_VERSION) +
+ format(ctx, FRAMEWORK_LANGUAGE) +
+ format(ctx, FRAMEWORK_OS_NAME) +
+ format(ctx, FRAMEWORK_OS_VERSION) +
+ format(ctx, FRAMEWORK_PROCESSOR) +
+ "\nTest Bundle is " +
+ ctx.getBundle().getSymbolicName());
+ }
+
+ private static String format(
+ BundleContext ctx, String key) {
+
+ return String.format("%-32s = %s\n",
+ key, ctx.getProperty(key));
+ }
+}
+
diff --git a/chapter07/testing-example/it/build.xml b/chapter07/testing-example/it/build.xml
new file mode 100644
index 0000000..cff4bbd
--- /dev/null
+++ b/chapter07/testing-example/it/build.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/chapter07/testing-example/it/configadmin/build.properties b/chapter07/testing-example/it/configadmin/build.properties
new file mode 100644
index 0000000..f76fa0a
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/build.properties
@@ -0,0 +1,13 @@
+#-------------------------------------------------
+title=Testing Example - Integration Tests
+#-------------------------------------------------
+
+module=org.apache.felix.cm.integration
+custom=true
+
+Export-Package:\
+ ${module}.*
+
+Import-Package:\
+ *;resolution:=optional
+
diff --git a/chapter07/testing-example/it/configadmin/build.xml b/chapter07/testing-example/it/configadmin/build.xml
new file mode 100644
index 0000000..5550dcb
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/build.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigUpdateStressTest.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigUpdateStressTest.java
new file mode 100644
index 0000000..298932f
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigUpdateStressTest.java
@@ -0,0 +1,227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ConfigureThread;
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryThread;
+import org.apache.felix.cm.integration.helper.ManagedServiceThread;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+
+
+/**
+ * The ConfigUpdateStressTest
class tests the issues related to
+ * concurrency between configuration update (Configuration.update(Dictionary))
+ * and ManagedService[Factory] registration.
+ *
+ * @see FELIX-1545
+ */
+@RunWith(JUnit4TestRunner.class)
+public class ConfigUpdateStressTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_ManagedService_race_condition_test()
+ {
+ int counterMax = 30;
+ int failures = 0;
+
+ for ( int counter = 0; counter < counterMax; counter++ )
+ {
+ try
+ {
+ single_test_ManagedService_race_condition_test( counter );
+ }
+ catch ( Throwable ae )
+ {
+ System.out.println( "single_test_ManagedService_race_condition_test#" + counter + " failed: " + ae );
+ ae.printStackTrace( System.out );
+ failures++;
+ }
+ }
+
+ // fail the test if there is at least one failure
+ if ( failures != 0 )
+ {
+ TestCase.fail( failures + "/" + counterMax + " iterations failed" );
+ }
+ }
+
+
+ @Test
+ public void test_ManagedServiceFactory_race_condition_test()
+ {
+ int counterMax = 30;
+ int failures = 0;
+
+ for ( int counter = 0; counter < counterMax; counter++ )
+ {
+ try
+ {
+ single_test_ManagedServiceFactory_race_condition_test( counter );
+ }
+ catch ( Throwable ae )
+ {
+ System.out.println( "single_test_ManagedServiceFactory_race_condition_test#" + counter + " failed: "
+ + ae );
+ ae.printStackTrace( System.out );
+ failures++;
+ }
+ }
+
+ // fail the test if there is at least one failure
+ if ( failures != 0 )
+ {
+ TestCase.fail( failures + "/" + counterMax + " iterations failed" );
+ }
+ }
+
+
+ // runs a single test to encounter the race condition between ManagedService
+ // registration and Configuration.update(Dictionary)
+ // This test creates/updates configuration and registers a ManagedService
+ // almost at the same time. The ManagedService must receive the
+ // configuration
+ // properties exactly once.
+ private void single_test_ManagedService_race_condition_test( final int counter ) throws IOException,
+ InterruptedException
+ {
+
+ final String pid = "single_test_ManagedService_race_condition_test." + counter;
+
+ final ConfigureThread ct = new ConfigureThread( getConfigurationAdmin(), pid, false );
+ final ManagedServiceThread mt = new ManagedServiceThread( bundleContext, pid );
+
+ try
+ {
+ // start threads -- both are waiting to be triggered
+ ct.start();
+ mt.start();
+
+ // trigger for action
+ ct.trigger();
+ mt.trigger();
+
+ // wait for threads to terminate
+ ct.join();
+ mt.join();
+
+ // wait for all tasks to terminate
+ delay();
+
+ final ArrayList configs = mt.getConfigs();
+
+ // terminate mt to ensure no further config updates
+ mt.cleanup();
+
+ if ( configs.size() == 0 )
+ {
+ TestCase.fail( "No configuration provided to ManagedService at all" );
+ }
+ else if ( configs.size() == 2 )
+ {
+ final Dictionary props0 = configs.get( 0 );
+ final Dictionary props1 = configs.get( 1 );
+
+ TestCase.assertNull( "Expected first (of two) updates without configuration", props0 );
+ TestCase.assertNotNull( "Expected second (of two) updates with configuration", props1 );
+ }
+ else if ( configs.size() == 1 )
+ {
+ final Dictionary props = configs.get( 0 );
+ TestCase.assertNotNull( "Expected non-null configuration: " + props, props );
+ }
+ else
+ {
+ TestCase.fail( "Unexpectedly got " + configs.size() + " updated" );
+ }
+ }
+ finally
+ {
+ mt.cleanup();
+ ct.cleanup();
+ }
+ }
+
+
+ // runs a single test to encounter the race condition between
+ // ManagedServiceFactory registration and Configuration.update(Dictionary)
+ // This test creates/updates configuration and registers a
+ // ManagedServiceFactory almost at the same time. The ManagedServiceFactory
+ // must receive the configuration properties exactly once.
+ private void single_test_ManagedServiceFactory_race_condition_test( final int counter ) throws IOException,
+ InterruptedException
+ {
+
+ final String factoryPid = "single_test_ManagedServiceFactory_race_condition_test." + counter;
+
+ final ConfigureThread ct = new ConfigureThread( getConfigurationAdmin(), factoryPid, true );
+ final ManagedServiceFactoryThread mt = new ManagedServiceFactoryThread( bundleContext, factoryPid );
+
+ try
+ {
+ // start threads -- both are waiting to be triggered
+ ct.start();
+ mt.start();
+
+ // trigger for action
+ ct.trigger();
+ mt.trigger();
+
+ // wait for threads to terminate
+ ct.join();
+ mt.join();
+
+ // wait for all tasks to terminate
+ delay();
+
+ final ArrayList configs = mt.getConfigs();
+
+ // terminate mt to ensure no further config updates
+ mt.cleanup();
+
+ if ( configs.size() == 0 )
+ {
+ TestCase.fail( "No configuration provided to ManagedServiceFactory at all" );
+ }
+ else if ( configs.size() == 1 )
+ {
+ final Dictionary props = configs.get( 0 );
+ TestCase.assertNotNull( "Expected non-null configuration: " + props, props );
+ }
+ else
+ {
+ TestCase.fail( "Unexpectedly got " + configs.size() + " updated" );
+ }
+ }
+ finally
+ {
+ mt.cleanup();
+ ct.cleanup();
+ }
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationBaseTest.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationBaseTest.java
new file mode 100644
index 0000000..6e4abb2
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationBaseTest.java
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import java.io.IOException;
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator;
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+
+@RunWith(JUnit4TestRunner.class)
+public class ConfigurationBaseTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_basic_configuration_configure_then_start() throws BundleException, IOException
+ {
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final String pid = "test_basic_configuration_configure_then_start";
+ final Configuration config = configure( pid, null, true );
+
+ // 3. register ManagedService ms1 with pid from said locationA
+ bundle = installBundle( pid, ManagedServiceTestActivator.class );
+ bundle.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( tester.props );
+ TestCase.assertEquals( pid, tester.props.get( Constants.SERVICE_PID ) );
+ TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) );
+ TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) );
+ TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) );
+ TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls );
+
+ // delete
+ config.delete();
+ delay();
+
+ // ==> update with null
+ TestCase.assertNull( tester.props );
+ TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_basic_configuration_start_then_configure() throws BundleException, IOException
+ {
+ final String pid = "test_basic_configuration_start_then_configure";
+
+ // 1. register ManagedService ms1 with pid from said locationA
+ bundle = installBundle( pid, ManagedServiceTestActivator.class );
+ bundle.start();
+ delay();
+
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final Configuration config = configure( pid, null, true );
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( tester.props );
+ TestCase.assertEquals( pid, tester.props.get( Constants.SERVICE_PID ) );
+ TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) );
+ TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) );
+ TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) );
+ TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls );
+
+ // delete
+ config.delete();
+ delay();
+
+ // ==> update with null
+ TestCase.assertNull( tester.props );
+ TestCase.assertEquals( 3, tester.numManagedServiceUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_basic_configuration_factory_configure_then_start() throws BundleException, IOException
+ {
+ final String factoryPid = "test_basic_configuration_factory_configure_then_start";
+ bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class );
+ bundle.start();
+ delay();
+
+ final Configuration config = createFactoryConfiguration( factoryPid, null, true );
+ final String pid = config.getPid();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE;
+ Dictionary, ?> props = tester.configs.get( pid );
+ TestCase.assertNotNull( props );
+ TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) );
+ TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) );
+ TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) );
+ TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) );
+ TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls );
+ TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls );
+
+ // delete
+ config.delete();
+ delay();
+
+ // ==> update with null
+ TestCase.assertNull( tester.configs.get( pid ) );
+ TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls );
+ TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls );
+ }
+
+
+ @Test
+ public void test_basic_configuration_factory_start_then_configure() throws BundleException, IOException
+ {
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final String factoryPid = "test_basic_configuration_factory_start_then_configure";
+ final Configuration config = createFactoryConfiguration( factoryPid, null, true );
+ final String pid = config.getPid();
+
+ // 3. register ManagedService ms1 with pid from said locationA
+ bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class );
+ bundle.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE;
+ Dictionary, ?> props = tester.configs.get( pid );
+ TestCase.assertNotNull( props );
+ TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) );
+ TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) );
+ TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) );
+ TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) );
+ TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls );
+ TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls );
+
+ // delete
+ config.delete();
+ delay();
+
+ // ==> update with null
+ TestCase.assertNull( tester.configs.get( pid ) );
+ TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls );
+ TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls );
+ }
+
+
+ @Test
+ public void test_start_bundle_configure_stop_start_bundle() throws BundleException
+ {
+ String pid = "test_start_bundle_configure_stop_start_bundle";
+
+ // start the bundle and assert this
+ bundle = installBundle( pid );
+ bundle.start();
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // give cm time for distribution
+ delay();
+
+ // assert activater has no configuration
+ TestCase.assertNull( "Expect no Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect no update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // configure after ManagedServiceRegistration --> configure via update
+ configure( pid );
+ delay();
+
+ // assert activater has configuration
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls );
+
+ // stop the bundle now
+ bundle.stop();
+
+ // assert INSTANCE is null
+ TestCase.assertNull( ManagedServiceTestActivator.INSTANCE );
+
+ delay();
+
+ // start the bundle again (and check)
+ bundle.start();
+ final ManagedServiceTestActivator tester2 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started the second time!!", tester2 );
+ TestCase.assertNotSame( "Instances must not be the same", tester, tester2 );
+
+ // give cm time for distribution
+ delay();
+
+ // assert activater has configuration
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester2.props );
+ TestCase.assertEquals( "Expect a second update call", 1, tester2.numManagedServiceUpdatedCalls );
+
+ // cleanup
+ bundle.uninstall();
+ bundle = null;
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_configure_start_bundle_stop_start_bundle() throws BundleException
+ {
+ String pid = "test_configure_start_bundle_stop_start_bundle";
+ configure( pid );
+
+ // start the bundle and assert this
+ bundle = installBundle( pid );
+ bundle.start();
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // give cm time for distribution
+ delay();
+
+ // assert activater has configuration
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect no update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // stop the bundle now
+ bundle.stop();
+
+ // assert INSTANCE is null
+ TestCase.assertNull( ManagedServiceTestActivator.INSTANCE );
+
+ delay();
+
+ // start the bundle again (and check)
+ bundle.start();
+ final ManagedServiceTestActivator tester2 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started the second time!!", tester2 );
+ TestCase.assertNotSame( "Instances must not be the same", tester, tester2 );
+
+ // give cm time for distribution
+ delay();
+
+ // assert activater has configuration
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester2.props );
+ TestCase.assertEquals( "Expect a second update call", 1, tester2.numManagedServiceUpdatedCalls );
+
+ // cleanup
+ bundle.uninstall();
+ bundle = null;
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_listConfiguration() throws BundleException, IOException
+ {
+ // 1. create a new Conf1 with pid1 and null location.
+ // 2. Conf1#update(props) is called.
+ final String pid = "test_listConfiguration";
+ final Configuration config = configure( pid, null, true );
+
+ // 3. bundleA will locationA registers ManagedServiceA with pid1.
+ bundle = installBundle( pid );
+ bundle.start();
+ delay();
+
+ // ==> ManagedServiceA is called back.
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( tester );
+ TestCase.assertNotNull( tester.props );
+ TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls );
+
+ // 4. bundleA is stopped but *NOT uninstalled*.
+ bundle.stop();
+ delay();
+
+ // 5. test bundle calls cm.listConfigurations(null).
+ final Configuration listed = getConfiguration( pid );
+
+ // ==> Conf1 is included in the returned list and
+ // it has locationA.
+ // (In debug mode, dynamicBundleLocation==locationA
+ // and staticBundleLocation==null)
+ TestCase.assertNotNull( listed );
+ TestCase.assertEquals( bundle.getLocation(), listed.getBundleLocation() );
+
+ // 6. test bundle calls cm.getConfiguration(pid1)
+ final Configuration get = getConfigurationAdmin().getConfiguration( pid );
+ TestCase.assertEquals( bundle.getLocation(), get.getBundleLocation() );
+
+ final Bundle cmBundle = getCmBundle();
+ cmBundle.stop();
+ delay();
+ cmBundle.start();
+ delay();
+
+ // 5. test bundle calls cm.listConfigurations(null).
+ final Configuration listed2 = getConfiguration( pid );
+
+ // ==> Conf1 is included in the returned list and
+ // it has locationA.
+ // (In debug mode, dynamicBundleLocation==locationA
+ // and staticBundleLocation==null)
+ TestCase.assertNotNull( listed2 );
+ TestCase.assertEquals( bundle.getLocation(), listed2.getBundleLocation() );
+
+ // 6. test bundle calls cm.getConfiguration(pid1)
+ final Configuration get2 = getConfigurationAdmin().getConfiguration( pid );
+ TestCase.assertEquals( bundle.getLocation(), get2.getBundleLocation() );
+}
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationBindingTest.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationBindingTest.java
new file mode 100644
index 0000000..d4c4be7
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationBindingTest.java
@@ -0,0 +1,993 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import java.io.IOException;
+import java.util.Hashtable;
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator;
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator2;
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator;
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator2;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.service.cm.Configuration;
+
+
+@RunWith(JUnit4TestRunner.class)
+public class ConfigurationBindingTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_configuration_unbound_on_uninstall() throws BundleException
+ {
+ String pid = "test_configuration_unbound_on_uninstall";
+ configure( pid );
+
+ // ensure configuration is unbound
+ final Configuration beforeInstall = getConfiguration( pid );
+ TestCase.assertNull( beforeInstall.getBundleLocation() );
+
+ bundle = installBundle( pid );
+
+ // ensure no configuration bound before start
+ final Configuration beforeStart = getConfiguration( pid );
+ TestCase.assertNull( beforeInstall.getBundleLocation() );
+ TestCase.assertNull( beforeStart.getBundleLocation() );
+
+ bundle.start();
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // give cm time for distribution
+ delay();
+
+ // assert activater has configuration
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // ensure a freshly retrieved object also has the location
+ final Configuration beforeStop = getConfiguration( pid );
+ TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() );
+
+ // check whether bundle context is set on first configuration
+ TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() );
+ TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() );
+
+ bundle.stop();
+
+ delay();
+
+ // ensure configuration still bound
+ TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() );
+ TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() );
+ TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() );
+
+ // ensure a freshly retrieved object also has the location
+ final Configuration beforeUninstall = getConfiguration( pid );
+ TestCase.assertEquals( beforeUninstall.getBundleLocation(), bundle.getLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ // ensure configuration is not bound any more
+ TestCase.assertNull( beforeInstall.getBundleLocation() );
+ TestCase.assertNull( beforeStart.getBundleLocation() );
+ TestCase.assertNull( beforeStop.getBundleLocation() );
+ TestCase.assertNull( beforeUninstall.getBundleLocation() );
+
+ // ensure a freshly retrieved object also does not have the location
+ final Configuration atEnd = getConfiguration( pid );
+ TestCase.assertNull( atEnd.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_configuration_unbound_on_uninstall_with_cm_restart() throws BundleException
+ {
+ final String pid = "test_configuration_unbound_on_uninstall_with_cm_restart";
+ configure( pid );
+ final Bundle cmBundle = getCmBundle();
+
+ // ensure configuration is unbound
+ final Configuration beforeInstall = getConfiguration( pid );
+ TestCase.assertNull( beforeInstall.getBundleLocation() );
+
+ bundle = installBundle( pid );
+
+ // ensure no configuration bound before start
+ final Configuration beforeStart = getConfiguration( pid );
+ TestCase.assertNull( beforeInstall.getBundleLocation() );
+ TestCase.assertNull( beforeStart.getBundleLocation() );
+
+ bundle.start();
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "IOActivator not started !!", tester );
+
+ // give cm time for distribution
+ delay();
+
+ // assert activater has configuration
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // ensure a freshly retrieved object also has the location
+ final Configuration beforeStop = getConfiguration( pid );
+ TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() );
+
+ // check whether bundle context is set on first configuration
+ TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() );
+ TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() );
+
+ bundle.stop();
+
+ // ensure configuration still bound
+ TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() );
+ TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() );
+ TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() );
+
+ // ensure a freshly retrieved object also has the location
+ final Configuration beforeUninstall = getConfiguration( pid );
+ TestCase.assertEquals( beforeUninstall.getBundleLocation(), bundle.getLocation() );
+
+ // stop cm bundle now before uninstalling configured bundle
+ cmBundle.stop();
+ delay();
+
+ // assert configuration admin service is gone
+ TestCase.assertNull( configAdminTracker.getService() );
+
+ // uninstall bundle while configuration admin is stopped
+ bundle.uninstall();
+ bundle = null;
+
+ // start cm bundle again after uninstallation
+ cmBundle.start();
+ delay();
+
+ // ensure a freshly retrieved object also does not have the location
+ // FELIX-1484: this test fails due to bundle location not verified
+ // at first configuration access
+ final Configuration atEnd = getConfiguration( pid );
+ TestCase.assertNull( atEnd.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_not_updated_new_configuration_not_bound_after_bundle_uninstall() throws IOException,
+ BundleException
+ {
+ final String pid = "test_not_updated_new_configuration_not_bound_after_bundle_uninstall";
+
+ // create a configuration but do not update with properties
+ final Configuration newConfig = configure( pid, null, false );
+ TestCase.assertNull( newConfig.getProperties() );
+ TestCase.assertNull( newConfig.getBundleLocation() );
+
+ // start and settle bundle
+ bundle = installBundle( pid );
+ bundle.start();
+ delay();
+
+ // ensure no properties provided to bundle
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+ TestCase.assertNull( "Expect no properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // assert configuration is still unset but bound
+ TestCase.assertNull( newConfig.getProperties() );
+ TestCase.assertEquals( bundle.getLocation(), newConfig.getBundleLocation() );
+
+ // uninstall bundle, should unbind configuration
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ // assert configuration is still unset and unbound
+ TestCase.assertNull( newConfig.getProperties() );
+ TestCase.assertNull( newConfig.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_create_with_location_unbind_before_service_supply() throws BundleException, IOException
+ {
+
+ final String pid = "test_create_with_location_unbind_before_service_supply";
+ final String dummyLocation = "http://some/dummy/location";
+
+ // 1. create and statically bind the configuration
+ final Configuration config = configure( pid, dummyLocation, false );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertEquals( dummyLocation, config.getBundleLocation() );
+
+ // 2. update configuration
+ Hashtable props = new Hashtable();
+ props.put( PROP_NAME, PROP_NAME );
+ config.update( props );
+ TestCase.assertEquals( PROP_NAME, config.getProperties().get( PROP_NAME ) );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertEquals( dummyLocation, config.getBundleLocation() );
+
+ // 3. (statically) set location to null
+ config.setBundleLocation( null );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // 4. install bundle with service
+ bundle = installBundle( pid );
+ bundle.start();
+ delay();
+
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ // statically bound configurations must remain bound after bundle
+ // uninstall
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_statically_bound() throws BundleException
+ {
+ final String pid = "test_statically_bound";
+
+ // install the bundle (we need the location)
+ bundle = installBundle( pid );
+ final String location = bundle.getLocation();
+
+ // create and statically bind the configuration
+ configure( pid );
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+ config.setBundleLocation( location );
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ // statically bound configurations must remain bound after bundle
+ // uninstall
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_static_binding_and_unbinding() throws BundleException
+ {
+ final String pid = "test_static_binding_and_unbinding";
+ final String location = bundleContext.getBundle().getLocation();
+
+ // create and statically bind the configuration
+ configure( pid );
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // bind the configuration
+ config.setBundleLocation( location );
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ // restart CM bundle
+ final Bundle cmBundle = getCmBundle();
+ cmBundle.stop();
+ delay();
+ cmBundle.start();
+
+ // assert configuration still bound
+ final Configuration configAfterRestart = getConfiguration( pid );
+ TestCase.assertEquals( pid, configAfterRestart.getPid() );
+ TestCase.assertEquals( location, configAfterRestart.getBundleLocation() );
+
+ // unbind the configuration
+ configAfterRestart.setBundleLocation( null );
+ TestCase.assertNull( configAfterRestart.getBundleLocation() );
+
+ // restart CM bundle
+ cmBundle.stop();
+ delay();
+ cmBundle.start();
+
+ // assert configuration unbound
+ final Configuration configUnboundAfterRestart = getConfiguration( pid );
+ TestCase.assertEquals( pid, configUnboundAfterRestart.getPid() );
+ TestCase.assertNull( configUnboundAfterRestart.getBundleLocation() );
+ }
+
+
+ @Test
+ public void test_dynamic_binding_and_unbinding() throws BundleException
+ {
+ final String pid = "test_dynamic_binding_and_unbinding";
+
+ // create and statically bind the configuration
+ configure( pid );
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // dynamically bind the configuration
+ bundle = installBundle( pid );
+ final String location = bundle.getLocation();
+ bundle.start();
+ delay();
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ // restart CM bundle
+ final Bundle cmBundle = getCmBundle();
+ cmBundle.stop();
+ delay();
+ cmBundle.start();
+
+ // assert configuration still bound
+ final Configuration configAfterRestart = getConfiguration( pid );
+ TestCase.assertEquals( pid, configAfterRestart.getPid() );
+ TestCase.assertEquals( location, configAfterRestart.getBundleLocation() );
+
+ // stop bundle (configuration remains bound !!)
+ bundle.stop();
+ delay();
+ TestCase.assertEquals( location, configAfterRestart.getBundleLocation() );
+
+ // restart CM bundle
+ cmBundle.stop();
+ delay();
+ cmBundle.start();
+
+ // assert configuration still bound
+ final Configuration configBoundAfterRestart = getConfiguration( pid );
+ TestCase.assertEquals( pid, configBoundAfterRestart.getPid() );
+ TestCase.assertEquals( location, configBoundAfterRestart.getBundleLocation() );
+ }
+
+
+ @Test
+ public void test_static_binding() throws BundleException
+ {
+ final String pid = "test_static_binding";
+
+ // install a bundle to get a location for binding
+ bundle = installBundle( pid );
+ final String location = bundle.getLocation();
+
+ // create and statically bind the configuration
+ configure( pid );
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+ config.setBundleLocation( location );
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ // start the bundle
+ bundle.start();
+ delay();
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ // assert the configuration is supplied
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // remove the static binding and assert still bound
+ config.setBundleLocation( null );
+ TestCase.assertEquals( location, config.getBundleLocation() );
+
+ // uninstall bundle and assert configuration unbound
+ bundle.uninstall();
+ bundle = null;
+ delay();
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+
+
+ @Test
+ public void test_two_bundles_one_pid() throws BundleException, IOException
+ {
+ // 1. Bundle registers service with pid1
+ final String pid = "test_two_bundles_one_pid";
+ final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class );
+ final String locationA = bundleA.getLocation();
+ bundleA.start();
+ delay();
+
+ // call back with null
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNull( tester.props );
+ TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls );
+
+ // 2. create new Conf with pid1 and locationA.
+ final Configuration config = configure( pid, locationA, false );
+ delay();
+
+ // ==> No call back.
+ TestCase.assertNull( tester.props );
+ TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls );
+
+ // 3. Configuration#update(prop) is called.
+ config.update( theConfig );
+ delay();
+
+ // ==> call back with the prop.
+ TestCase.assertNotNull( tester.props );
+ TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls );
+
+ // 4. Stop BundleA
+ bundleA.stop();
+ delay();
+
+ // 5. Start BundleA
+ bundleA.start();
+ delay();
+
+ // ==> call back with the prop.
+ final ManagedServiceTestActivator tester2 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( tester2.props );
+ TestCase.assertEquals( 1, tester2.numManagedServiceUpdatedCalls );
+
+ // 6. Configuration#deleted() is called.
+ config.delete();
+ delay();
+
+ // ==> call back with null.
+ TestCase.assertNull( tester2.props );
+ TestCase.assertEquals( 2, tester2.numManagedServiceUpdatedCalls );
+
+ // 7. uninstall Bundle A for cleanup.
+ bundleA.uninstall();
+ delay();
+
+ // Test 2
+
+ // 8. BundleA registers ManagedService with pid1.
+ final Bundle bundleA2 = installBundle( pid, ManagedServiceTestActivator.class );
+ final String locationA2 = bundleA.getLocation();
+ bundleA2.start();
+ delay();
+
+ // call back with null
+ final ManagedServiceTestActivator tester21 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNull( tester21.props );
+ TestCase.assertEquals( 1, tester21.numManagedServiceUpdatedCalls );
+
+ // 9. create new Conf with pid1 and locationB.
+ final String locationB = "test:locationB/" + pid;
+ final Configuration configB = configure( pid, locationB, false );
+ delay();
+
+ // ==> No call back.
+ TestCase.assertNull( tester21.props );
+ TestCase.assertEquals( 1, tester21.numManagedServiceUpdatedCalls );
+
+ // 10. Configuration#update(prop) is called.
+ configB.update( theConfig );
+ delay();
+
+ // ==> No call back because the Conf is not bound to locationA.
+ TestCase.assertNull( tester21.props );
+ TestCase.assertEquals( 1, tester21.numManagedServiceUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_switch_static_binding() throws BundleException
+ {
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final String pid = "test_switch_static_binding";
+ final String locationA = "test:location/A/" + pid;
+ final Configuration config = configure( pid, locationA, true );
+
+ // 3. register ManagedService ms1 with pid from said locationA
+ final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class, locationA );
+ bundleA.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceTestActivator testerA1 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( testerA1.props );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls );
+
+ // 4. register ManagedService ms2 with pid from locationB
+ final String locationB = "test:location/B/" + pid;
+ final Bundle bundleB = installBundle( pid, ManagedServiceTestActivator2.class, locationB );
+ bundleB.start();
+ delay();
+
+ // ==> configuration not supplied to service ms2
+ final ManagedServiceTestActivator2 testerB1 = ManagedServiceTestActivator2.INSTANCE;
+ TestCase.assertNull( testerB1.props );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceUpdatedCalls );
+
+ // 5. Call Configuration.setBundleLocation( "locationB" )
+ config.setBundleLocation( locationB );
+ delay();
+
+ // ==> configuration is bound to locationB
+ TestCase.assertEquals( locationB, config.getBundleLocation() );
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // ==> configuration removed from service ms1
+ TestCase.assertNull( testerA1.props );
+ TestCase.assertEquals( 2, testerA1.numManagedServiceUpdatedCalls );
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.props );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls );
+ }
+ else
+ {
+ // ==> configuration remains for service ms1
+ TestCase.assertNotNull( testerA1.props );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls );
+
+ // ==> configuration not supplied to the service ms2
+ TestCase.assertNull( testerB1.props );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceUpdatedCalls );
+ }
+ }
+
+
+ @Test
+ public void test_switch_dynamic_binding() throws BundleException, IOException
+ {
+ // 1. create config with pid with null location
+ // 2. update config with properties
+ final String pid = "test_switch_dynamic_binding";
+ final String locationA = "test:location/A/" + pid;
+ final Configuration config = configure( pid, null, true );
+
+ // 3. register ManagedService ms1 with pid from locationA
+ final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class, locationA );
+ bundleA.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceTestActivator testerA1 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( testerA1.props );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls );
+
+ // ==> configuration is dynamically bound to locationA
+ TestCase.assertEquals( locationA, config.getBundleLocation() );
+
+ // 4. register ManagedService ms2 with pid from locationB
+ final String locationB = "test:location/B/" + pid;
+ final Bundle bundleB = installBundle( pid, ManagedServiceTestActivator2.class, locationB );
+ bundleB.start();
+ delay();
+
+ // ==> configuration not supplied to service ms2
+ final ManagedServiceTestActivator2 testerB1 = ManagedServiceTestActivator2.INSTANCE;
+ TestCase.assertNull( testerB1.props );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceUpdatedCalls );
+
+ // 5. Call Configuration.setBundleLocation( "locationB" )
+ config.setBundleLocation( locationB );
+ delay();
+
+ // ==> configuration is bound to locationB
+ TestCase.assertEquals( locationB, config.getBundleLocation() );
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // ==> configuration removed from service ms1
+ TestCase.assertNull( testerA1.props );
+ TestCase.assertEquals( 2, testerA1.numManagedServiceUpdatedCalls );
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.props );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls );
+ }
+ else
+ {
+ // ==> configuration remains for service ms1
+ TestCase.assertNotNull( testerA1.props );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls );
+
+ // ==> configuration not supplied to the service ms2
+ TestCase.assertNull( testerB1.props );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceUpdatedCalls );
+ }
+
+ // 6. Update configuration now
+ config.update();
+ delay();
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.props );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_switch_static_binding_factory() throws BundleException, IOException
+ {
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final String factoryPid = "test_switch_static_binding_factory";
+ final String locationA = "test:location/A/" + factoryPid;
+ final Configuration config = createFactoryConfiguration( factoryPid, locationA, true );
+ final String pid = config.getPid();
+
+ // 3. register ManagedService ms1 with pid from said locationA
+ final Bundle bundleA = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class, locationA );
+ bundleA.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceFactoryTestActivator testerA1 = ManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( testerA1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+
+ // 4. register ManagedService ms2 with pid from locationB
+ final String locationB = "test:location/B/" + factoryPid;
+ final Bundle bundleB = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class, locationB );
+ bundleB.start();
+ delay();
+
+ // ==> configuration not supplied to service ms2
+ final ManagedServiceFactoryTestActivator2 testerB1 = ManagedServiceFactoryTestActivator2.INSTANCE;
+ TestCase.assertNull( testerB1.configs.get( pid ));
+ TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls );
+
+ // 5. Call Configuration.setBundleLocation( "locationB" )
+ config.setBundleLocation( locationB );
+ delay();
+
+ // ==> configuration is bound to locationB
+ TestCase.assertEquals( locationB, config.getBundleLocation() );
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // ==> configuration removed from service ms1
+ TestCase.assertNull( testerA1.configs.get( pid ));
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryDeleteCalls );
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+ else
+ {
+ // ==> configuration not removed from service ms1
+ TestCase.assertNotNull( testerA1.configs.get( pid ));
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 0, testerA1.numManagedServiceFactoryDeleteCalls );
+
+ // ==> configuration not supplied to the service ms2
+ TestCase.assertNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+
+ // 6. Update configuration now
+ config.update();
+ delay();
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_switch_dynamic_binding_factory() throws BundleException, IOException
+ {
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final String factoryPid = "test_switch_static_binding_factory";
+ final String locationA = "test:location/A/" + factoryPid;
+ final Configuration config = createFactoryConfiguration( factoryPid, null, true );
+ final String pid = config.getPid();
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // 3. register ManagedService ms1 with pid from said locationA
+ final Bundle bundleA = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class, locationA );
+ bundleA.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceFactoryTestActivator testerA1 = ManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( testerA1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( locationA, config.getBundleLocation() );
+
+ // 4. register ManagedService ms2 with pid from locationB
+ final String locationB = "test:location/B/" + factoryPid;
+ final Bundle bundleB = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class, locationB );
+ bundleB.start();
+ delay();
+
+ // ==> configuration not supplied to service ms2
+ final ManagedServiceFactoryTestActivator2 testerB1 = ManagedServiceFactoryTestActivator2.INSTANCE;
+ TestCase.assertNull( testerB1.configs.get( pid ));
+ TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( locationA, config.getBundleLocation() );
+
+ // 5. Call Configuration.setBundleLocation( "locationB" )
+ config.setBundleLocation( locationB );
+ delay();
+
+ // ==> configuration is bound to locationB
+ TestCase.assertEquals( locationB, config.getBundleLocation() );
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // ==> configuration removed from service ms1
+ TestCase.assertNull( testerA1.configs.get( pid ));
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryDeleteCalls );
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+ else
+ {
+ // ==> configuration not removed from service ms1
+ TestCase.assertNotNull( testerA1.configs.get( pid ));
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( 0, testerA1.numManagedServiceFactoryDeleteCalls );
+
+ // ==> configuration not supplied to the service ms2
+ TestCase.assertNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+
+ // 6. Update configuration now
+ config.update();
+ delay();
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_switch_dynamic_binding_after_uninstall() throws BundleException, IOException
+ {
+ // 1. create config with pid with null location
+ // 2. update config with properties
+ final String pid = "test_switch_dynamic_binding";
+ final String locationA = "test:location/A/" + pid;
+ final Configuration config = configure( pid, null, true );
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // 3. register ManagedService ms1 with pid from locationA
+ final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class, locationA );
+ bundleA.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceTestActivator testerA1 = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( testerA1.props );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls );
+
+ // ==> configuration is dynamically bound to locationA
+ TestCase.assertEquals( locationA, config.getBundleLocation() );
+
+ // 4. register ManagedService ms2 with pid from locationB
+ final String locationB = "test:location/B/" + pid;
+ final Bundle bundleB = installBundle( pid, ManagedServiceTestActivator2.class, locationB );
+ bundleB.start();
+ delay();
+
+ // ==> configuration not supplied to service ms2
+ final ManagedServiceTestActivator2 testerB1 = ManagedServiceTestActivator2.INSTANCE;
+ TestCase.assertNull( testerB1.props );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceUpdatedCalls );
+
+ // 5. Uninstall bundle A
+ bundleA.uninstall();
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // ==> configuration is bound to locationB
+ TestCase.assertEquals( locationB, config.getBundleLocation() );
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.props );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls );
+ }
+ else
+ {
+ // ==> configuration is unbound
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // ==> configuration not supplied to the service ms2
+ TestCase.assertNull( testerB1.props );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceUpdatedCalls );
+ }
+
+ // 6. Update configuration now
+ config.update();
+ delay();
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.props );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls );
+ }
+
+
+ @Test
+ public void test_switch_dynamic_binding_factory_after_uninstall() throws BundleException, IOException
+ {
+ // 1. create config with pid and locationA
+ // 2. update config with properties
+ final String factoryPid = "test_switch_static_binding_factory";
+ final String locationA = "test:location/A/" + factoryPid;
+ final Configuration config = createFactoryConfiguration( factoryPid, null, true );
+ final String pid = config.getPid();
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // 3. register ManagedService ms1 with pid from said locationA
+ final Bundle bundleA = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class, locationA );
+ bundleA.start();
+ delay();
+
+ // ==> configuration supplied to the service ms1
+ final ManagedServiceFactoryTestActivator testerA1 = ManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( testerA1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( locationA, config.getBundleLocation() );
+
+ // 4. register ManagedService ms2 with pid from locationB
+ final String locationB = "test:location/B/" + factoryPid;
+ final Bundle bundleB = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class, locationB );
+ bundleB.start();
+ delay();
+
+ // ==> configuration not supplied to service ms2
+ final ManagedServiceFactoryTestActivator2 testerB1 = ManagedServiceFactoryTestActivator2.INSTANCE;
+ TestCase.assertNull( testerB1.configs.get( pid ));
+ TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertEquals( locationA, config.getBundleLocation() );
+
+ // 5. Uninstall bundle A
+ bundleA.uninstall();
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // ==> configuration is bound to locationB
+ TestCase.assertEquals( locationB, config.getBundleLocation() );
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+ else
+ {
+ // ==> configuration is unbound
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // ==> configuration not supplied to the service ms2
+ TestCase.assertNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+
+ // 6. Update configuration now
+ config.update();
+ delay();
+
+ // ==> configuration supplied to the service ms2
+ TestCase.assertNotNull( testerB1.configs.get( pid ) );
+ TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls );
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationTestBase.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationTestBase.java
new file mode 100644
index 0000000..c360caa
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/ConfigurationTestBase.java
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.BaseTestActivator;
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator;
+import org.apache.felix.cm.integration.helper.MyTinyBundle;
+import org.apache.felix.cm.integration.helper.UpdateThreadSignalTask;
+import org.junit.After;
+import org.junit.Before;
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Inject;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.swissbox.tinybundles.core.TinyBundles;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+
+public abstract class ConfigurationTestBase
+{
+
+ /**
+ * There is currently an open issue in the specification in whether a
+ * call to Configuration.setBundleLocation() might trigger a configuration
+ * update or not.
+ * We have test cases in our integration test suite for both cases. To
+ * enable the respective tests set this field accordingly:
+ *
+ * - false
+ * - Expect configuration to NOT be redispatched. That is existing
+ * configurations are kept and other services are not updated
+ * - true
+ * - Expect configuration to be redispatched. That is existing configuration
+ * is revoked (update(null) or delete calls) and new matching services are
+ * updated.
+ *
+ */
+ public static final boolean REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION = false;
+
+ @Inject
+ protected BundleContext bundleContext;
+
+ protected Bundle bundle;
+
+ protected ServiceTracker configAdminTracker;
+
+ protected static final String PROP_NAME = "theValue";
+ protected static final Dictionary theConfig;
+
+ static
+ {
+ theConfig = new Hashtable();
+ theConfig.put( PROP_NAME, PROP_NAME );
+ }
+
+
+ @org.ops4j.pax.exam.junit.Configuration
+ public static Option[] configuration()
+ {
+ return CoreOptions.options(
+ CoreOptions.provision(
+ CoreOptions.bundle( new File("bundles/configadmin.jar").toURI().toString() ),
+ CoreOptions.mavenBundle( "org.ops4j.pax.swissbox", "pax-swissbox-tinybundles", "1.0.0" )
+ )
+// , PaxRunnerOptions.vmOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=30303" )
+ // , PaxRunnerOptions.logProfile()
+ );
+ }
+
+
+ @Before
+ public void setUp()
+ {
+ configAdminTracker = new ServiceTracker( bundleContext, ConfigurationAdmin.class.getName(), null );
+ configAdminTracker.open();
+ }
+
+
+ @After
+ public void tearDown() throws BundleException
+ {
+ if ( bundle != null )
+ {
+ bundle.uninstall();
+ }
+
+ configAdminTracker.close();
+ configAdminTracker = null;
+ }
+
+
+ protected Bundle installBundle( final String pid ) throws BundleException
+ {
+ return installBundle( pid, ManagedServiceTestActivator.class );
+ }
+
+
+ protected Bundle installBundle( final String pid, final Class> activatorClass ) throws BundleException
+ {
+ return installBundle( pid, activatorClass, activatorClass.getName() );
+ }
+
+
+ protected Bundle installBundle( final String pid, final Class> activatorClass, final String location )
+ throws BundleException
+ {
+ final String activatorClassName = activatorClass.getName();
+ final InputStream bundleStream = new MyTinyBundle()
+ .prepare(
+ TinyBundles.withBnd()
+ .set( Constants.BUNDLE_SYMBOLICNAME, activatorClassName )
+ .set( Constants.BUNDLE_VERSION, "0.0.11" )
+ .set( Constants.IMPORT_PACKAGE, "org.apache.felix.cm.integration.helper" )
+ .set( Constants.BUNDLE_ACTIVATOR, activatorClassName )
+ .set( BaseTestActivator.HEADER_PID, pid )
+ ).build( TinyBundles.asStream() );
+
+ try
+ {
+ return bundleContext.installBundle( location, bundleStream );
+ }
+ finally
+ {
+ try
+ {
+ bundleStream.close();
+ }
+ catch ( IOException ioe )
+ {
+ }
+ }
+ }
+
+
+ protected void delay()
+ {
+ Object ca = configAdminTracker.getService();
+ if ( ca != null )
+ {
+ try
+ {
+
+ Field caf = ca.getClass().getDeclaredField( "configurationManager" );
+ caf.setAccessible( true );
+ Object cm = caf.get( ca );
+
+ Field cmf = cm.getClass().getDeclaredField( "updateThread" );
+ cmf.setAccessible( true );
+ Object ut = cmf.get( cm );
+
+ Method utm = ut.getClass().getDeclaredMethod( "schedule" );
+ utm.setAccessible( true );
+
+ UpdateThreadSignalTask signalTask = new UpdateThreadSignalTask();
+ utm.invoke( ut, signalTask );
+ signalTask.waitSignal();
+
+ return;
+ }
+ catch ( AssertionFailedError afe )
+ {
+ throw afe;
+ }
+ catch ( Throwable t )
+ {
+ // ignore any problem and revert to timed delay (might log this)
+ }
+ }
+
+ // no configadmin or failure while setting up task
+ try
+ {
+ Thread.sleep( 300 );
+ }
+ catch ( InterruptedException ie )
+ {
+ // dont care
+ }
+ }
+
+
+ protected Bundle getCmBundle()
+ {
+ final ServiceReference caref = configAdminTracker.getServiceReference();
+ return ( caref == null ) ? null : caref.getBundle();
+ }
+
+
+ protected ConfigurationAdmin getConfigurationAdmin()
+ {
+ ConfigurationAdmin ca = ( ConfigurationAdmin ) configAdminTracker.getService();
+ if ( ca == null )
+ {
+ TestCase.fail( "Missing ConfigurationAdmin service" );
+ }
+ return ca;
+ }
+
+
+ protected Configuration configure( final String pid )
+ {
+ return configure( pid, null, true );
+ }
+
+
+ protected Configuration configure( final String pid, final String location, final boolean withProps )
+ {
+ final ConfigurationAdmin ca = getConfigurationAdmin();
+ try
+ {
+ final Configuration config = ca.getConfiguration( pid, location );
+ if ( withProps )
+ {
+ config.update( theConfig );
+ }
+ return config;
+ }
+ catch ( IOException ioe )
+ {
+ TestCase.fail( "Failed updating configuration " + pid + ": " + ioe.toString() );
+ return null; // keep the compiler quiet
+ }
+ }
+
+
+ protected Configuration createFactoryConfiguration( final String factoryPid )
+ {
+ return createFactoryConfiguration( factoryPid, null, true );
+ }
+
+
+ protected Configuration createFactoryConfiguration( final String factoryPid, final String location,
+ final boolean withProps )
+ {
+ final ConfigurationAdmin ca = getConfigurationAdmin();
+ try
+ {
+ final Configuration config = ca.createFactoryConfiguration( factoryPid, null );
+ if ( withProps )
+ {
+ config.update( theConfig );
+ }
+ return config;
+ }
+ catch ( IOException ioe )
+ {
+ TestCase.fail( "Failed updating factory configuration " + factoryPid + ": " + ioe.toString() );
+ return null; // keep the compiler quiet
+ }
+ }
+
+
+ protected Configuration getConfiguration( final String pid )
+ {
+ final ConfigurationAdmin ca = getConfigurationAdmin();
+ try
+ {
+ final String filter = "(" + Constants.SERVICE_PID + "=" + pid + ")";
+ final Configuration[] configs = ca.listConfigurations( filter );
+ if ( configs != null && configs.length > 0 )
+ {
+ return configs[0];
+ }
+ }
+ catch ( InvalidSyntaxException ise )
+ {
+ // unexpected
+ }
+ catch ( IOException ioe )
+ {
+ TestCase.fail( "Failed listing configurations " + pid + ": " + ioe.toString() );
+ }
+
+ TestCase.fail( "No Configuration " + pid + " found" );
+ return null;
+ }
+
+
+ protected void deleteConfig( final String pid )
+ {
+ final ConfigurationAdmin ca = getConfigurationAdmin();
+ try
+ {
+ final Configuration config = ca.getConfiguration( pid );
+ config.delete();
+ }
+ catch ( IOException ioe )
+ {
+ TestCase.fail( "Failed deleting configuration " + pid + ": " + ioe.toString() );
+ }
+ }
+
+
+ protected void deleteFactoryConfigurations( String factoryPid )
+ {
+ ConfigurationAdmin ca = getConfigurationAdmin();
+ try
+ {
+ final String filter = "(service.factoryPid=" + factoryPid + ")";
+ Configuration[] configs = ca.listConfigurations( filter );
+ if ( configs != null )
+ {
+ for ( Configuration configuration : configs )
+ {
+ configuration.delete();
+ }
+ }
+ }
+ catch ( InvalidSyntaxException ise )
+ {
+ // unexpected
+ }
+ catch ( IOException ioe )
+ {
+ TestCase.fail( "Failed deleting configurations " + factoryPid + ": " + ioe.toString() );
+ }
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiServiceFactoryPIDTest.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiServiceFactoryPIDTest.java
new file mode 100644
index 0000000..1ce490e
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiServiceFactoryPIDTest.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator;
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator2;
+import org.apache.felix.cm.integration.helper.MultiManagedServiceFactoryTestActivator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.service.cm.Configuration;
+
+
+/**
+ * The MultiServicePIDTest
tests the case of multiple services
+ * bound with the same PID
+ */
+@RunWith(JUnit4TestRunner.class)
+public class MultiServiceFactoryPIDTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_two_services_same_pid_in_same_bundle_configure_before_registration() throws BundleException
+ {
+ final String factoryPid = "test.pid";
+
+ final Configuration config = createFactoryConfiguration( factoryPid );
+ final String pid = config.getPid();
+ TestCase.assertEquals( factoryPid, config.getFactoryPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ bundle = installBundle( factoryPid, MultiManagedServiceFactoryTestActivator.class );
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final MultiManagedServiceFactoryTestActivator tester = MultiManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) );
+ TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceFactoryUpdatedCalls );
+
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_two_services_same_pid_in_same_bundle_configure_after_registration() throws BundleException
+ {
+ final String factoryPid = "test.pid";
+
+ bundle = installBundle( factoryPid, MultiManagedServiceFactoryTestActivator.class );
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final MultiManagedServiceFactoryTestActivator tester = MultiManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // no configuration yet
+ TestCase.assertTrue( "Expect Properties after Service Registration", tester.configs.isEmpty() );
+ TestCase.assertEquals( "Expect two update calls", 0, tester.numManagedServiceFactoryUpdatedCalls );
+
+ final Configuration config = createFactoryConfiguration( factoryPid );
+ final String pid = config.getPid();
+
+ delay();
+
+ TestCase.assertEquals( factoryPid, config.getFactoryPid() );
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) );
+ TestCase.assertEquals( "Expect another two single update call", 2, tester.numManagedServiceFactoryUpdatedCalls );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_two_services_same_pid_in_two_bundle_configure_before_registration() throws BundleException
+ {
+ Bundle bundle2 = null;
+ try
+ {
+ final String factoryPid = "test.pid";
+ final Configuration config = createFactoryConfiguration( factoryPid );
+ final String pid = config.getPid();
+
+ TestCase.assertEquals( factoryPid, config.getFactoryPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class );
+ bundle.start();
+
+ bundle2 = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class );
+ bundle2.start();
+
+ // give cm time for distribution
+ delay();
+
+ final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ final ManagedServiceFactoryTestActivator2 tester2 = ManagedServiceFactoryTestActivator2.INSTANCE;
+ TestCase.assertNotNull( "Activator 2 not started !!", tester2 );
+
+ // expect first activator to have received properties
+
+ // assert first bundle has configuration (two calls, one per srv)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceFactoryUpdatedCalls );
+
+ // assert second bundle has no configuration
+ TestCase.assertTrue( tester2.configs.isEmpty() );
+ TestCase.assertEquals( 0, tester2.numManagedServiceFactoryUpdatedCalls );
+
+ // expect configuration bound to first bundle
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // expect configuration reassigned
+ TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() );
+ }
+ else
+ {
+ // expected configuration unbound
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+ finally
+ {
+ if ( bundle2 != null )
+ {
+ bundle2.uninstall();
+ }
+ }
+ }
+
+
+ @Test
+ public void test_two_services_same_pid_in_two_bundle_configure_after_registration() throws BundleException
+ {
+ Bundle bundle2 = null;
+ try
+ {
+ final String factoryPid = "test.pid";
+
+ bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class );
+ bundle.start();
+
+ bundle2 = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class );
+ bundle2.start();
+
+ final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ final ManagedServiceFactoryTestActivator2 tester2 = ManagedServiceFactoryTestActivator2.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester2 );
+
+ delay();
+
+ // expect no configuration but a call in each service
+ TestCase.assertTrue( "Expect Properties after Service Registration", tester.configs.isEmpty() );
+ TestCase.assertEquals( "Expect a single update call", 0, tester.numManagedServiceFactoryUpdatedCalls );
+ TestCase.assertTrue( "Expect Properties after Service Registration", tester2.configs.isEmpty() );
+ TestCase.assertEquals( "Expect a single update call", 0, tester2.numManagedServiceFactoryUpdatedCalls );
+
+ final Configuration config = createFactoryConfiguration( factoryPid );
+ final String pid = config.getPid();
+
+ delay();
+
+ TestCase.assertEquals( factoryPid, config.getFactoryPid() );
+ TestCase.assertNotNull( config.getBundleLocation() );
+
+ if ( bundle.getLocation().equals( config.getBundleLocation() ) )
+ {
+ // configuration assigned to the first bundle
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceFactoryUpdatedCalls );
+
+ TestCase.assertTrue( "Expect Properties after Service Registration", tester2.configs.isEmpty() );
+ TestCase.assertEquals( "Expect a single update call", 0, tester2.numManagedServiceFactoryUpdatedCalls );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // expect configuration reassigned
+ TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() );
+ }
+ else
+ {
+ // expected configuration unbound
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+ }
+ else if ( bundle2.getLocation().equals( config.getBundleLocation() ) )
+ {
+ // configuration assigned to the second bundle
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester2.configs.get( pid ) );
+ TestCase.assertEquals( "Expect a single update call", 1, tester2.numManagedServiceFactoryUpdatedCalls );
+
+ TestCase.assertTrue( "Expect Properties after Service Registration", tester.configs.isEmpty() );
+ TestCase.assertEquals( "Expect a single update call", 0, tester.numManagedServiceFactoryUpdatedCalls );
+
+ bundle2.uninstall();
+ bundle2 = null;
+
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // expect configuration reassigned
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+ }
+ else
+ {
+ // expected configuration unbound
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+ }
+ else
+ {
+ // configuration assigned to some other bundle ... fail
+ TestCase.fail( "Configuration assigned to unexpected bundle " + config.getBundleLocation() );
+ }
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+ finally
+ {
+ if ( bundle2 != null )
+ {
+ bundle2.uninstall();
+ }
+ }
+ }
+
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiServicePIDTest.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiServicePIDTest.java
new file mode 100644
index 0000000..9ff808a
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiServicePIDTest.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator;
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator2;
+import org.apache.felix.cm.integration.helper.MultiManagedServiceTestActivator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.service.cm.Configuration;
+
+
+/**
+ * The MultiServicePIDTest
tests the case of multiple services
+ * bound with the same PID
+ */
+@RunWith(JUnit4TestRunner.class)
+public class MultiServicePIDTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_two_services_same_pid_in_same_bundle_configure_before_registration() throws BundleException
+ {
+ final String pid = "test.pid";
+
+ configure( pid );
+
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ bundle = installBundle( pid, MultiManagedServiceTestActivator.class );
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final MultiManagedServiceTestActivator tester = MultiManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls );
+
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_two_services_same_pid_in_same_bundle_configure_after_registration() throws BundleException
+ {
+ final String pid = "test.pid";
+
+ bundle = installBundle( pid, MultiManagedServiceTestActivator.class );
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final MultiManagedServiceTestActivator tester = MultiManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // no configuration yet
+ TestCase.assertNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect two update calls", 2, tester.numManagedServiceUpdatedCalls );
+
+ configure( pid );
+ delay();
+
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect another two single update call", 4, tester.numManagedServiceUpdatedCalls );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ TestCase.assertNull( config.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+
+
+ @Test
+ public void test_two_services_same_pid_in_two_bundle_configure_before_registration() throws BundleException
+ {
+ Bundle bundle2 = null;
+ try
+ {
+ final String pid = "test.pid";
+
+ configure( pid );
+
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNull( config.getBundleLocation() );
+
+ bundle = installBundle( pid, ManagedServiceTestActivator.class );
+ bundle.start();
+
+ bundle2 = installBundle( pid, ManagedServiceTestActivator2.class );
+ bundle2.start();
+
+ // give cm time for distribution
+ delay();
+
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ final ManagedServiceTestActivator2 tester2 = ManagedServiceTestActivator2.INSTANCE;
+ TestCase.assertNotNull( "Activator 2 not started !!", tester2 );
+
+ // expect first activator to have received properties
+
+ // assert first bundle has configuration (two calls, one per srv)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ // assert second bundle has no configuration
+ TestCase.assertNull( tester2.props );
+ TestCase.assertEquals( 0, tester2.numManagedServiceUpdatedCalls );
+
+ // expect configuration bound to first bundle
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // expect configuration reassigned
+ TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() );
+ }
+ else
+ {
+ // expected configuration unbound
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+ finally
+ {
+ if ( bundle2 != null )
+ {
+ bundle2.uninstall();
+ }
+ }
+ }
+
+
+ @Test
+ public void test_two_services_same_pid_in_two_bundle_configure_after_registration() throws BundleException
+ {
+ Bundle bundle2 = null;
+ try
+ {
+ final String pid = "test.pid";
+
+ bundle = installBundle( pid, ManagedServiceTestActivator.class );
+ bundle.start();
+
+ bundle2 = installBundle( pid, ManagedServiceTestActivator2.class );
+ bundle2.start();
+
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ final ManagedServiceTestActivator2 tester2 = ManagedServiceTestActivator2.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester2 );
+
+ delay();
+
+ // expect no configuration but a call in each service
+ TestCase.assertNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+ TestCase.assertNull( "Expect Properties after Service Registration", tester2.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester2.numManagedServiceUpdatedCalls );
+
+ configure( pid );
+
+ delay();
+
+ final Configuration config = getConfiguration( pid );
+ TestCase.assertEquals( pid, config.getPid() );
+ TestCase.assertNotNull( config.getBundleLocation() );
+
+ if ( bundle.getLocation().equals( config.getBundleLocation() ) )
+ {
+ // configuration assigned to the first bundle
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls );
+
+ TestCase.assertNull( "Expect Properties after Service Registration", tester2.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester2.numManagedServiceUpdatedCalls );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // expect configuration reassigned
+ TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() );
+ }
+ else
+ {
+ // expected configuration unbound
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+ }
+ else if ( bundle2.getLocation().equals( config.getBundleLocation() ) )
+ {
+ // configuration assigned to the second bundle
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester2.props );
+ TestCase.assertEquals( "Expect a single update call", 2, tester2.numManagedServiceUpdatedCalls );
+
+ TestCase.assertNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls );
+
+ bundle2.uninstall();
+ bundle2 = null;
+
+ delay();
+
+ /*
+ * According to BJ Hargrave configuration is not re-dispatched
+ * due to setting the bundle location.
+ *
+ * Therefore, we have two sets one with re-dispatch expectation and
+ * one without re-dispatch expectation.
+ */
+ if ( REDISPATCH_CONFIGURATION_ON_SET_BUNDLE_LOCATION )
+ {
+ // expect configuration reassigned
+ TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() );
+ }
+ else
+ {
+ // expected configuration unbound
+ TestCase.assertNull( config.getBundleLocation() );
+ }
+ }
+ else
+ {
+ // configuration assigned to some other bundle ... fail
+ TestCase.fail( "Configuration assigned to unexpected bundle " + config.getBundleLocation() );
+ }
+
+ // remove the configuration for good
+ deleteConfig( pid );
+ }
+ finally
+ {
+ if ( bundle2 != null )
+ {
+ bundle2.uninstall();
+ }
+ }
+ }
+
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiValuePIDTest.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiValuePIDTest.java
new file mode 100644
index 0000000..e0ed8da
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/MultiValuePIDTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration;
+
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.BundleException;
+import org.osgi.service.cm.Configuration;
+
+
+@RunWith(JUnit4TestRunner.class)
+public class MultiValuePIDTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_multi_value_pid_array() throws BundleException
+ {
+ final String pid1 = "test.pid.1";
+ final String pid2 = "test.pid.2";
+
+ configure( pid1 );
+ configure( pid2 );
+
+ final Configuration config1 = getConfiguration( pid1 );
+ TestCase.assertEquals( pid1, config1.getPid() );
+ TestCase.assertNull( config1.getBundleLocation() );
+
+ final Configuration config2 = getConfiguration( pid2 );
+ TestCase.assertEquals( pid2, config2.getPid() );
+ TestCase.assertNull( config2.getBundleLocation() );
+
+ // multi-pid with array
+ bundle = installBundle( pid1 + "," + pid2 );
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls );
+
+ TestCase.assertEquals( bundle.getLocation(), config1.getBundleLocation() );
+ TestCase.assertEquals( bundle.getLocation(), config2.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ TestCase.assertNull( config1.getBundleLocation() );
+ TestCase.assertNull( config2.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid1 );
+ deleteConfig( pid2 );
+ }
+
+
+ @Test
+ public void test_multi_value_pid_collection() throws BundleException
+ {
+ String pid1 = "test.pid.1";
+ String pid2 = "test.pid.2";
+
+ configure( pid1 );
+ configure( pid2 );
+
+ final Configuration config1 = getConfiguration( pid1 );
+ TestCase.assertEquals( pid1, config1.getPid() );
+ TestCase.assertNull( config1.getBundleLocation() );
+
+ final Configuration config2 = getConfiguration( pid2 );
+ TestCase.assertEquals( pid2, config2.getPid() );
+ TestCase.assertNull( config2.getBundleLocation() );
+
+ // multi-pid with collection
+ bundle = installBundle( pid1 + ";" + pid2 );
+ bundle.start();
+
+ // give cm time for distribution
+ delay();
+
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull( "Activator not started !!", tester );
+
+ // assert activater has configuration (two calls, one per pid)
+ TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props );
+ TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls );
+
+ TestCase.assertEquals( bundle.getLocation(), config1.getBundleLocation() );
+ TestCase.assertEquals( bundle.getLocation(), config2.getBundleLocation() );
+
+ bundle.uninstall();
+ bundle = null;
+
+ delay();
+
+ TestCase.assertNull( config1.getBundleLocation() );
+ TestCase.assertNull( config2.getBundleLocation() );
+
+ // remove the configuration for good
+ deleteConfig( pid1 );
+ deleteConfig( pid2 );
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/BaseTestActivator.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/BaseTestActivator.java
new file mode 100644
index 0000000..8d5dce6
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/BaseTestActivator.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+
+public abstract class BaseTestActivator implements BundleActivator, ManagedService, ManagedServiceFactory
+{
+
+ // the bundle manifest header naming a pid of configurations we require
+ public static final String HEADER_PID = "The-Test-PID";
+
+ public int numManagedServiceUpdatedCalls = 0;
+ public int numManagedServiceFactoryUpdatedCalls = 0;
+ public int numManagedServiceFactoryDeleteCalls = 0;
+
+ public Dictionary props = null;
+
+ public Map configs = new HashMap();
+
+
+ // ---------- ManagedService
+
+ public void updated( Dictionary props )
+ {
+ numManagedServiceUpdatedCalls++;
+ this.props = props;
+ }
+
+
+ // ---------- ManagedServiceFactory
+
+ public String getName()
+ {
+ return getClass().getName();
+ }
+
+
+ public void deleted( String pid )
+ {
+ numManagedServiceFactoryDeleteCalls++;
+ this.configs.remove( pid );
+ }
+
+
+ public void updated( String pid, Dictionary props )
+ {
+ numManagedServiceFactoryUpdatedCalls++;
+ this.configs.put( pid, props );
+ }
+
+
+ protected Dictionary getServiceProperties( BundleContext bundleContext ) throws Exception
+ {
+ final Object prop = bundleContext.getBundle().getHeaders().get( HEADER_PID );
+ if ( prop instanceof String )
+ {
+ final Hashtable props = new Hashtable();
+
+ // multi-value PID support
+ final String pid = ( String ) prop;
+ if ( pid.indexOf( ',' ) > 0 )
+ {
+ final String[] pids = pid.split( "," );
+ props.put( Constants.SERVICE_PID, pids );
+ }
+ else if ( pid.indexOf( ';' ) > 0 )
+ {
+ final String[] pids = pid.split( ";" );
+ props.put( Constants.SERVICE_PID, Arrays.asList( pids ) );
+ }
+ else
+ {
+ props.put( Constants.SERVICE_PID, pid );
+ }
+
+ return props;
+ }
+
+ // missing pid, fail
+ throw new Exception( "Missing " + HEADER_PID + " manifest header, cannot start" );
+ }
+
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ConfigureThread.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ConfigureThread.java
new file mode 100644
index 0000000..db5c114
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ConfigureThread.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import java.io.IOException;
+import java.util.Hashtable;
+
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+
+/**
+ * The ConfigureThread
class is extends the {@link TestThread} for
+ * use as the configuration creator and updater in the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}.
+ */
+public class ConfigureThread extends TestThread
+{
+ private final Configuration config;
+
+ private final Hashtable props;
+
+
+ public ConfigureThread( final ConfigurationAdmin configAdmin, final String pid, final boolean isFactory )
+ throws IOException
+ {
+ // ensure configuration and disown it
+ final Configuration config;
+ if ( isFactory )
+ {
+ config = configAdmin.createFactoryConfiguration( pid );
+ }
+ else
+ {
+ config = configAdmin.getConfiguration( pid );
+ }
+ config.setBundleLocation( null );
+
+ Hashtable props = new Hashtable();
+ props.put( "prop1", "aValue" );
+ props.put( "prop2", 4711 );
+
+ this.config = config;
+ this.props = props;
+ }
+
+
+ @Override
+ public void doRun()
+ {
+ try
+ {
+ config.update( props );
+ }
+ catch ( IOException ioe )
+ {
+ // TODO: log !!
+ }
+ }
+
+
+ @Override
+ public void cleanup()
+ {
+ try
+ {
+ config.delete();
+ }
+ catch ( IOException ioe )
+ {
+ // TODO: log !!
+ }
+ }
+}
\ No newline at end of file
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator.java
new file mode 100644
index 0000000..0301447
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+
+public class ManagedServiceFactoryTestActivator extends BaseTestActivator
+{
+
+ public static ManagedServiceFactoryTestActivator INSTANCE;
+
+
+ public void start( BundleContext context ) throws Exception
+ {
+ context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) );
+ INSTANCE = this;
+ }
+
+
+ public void stop( BundleContext arg0 ) throws Exception
+ {
+ INSTANCE = null;
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator2.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator2.java
new file mode 100644
index 0000000..398cbe9
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator2.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+
+public class ManagedServiceFactoryTestActivator2 extends BaseTestActivator
+{
+ public static ManagedServiceFactoryTestActivator2 INSTANCE;
+
+
+ public void start( BundleContext context ) throws Exception
+ {
+ context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) );
+ INSTANCE = this;
+ }
+
+
+ public void stop( BundleContext arg0 ) throws Exception
+ {
+ INSTANCE = null;
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java
new file mode 100644
index 0000000..94dcec6
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+
+/**
+ * The ManagedServiceFactoryThread
class is a ManagedServiceFactory
+ * and extends the {@link TestThread} for use in the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}.
+ */
+public class ManagedServiceFactoryThread extends TestThread implements ManagedServiceFactory
+{
+
+ private final BundleContext bundleContext;
+
+ private final Hashtable serviceProps;
+
+ private ServiceRegistration service;
+
+ private final ArrayList configs;
+
+ private boolean configured;
+
+
+ public ManagedServiceFactoryThread( final BundleContext bundleContext, final String pid )
+ {
+ Hashtable serviceProps = new Hashtable();
+ serviceProps.put( Constants.SERVICE_PID, pid );
+
+ this.bundleContext = bundleContext;
+ this.serviceProps = serviceProps;
+ this.configs = new ArrayList();
+ }
+
+
+ public ArrayList getConfigs()
+ {
+ synchronized ( configs )
+ {
+ return new ArrayList( configs );
+ }
+ }
+
+
+ public boolean isConfigured()
+ {
+ return configured;
+ }
+
+
+ @Override
+ public void doRun()
+ {
+ service = bundleContext.registerService( ManagedServiceFactory.class.getName(), this, serviceProps );
+ }
+
+
+ @Override
+ public void cleanup()
+ {
+ if ( service != null )
+ {
+ service.unregister();
+ service = null;
+ }
+ }
+
+
+ public void deleted( String pid )
+ {
+ synchronized ( configs )
+ {
+ configs.add( null );
+ }
+ }
+
+
+ public void updated( String pid, Dictionary properties )
+ {
+ synchronized ( configs )
+ {
+ configs.add( properties );
+ configured = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator.java
new file mode 100644
index 0000000..0ed1261
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedService;
+
+
+public class ManagedServiceTestActivator extends BaseTestActivator
+{
+
+ public static ManagedServiceTestActivator INSTANCE;
+
+
+ public void start( BundleContext context ) throws Exception
+ {
+ context.registerService( ManagedService.class.getName(), this, getServiceProperties( context ) );
+ INSTANCE = this;
+ }
+
+
+ public void stop( BundleContext arg0 ) throws Exception
+ {
+ INSTANCE = null;
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator2.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator2.java
new file mode 100644
index 0000000..1e95d40
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator2.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedService;
+
+
+public class ManagedServiceTestActivator2 extends BaseTestActivator
+{
+
+ public static ManagedServiceTestActivator2 INSTANCE;
+
+
+ public void start( BundleContext context ) throws Exception
+ {
+ context.registerService( ManagedService.class.getName(), this, getServiceProperties( context ) );
+ INSTANCE = this;
+ }
+
+
+ public void stop( BundleContext arg0 ) throws Exception
+ {
+ INSTANCE = null;
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceThread.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceThread.java
new file mode 100644
index 0000000..dc14317
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/ManagedServiceThread.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ManagedService;
+
+
+/**
+ * The ManagedServiceThread
class is a ManagedService and extends
+ * the {@link TestThread} for use in the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}.
+ */
+public class ManagedServiceThread extends TestThread implements ManagedService
+{
+
+ private final BundleContext bundleContext;
+
+ private final Hashtable serviceProps;
+
+ private ServiceRegistration service;
+
+ private final ArrayList configs;
+
+ private boolean configured;
+
+
+ public ManagedServiceThread( final BundleContext bundleContext, final String pid )
+ {
+ Hashtable serviceProps = new Hashtable();
+ serviceProps.put( Constants.SERVICE_PID, pid );
+
+ this.bundleContext = bundleContext;
+ this.serviceProps = serviceProps;
+ this.configs = new ArrayList();
+ }
+
+
+ public ArrayList getConfigs()
+ {
+ synchronized ( configs )
+ {
+ return new ArrayList( configs );
+ }
+ }
+
+
+ public boolean isConfigured()
+ {
+ return configured;
+ }
+
+
+ @Override
+ public void doRun()
+ {
+ service = bundleContext.registerService( ManagedService.class.getName(), this, serviceProps );
+ }
+
+
+ @Override
+ public void cleanup()
+ {
+ if ( service != null )
+ {
+ service.unregister();
+ service = null;
+ }
+ }
+
+
+ public void updated( Dictionary properties )
+ {
+ synchronized ( configs )
+ {
+ configs.add( properties );
+ configured = properties != null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MultiManagedServiceFactoryTestActivator.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MultiManagedServiceFactoryTestActivator.java
new file mode 100644
index 0000000..3b2dd3c
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MultiManagedServiceFactoryTestActivator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+
+public class MultiManagedServiceFactoryTestActivator extends ManagedServiceFactoryTestActivator
+{
+
+ public static MultiManagedServiceFactoryTestActivator INSTANCE;
+
+
+ public void start( BundleContext context ) throws Exception
+ {
+ super.start( context );
+ context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) );
+ INSTANCE = this;
+ }
+
+
+ @Override
+ public void stop( BundleContext context ) throws Exception
+ {
+ INSTANCE = null;
+ super.stop( context );
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MultiManagedServiceTestActivator.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MultiManagedServiceTestActivator.java
new file mode 100644
index 0000000..ff11dca
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MultiManagedServiceTestActivator.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedService;
+
+
+public class MultiManagedServiceTestActivator extends ManagedServiceTestActivator
+{
+
+ public static MultiManagedServiceTestActivator INSTANCE;
+
+
+ @Override
+ public void start( BundleContext context ) throws Exception
+ {
+ super.start( context );
+ context.registerService( ManagedService.class.getName(), this, getServiceProperties( context ) );
+ INSTANCE = this;
+ }
+
+
+ @Override
+ public void stop( BundleContext context ) throws Exception
+ {
+ INSTANCE = null;
+ super.stop( context );
+ }
+}
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MyTinyBundle.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MyTinyBundle.java
new file mode 100644
index 0000000..238cd5d
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/MyTinyBundle.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.ops4j.pax.swissbox.tinybundles.core.BuildableBundle;
+import org.ops4j.pax.swissbox.tinybundles.core.TinyBundle;
+import org.ops4j.pax.swissbox.tinybundles.core.metadata.RawBuilder;
+
+public class MyTinyBundle implements TinyBundle {
+
+ private Map m_resources = new HashMap();
+
+ @SuppressWarnings("unchecked")
+ public TinyBundle addClass( Class clazz )
+ {
+ String name = clazz.getName().replaceAll( "\\.", "/" ) + ".class";
+ addResource( name, clazz.getResource( "/" + name ) );
+ return this;
+ }
+
+ public TinyBundle addResource( String name, URL url )
+ {
+ m_resources.put( name, url );
+ return this;
+ }
+
+ public BuildableBundle prepare( BuildableBundle builder )
+ {
+ return builder.setResources( m_resources );
+ }
+
+ public BuildableBundle prepare()
+ {
+ return new RawBuilder().setResources( m_resources );
+ }
+
+}
\ No newline at end of file
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/TestThread.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/TestThread.java
new file mode 100644
index 0000000..8c258fd
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/TestThread.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+/**
+ * The TestThread
class is a base helper class for the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}. It implements
+ * basic mechanics to be able to run two task at quasi the same time.
+ *
+ * It is not important to have exact timings because running the tests multiple
+ * times and based on low-level Java VM timings thread execution will in the end
+ * be more or less random.
+ */
+abstract class TestThread extends Thread
+{
+ private final Object flag = new Object();
+
+ private volatile boolean notified;
+
+
+ @Override
+ public void run()
+ {
+ synchronized ( flag )
+ {
+ if ( !notified )
+ {
+ try
+ {
+ flag.wait();
+ }
+ catch ( InterruptedException ie )
+ {
+ // TODO: log
+ }
+ }
+ }
+
+ doRun();
+ }
+
+
+ protected abstract void doRun();
+
+
+ public abstract void cleanup();
+
+
+ public void trigger()
+ {
+ synchronized ( flag )
+ {
+ notified = true;
+ flag.notifyAll();
+ }
+ }
+}
\ No newline at end of file
diff --git a/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java
new file mode 100644
index 0000000..dd55aed
--- /dev/null
+++ b/chapter07/testing-example/it/configadmin/src/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.integration.helper;
+
+
+import junit.framework.TestCase;
+
+
+/**
+ * The UpdateThreadSignalTask
class is a special task used by the
+ * {@link org.apache.felix.cm.integration.ConfigurationTestBase#delay} method.
+ *
+ * This task is intended to be added to the update thread schedule and signals
+ * to the tests that all current tasks on the queue have terminated and tests
+ * may continue checking results.
+ */
+public class UpdateThreadSignalTask implements Runnable
+{
+
+ private final Object trigger = new Object();
+
+ private volatile boolean signal;
+
+
+ public void run()
+ {
+ synchronized ( trigger )
+ {
+ signal = true;
+ trigger.notifyAll();
+ }
+ }
+
+
+ public void waitSignal()
+ {
+ synchronized ( trigger )
+ {
+ if ( !signal )
+ {
+ try
+ {
+ trigger.wait( 10 * 1000L ); // seconds
+ }
+ catch ( InterruptedException ie )
+ {
+ // sowhat ??
+ }
+ }
+
+ if ( !signal )
+ {
+ TestCase.fail( "Timed out waiting for the queue to keep up" );
+ }
+ }
+ }
+
+
+ @Override
+ public String toString()
+ {
+ return "Update Thread Signal Task";
+ }
+}
diff --git a/chapter07/testing-example/lib/org.apache.felix.configadmin-1.0.0.jar b/chapter07/testing-example/lib/org.apache.felix.configadmin-1.0.0.jar
new file mode 100644
index 0000000..b3ee68d
Binary files /dev/null and b/chapter07/testing-example/lib/org.apache.felix.configadmin-1.0.0.jar differ
diff --git a/chapter07/testing-example/log4j.properties b/chapter07/testing-example/log4j.properties
new file mode 100644
index 0000000..6d1b6fa
--- /dev/null
+++ b/chapter07/testing-example/log4j.properties
@@ -0,0 +1,4 @@
+log4j.rootCategory=WARN, stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout.ConversionPattern=[%30.30c{1}] - %m%n
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
diff --git a/chapter07/testing-example/mt/build.xml b/chapter07/testing-example/mt/build.xml
new file mode 100644
index 0000000..c7ef72e
--- /dev/null
+++ b/chapter07/testing-example/mt/build.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/chapter07/testing-example/mt/upgrade_configadmin_bundle/build.properties b/chapter07/testing-example/mt/upgrade_configadmin_bundle/build.properties
new file mode 100644
index 0000000..ed148b6
--- /dev/null
+++ b/chapter07/testing-example/mt/upgrade_configadmin_bundle/build.properties
@@ -0,0 +1,10 @@
+#-------------------------------------------------
+title=Testing Example - Management Tests
+#-------------------------------------------------
+
+module=org.apache.felix.cm.integration.mgmt
+custom=true
+
+Private-Package:\
+ ${module}.*
+
diff --git a/chapter07/testing-example/mt/upgrade_configadmin_bundle/build.xml b/chapter07/testing-example/mt/upgrade_configadmin_bundle/build.xml
new file mode 100644
index 0000000..07c67d9
--- /dev/null
+++ b/chapter07/testing-example/mt/upgrade_configadmin_bundle/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter07/testing-example/mt/upgrade_configadmin_bundle/src/org/apache/felix/cm/integration/mgmt/ConfigAdminUpgradeTest.java b/chapter07/testing-example/mt/upgrade_configadmin_bundle/src/org/apache/felix/cm/integration/mgmt/ConfigAdminUpgradeTest.java
new file mode 100644
index 0000000..c853dd5
--- /dev/null
+++ b/chapter07/testing-example/mt/upgrade_configadmin_bundle/src/org/apache/felix/cm/integration/mgmt/ConfigAdminUpgradeTest.java
@@ -0,0 +1,110 @@
+package org.apache.felix.cm.integration.mgmt;
+
+import static org.ops4j.pax.exam.CoreOptions.*;
+
+import java.io.*;
+import java.net.URL;
+import java.util.Dictionary;
+import junit.framework.TestCase;
+import org.apache.felix.cm.integration.ConfigurationTestBase;
+import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.*;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.*;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.Configuration;
+
+@RunWith(JUnit4TestRunner.class)
+public class ConfigAdminUpgradeTest extends ConfigurationTestBase {
+
+ private static String toFileURI(String path) {
+ return new File(path).toURI().toString();
+ }
+
+ @org.ops4j.pax.exam.junit.Configuration
+ public static Option[] configuration() {
+ return options(
+ provision(
+ bundle(toFileURI("bundles/integration_tests-1.0.jar")),
+ bundle(toFileURI("bundles/old.configadmin.jar")),
+ mavenBundle("org.osgi", "org.osgi.compendium", "4.2.0"),
+ mavenBundle("org.ops4j.pax.swissbox", "pax-swissbox-tinybundles",
+ "1.0.0")
+ ),
+ systemProperty("new.configadmin.uri").
+ value(toFileURI("bundles/configadmin.jar"))
+ );
+ }
+
+ @Test
+ public void test_upgradeConfigAdmin() throws BundleException, IOException {
+
+ Dictionary headers = getCmBundle().getHeaders();
+ TestCase.assertEquals("org.apache.felix.configadmin", headers.get(Constants.BUNDLE_SYMBOLICNAME));
+ TestCase.assertEquals("1.0.0", headers.get(Constants.BUNDLE_VERSION));
+
+ // 1. create a new Conf1 with pid1 and null location.
+ // 2. Conf1#update(props) is called.
+ final String pid = "test_listConfiguration";
+ final Configuration config = configure(pid, null, true);
+
+ // 3. bundleA will locationA registers ManagedServiceA with pid1.
+ bundle = installBundle(pid);
+ bundle.start();
+ delay();
+
+ // ==> ManagedServiceA is called back.
+ final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE;
+ TestCase.assertNotNull(tester);
+ TestCase.assertNotNull(tester.props);
+ TestCase.assertEquals(1, tester.numManagedServiceUpdatedCalls);
+
+ // 4. bundleA is stopped but *NOT uninstalled*.
+ bundle.stop();
+ delay();
+
+ // 5. test bundle calls cm.listConfigurations(null).
+ final Configuration listed = getConfiguration(pid);
+
+ // ==> Conf1 is included in the returned list and
+ // it has locationA.
+ // (In debug mode, dynamicBundleLocation==locationA
+ // and staticBundleLocation==null)
+ TestCase.assertNotNull(listed);
+ TestCase.assertEquals(bundle.getLocation(), listed.getBundleLocation());
+
+ // 6. test bundle calls cm.getConfiguration(pid1)
+ final Configuration get = getConfigurationAdmin().getConfiguration(pid);
+ TestCase.assertEquals(bundle.getLocation(), get.getBundleLocation());
+
+ final Bundle cmBundle = getCmBundle();
+ cmBundle.stop();
+ delay();
+
+ // 7. in-place upgrade of the configadmin bundle
+ cmBundle.update(new URL(System.getProperty("new.configadmin.uri")).openStream());
+
+ cmBundle.start();
+ delay();
+
+ headers = cmBundle.getHeaders();
+ TestCase.assertEquals("org.apache.felix.configadmin", headers.get(Constants.BUNDLE_SYMBOLICNAME));
+ TestCase.assertEquals("1.2.7.SNAPSHOT", headers.get(Constants.BUNDLE_VERSION));
+
+ // 8. test bundle calls cm.listConfigurations(null).
+ final Configuration listed2 = getConfiguration(pid);
+
+ // ==> Conf1 is included in the returned list and
+ // it has locationA.
+ // (In debug mode, dynamicBundleLocation==locationA
+ // and staticBundleLocation==null)
+ TestCase.assertNotNull(listed2);
+ TestCase.assertEquals(bundle.getLocation(), listed2.getBundleLocation());
+
+ // 9. test bundle calls cm.getConfiguration(pid1)
+ final Configuration get2 = getConfigurationAdmin().getConfiguration(pid);
+ TestCase.assertEquals(bundle.getLocation(), get2.getBundleLocation());
+ }
+}
diff --git a/chapter07/testing-example/ut/build.xml b/chapter07/testing-example/ut/build.xml
new file mode 100644
index 0000000..29721e9
--- /dev/null
+++ b/chapter07/testing-example/ut/build.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/chapter07/testing-example/ut/configadmin/build.properties b/chapter07/testing-example/ut/configadmin/build.properties
new file mode 100644
index 0000000..88e791a
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/build.properties
@@ -0,0 +1,10 @@
+#-------------------------------------------------
+title=Testing Example - Unit Tests
+#-------------------------------------------------
+
+module=org.apache.felix.cm
+custom=true
+
+Private-Package:\
+ ${module}.*
+
diff --git a/chapter07/testing-example/ut/configadmin/build.xml b/chapter07/testing-example/ut/configadmin/build.xml
new file mode 100644
index 0000000..e4bb17f
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/build.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockBundle.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockBundle.java
new file mode 100644
index 0000000..d1ef4e7
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockBundle.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Dictionary;
+import java.util.Enumeration;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.ServiceReference;
+
+
+public class MockBundle implements Bundle
+{
+
+ private final BundleContext context;
+ private final String location;
+
+
+ public MockBundle( BundleContext context, String location )
+ {
+ this.context = context;
+ this.location = location;
+ }
+
+
+ public Enumeration findEntries( String arg0, String arg1, boolean arg2 )
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public BundleContext getBundleContext()
+ {
+ return context;
+ }
+
+
+ public long getBundleId()
+ {
+ return 0;
+ }
+
+
+ public URL getEntry( String arg0 )
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public Enumeration getEntryPaths( String arg0 )
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public Dictionary getHeaders()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public Dictionary getHeaders( String arg0 )
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public long getLastModified()
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+
+ public String getLocation()
+ {
+ return location;
+ }
+
+
+ public ServiceReference[] getRegisteredServices()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public URL getResource( String arg0 )
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public Enumeration getResources( String arg0 ) throws IOException
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public ServiceReference[] getServicesInUse()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public int getState()
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+
+ public String getSymbolicName()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public boolean hasPermission( Object arg0 )
+ {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+
+ public Class loadClass( String arg0 ) throws ClassNotFoundException
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+
+ public void start() throws BundleException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+
+ public void stop() throws BundleException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+
+ public void uninstall() throws BundleException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+
+ public void update() throws BundleException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+
+ public void update( InputStream arg0 ) throws BundleException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockBundleContext.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockBundleContext.java
new file mode 100644
index 0000000..8cb6353
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockBundleContext.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm;
+
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.Dictionary;
+import java.util.Properties;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+
+/**
+ * The MockBundleContext
is a dummy implementation of the
+ * BundleContext
interface. No methods are implemented here, that
+ * is all methods have no effect and return null
if a return value
+ * is specified.
+ *
+ * Extensions may overwrite methods as see fit.
+ */
+public class MockBundleContext implements BundleContext
+{
+
+ private final Properties properties = new Properties();
+
+
+ public void setProperty( String name, String value )
+ {
+ if ( value == null )
+ {
+ properties.remove( name );
+ }
+ else
+ {
+ properties.setProperty( name, value );
+ }
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#addBundleListener(org.osgi.framework
+ * .BundleListener)
+ */
+ public void addBundleListener( BundleListener arg0 )
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#addFrameworkListener(org.osgi.framework
+ * .FrameworkListener)
+ */
+ public void addFrameworkListener( FrameworkListener arg0 )
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#addServiceListener(org.osgi.framework
+ * .ServiceListener)
+ */
+ public void addServiceListener( ServiceListener arg0 )
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#addServiceListener(org.osgi.framework
+ * .ServiceListener, java.lang.String)
+ */
+ public void addServiceListener( ServiceListener arg0, String arg1 ) throws InvalidSyntaxException
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#createFilter(java.lang.String)
+ */
+ public Filter createFilter( String arg0 ) throws InvalidSyntaxException
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#getAllServiceReferences(java.lang.String
+ * , java.lang.String)
+ */
+ public ServiceReference[] getAllServiceReferences( String arg0, String arg1 ) throws InvalidSyntaxException
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#getBundle()
+ */
+ public Bundle getBundle()
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#getBundle(long)
+ */
+ public Bundle getBundle( long arg0 )
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#getBundles()
+ */
+ public Bundle[] getBundles()
+ {
+ return new Bundle[0];
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#getDataFile(java.lang.String)
+ */
+ public File getDataFile( String arg0 )
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#getProperty(java.lang.String)
+ */
+ public String getProperty( String name )
+ {
+ return properties.getProperty( name );
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @seeorg.osgi.framework.BundleContext#getService(org.osgi.framework.
+ * ServiceReference)
+ */
+ public Object getService( ServiceReference arg0 )
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#getServiceReference(java.lang.String)
+ */
+ public ServiceReference getServiceReference( String arg0 )
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#getServiceReferences(java.lang.String,
+ * java.lang.String)
+ */
+ public ServiceReference[] getServiceReferences( String arg0, String arg1 ) throws InvalidSyntaxException
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#installBundle(java.lang.String)
+ */
+ public Bundle installBundle( String arg0 ) throws BundleException
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#installBundle(java.lang.String,
+ * java.io.InputStream)
+ */
+ public Bundle installBundle( String arg0, InputStream arg1 ) throws BundleException
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#registerService(java.lang.String[],
+ * java.lang.Object, java.util.Dictionary)
+ */
+ public ServiceRegistration registerService( String[] arg0, Object arg1, Dictionary arg2 )
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleContext#registerService(java.lang.String,
+ * java.lang.Object, java.util.Dictionary)
+ */
+ public ServiceRegistration registerService( String arg0, Object arg1, Dictionary arg2 )
+ {
+ return null;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#removeBundleListener(org.osgi.framework
+ * .BundleListener)
+ */
+ public void removeBundleListener( BundleListener arg0 )
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#removeFrameworkListener(org.osgi.framework
+ * .FrameworkListener)
+ */
+ public void removeFrameworkListener( FrameworkListener arg0 )
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.osgi.framework.BundleContext#removeServiceListener(org.osgi.framework
+ * .ServiceListener)
+ */
+ public void removeServiceListener( ServiceListener arg0 )
+ {
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @seeorg.osgi.framework.BundleContext#ungetService(org.osgi.framework.
+ * ServiceReference)
+ */
+ public boolean ungetService( ServiceReference arg0 )
+ {
+ return false;
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockLogService.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockLogService.java
new file mode 100644
index 0000000..7b8aea4
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockLogService.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm;
+
+
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+
+
+/**
+ * The MockLogService
is a very simple log service, which just
+ * prints the loglevel and message to StdErr.
+ */
+public class MockLogService implements LogService
+{
+
+ public void log( int logLevel, String message )
+ {
+ System.err.print( toMessageLine( logLevel, message ) );
+ }
+
+
+ public void log( int logLevel, String message, Throwable t )
+ {
+ log( logLevel, message );
+ }
+
+
+ public void log( ServiceReference ref, int logLevel, String message )
+ {
+ log( logLevel, message );
+ }
+
+
+ public void log( ServiceReference ref, int logLevel, String message, Throwable t )
+ {
+ log( logLevel, message );
+ }
+
+
+ /**
+ * Helper method to format log level and log message exactly the same as the
+ * ConfigurationManager.log()
does.
+ */
+ public static String toMessageLine( int level, String message )
+ {
+ String messageLine;
+ switch ( level )
+ {
+ case LogService.LOG_INFO:
+ messageLine = "*INFO *";
+ break;
+
+ case LogService.LOG_WARNING:
+ messageLine = "*WARN *";
+ break;
+
+ case LogService.LOG_ERROR:
+ messageLine = "*ERROR*";
+ break;
+
+ case LogService.LOG_DEBUG:
+ default:
+ messageLine = "*DEBUG*";
+ }
+ return messageLine + " " + message + System.getProperty( "line.separator" );
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockPersistenceManager.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockPersistenceManager.java
new file mode 100644
index 0000000..95a4ca1
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/MockPersistenceManager.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm;
+
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class MockPersistenceManager implements PersistenceManager
+{
+
+ private final Map configs = new HashMap();
+
+
+ public void delete( String pid )
+ {
+ configs.remove( pid );
+ }
+
+
+ public boolean exists( String pid )
+ {
+ return configs.containsKey( pid );
+ }
+
+
+ public Enumeration getDictionaries()
+ {
+ return Collections.enumeration( configs.values() );
+ }
+
+
+ public Dictionary load( String pid ) throws IOException
+ {
+ Dictionary config = ( Dictionary ) configs.get( pid );
+ if ( config != null )
+ {
+ return config;
+ }
+
+ throw new IOException( "No such configuration: " + pid );
+ }
+
+
+ public void store( String pid, Dictionary properties )
+ {
+ configs.put( pid, properties );
+ }
+
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/file/FilePersistenceManagerConstructorTest.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/file/FilePersistenceManagerConstructorTest.java
new file mode 100644
index 0000000..08e5d3c
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/file/FilePersistenceManagerConstructorTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.file;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.MockBundleContext;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The FilePersistenceManagerConstructorTest
TODO
+ */
+public class FilePersistenceManagerConstructorTest extends TestCase
+{
+
+ /** The current working directory for the tests */
+ private static final String TEST_WORKING_DIRECTORY = "target";
+
+ /** The Bundle Data Area directory for testing */
+ private static final String BUNDLE_DATA = "bundleData";
+
+ /** The Configuration location path for testing */
+ private static final String LOCATION_TEST = "test";
+
+ /** the previous working directory to return to on tearDown */
+ private String oldWorkingDirectory;
+
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ String testDir = new File(TEST_WORKING_DIRECTORY).getAbsolutePath();
+
+ oldWorkingDirectory = System.getProperty( "user.dir" );
+ System.setProperty( "user.dir", testDir );
+ }
+
+ protected void tearDown() throws Exception
+ {
+ System.setProperty( "user.dir", oldWorkingDirectory );
+
+ super.tearDown();
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.cm.file.FilePersistenceManager#FilePersistenceManager(java.lang.String)}.
+ */
+ public void testFilePersistenceManagerString()
+ {
+ // variables used in these tests
+ FilePersistenceManager fpm;
+ String relPath;
+ String absPath;
+
+ // with null
+ fpm = new FilePersistenceManager(null);
+ assertFpm(fpm, new File(FilePersistenceManager.DEFAULT_CONFIG_DIR) );
+
+ // with a relative path
+ relPath = LOCATION_TEST;
+ fpm = new FilePersistenceManager(relPath);
+ assertFpm(fpm, new File(relPath) );
+
+ // with an absolute path
+ absPath = new File(LOCATION_TEST).getAbsolutePath();
+ fpm = new FilePersistenceManager(absPath);
+ assertFpm(fpm, new File(absPath) );
+ }
+
+
+ /**
+ * Test method for {@link org.apache.felix.cm.file.FilePersistenceManager#FilePersistenceManager(org.osgi.framework.BundleContext, java.lang.String)}.
+ */
+ public void testFilePersistenceManagerBundleContextString()
+ {
+ // variables used in these tests
+ BundleContext bundleContext;
+ FilePersistenceManager fpm;
+ String relPath;
+ String absPath;
+ File dataArea;
+
+ // first suite: no BundleContext at all
+
+ // with null
+ fpm = new FilePersistenceManager(null);
+ assertFpm(fpm, new File(FilePersistenceManager.DEFAULT_CONFIG_DIR) );
+
+ // with a relative path
+ relPath = LOCATION_TEST;
+ fpm = new FilePersistenceManager(relPath);
+ assertFpm(fpm, new File(relPath) );
+
+ // with an absolute path
+ absPath = new File(LOCATION_TEST).getAbsolutePath();
+ fpm = new FilePersistenceManager(absPath);
+ assertFpm(fpm, new File(absPath) );
+
+
+ // second suite: BundleContext without data file
+ bundleContext = new FilePersistenceManagerBundleContext(null);
+
+ // with null
+ fpm = new FilePersistenceManager(bundleContext, null);
+ assertFpm(fpm, new File(FilePersistenceManager.DEFAULT_CONFIG_DIR) );
+
+ // with a relative path
+ relPath = LOCATION_TEST;
+ fpm = new FilePersistenceManager(bundleContext, relPath);
+ assertFpm(fpm, new File(relPath) );
+
+ // with an absolute path
+ absPath = new File(LOCATION_TEST).getAbsolutePath();
+ fpm = new FilePersistenceManager(bundleContext, absPath);
+ assertFpm(fpm, new File(absPath) );
+
+
+ // third suite: BundleContext with relative data file
+ dataArea = new File(BUNDLE_DATA);
+ bundleContext = new FilePersistenceManagerBundleContext(dataArea);
+
+ // with null
+ fpm = new FilePersistenceManager(bundleContext, null);
+ assertFpm(fpm, new File(dataArea, FilePersistenceManager.DEFAULT_CONFIG_DIR) );
+
+ // with a relative path
+ relPath = LOCATION_TEST;
+ fpm = new FilePersistenceManager(bundleContext, relPath);
+ assertFpm(fpm, new File(dataArea, relPath) );
+
+ // with an absolute path
+ absPath = new File(LOCATION_TEST).getAbsolutePath();
+ fpm = new FilePersistenceManager(bundleContext, absPath);
+ assertFpm(fpm, new File(absPath) );
+
+ // fourth suite: BundleContext with absolute data file
+ dataArea = new File(BUNDLE_DATA).getAbsoluteFile();
+ bundleContext = new FilePersistenceManagerBundleContext(dataArea);
+
+ // with null
+ fpm = new FilePersistenceManager(bundleContext, null);
+ assertFpm(fpm, new File(dataArea, FilePersistenceManager.DEFAULT_CONFIG_DIR) );
+
+ // with a relative path
+ relPath = LOCATION_TEST;
+ fpm = new FilePersistenceManager(bundleContext, relPath);
+ assertFpm(fpm, new File(dataArea, relPath) );
+
+ // with an absolute path
+ absPath = new File(LOCATION_TEST).getAbsolutePath();
+ fpm = new FilePersistenceManager(bundleContext, absPath);
+ assertFpm(fpm, new File(absPath) );
+ }
+
+
+ private void assertFpm(FilePersistenceManager fpm, File expected) {
+ assertEquals( expected.getAbsoluteFile(), fpm.getLocation() );
+ }
+
+ private static final class FilePersistenceManagerBundleContext extends MockBundleContext {
+
+ private File dataArea;
+
+ private FilePersistenceManagerBundleContext( File dataArea )
+ {
+ this.dataArea = dataArea;
+ }
+
+ public File getDataFile( String path )
+ {
+ return (dataArea != null) ? new File(dataArea, path) : null;
+ }
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/file/FilePersistenceManagerTest.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/file/FilePersistenceManagerTest.java
new file mode 100644
index 0000000..93d3776
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/file/FilePersistenceManagerTest.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.file;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Vector;
+
+import junit.framework.TestCase;
+
+
+/**
+ * The FilePersistenceManagerTest
TODO
+ */
+public class FilePersistenceManagerTest extends TestCase
+{
+ private File file = new File( System.getProperty( "java.io.tmpdir" ), "config" );
+
+ private FilePersistenceManager fpm;
+
+
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ fpm = new FilePersistenceManager( file.getAbsolutePath() );
+ }
+
+
+ protected void tearDown() throws Exception
+ {
+ File[] children = file.listFiles();
+ for ( int i = 0; children != null && i < children.length; i++ )
+ {
+ children[i].delete();
+ }
+ file.delete();
+
+ super.tearDown();
+ }
+
+
+ public void testPidPlain()
+ {
+ assertEquals( "plain", FilePersistenceManager.encodePid( "plain" ) );
+ assertEquals( "plain" + File.separatorChar + "path", FilePersistenceManager.encodePid( "plain.path" ) );
+ assertEquals( "encod%00e8", FilePersistenceManager.encodePid( "encod\u00E8" ) );
+ assertEquals( "encod%00e8" + File.separatorChar + "path", FilePersistenceManager.encodePid( "encod\u00E8/path" ) );
+ assertEquals( "encode" + File.separatorChar + "%1234" + File.separatorChar + "path", FilePersistenceManager
+ .encodePid( "encode/\u1234/path" ) );
+ assertEquals( "encode" + File.separatorChar + " %0025 " + File.separatorChar + "path", FilePersistenceManager
+ .encodePid( "encode/ % /path" ) );
+ }
+
+
+ public void testCreateDir()
+ {
+ assertTrue( file.isDirectory() );
+ }
+
+
+ public void testSimple() throws IOException
+ {
+ check( "String", "String Value" );
+ check( "Integer", new Integer( 2 ) );
+ check( "Long", new Long( 2 ) );
+ check( "Float", new Float( 2 ) );
+ check( "Double", new Double( 2 ) );
+ check( "Byte", new Byte( ( byte ) 2 ) );
+ check( "Short", new Short( ( short ) 2 ) );
+ check( "Character", new Character( 'a' ) );
+ check( "Boolean", Boolean.TRUE );
+ }
+
+
+ public void testQuoting() throws IOException
+ {
+ check( "QuotingSeparators", "\\()[]{}.,=" );
+ check( "QuotingWellKnown", "BSP:\b, TAB:\t, LF:\n, FF:\f, CR:\r" );
+ check( "QuotingControl", new String( new char[]
+ { 5, 10, 32, 64 } ) );
+ }
+
+
+ public void testArray() throws IOException
+ {
+ check( "StringArray", new String[]
+ { "one", "two", "three" } );
+ check( "IntArray", new int[]
+ { 0, 1, 2 } );
+ check( "IntegerArray", new Integer[]
+ { new Integer( 0 ), new Integer( 1 ), new Integer( 2 ) } );
+ }
+
+
+ public void testVector() throws IOException
+ {
+ check( "StringVector", new Vector( Arrays.asList( new String[]
+ { "one", "two", "three" } ) ) );
+ check( "IntegerVector", new Vector( Arrays.asList( new Integer[]
+ { new Integer( 0 ), new Integer( 1 ), new Integer( 2 ) } ) ) );
+ }
+
+
+ public void testList() throws IOException
+ {
+ check( "StringList", Arrays.asList( new String[]
+ { "one", "two", "three" } ) );
+ check( "IntegerList", Arrays.asList( new Integer[]
+ { new Integer( 0 ), new Integer( 1 ), new Integer( 2 ) } ) );
+ }
+
+
+ public void testMultiValue() throws IOException
+ {
+ Dictionary props = new Hashtable();
+ props.put( "String", "String Value" );
+ props.put( "Integer", new Integer( 2 ) );
+ props.put( "Long", new Long( 2 ) );
+ props.put( "Float", new Float( 2 ) );
+ props.put( "Double", new Double( 2 ) );
+ props.put( "Byte", new Byte( ( byte ) 2 ) );
+ props.put( "Short", new Short( ( short ) 2 ) );
+ props.put( "Character", new Character( 'a' ) );
+ props.put( "Boolean", Boolean.TRUE );
+ props.put( "Array", new boolean[]
+ { true, false } );
+
+ check( "MultiValue", props );
+ }
+
+
+ private void check( String name, Object value ) throws IOException
+ {
+ Dictionary props = new Hashtable();
+ props.put( name, value );
+
+ check( name, props );
+ }
+
+
+ private void check( String pid, Dictionary props ) throws IOException
+ {
+ fpm.store( pid, props );
+
+ assertTrue( new File( file, pid + ".config" ).exists() );
+
+ Dictionary loaded = fpm.load( pid );
+ assertNotNull( loaded );
+ assertEquals( props.size(), loaded.size() );
+
+ for ( Enumeration pe = props.keys(); pe.hasMoreElements(); )
+ {
+ String key = ( String ) pe.nextElement();
+ checkValues( props.get( key ), loaded.get( key ) );
+ }
+ }
+
+
+ private void checkValues( Object value1, Object value2 )
+ {
+ assertNotNull( value2 );
+ if ( value1.getClass().isArray() )
+ {
+ assertTrue( value2.getClass().isArray() );
+ assertEquals( value1.getClass().getComponentType(), value2.getClass().getComponentType() );
+ assertEquals( Array.getLength( value1 ), Array.getLength( value2 ) );
+ for ( int i = 0; i < Array.getLength( value1 ); i++ )
+ {
+ assertEquals( Array.get( value1, i ), Array.get( value2, i ) );
+ }
+ }
+ else
+ {
+ assertEquals( value1, value2 );
+ }
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/CaseInsensitiveDictionaryTest.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/CaseInsensitiveDictionaryTest.java
new file mode 100644
index 0000000..b83bdb7
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/CaseInsensitiveDictionaryTest.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.impl;
+
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Vector;
+
+import junit.framework.TestCase;
+
+
+public class CaseInsensitiveDictionaryTest extends TestCase
+{
+
+ public void testCheckValueNull()
+ {
+ // null which must throw IllegalArgumentException
+ try
+ {
+ CaseInsensitiveDictionary.checkValue( null );
+ fail( "Expected IllegalArgumentException for null value" );
+ }
+ catch ( IllegalArgumentException iae )
+ {
+
+ }
+
+ }
+
+
+ public void testCheckValueSimple()
+ {
+ internalTestCheckValue( "String" );
+ internalTestCheckValue( new Integer( 1 ) );
+ internalTestCheckValue( new Long( 1 ) );
+ internalTestCheckValue( new Float( 1 ) );
+ internalTestCheckValue( new Double( 1 ) );
+ internalTestCheckValue( new Byte( ( byte ) 1 ) );
+ internalTestCheckValue( new Short( ( short ) 1 ) );
+ internalTestCheckValue( new Character( 'a' ) );
+ internalTestCheckValue( Boolean.TRUE );
+ }
+
+
+ public void testCheckValueSimpleArray()
+ {
+ internalTestCheckValue( new String[]
+ { "String" } );
+ internalTestCheckValue( new Integer[]
+ { new Integer( 1 ) } );
+ internalTestCheckValue( new Long[]
+ { new Long( 1 ) } );
+ internalTestCheckValue( new Float[]
+ { new Float( 1 ) } );
+ internalTestCheckValue( new Double[]
+ { new Double( 1 ) } );
+ internalTestCheckValue( new Byte[]
+ { new Byte( ( byte ) 1 ) } );
+ internalTestCheckValue( new Short[]
+ { new Short( ( short ) 1 ) } );
+ internalTestCheckValue( new Character[]
+ { new Character( 'a' ) } );
+ internalTestCheckValue( new Boolean[]
+ { Boolean.TRUE } );
+ }
+
+
+ public void testCheckValuePrimitiveArray()
+ {
+ internalTestCheckValue( new long[]
+ { 1 } );
+ internalTestCheckValue( new int[]
+ { 1 } );
+ internalTestCheckValue( new short[]
+ { 1 } );
+ internalTestCheckValue( new char[]
+ { 1 } );
+ internalTestCheckValue( new byte[]
+ { 1 } );
+ internalTestCheckValue( new double[]
+ { 1 } );
+ internalTestCheckValue( new float[]
+ { 1 } );
+ internalTestCheckValue( new boolean[]
+ { true } );
+ }
+
+
+ public void testCheckValueSimpleVector()
+ {
+ internalTestCheckValue( "String", Vector.class );
+ internalTestCheckValue( new Integer( 1 ), Vector.class );
+ internalTestCheckValue( new Long( 1 ), Vector.class );
+ internalTestCheckValue( new Float( 1 ), Vector.class );
+ internalTestCheckValue( new Double( 1 ), Vector.class );
+ internalTestCheckValue( new Byte( ( byte ) 1 ), Vector.class );
+ internalTestCheckValue( new Short( ( short ) 1 ), Vector.class );
+ internalTestCheckValue( new Character( 'a' ), Vector.class );
+ internalTestCheckValue( Boolean.TRUE, Vector.class );
+ }
+
+
+ public void testCheckValueSimpleSet()
+ {
+ internalTestCheckValue( "String", HashSet.class );
+ internalTestCheckValue( new Integer( 1 ), HashSet.class );
+ internalTestCheckValue( new Long( 1 ), HashSet.class );
+ internalTestCheckValue( new Float( 1 ), HashSet.class );
+ internalTestCheckValue( new Double( 1 ), HashSet.class );
+ internalTestCheckValue( new Byte( ( byte ) 1 ), HashSet.class );
+ internalTestCheckValue( new Short( ( short ) 1 ), HashSet.class );
+ internalTestCheckValue( new Character( 'a' ), HashSet.class );
+ internalTestCheckValue( Boolean.TRUE, HashSet.class );
+ }
+
+
+ public void testCheckValueSimpleArrayList()
+ {
+ internalTestCheckValue( "String", ArrayList.class );
+ internalTestCheckValue( new Integer( 1 ), ArrayList.class );
+ internalTestCheckValue( new Long( 1 ), ArrayList.class );
+ internalTestCheckValue( new Float( 1 ), ArrayList.class );
+ internalTestCheckValue( new Double( 1 ), ArrayList.class );
+ internalTestCheckValue( new Byte( ( byte ) 1 ), ArrayList.class );
+ internalTestCheckValue( new Short( ( short ) 1 ), ArrayList.class );
+ internalTestCheckValue( new Character( 'a' ), ArrayList.class );
+ internalTestCheckValue( Boolean.TRUE, ArrayList.class );
+ }
+
+
+ private void internalTestCheckValue( Object value, Class collectionType )
+ {
+ Collection coll;
+ try
+ {
+ coll = ( Collection ) collectionType.newInstance();
+ }
+ catch ( Throwable t )
+ {
+ throw new IllegalArgumentException( collectionType + " cannot be instantiated as a Collection" );
+ }
+
+ coll.add( value );
+ internalTestCheckValue( coll );
+ }
+
+
+ private void internalTestCheckValue( Object value )
+ {
+ assertEqualValue( value, CaseInsensitiveDictionary.checkValue( value ) );
+ }
+
+
+ private void assertEqualValue( Object expected, Object actual )
+ {
+ if ( ( expected instanceof Collection ) && ( actual instanceof Collection ) )
+ {
+ Collection eColl = ( Collection ) expected;
+ Collection aColl = ( Collection ) actual;
+ if ( eColl.size() != aColl.size() )
+ {
+ fail( "Unexpected size. expected:" + eColl.size() + ", actual: " + aColl.size() );
+ }
+
+ // create a list from the expected collection and remove
+ // all values from the actual collection, this should get
+ // an empty collection
+ List eList = new ArrayList( eColl );
+ eList.removeAll( aColl );
+ assertTrue( "Collections do not match. expected:" + eColl + ", actual: " + aColl, eList.isEmpty() );
+ }
+ else
+ {
+ assertEquals( expected, actual );
+ }
+ }
+
+
+ public void testValidKeys()
+ {
+ CaseInsensitiveDictionary.checkKey( "a" );
+ CaseInsensitiveDictionary.checkKey( "1" );
+ CaseInsensitiveDictionary.checkKey( "-" );
+ CaseInsensitiveDictionary.checkKey( "_" );
+ CaseInsensitiveDictionary.checkKey( "A" );
+ CaseInsensitiveDictionary.checkKey( "a.b.c" );
+ CaseInsensitiveDictionary.checkKey( "a.1.c" );
+ CaseInsensitiveDictionary.checkKey( "a-sample.dotted_key.end" );
+ }
+
+
+ public void testKeyDots()
+ {
+ testFailingKey( "." );
+ CaseInsensitiveDictionary.checkKey( ".a.b.c" );
+ testFailingKey( "a.b.c." );
+ testFailingKey( ".a.b.c." );
+ testFailingKey( "a..b" );
+ }
+
+
+ public void testKeyIllegalCharacters()
+ {
+ testFailingKey( null );
+ testFailingKey( "" );
+ testFailingKey( " " );
+ testFailingKey( "§" );
+ testFailingKey( "${yikes}" );
+ testFailingKey( "a key with spaces" );
+ testFailingKey( "fail:key" );
+ }
+
+
+ private void testFailingKey( String key )
+ {
+ try
+ {
+ CaseInsensitiveDictionary.checkKey( key );
+ fail( "Expected IllegalArgumentException for key [" + key + "]" );
+ }
+ catch ( IllegalArgumentException iae )
+ {
+ // expected
+ }
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/ConfigurationAdapterTest.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/ConfigurationAdapterTest.java
new file mode 100644
index 0000000..5d5bb99
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/ConfigurationAdapterTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.impl;
+
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.MockPersistenceManager;
+import org.apache.felix.cm.PersistenceManager;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.Configuration;
+
+
+public class ConfigurationAdapterTest extends TestCase
+{
+
+ private static final String SCALAR = "scalar";
+ private static final String STRING_VALUE = "String Value";
+ private static final String STRING_VALUE2 = "Another String Value";
+
+ private static final String ARRAY = "array";
+ private final String[] ARRAY_VALUE;
+
+ private static final String COLLECTION = "collection";
+ private final Collection COLLECTION_VALUE;
+
+ private static final String TEST_PID = "test.pid";
+ private static final String TEST_LOCATION = "test:location";
+
+ private final PersistenceManager pm = new MockPersistenceManager();
+ private final MockConfigurationManager configMgr = new MockConfigurationManager();
+
+ {
+ ARRAY_VALUE = new String[]
+ { STRING_VALUE };
+ COLLECTION_VALUE = new ArrayList();
+ COLLECTION_VALUE.add( STRING_VALUE );
+ }
+
+
+ private Configuration getConfiguration() throws IOException
+ {
+ ConfigurationImpl cimpl = new ConfigurationImpl( configMgr, pm, TEST_PID, null, TEST_LOCATION );
+ return new ConfigurationAdapter( null, cimpl );
+ }
+
+
+ public void testScalar() throws IOException
+ {
+ Configuration cimpl = getConfiguration();
+ Dictionary props = cimpl.getProperties();
+ assertNull( "Configuration is fresh", props );
+
+ props = new Hashtable();
+ props.put( SCALAR, STRING_VALUE );
+ cimpl.update( props );
+
+ Dictionary newProps = cimpl.getProperties();
+ assertNotNull( "Configuration is not fresh", newProps );
+ assertEquals( "Expect 2 elements", 2, newProps.size() );
+ assertEquals( "Service.pid must match", TEST_PID, newProps.get( Constants.SERVICE_PID ) );
+ assertEquals( "Scalar value must match", STRING_VALUE, newProps.get( SCALAR ) );
+ }
+
+
+ public void testArray() throws IOException
+ {
+ Configuration cimpl = getConfiguration();
+
+ Dictionary props = cimpl.getProperties();
+ assertNull( "Configuration is fresh", props );
+
+ props = new Hashtable();
+ props.put( ARRAY, ARRAY_VALUE );
+ cimpl.update( props );
+
+ Dictionary newProps = cimpl.getProperties();
+ assertNotNull( "Configuration is not fresh", newProps );
+ assertEquals( "Expect 2 elements", 2, newProps.size() );
+ assertEquals( "Service.pid must match", TEST_PID, newProps.get( Constants.SERVICE_PID ) );
+
+ Object testProp = newProps.get( ARRAY );
+ assertNotNull( testProp );
+ assertTrue( testProp.getClass().isArray() );
+ assertEquals( 1, Array.getLength( testProp ) );
+ assertEquals( STRING_VALUE, Array.get( testProp, 0 ) );
+
+ // modify the array property
+ Array.set( testProp, 0, STRING_VALUE2 );
+
+ // the array element change must not be reflected in the configuration
+ Dictionary newProps2 = cimpl.getProperties();
+ Object testProp2 = newProps2.get( ARRAY );
+ assertNotNull( testProp2 );
+ assertTrue( testProp2.getClass().isArray() );
+ assertEquals( 1, Array.getLength( testProp2 ) );
+ assertEquals( STRING_VALUE, Array.get( testProp2, 0 ) );
+ }
+
+
+ public void testCollection() throws IOException
+ {
+ Configuration cimpl = getConfiguration();
+
+ Dictionary props = cimpl.getProperties();
+ assertNull( "Configuration is fresh", props );
+
+ props = new Hashtable();
+ props.put( COLLECTION, COLLECTION_VALUE );
+ cimpl.update( props );
+
+ Dictionary newProps = cimpl.getProperties();
+ assertNotNull( "Configuration is not fresh", newProps );
+ assertEquals( "Expect 2 elements", 2, newProps.size() );
+ assertEquals( "Service.pid must match", TEST_PID, newProps.get( Constants.SERVICE_PID ) );
+
+ Object testProp = newProps.get( COLLECTION );
+ assertNotNull( testProp );
+ assertTrue( testProp instanceof Collection );
+ Collection coll = ( Collection ) testProp;
+ assertEquals( 1, coll.size() );
+ assertEquals( STRING_VALUE, coll.iterator().next() );
+
+ // modify the array property
+ coll.clear();
+ coll.add( STRING_VALUE2 );
+
+ // the array element change must not be reflected in the configuration
+ Dictionary newProps2 = cimpl.getProperties();
+ Object testProp2 = newProps2.get( COLLECTION );
+ assertNotNull( testProp2 );
+ assertTrue( testProp2 instanceof Collection );
+ Collection coll2 = ( Collection ) testProp2;
+ assertEquals( 1, coll2.size() );
+ assertEquals( STRING_VALUE, coll2.iterator().next() );
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/ConfigurationManagerTest.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/ConfigurationManagerTest.java
new file mode 100644
index 0000000..390517d
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/ConfigurationManagerTest.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.impl;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.MockBundleContext;
+import org.apache.felix.cm.MockLogService;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+
+
+public class ConfigurationManagerTest extends TestCase
+{
+
+ private PrintStream replacedStdErr;
+
+ private ByteArrayOutputStream output;
+
+
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ replacedStdErr = System.err;
+
+ output = new ByteArrayOutputStream();
+ System.setErr( new PrintStream( output ) );
+ }
+
+
+ protected void tearDown() throws Exception
+ {
+ System.setErr( replacedStdErr );
+
+ super.tearDown();
+ }
+
+
+ public void testLogNoLogService()
+ {
+ ConfigurationManager configMgr = createConfigurationManager( null );
+
+ setLogLevel( configMgr, LogService.LOG_WARNING );
+ assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_ERROR );
+ assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ // lower than error -- no output
+ setLogLevel( configMgr, LogService.LOG_ERROR - 1 );
+ assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ // minimal log level -- no output
+ setLogLevel( configMgr, Integer.MIN_VALUE );
+ assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertNoLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_INFO );
+ assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_DEBUG );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ // maximal log level -- all output
+ setLogLevel( configMgr, Integer.MAX_VALUE );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+ }
+
+
+ // this test always expects output since when using a LogService, the log
+ // level property is ignored
+ public void testLogWithLogService()
+ {
+ LogService logService = new MockLogService();
+ ConfigurationManager configMgr = createConfigurationManager( logService );
+
+ setLogLevel( configMgr, LogService.LOG_WARNING );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_ERROR );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_ERROR - 1 );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, Integer.MIN_VALUE );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_INFO );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, LogService.LOG_DEBUG );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+
+ setLogLevel( configMgr, Integer.MAX_VALUE );
+ assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null );
+ assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null );
+ assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null );
+ assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null );
+ }
+
+
+ public void testLogSetup()
+ {
+ final MockBundleContext bundleContext = new MockBundleContext();
+ ConfigurationManager configMgr = createConfigurationManager( null );
+
+ // ensure the configuration data goes to target
+ bundleContext.setProperty( "felix.cm.dir", "target/config" );
+
+ // default value is 2
+ bundleContext.setProperty( "felix.cm.loglevel", null );
+ configMgr.start( bundleContext );
+ assertEquals( 2, getLogLevel( configMgr ) );
+ configMgr.stop( bundleContext );
+
+ // illegal number yields default value
+ bundleContext.setProperty( "felix.cm.loglevel", "not-a-number" );
+ configMgr.start( bundleContext );
+ assertEquals( 2, getLogLevel( configMgr ) );
+ configMgr.stop( bundleContext );
+
+ bundleContext.setProperty( "felix.cm.loglevel", "-100" );
+ configMgr.start( bundleContext );
+ assertEquals( -100, getLogLevel( configMgr ) );
+ configMgr.stop( bundleContext );
+
+ bundleContext.setProperty( "felix.cm.loglevel", "4" );
+ configMgr.start( bundleContext );
+ assertEquals( 4, getLogLevel( configMgr ) );
+ configMgr.stop( bundleContext );
+ }
+
+
+ private void assertNoLog( ConfigurationManager configMgr, int level, String message, Throwable t )
+ {
+ try
+ {
+ configMgr.log( level, message, t );
+ assertTrue( "Expecting no log output", output.size() == 0 );
+ }
+ finally
+ {
+ // clear the output for future data
+ output.reset();
+ }
+ }
+
+
+ private void assertLog( ConfigurationManager configMgr, int level, String message, Throwable t )
+ {
+ try
+ {
+ configMgr.log( level, message, t );
+ assertTrue( "Expecting log output", output.size() > 0 );
+
+ final String expectedLog = MockLogService.toMessageLine( level, message );
+ final String actualLog = new String( output.toByteArray() );
+ assertEquals( "Log Message not correct", expectedLog, actualLog );
+
+ }
+ finally
+ {
+ // clear the output for future data
+ output.reset();
+ }
+ }
+
+
+ private static void setLogLevel( ConfigurationManager configMgr, int level )
+ {
+ final String fieldName = "logLevel";
+ try
+ {
+ Field field = configMgr.getClass().getDeclaredField( fieldName );
+ field.setAccessible( true );
+ field.setInt( configMgr, level );
+ }
+ catch ( Throwable ignore )
+ {
+ throw ( IllegalArgumentException ) new IllegalArgumentException( "Cannot set logLevel field value" )
+ .initCause( ignore );
+ }
+ }
+
+
+ private static int getLogLevel( ConfigurationManager configMgr )
+ {
+ final String fieldName = "logLevel";
+ try
+ {
+ Field field = configMgr.getClass().getDeclaredField( fieldName );
+ field.setAccessible( true );
+ return field.getInt( configMgr );
+ }
+ catch ( Throwable ignore )
+ {
+ throw ( IllegalArgumentException ) new IllegalArgumentException( "Cannot get logLevel field value" )
+ .initCause( ignore );
+ }
+ }
+
+
+ private static ConfigurationManager createConfigurationManager( final LogService logService )
+ {
+ ConfigurationManager configMgr = new ConfigurationManager();
+
+ try
+ {
+ Field field = configMgr.getClass().getDeclaredField( "logTracker" );
+ field.setAccessible( true );
+ field.set( configMgr, new ServiceTracker( new MockBundleContext(), "", null )
+ {
+ public Object getService()
+ {
+ return logService;
+ }
+ } );
+ }
+ catch ( Throwable ignore )
+ {
+ throw ( IllegalArgumentException ) new IllegalArgumentException( "Cannot set logTracker field value" )
+ .initCause( ignore );
+ }
+
+ return configMgr;
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/DynamicBindingsTest.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/DynamicBindingsTest.java
new file mode 100644
index 0000000..b3a3244
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/DynamicBindingsTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.impl;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.MockBundle;
+import org.apache.felix.cm.MockBundleContext;
+import org.apache.felix.cm.file.FilePersistenceManager;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+
+public class DynamicBindingsTest extends TestCase
+{
+
+ private File configLocation;
+
+ private File bindingsFile;
+
+ private FilePersistenceManager persistenceManager;
+
+ private static final String PID1 = "test.pid.1";
+
+ private static final String PID2 = "test.pid.2";
+
+ private static final String LOCATION1 = "test://location.1";
+
+ private static final String LOCATION2 = "test://location.2";
+
+
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ configLocation = new File( "target/config." + System.currentTimeMillis() );
+ persistenceManager = new FilePersistenceManager( configLocation.getAbsolutePath() );
+
+ bindingsFile = new File( configLocation, DynamicBindings.BINDINGS_FILE_NAME + ".config" );
+ }
+
+
+ protected void tearDown() throws Exception
+ {
+ bindingsFile.delete();
+ configLocation.delete();
+
+ super.tearDown();
+ }
+
+
+ public void test_no_bindings() throws IOException
+ {
+
+ // ensure there is no file
+ bindingsFile.delete();
+
+ final BundleContext ctx = new MockBundleContext();
+ final DynamicBindings dm = new DynamicBindings( ctx, persistenceManager );
+ final Dictionary bindings = getBindings( dm );
+
+ assertNotNull( bindings );
+ assertTrue( bindings.isEmpty() );
+ }
+
+
+ public void test_store_bindings() throws IOException
+ {
+ // ensure there is no file
+ bindingsFile.delete();
+
+ final BundleContext ctx = new MockBundleContext();
+ final DynamicBindings dm = new DynamicBindings( ctx, persistenceManager );
+
+ dm.putLocation( PID1, LOCATION1 );
+ assertEquals( LOCATION1, dm.getLocation( PID1 ) );
+
+ assertTrue( bindingsFile.exists() );
+
+ final Dictionary bindings = persistenceManager.load( DynamicBindings.BINDINGS_FILE_NAME );
+ assertNotNull( bindings );
+ assertEquals( 1, bindings.size() );
+ assertEquals( LOCATION1, bindings.get( PID1 ) );
+ }
+
+
+ public void test_store_and_load_bindings() throws IOException
+ {
+ // ensure there is no file
+ bindingsFile.delete();
+
+ // preset bindings
+ final DynamicBindings dm0 = new DynamicBindings( new MockBundleContext(), persistenceManager );
+ dm0.putLocation( PID1, LOCATION1 );
+
+ // check bindings
+ final BundleContext ctx = new DMTestMockBundleContext();
+ final DynamicBindings dm = new DynamicBindings( ctx, persistenceManager );
+
+ // API check
+ assertEquals( LOCATION1, dm.getLocation( PID1 ) );
+
+ // low level check
+ final Dictionary bindings = getBindings( dm );
+ assertNotNull( bindings );
+ assertEquals( 1, bindings.size() );
+ assertEquals( LOCATION1, bindings.get( PID1 ) );
+ }
+
+
+ public void test_store_and_load_bindings_with_cleanup() throws IOException
+ {
+ // ensure there is no file
+ bindingsFile.delete();
+
+ // preset bindings
+ final DynamicBindings dm0 = new DynamicBindings( new MockBundleContext(), persistenceManager );
+ dm0.putLocation( PID1, LOCATION1 );
+
+ // check bindings
+ final DynamicBindings dm = new DynamicBindings( new MockBundleContext(), persistenceManager );
+
+ // API check
+ assertNull( dm.getLocation( PID1 ) );
+
+ // low level check
+ final Dictionary bindings = getBindings( dm );
+ assertNotNull( bindings );
+ assertTrue( bindings.isEmpty() );
+ }
+
+
+ private static Dictionary getBindings( DynamicBindings dm )
+ {
+ try
+ {
+ final Field bindings = dm.getClass().getDeclaredField( "bindings" );
+ bindings.setAccessible( true );
+ return ( Dictionary ) bindings.get( dm );
+ }
+ catch ( Throwable t )
+ {
+ fail( "Cannot get bindings field: " + t );
+ return null;
+ }
+ }
+
+ private static class DMTestMockBundleContext extends MockBundleContext
+ {
+ public Bundle[] getBundles()
+ {
+ return new Bundle[]
+ { new MockBundle( this, LOCATION1 ) };
+ }
+ }
+}
diff --git a/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/MockConfigurationManager.java b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/MockConfigurationManager.java
new file mode 100644
index 0000000..31a8bb0
--- /dev/null
+++ b/chapter07/testing-example/ut/configadmin/src/org/apache/felix/cm/impl/MockConfigurationManager.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.cm.impl;
+
+
+public class MockConfigurationManager extends ConfigurationManager
+{
+
+ void updated( ConfigurationImpl config, boolean fireEvent )
+ {
+ // do nothing, might register the update call
+ }
+
+
+ void deleted( ConfigurationImpl config )
+ {
+ // do nothing, might register the update call
+ }
+
+
+ void revokeConfiguration( ConfigurationImpl config )
+ {
+ // do nothing, might register the update call
+ }
+
+
+ void reassignConfiguration( ConfigurationImpl config )
+ {
+ // do nothing, might register the update call
+ }
+
+
+ void log( int level, String message, Throwable t )
+ {
+ // no logging for now
+ }
+}
diff --git a/chapter08/build.xml b/chapter08/build.xml
new file mode 100644
index 0000000..f8e30c4
--- /dev/null
+++ b/chapter08/build.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/chapter08/classloading/PICK_EXAMPLE b/chapter08/classloading/PICK_EXAMPLE
new file mode 100755
index 0000000..c8f0bdf
--- /dev/null
+++ b/chapter08/classloading/PICK_EXAMPLE
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+_EXAMPLE_DIR_=`dirname "$0"`
+
+cd ${_EXAMPLE_DIR_}
+
+_OPTION_=$1
+_NOUSES_=false
+
+if [ "${_OPTION_}" = "" ]
+then
+ echo
+ echo "Example classloading issues"
+ echo "---------------------------"
+ echo
+ echo "1) ClassNotFoundException"
+ echo "2) NoClassDefFoundException"
+ echo "3) ClassCastException"
+ echo "4) No 'uses' constraints"
+ echo "5) Mismatched 'uses'"
+ echo "6) Class.forName issues"
+ echo "7) TCCL loading issues"
+ echo "0) exit"
+ echo
+
+ read -p "Choose an example (1-7): " _OPTION_
+
+ echo
+fi
+
+if [ "${_OPTION_}" = "0" ]
+then
+ exit
+fi
+
+if [ "${_OPTION_}" = "4" ]
+then
+ _NOUSES_=true
+fi
+
+ant "build_${_OPTION_}" "-Dno.uses=${_NOUSES_}"
+
+if [ "${?}" != "0" ]
+then
+ exit
+fi
+
+echo
+echo "****************************"
+echo "* *"
+echo "* Launching OSGi container *"
+echo "* *"
+echo "****************************"
+echo
+
+java -jar launcher.jar bundles
+
diff --git a/chapter08/classloading/PICK_EXAMPLE.bat b/chapter08/classloading/PICK_EXAMPLE.bat
new file mode 100755
index 0000000..3e5382f
--- /dev/null
+++ b/chapter08/classloading/PICK_EXAMPLE.bat
@@ -0,0 +1,61 @@
+@ECHO OFF
+
+SET _EXAMPLE_DIR_=%~dp0
+
+pushd %_EXAMPLE_DIR_%
+
+SET _OPTION_=%1
+SET _NOUSES_=false
+
+IF NOT "%_OPTION_%"=="" GOTO CHOSEN
+
+ ECHO.
+ ECHO.Example classloading issues
+ ECHO.---------------------------
+ ECHO.
+ ECHO.1) ClassNotFoundException
+ ECHO.2) NoClassDefFoundException
+ ECHO.3) ClassCastException
+ ECHO.4) No 'uses' constraints
+ ECHO.5) Mismatched 'uses'
+ ECHO.6) Class.forName issues
+ ECHO.7) TCCL loading issues
+ ECHO.0) exit
+ ECHO.
+
+ SET _OPTION_=
+
+ SET /P _OPTION_="Choose an example (1-7): "
+
+ ECHO.
+
+:CHOSEN
+
+IF "%_OPTION_%"=="0" GOTO FIN
+IF "%_OPTION_%"=="" GOTO FIN
+
+IF NOT "%_OPTION_%"=="4" GOTO BUILD
+
+ SET _NOUSES_=true
+
+:BUILD
+
+CALL ant "build_%_OPTION_%" "-Dno.uses=%_NOUSES_%"
+
+IF NOT "%ERRORLEVEL%"=="0" GOTO FIN
+
+ECHO.
+ECHO.****************************
+ECHO.* *
+ECHO.* Launching OSGi container *
+ECHO.* *
+ECHO.****************************
+ECHO.
+
+java -jar launcher.jar bundles
+
+:FIN
+
+popd
+
+@ECHO ON
diff --git a/chapter08/classloading/README.TXT b/chapter08/classloading/README.TXT
new file mode 100644
index 0000000..362f25a
--- /dev/null
+++ b/chapter08/classloading/README.TXT
@@ -0,0 +1,27 @@
+
+Example classloading issues
+===========================
+
+This folder contains a selection of different classloading issues you might
+encounter when using OSGi with third-party libraries or legacy code.
+
+To see a menu of the different examples, use the "PICK_EXAMPLE" script:
+
+ Example classloading issues
+ ---------------------------
+
+ 1) ClassNotFoundException
+ 2) NoClassDefFoundException
+ 3) ClassCastException
+ 4) No 'uses' constraints
+ 5) Mismatched 'uses'
+ 6) Class.forName issues
+ 7) TCCL loading issues
+ 0) exit
+
+ Choose an example (1-7):
+
+Enter a number, press return and the example should build and launch itself.
+You can also pass this directly to the script, for example "PICK_EXAMPLE 1",
+in which case the menu won't appear.
+
diff --git a/chapter08/classloading/build.xml b/chapter08/classloading/build.xml
new file mode 100644
index 0000000..9e6d916
--- /dev/null
+++ b/chapter08/classloading/build.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/class_cast_issues/build.properties b/chapter08/classloading/class_cast_issues/build.properties
new file mode 100644
index 0000000..2a8893e
--- /dev/null
+++ b/chapter08/classloading/class_cast_issues/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Classloading Tips - ClassCast example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ org.foo.hub.spi, \
+ ${module}
+
+Spoke-Class:\
+ ${module}.SpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
diff --git a/chapter08/classloading/class_cast_issues/build.xml b/chapter08/classloading/class_cast_issues/build.xml
new file mode 100644
index 0000000..fe60211
--- /dev/null
+++ b/chapter08/classloading/class_cast_issues/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/class_cast_issues/src/org/foo/hub/spi/Spoke.java b/chapter08/classloading/class_cast_issues/src/org/foo/hub/spi/Spoke.java
new file mode 100644
index 0000000..129f3b8
--- /dev/null
+++ b/chapter08/classloading/class_cast_issues/src/org/foo/hub/spi/Spoke.java
@@ -0,0 +1,10 @@
+package org.foo.hub.spi;
+
+import org.foo.hub.Message;
+
+/**
+ * Duplicate definition to cause a ClassCastException.
+ */
+public interface Spoke {
+ boolean receive(Message message);
+}
diff --git a/chapter08/classloading/class_cast_issues/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/class_cast_issues/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..4aa6ae3
--- /dev/null
+++ b/chapter08/classloading/class_cast_issues/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,26 @@
+package org.foo.spoke;
+
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Simple spoke that just echoes matching messages to the console.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+
+ if (address.matches(message.getAddress())) {
+ System.out.println("SPOKE " + address + " RECEIVED " + message);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/classloading/class_not_found/build.properties b/chapter08/classloading/class_not_found/build.properties
new file mode 100644
index 0000000..2485e5b
--- /dev/null
+++ b/chapter08/classloading/class_not_found/build.properties
@@ -0,0 +1,16 @@
+#-------------------------------------------------
+title=Classloading Tips - ClassNotFound example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ ${module}
+
+Spoke-Class:\
+ ${module}.MySpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
diff --git a/chapter08/classloading/class_not_found/build.xml b/chapter08/classloading/class_not_found/build.xml
new file mode 100644
index 0000000..7008435
--- /dev/null
+++ b/chapter08/classloading/class_not_found/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/class_not_found/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/class_not_found/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..4aa6ae3
--- /dev/null
+++ b/chapter08/classloading/class_not_found/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,26 @@
+package org.foo.spoke;
+
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Simple spoke that just echoes matching messages to the console.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+
+ if (address.matches(message.getAddress())) {
+ System.out.println("SPOKE " + address + " RECEIVED " + message);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/classloading/for_name_issues/build.properties b/chapter08/classloading/for_name_issues/build.properties
new file mode 100644
index 0000000..26f74b2
--- /dev/null
+++ b/chapter08/classloading/for_name_issues/build.properties
@@ -0,0 +1,16 @@
+#-------------------------------------------------
+title=Classloading Tips - Class.forName example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ ${module}
+
+Spoke-Class:\
+ ${module}.SpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
diff --git a/chapter08/classloading/for_name_issues/build.xml b/chapter08/classloading/for_name_issues/build.xml
new file mode 100644
index 0000000..a1170f9
--- /dev/null
+++ b/chapter08/classloading/for_name_issues/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/for_name_issues/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/for_name_issues/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..93691ee
--- /dev/null
+++ b/chapter08/classloading/for_name_issues/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,45 @@
+package org.foo.spoke;
+
+import java.lang.reflect.Method;
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Spoke that audits matching messages using the hub's own Auditor class.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+ if (address.matches(message.getAddress())) {
+
+ Class msgClazz = message.getClass();
+ String auditorName = msgClazz.getPackage().getName() + ".Auditor";
+
+ try {
+
+ // attempt to load the hub's own Auditor class using Class.forName (boo!)
+ Class auditClazz = Class.forName(auditorName);
+
+ // NOTE: loadClass is preferred as it works better with OSGi dynamics
+ // Class auditClazz = msgClazz.getClassLoader().loadClass(auditorName);
+
+ Method method = auditClazz.getDeclaredMethod("audit", Spoke.class, Message.class);
+ method.invoke(null, this, message);
+
+ return true;
+
+ } catch (Throwable e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/classloading/mismatched_uses/build.properties b/chapter08/classloading/mismatched_uses/build.properties
new file mode 100644
index 0000000..ff0d236
--- /dev/null
+++ b/chapter08/classloading/mismatched_uses/build.properties
@@ -0,0 +1,23 @@
+#-------------------------------------------------
+title=Classloading Tips - Mismatched uses example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ ${module}
+
+Spoke-Class:\
+ ${module}.SpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
+Export-Package:\
+ org.foo.hub;version="2.0"
+
+Import-Package:\
+ org.foo.hub.spi;version="[1.0,3.0)", \
+ org.foo.hub;version="[2.0,3.0)"
+
diff --git a/chapter08/classloading/mismatched_uses/build.xml b/chapter08/classloading/mismatched_uses/build.xml
new file mode 100644
index 0000000..30ea856
--- /dev/null
+++ b/chapter08/classloading/mismatched_uses/build.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/mismatched_uses/src/org/foo/hub/Message.java b/chapter08/classloading/mismatched_uses/src/org/foo/hub/Message.java
new file mode 100644
index 0000000..23662d9
--- /dev/null
+++ b/chapter08/classloading/mismatched_uses/src/org/foo/hub/Message.java
@@ -0,0 +1,11 @@
+package org.foo.hub;
+
+/**
+ * Extended definition to show how "uses" constraints can detect mis-matched APIs.
+ */
+public interface Message {
+
+ String getAddress();
+
+ String getSubject();
+}
diff --git a/chapter08/classloading/mismatched_uses/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/mismatched_uses/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..4aa6ae3
--- /dev/null
+++ b/chapter08/classloading/mismatched_uses/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,26 @@
+package org.foo.spoke;
+
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Simple spoke that just echoes matching messages to the console.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+
+ if (address.matches(message.getAddress())) {
+ System.out.println("SPOKE " + address + " RECEIVED " + message);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/classloading/no_class_def_found/build.properties b/chapter08/classloading/no_class_def_found/build.properties
new file mode 100644
index 0000000..d1c640f
--- /dev/null
+++ b/chapter08/classloading/no_class_def_found/build.properties
@@ -0,0 +1,20 @@
+#-------------------------------------------------
+title=Classloading Tips - NoClassDefFound example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ ${module}
+
+Spoke-Class:\
+ ${module}.SpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
+Import-Package:
+
+-failok: true
+
diff --git a/chapter08/classloading/no_class_def_found/build.xml b/chapter08/classloading/no_class_def_found/build.xml
new file mode 100644
index 0000000..73c730e
--- /dev/null
+++ b/chapter08/classloading/no_class_def_found/build.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/no_class_def_found/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/no_class_def_found/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..4aa6ae3
--- /dev/null
+++ b/chapter08/classloading/no_class_def_found/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,26 @@
+package org.foo.spoke;
+
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Simple spoke that just echoes matching messages to the console.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+
+ if (address.matches(message.getAddress())) {
+ System.out.println("SPOKE " + address + " RECEIVED " + message);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/classloading/no_uses_constraints/build.properties b/chapter08/classloading/no_uses_constraints/build.properties
new file mode 100644
index 0000000..e1489df
--- /dev/null
+++ b/chapter08/classloading/no_uses_constraints/build.properties
@@ -0,0 +1,23 @@
+#-------------------------------------------------
+title=Classloading Tips - No uses example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ ${module}
+
+Spoke-Class:\
+ ${module}.SpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
+Export-Package:\
+ org.foo.hub;version="2.0"
+
+Import-Package:\
+ org.foo.hub.spi;version="1.0", \
+ org.foo.hub;version="1.0"
+
diff --git a/chapter08/classloading/no_uses_constraints/build.xml b/chapter08/classloading/no_uses_constraints/build.xml
new file mode 100644
index 0000000..04a08b0
--- /dev/null
+++ b/chapter08/classloading/no_uses_constraints/build.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/no_uses_constraints/src/org/foo/hub/Message.java b/chapter08/classloading/no_uses_constraints/src/org/foo/hub/Message.java
new file mode 100644
index 0000000..23662d9
--- /dev/null
+++ b/chapter08/classloading/no_uses_constraints/src/org/foo/hub/Message.java
@@ -0,0 +1,11 @@
+package org.foo.hub;
+
+/**
+ * Extended definition to show how "uses" constraints can detect mis-matched APIs.
+ */
+public interface Message {
+
+ String getAddress();
+
+ String getSubject();
+}
diff --git a/chapter08/classloading/no_uses_constraints/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/no_uses_constraints/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..4aa6ae3
--- /dev/null
+++ b/chapter08/classloading/no_uses_constraints/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,26 @@
+package org.foo.spoke;
+
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Simple spoke that just echoes matching messages to the console.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+
+ if (address.matches(message.getAddress())) {
+ System.out.println("SPOKE " + address + " RECEIVED " + message);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/classloading/org.foo.hub.extender/build.properties b/chapter08/classloading/org.foo.hub.extender/build.properties
new file mode 100644
index 0000000..1226cf8
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.extender/build.properties
@@ -0,0 +1,18 @@
+#-------------------------------------------------
+title=Classloading Tips - Hub'n'Spoke extender
+#-------------------------------------------------
+
+module=org.foo.hub.extender
+custom=true
+
+Bundle-SymbolicName: ${module}
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.util.tracker;version="[1.3,2.0)", \
+ org.foo.hub.*;version="[1.0,2.0)"
+
diff --git a/chapter08/classloading/org.foo.hub.extender/build.xml b/chapter08/classloading/org.foo.hub.extender/build.xml
new file mode 100644
index 0000000..0a11313
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.extender/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/org.foo.hub.extender/src/org/foo/hub/extender/Activator.java b/chapter08/classloading/org.foo.hub.extender/src/org/foo/hub/extender/Activator.java
new file mode 100644
index 0000000..0cd4a97
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.extender/src/org/foo/hub/extender/Activator.java
@@ -0,0 +1,74 @@
+package org.foo.hub.extender;
+
+import java.lang.reflect.Constructor;
+import java.util.*;
+import org.foo.hub.Message;
+import org.foo.hub.api.Hub;
+import org.foo.hub.spi.Spoke;
+import org.osgi.framework.*;
+
+/**
+ * Message hub that uses the OSGi extender pattern to attach spokes.
+ * (Assumes one spoke per-bundle and one message hub per-framework.)
+ */
+public class Activator implements BundleActivator, Hub {
+
+ BundleTracker spokeTracker;
+
+ Map spokes = new HashMap();
+
+ public void start(final BundleContext ctx) {
+ spokeTracker = new BundleTracker(ctx) {
+
+ public void addedBundle(Bundle bundle) {
+ Dictionary headers = bundle.getHeaders();
+
+ // we're only interested in bundles which have spokes
+ String clazzName = (String) headers.get("Spoke-Class");
+ String spokeName = (String) headers.get("Spoke-Name");
+
+ if (clazzName != null && spokeName != null) {
+ try {
+ System.out.println("START ADDING SPOKE " + spokeName);
+
+ // load the spoke class using the bundle's own ClassLoader
+ Constructor ctor = bundle.loadClass(clazzName).getConstructor(String.class);
+ spokes.put(bundle, (Spoke) ctor.newInstance(spokeName));
+
+ } catch (Throwable e) {
+ e.printStackTrace();
+ } finally {
+ System.out.println("DONE ADDING SPOKE " + spokeName);
+ }
+ }
+ }
+
+ public void removedBundle(Bundle bundle) {
+ spokes.remove(bundle);
+ }
+ };
+
+ spokeTracker.open();
+
+ ctx.registerService(Hub.class.getName(), this, null);
+ }
+
+ public int send(Message message) {
+ int count = 0;
+
+ // broadcast message to all spokes
+ for (Spoke s : spokes.values()) {
+ if (s.receive(message)) {
+ count++; // matching address
+ }
+ }
+
+ return count;
+ }
+
+ public void stop(BundleContext ctx) {
+ spokeTracker.close();
+ spokeTracker = null;
+ spokes.clear();
+ }
+}
diff --git a/chapter08/classloading/org.foo.hub.extender/src/org/foo/hub/extender/BundleTracker.java b/chapter08/classloading/org.foo.hub.extender/src/org/foo/hub/extender/BundleTracker.java
new file mode 100644
index 0000000..fb64206
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.extender/src/org/foo/hub/extender/BundleTracker.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.hub.extender;
+
+import java.util.*;
+import org.osgi.framework.*;
+
+/**
+ * This is a very simple bundle tracker utility class that tracks active
+ * bundles. The tracker must be given a bundle context upon creation, which it
+ * uses to listen for bundle events. The bundle tracker must be opened to track
+ * objects and closed when it is no longer needed. This class is abstract, which
+ * means in order to use it you must create a subclass of it. Subclasses must
+ * implement the addedBundle() and removedBundle() methods,
+ * which can be used to perform some custom action upon the activation or
+ * deactivation of bundles. Since this tracker is quite simple, its concurrency
+ * control approach is also simplistic. This means that subclasses should take
+ * great care to ensure that their addedBundle() and
+ * removedBundle() methods are very simple and do not do anything to
+ * change the state of any bundles.
+ **/
+public abstract class BundleTracker {
+ final Set m_bundleSet = new HashSet();
+ final BundleContext m_context;
+ final SynchronousBundleListener m_listener;
+ boolean m_open;
+
+ /**
+ * Constructs a bundle tracker object that will use the specified bundle
+ * context.
+ *
+ * @param context The bundle context to use to track bundles.
+ **/
+ public BundleTracker(BundleContext context) {
+ m_context = context;
+ m_listener = new SynchronousBundleListener() {
+ public void bundleChanged(BundleEvent evt) {
+ synchronized (BundleTracker.this) {
+ if (!m_open) {
+ return;
+ }
+
+ if (evt.getType() == BundleEvent.STARTED) {
+ if (!m_bundleSet.contains(evt.getBundle())) {
+ m_bundleSet.add(evt.getBundle());
+ addedBundle(evt.getBundle());
+ }
+ } else if (evt.getType() == BundleEvent.STOPPED) {
+ if (m_bundleSet.contains(evt.getBundle())) {
+ m_bundleSet.remove(evt.getBundle());
+ removedBundle(evt.getBundle());
+ }
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns the current set of active bundles.
+ *
+ * @return The current set of active bundles.
+ **/
+ public synchronized Bundle[] getBundles() {
+ return (Bundle[]) m_bundleSet.toArray(new Bundle[m_bundleSet.size()]);
+ }
+
+ /**
+ * Call this method to start the tracking of active bundles.
+ **/
+ public synchronized void open() {
+ if (!m_open) {
+ m_open = true;
+
+ m_context.addBundleListener(m_listener);
+
+ Bundle[] bundles = m_context.getBundles();
+ for (int i = 0; i < bundles.length; i++) {
+ if (bundles[i].getState() == Bundle.ACTIVE) {
+ m_bundleSet.add(bundles[i]);
+ addedBundle(bundles[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Call this method to stop the tracking of active bundles.
+ **/
+ public synchronized void close() {
+ if (m_open) {
+ m_open = false;
+
+ m_context.removeBundleListener(m_listener);
+
+ Bundle[] bundles = (Bundle[]) m_bundleSet.toArray(new Bundle[m_bundleSet.size()]);
+ for (int i = 0; i < bundles.length; i++) {
+ if (m_bundleSet.remove(bundles[i])) {
+ removedBundle(bundles[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Subclasses must implement this method; it can be used to perform actions
+ * upon the activation of a bundle. Subclasses should keep this method
+ * implementation as simple as possible and should not cause the change in any
+ * bundle state to avoid concurrency issues.
+ *
+ * @param bundle The bundle being added to the active set.
+ **/
+ protected abstract void addedBundle(Bundle bundle);
+
+ /**
+ * Subclasses must implement this method; it can be used to perform actions
+ * upon the deactivation of a bundle. Subclasses should keep this method
+ * implementation as simple as possible and should not cause the change in any
+ * bundle state to avoid concurrency issues.
+ *
+ * @param bundle The bundle being removed from the active set.
+ **/
+ protected abstract void removedBundle(Bundle bundle);
+}
diff --git a/chapter08/classloading/org.foo.hub.test/build.properties b/chapter08/classloading/org.foo.hub.test/build.properties
new file mode 100644
index 0000000..501869e
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.test/build.properties
@@ -0,0 +1,18 @@
+#-------------------------------------------------
+title=Classloading Tips - Hub'n'Spoke testcase
+#-------------------------------------------------
+
+module=org.foo.hub.test
+custom=true
+
+Bundle-SymbolicName: ${module}
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.util.tracker;version="[1.3,2.0)", \
+ org.foo.hub.*;version="[1.0,2.0)"
+
diff --git a/chapter08/classloading/org.foo.hub.test/build.xml b/chapter08/classloading/org.foo.hub.test/build.xml
new file mode 100644
index 0000000..a6228b5
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.test/build.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/Activator.java b/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/Activator.java
new file mode 100644
index 0000000..e2f9915
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/Activator.java
@@ -0,0 +1,50 @@
+package org.foo.hub.test;
+
+import org.foo.hub.api.Hub;
+import org.osgi.framework.*;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Test bundle that sends a message whenever the hub comes online.
+ */
+public class Activator implements BundleActivator {
+
+ ServiceTracker hubTracker;
+
+ public void start(final BundleContext ctx) {
+ hubTracker = new ServiceTracker(ctx, Hub.class.getName(), null) {
+
+ /*
+ * Uncomment A, B ,and C to use the bundle ClassLoader as the Thread Context ClassLoader.
+ */
+ public Object addingService(ServiceReference reference) {
+
+ //A// ClassLoader oldTCCL = Thread.currentThread().getContextClassLoader();
+
+ try {
+
+ //B// Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+
+ Hub hub = (Hub) ctx.getService(reference);
+ hub.send(new TextMessage(".*", "Testing Testing 1, 2, 3..."));
+
+ } catch (Throwable e) {
+ e.printStackTrace();
+ } finally {
+
+ //C// Thread.currentThread().setContextClassLoader(oldTCCL);
+
+ }
+
+ return null;
+ }
+ };
+
+ hubTracker.open();
+ }
+
+ public void stop(BundleContext ctx) {
+ hubTracker.close();
+ hubTracker = null;
+ }
+}
diff --git a/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/Auditor.java b/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/Auditor.java
new file mode 100644
index 0000000..5566723
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/Auditor.java
@@ -0,0 +1,14 @@
+package org.foo.hub.test;
+
+import java.util.Date;
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Quick'n'dirty utility class, used to demonstrate reflection issues in OSGi.
+ */
+public class Auditor {
+ public static void audit(Spoke spoke, Message message) {
+ System.out.println(new Date() + " - " + spoke + " RECEIVED " + message);
+ }
+}
diff --git a/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/TextMessage.java b/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/TextMessage.java
new file mode 100644
index 0000000..c247615
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub.test/src/org/foo/hub/test/TextMessage.java
@@ -0,0 +1,30 @@
+package org.foo.hub.test;
+
+import org.foo.hub.Message;
+
+/**
+ * Simple text message implementation.
+ */
+public class TextMessage implements Message {
+
+ String address;
+
+ String contents;
+
+ public TextMessage(String address, String contents) {
+ this.address = address;
+ this.contents = contents;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getSubject() {
+ return "Unknown";
+ }
+
+ public String toString() {
+ return contents;
+ }
+}
diff --git a/chapter08/classloading/org.foo.hub/build.properties b/chapter08/classloading/org.foo.hub/build.properties
new file mode 100644
index 0000000..843431e
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub/build.properties
@@ -0,0 +1,18 @@
+#-------------------------------------------------
+title=Classloading Tips - Simple Hub'n'Spoke API
+#-------------------------------------------------
+
+module=org.foo.hub
+custom=true
+
+Bundle-SymbolicName: ${module}
+
+Export-Package:\
+ ${module}.*;version="1.0"
+
+Import-Package:\
+ ${module}.*;version="[1.0,2.0)"
+
+# -nouses: false
+-nouses: ${no.uses}
+
diff --git a/chapter08/classloading/org.foo.hub/build.xml b/chapter08/classloading/org.foo.hub/build.xml
new file mode 100644
index 0000000..d100a56
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/org.foo.hub/src/org/foo/hub/Message.java b/chapter08/classloading/org.foo.hub/src/org/foo/hub/Message.java
new file mode 100644
index 0000000..67e9238
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub/src/org/foo/hub/Message.java
@@ -0,0 +1,8 @@
+package org.foo.hub;
+
+/**
+ * An addressable message.
+ */
+public interface Message {
+ String getAddress();
+}
diff --git a/chapter08/classloading/org.foo.hub/src/org/foo/hub/api/Hub.java b/chapter08/classloading/org.foo.hub/src/org/foo/hub/api/Hub.java
new file mode 100644
index 0000000..de09c4e
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub/src/org/foo/hub/api/Hub.java
@@ -0,0 +1,10 @@
+package org.foo.hub.api;
+
+import org.foo.hub.Message;
+
+/**
+ * A hub sends messages to its spokes.
+ */
+public interface Hub {
+ int send(Message message);
+}
diff --git a/chapter08/classloading/org.foo.hub/src/org/foo/hub/spi/Spoke.java b/chapter08/classloading/org.foo.hub/src/org/foo/hub/spi/Spoke.java
new file mode 100644
index 0000000..33892e9
--- /dev/null
+++ b/chapter08/classloading/org.foo.hub/src/org/foo/hub/spi/Spoke.java
@@ -0,0 +1,10 @@
+package org.foo.hub.spi;
+
+import org.foo.hub.Message;
+
+/**
+ * A spoke receives messages from its hub.
+ */
+public interface Spoke {
+ boolean receive(Message message);
+}
diff --git a/chapter08/classloading/tccl_issues/build.properties b/chapter08/classloading/tccl_issues/build.properties
new file mode 100644
index 0000000..9181c64
--- /dev/null
+++ b/chapter08/classloading/tccl_issues/build.properties
@@ -0,0 +1,16 @@
+#-------------------------------------------------
+title=Classloading Tips - TCCL example
+#-------------------------------------------------
+
+module=org.foo.spoke
+custom=true
+
+Private-Package:\
+ ${module}
+
+Spoke-Class:\
+ ${module}.SpokeImpl
+
+Spoke-Name:\
+ ${module}.${ant.project.name}
+
diff --git a/chapter08/classloading/tccl_issues/build.xml b/chapter08/classloading/tccl_issues/build.xml
new file mode 100644
index 0000000..1770cd2
--- /dev/null
+++ b/chapter08/classloading/tccl_issues/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/classloading/tccl_issues/src/org/foo/spoke/SpokeImpl.java b/chapter08/classloading/tccl_issues/src/org/foo/spoke/SpokeImpl.java
new file mode 100644
index 0000000..47493c6
--- /dev/null
+++ b/chapter08/classloading/tccl_issues/src/org/foo/spoke/SpokeImpl.java
@@ -0,0 +1,40 @@
+package org.foo.spoke;
+
+import java.lang.reflect.Method;
+import org.foo.hub.Message;
+import org.foo.hub.spi.Spoke;
+
+/**
+ * Spoke that audits matching messages using the hub's own Auditor class.
+ */
+public class SpokeImpl implements Spoke {
+
+ String address;
+
+ public SpokeImpl(String address) {
+ this.address = address;
+ }
+
+ public boolean receive(Message message) {
+ if (address.matches(message.getAddress())) {
+
+ String auditorName = message.getClass().getPackage().getName() + ".Auditor";
+
+ try {
+
+ // attempt to load the hub's own Auditor class using the Thread Context ClassLoader
+ Class auditClazz = Thread.currentThread().getContextClassLoader().loadClass(auditorName);
+ Method method = auditClazz.getDeclaredMethod("audit", Spoke.class, Message.class);
+ method.invoke(null, this, message);
+
+ return true;
+
+ } catch (Throwable e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/chapter08/dangling-services/broken_lookup_field/build.properties b/chapter08/dangling-services/broken_lookup_field/build.properties
new file mode 100644
index 0000000..65e2b78
--- /dev/null
+++ b/chapter08/dangling-services/broken_lookup_field/build.properties
@@ -0,0 +1,16 @@
+#---------------------------------------
+title=Broken Lookup (field) Example
+#---------------------------------------
+
+module=org.foo.log
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package: \
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.service.log;version="[1.3,2.0)"
+
diff --git a/chapter08/dangling-services/broken_lookup_field/build.xml b/chapter08/dangling-services/broken_lookup_field/build.xml
new file mode 100644
index 0000000..fa5ad8f
--- /dev/null
+++ b/chapter08/dangling-services/broken_lookup_field/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter08/dangling-services/broken_lookup_field/src/org/foo/log/Activator.java b/chapter08/dangling-services/broken_lookup_field/src/org/foo/log/Activator.java
new file mode 100644
index 0000000..3662c0c
--- /dev/null
+++ b/chapter08/dangling-services/broken_lookup_field/src/org/foo/log/Activator.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.log;
+
+import org.osgi.framework.*;
+import org.osgi.service.log.LogService;
+
+/**
+ * Broken code example showing why you shouldn't store a service instance in a
+ * long-lived variable like a field. Firstly you can't tell if the service has
+ * been removed, and should not be used anymore. Secondly the strong reference
+ * to the instance will stop it from being GC'd until the client is GC'd which
+ * could be a long time after the original service bundle is uninstalled.
+ **/
+public class Activator implements BundleActivator {
+
+ LogService m_logService;
+
+ /**
+ * START LOG TEST
+ **/
+ public void start(BundleContext context) {
+
+ // find a single LogService service - notice the refactor-friendly use of Class.getName()
+ ServiceReference logServiceRef = context.getServiceReference(LogService.class.getName());
+
+ // dereference handle to get the service instance and store it in a field (not a good idea)
+ m_logService = (LogService) context.getService(logServiceRef);
+
+ // start new thread to test LogService - remember to keep bundle activator methods short!
+ startTestThread();
+ }
+
+ /**
+ * STOP LOG TEST
+ **/
+ public void stop(BundleContext context) {
+
+ stopTestThread();
+ }
+
+ // Test LogService by periodically sending a message
+ class LogServiceTest implements Runnable {
+ public void run() {
+
+ while (Thread.currentThread() == m_logTestThread) {
+ m_logService.log(LogService.LOG_INFO, "ping");
+ pauseTestThread();
+ }
+ }
+ }
+
+ //------------------------------------------------------------------------------------------
+ // The rest of this is just support code, not meant to show any particular best practices
+ //------------------------------------------------------------------------------------------
+
+ volatile Thread m_logTestThread;
+
+ void startTestThread() {
+ // start separate worker thread to run the actual tests, managed by the bundle lifecycle
+ m_logTestThread = new Thread(new LogServiceTest(), "LogService Tester");
+ m_logTestThread.setDaemon(true);
+ m_logTestThread.start();
+ }
+
+ void stopTestThread() {
+ // thread should cooperatively shutdown on the next iteration, because field is now null
+ Thread testThread = m_logTestThread;
+ m_logTestThread = null;
+ if (testThread != null) {
+ testThread.interrupt();
+ try {testThread.join();} catch (InterruptedException e) {}
+ }
+ }
+
+ void pauseTestThread() {
+ try {
+ // sleep for a bit
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {}
+ }
+}
diff --git a/chapter08/dangling-services/build.xml b/chapter08/dangling-services/build.xml
new file mode 100644
index 0000000..dd6e39c
--- /dev/null
+++ b/chapter08/dangling-services/build.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/dangling-services/org.foo.log.service/build.properties b/chapter08/dangling-services/org.foo.log.service/build.properties
new file mode 100644
index 0000000..84b6a86
--- /dev/null
+++ b/chapter08/dangling-services/org.foo.log.service/build.properties
@@ -0,0 +1,20 @@
+#---------------------------------------
+title=Dummy Log Service
+#---------------------------------------
+
+module=org.foo.log.service
+custom=true
+
+Bundle-SymbolicName: ${module}
+Bundle-Activator: ${module}.Activator
+
+Export-Package: \
+ org.osgi.service.log;version="1.3"
+
+Private-Package:\
+ ${module}
+
+Import-Package: \
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.service.log;version="[1.3,2.0)"
+
diff --git a/chapter08/dangling-services/org.foo.log.service/build.xml b/chapter08/dangling-services/org.foo.log.service/build.xml
new file mode 100644
index 0000000..99273d3
--- /dev/null
+++ b/chapter08/dangling-services/org.foo.log.service/build.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/dangling-services/org.foo.log.service/src/org/foo/log/service/Activator.java b/chapter08/dangling-services/org.foo.log.service/src/org/foo/log/service/Activator.java
new file mode 100644
index 0000000..3da47d5
--- /dev/null
+++ b/chapter08/dangling-services/org.foo.log.service/src/org/foo/log/service/Activator.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.log.service;
+
+import java.lang.reflect.*;
+import java.util.*;
+import org.osgi.framework.*;
+import org.osgi.service.log.LogService;
+
+/**
+ * Partial implementation of the OSGi LogService API.
+ **/
+public class Activator implements BundleActivator {
+
+ /**
+ * @param context The context of the bundle.
+ **/
+ public void start(BundleContext context) {
+ context.registerService(LogService.class.getName(), new DummyLogServiceFactory(), null);
+ }
+
+ /**
+ * @param context The context of the bundle.
+ **/
+ public void stop(BundleContext context) {}
+
+ /**
+ * Customize the LogService for each bundle that requests it.
+ **/
+ static class DummyLogServiceFactory implements ServiceFactory {
+
+ // currently active loggers
+ Map loggerMap = new HashMap();
+
+ public Object getService(final Bundle bundle, final ServiceRegistration registration) {
+
+ LogService service = new LogService() {
+ public void log(int level, String message) {
+
+ String tid = "thread=\"" + Thread.currentThread().getName() + "\"";
+ String bid = "bundle=" + bundle.getBundleId();
+
+ Object sid;
+ try {
+ sid = registration.getReference().getProperty(Constants.SERVICE_ID);
+ } catch (RuntimeException re) {
+ sid = "!!"; // this service is no longer valid and shouldn't be used
+ }
+
+ System.out.println("<" + sid + "> " + tid + ", " + bid + " : " + message);
+ }
+
+ public void log(int level, String message, Throwable exception) {}
+
+ public void log(ServiceReference sr, int level, String message) {}
+
+ public void log(ServiceReference sr, int level, String message, Throwable exception) {}
+ };
+
+ loggerMap.put(bundle, service); // this logger can now be used
+
+ service.log(LogService.LOG_INFO, "logging ON");
+
+ /*
+ * Instead of the real service, return a proxy that checks to see if the service is still active
+ */
+ return Proxy.newProxyInstance(LogService.class.getClassLoader(), new Class[] { LogService.class },
+ new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ LogService cachedService = (LogService) loggerMap.get(bundle);
+ if (cachedService != null) {
+ return method.invoke(cachedService, args);
+ }
+ throw new IllegalStateException("LogService has been deactivated");
+ }
+ });
+ }
+
+ public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
+ ((LogService) service).log(LogService.LOG_INFO, "logging OFF");
+
+ loggerMap.remove(bundle); // this logger shouldn't be used
+ }
+ }
+}
diff --git a/chapter08/debugging-bundles/build.xml b/chapter08/debugging-bundles/build.xml
new file mode 100644
index 0000000..83edef9
--- /dev/null
+++ b/chapter08/debugging-bundles/build.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/debugging-bundles/org.foo.paint/build.properties b/chapter08/debugging-bundles/org.foo.paint/build.properties
new file mode 100644
index 0000000..0017373
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/build.properties
@@ -0,0 +1,18 @@
+#-------------------------------------------------
+title=Paint Example - Paint Frame
+#-------------------------------------------------
+
+module=org.foo.paint
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.foo.shape;version="[6.0,7.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ org.osgi.util.tracker;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter08/debugging-bundles/org.foo.paint/build.xml b/chapter08/debugging-bundles/org.foo.paint/build.xml
new file mode 100644
index 0000000..37d3c10
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/build.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/Activator.java b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/Activator.java
new file mode 100644
index 0000000..608f3b7
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/Activator.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+import org.osgi.framework.*;
+
+/**
+ * The activator of the host application bundle. The activator creates the main
+ * application JFrame and starts tracking SimpleShape
+ * services. All activity is performed on the Swing event thread to avoid
+ * synchronization and repainting issues. Closing the application window will
+ * result in Bundle.stop() being called on the system bundle, which
+ * will cause the framework to shutdown and the JVM to exit.
+ **/
+public class Activator implements BundleActivator, Runnable {
+ private BundleContext m_context = null;
+ private PaintFrame m_frame = null;
+ private ShapeTracker m_shapetracker = null;
+
+ /**
+ * Displays the applications window and starts service tracking; everything is
+ * done on the Swing event thread to avoid synchronization and repainting
+ * issues.
+ *
+ * @param context The context of the bundle.
+ **/
+ public void start(BundleContext context) {
+ m_context = context;
+ if (SwingUtilities.isEventDispatchThread()) {
+ run();
+ } else {
+ try {
+ javax.swing.SwingUtilities.invokeAndWait(this);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Stops service tracking and disposes of the application window.
+ *
+ * @param context The context of the bundle.
+ **/
+ public void stop(BundleContext context) {
+ m_shapetracker.close();
+ final PaintFrame frame = m_frame;
+ javax.swing.SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ frame.setVisible(false);
+ frame.dispose();
+ }
+ });
+ }
+
+ /**
+ * This method actually performs the creation of the application window. It is
+ * intended to be called by the Swing event thread and should not be called
+ * directly.
+ **/
+ public void run() {
+ m_frame = new PaintFrame();
+
+ m_frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+ m_frame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent evt) {
+ try {
+ m_context.getBundle(0).stop();
+ } catch (BundleException ex) {
+ ex.printStackTrace();
+ }
+ }
+ });
+
+ m_frame.setVisible(true);
+
+ m_shapetracker = new ShapeTracker(m_context, m_frame);
+ m_shapetracker.open();
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/DefaultShape.java b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/DefaultShape.java
new file mode 100644
index 0000000..9fbff5c
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/DefaultShape.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.*;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * This class is used as a proxy to defer object creation from shape provider
+ * bundles and also as a placeholder shape when previously used shapes are no
+ * longer available. These two purposes are actually orthogonal, but were
+ * combined into a single class to reduce the number of classes in the
+ * application. The proxy-related functionality is introduced as a way to lazily
+ * create shape objects in an effort to improve performance; this level of
+ * indirection could be removed if eager creation of objects is not a concern.
+ * Since this application uses the service-based extension appraoch, lazy shape
+ * creation will only come into effect if service providers register service
+ * factories instead of directly registering SimpleShape or if they use
+ * a technology like Declarative Services or iPOJO to register services. Since
+ * the example providers register services instances directly there is no
+ * laziness in the example, but the proxy approach is still used to demonstrate
+ * how to make laziness possible and to keep it similar to the extender-based
+ * approach.
+ **/
+class DefaultShape implements SimpleShape {
+ private SimpleShape m_shape;
+ private ImageIcon m_icon;
+ private BundleContext m_context;
+ private ServiceReference m_ref;
+
+ /**
+ * This constructs a placeholder shape that draws a default icon. It is used
+ * when a previously drawn shape is no longer available.
+ **/
+ public DefaultShape() {
+ // Do nothing.
+ }
+
+ /**
+ * This constructs a proxy shape that lazily gets the shape service.
+ *
+ * @param context The bundle context to use for retrieving the shape service.
+ * @param ref The service reference of the service.
+ **/
+ public DefaultShape(BundleContext context, ServiceReference ref) {
+ m_context = context;
+ m_ref = ref;
+ }
+
+ /**
+ * This method tells the proxy to dispose of its service object; this is
+ * called when the underlying service goes away.
+ **/
+ public void dispose() {
+ if (m_shape != null) {
+ m_context.ungetService(m_ref);
+ m_context = null;
+ m_ref = null;
+ m_shape = null;
+ }
+ }
+
+ /**
+ * Implements the SimpleShape interface method. When acting as a
+ * proxy, this method gets the shape service and then uses it to draw the
+ * shape. When acting as a placeholder shape, this method draws the default
+ * icon.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ // If this is a proxy shape, instantiate the shape class
+ // and use it to draw the shape.
+ if (m_context != null) {
+ try {
+ if (m_shape == null) {
+ // Get the shape service.
+ m_shape = (SimpleShape) m_context.getService(m_ref);
+ }
+ // Draw the shape.
+ m_shape.draw(g2, p);
+ // If everything was successful, then simply return.
+ return;
+ } catch (Exception ex) {
+ // This generally should not happen, but if it does then
+ // we can just fall through and paint the default icon.
+ }
+ }
+
+ // If the proxied shape could not be drawn for any reason or if
+ // this shape is simply a placeholder, then draw the default icon.
+ if (m_icon == null) {
+ try {
+ m_icon = new ImageIcon(this.getClass().getResource("underc.png"));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ g2.setColor(Color.red);
+ g2.fillRect(0, 0, 60, 60);
+ return;
+ }
+ }
+ g2.drawImage(m_icon.getImage(), 0, 0, null);
+ }
+
+ public void setColor(Color color) {
+ m_shape.setColor(color);
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/PaintFrame.java b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/PaintFrame.java
new file mode 100644
index 0000000..c421e1c
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/PaintFrame.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.HashMap;
+import java.util.Map;
+import javax.swing.*;
+import org.foo.shape.SimpleShape;
+
+/**
+ * This class represents the main application class, which is a JFrame subclass
+ * that manages a toolbar of shapes and a drawing canvas. This class does not
+ * directly interact with the underlying OSGi framework; instead, it is injected
+ * with the available SimpleShape instances to eliminate any
+ * dependencies on the OSGi application programming interfaces.
+ **/
+public class PaintFrame extends JFrame implements MouseListener, MouseMotionListener {
+ private static final long serialVersionUID = 1L;
+ private static final int BOX = 54;
+ private JToolBar m_toolbar;
+ private JButton m_palette;
+ private String m_selected;
+ private JPanel m_panel;
+ private ShapeComponent m_selectedComponent;
+ private Map m_shapes = new HashMap();
+ private ActionListener m_reusableActionListener = new ShapeActionListener();
+ private SimpleShape m_defaultShape = new DefaultShape();
+
+ /**
+ * Default constructor that populates the main window.
+ **/
+ public PaintFrame() {
+ super("PaintFrame");
+
+ m_toolbar = new JToolBar("Toolbar");
+ m_palette = createPaletteButton();
+ m_toolbar.add(m_palette);
+ m_panel = new JPanel();
+ m_panel.setBackground(Color.WHITE);
+ m_panel.setLayout(null);
+ m_panel.setMinimumSize(new Dimension(400, 400));
+ m_panel.addMouseListener(this);
+ getContentPane().setLayout(new BorderLayout());
+ getContentPane().add(m_toolbar, BorderLayout.NORTH);
+ getContentPane().add(m_panel, BorderLayout.CENTER);
+ setSize(400, 400);
+ }
+
+ /**
+ * This method sets the currently selected shape to be used for drawing on the
+ * canvas.
+ *
+ * @param name The name of the shape to use for drawing on the canvas.
+ **/
+ public void selectShape(String name) {
+ m_selected = name;
+ }
+
+ /**
+ * Retrieves the available SimpleShape associated with the given
+ * name.
+ *
+ * @param name The name of the SimpleShape to retrieve.
+ * @return The corresponding SimpleShape instance if available or
+ * null.
+ **/
+ public SimpleShape getShape(String name) {
+ ShapeInfo info = (ShapeInfo) m_shapes.get(name);
+ if (info == null) {
+ return m_defaultShape;
+ } else {
+ return info.m_shape;
+ }
+ }
+
+ /**
+ * Injects an available SimpleShape into the drawing frame.
+ *
+ * @param name The name of the injected SimpleShape.
+ * @param icon The icon associated with the injected SimpleShape.
+ * @param shape The injected SimpleShape instance.
+ **/
+ public void addShape(String name, Icon icon, SimpleShape shape) {
+ m_shapes.put(name, new ShapeInfo(name, icon, shape));
+ JButton button = new JButton(icon);
+ button.setActionCommand(name);
+ button.setToolTipText(name);
+ button.addActionListener(m_reusableActionListener);
+
+ if (m_selected == null) {
+ button.doClick();
+ }
+
+ m_toolbar.add(button);
+ m_toolbar.validate();
+ repaint();
+ }
+
+ /**
+ * Removes a no longer available SimpleShape from the drawing frame.
+ *
+ * @param name The name of the SimpleShape to remove.
+ **/
+ public void removeShape(String name) {
+ m_shapes.remove(name);
+
+ if ((m_selected != null) && m_selected.equals(name)) {
+ m_selected = null;
+ }
+
+ for (int i = 1; i < m_toolbar.getComponentCount(); i++) {
+ JButton sb = (JButton) m_toolbar.getComponent(i);
+ if (sb.getActionCommand().equals(name)) {
+ m_toolbar.remove(i);
+ m_toolbar.invalidate();
+ validate();
+ repaint();
+ break;
+ }
+ }
+
+ if ((m_selected == null) && (m_toolbar.getComponentCount() > 1)) {
+ ((JButton) m_toolbar.getComponent(1)).doClick();
+ }
+ }
+
+ /**
+ * Implements method for the MouseListener interface to draw the
+ * selected shape into the drawing canvas.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseClicked(MouseEvent evt) {
+ if (m_selected == null) {
+ return;
+ }
+
+ if (m_panel.contains(evt.getX(), evt.getY())) {
+ ShapeComponent sc = new ShapeComponent(this, m_selected);
+ sc.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX);
+ sc.setForeground(m_palette.getBackground());
+ m_panel.add(sc, 0);
+ m_panel.validate();
+ m_panel.repaint(sc.getBounds());
+ }
+ }
+
+ /**
+ * Implements an empty method for the MouseListener interface.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseEntered(MouseEvent evt) {}
+
+ /**
+ * Implements an empty method for the MouseListener interface.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseExited(MouseEvent evt) {}
+
+ /**
+ * Implements method for the MouseListener interface to initiate
+ * shape dragging.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mousePressed(MouseEvent evt) {
+ Component c = m_panel.getComponentAt(evt.getPoint());
+ if (c instanceof ShapeComponent) {
+ m_selectedComponent = (ShapeComponent) c;
+ m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+ m_panel.addMouseMotionListener(this);
+ m_selectedComponent.repaint();
+ }
+ }
+
+ /**
+ * Implements method for the MouseListener interface to complete
+ * shape dragging.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseReleased(MouseEvent evt) {
+ if (m_selectedComponent != null) {
+ m_panel.removeMouseMotionListener(this);
+ m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ m_selectedComponent.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX);
+ m_selectedComponent.repaint();
+ m_selectedComponent = null;
+ }
+ }
+
+ /**
+ * Implements method for the MouseMotionListener interface to move a
+ * dragged shape.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseDragged(MouseEvent evt) {
+ m_selectedComponent.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX);
+ }
+
+ /**
+ * Implements an empty method for the MouseMotionListener interface.
+ *
+ * @param evt The associated mouse event.
+ **/
+ public void mouseMoved(MouseEvent evt) {}
+
+ /**
+ * Simple action listener for shape tool bar buttons that sets the drawing
+ * frame's currently selected shape when receiving an action event.
+ **/
+ private class ShapeActionListener implements ActionListener {
+ public void actionPerformed(ActionEvent evt) {
+ selectShape(evt.getActionCommand());
+ }
+ }
+
+ /**
+ * This class is used to record the various information pertaining to an
+ * available shape.
+ **/
+ private static class ShapeInfo {
+ public String m_name;
+ public Icon m_icon;
+ public SimpleShape m_shape;
+
+ public ShapeInfo(String name, Icon icon, SimpleShape shape) {
+ m_name = name;
+ m_icon = icon;
+ m_shape = shape;
+ }
+ }
+
+ /**
+ * Create a simple palette button.
+ **/
+ private JButton createPaletteButton() {
+ Icon icon = new ImageIcon(PaintFrame.class.getResource("palette.png"));
+ final JButton button = new JButton(icon);
+ button.setActionCommand("Palette");
+ button.setToolTipText("Palette");
+ button.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ button.setBackground(JColorChooser.showDialog(
+ button, "Palette", button.getBackground()));
+ }
+ });
+ return button;
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/ShapeComponent.java b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/ShapeComponent.java
new file mode 100644
index 0000000..c73c9ff
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/ShapeComponent.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import java.awt.*;
+import javax.swing.JComponent;
+import org.foo.shape.SimpleShape;
+
+/**
+ * Simple component class used to represent a drawn shape. This component uses a
+ * SimpleShape to paint its contents.
+ **/
+public class ShapeComponent extends JComponent {
+ private static final long serialVersionUID = 1L;
+ private PaintFrame m_frame;
+ private String m_shapeName;
+
+ /**
+ * Construct a component for the specified drawing frame with the specified
+ * named shape. The component acquires the named shape from the drawing frame
+ * at the time of painting, which helps it account for dynamism.
+ *
+ * @param frame The drawing frame associated with the component.
+ * @param shapeName The name of the shape to draw.
+ **/
+ public ShapeComponent(PaintFrame frame, String shapeName) {
+ m_frame = frame;
+ m_shapeName = shapeName;
+ }
+
+ /**
+ * Paints the contents of the component. The component acquires the named
+ * shape from the drawing frame at the time of painting, which helps it
+ * account for dynamism.
+ *
+ * @param g The graphics object to use for painting.
+ **/
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D) g;
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ SimpleShape shape = m_frame.getShape(m_shapeName);
+ shape.setColor(getForeground());
+ shape.draw(g2, new Point(getWidth() / 2, getHeight() / 2));
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/ShapeTracker.java b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/ShapeTracker.java
new file mode 100644
index 0000000..a1fb7a1
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/ShapeTracker.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.paint;
+
+import javax.swing.Icon;
+import javax.swing.SwingUtilities;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.*;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Extends the ServiceTracker to create a tracker for
+ * SimpleShape services. The tracker is responsible for listener for
+ * the arrival/departure of SimpleShape services and informing the
+ * application about the availability of shapes. This tracker forces all
+ * notifications to be processed on the Swing event thread to avoid
+ * synchronization and redraw issues.
+ **/
+public class ShapeTracker extends ServiceTracker {
+ // Flag indicating an added shape.
+ private static final int ADDED = 1;
+ // Flag indicating a modified shape.
+ private static final int MODIFIED = 2;
+ // Flag indicating a removed shape.
+ private static final int REMOVED = 3;
+ // The bundle context used for tracking.
+ private BundleContext m_context;
+ // The application object to notify.
+ private PaintFrame m_frame;
+
+ /**
+ * Constructs a tracker that uses the specified bundle context to track
+ * services and notifies the specified application object about changes.
+ *
+ * @param context The bundle context to be used by the tracker.
+ * @param frame The application object to notify about service changes.
+ **/
+ public ShapeTracker(BundleContext context, PaintFrame frame) {
+ super(context, SimpleShape.class.getName(), null);
+ m_context = context;
+ m_frame = frame;
+ }
+
+ /**
+ * Overrides the ServiceTracker functionality to inform the
+ * application object about the added service.
+ *
+ * @param ref The service reference of the added service.
+ * @return The service object to be used by the tracker.
+ **/
+ public Object addingService(ServiceReference ref) {
+ SimpleShape shape = new DefaultShape(m_context, ref);
+ processShapeOnEventThread(ADDED, ref, shape);
+ return shape;
+ }
+
+ /**
+ * Overrides the ServiceTracker functionality to inform the
+ * application object about the modified service.
+ *
+ * @param ref The service reference of the modified service.
+ * @param svc The service object of the modified service.
+ **/
+ public void modifiedService(ServiceReference ref, Object svc) {
+ processShapeOnEventThread(MODIFIED, ref, (SimpleShape) svc);
+ }
+
+ /**
+ * Overrides the ServiceTracker functionality to inform the
+ * application object about the removed service.
+ *
+ * @param ref The service reference of the removed service.
+ * @param svc The service object of the removed service.
+ **/
+ public void removedService(ServiceReference ref, Object svc) {
+ processShapeOnEventThread(REMOVED, ref, (SimpleShape) svc);
+ ((DefaultShape) svc).dispose();
+ }
+
+ /**
+ * Processes a received service notification from the ServiceTracker,
+ * forcing the processing of the notification onto the Swing event thread if
+ * it is not already on it.
+ *
+ * @param action The type of action associated with the notification.
+ * @param ref The service reference of the corresponding service.
+ * @param shape The service object of the corresponding service.
+ **/
+ private void processShapeOnEventThread(int action, ServiceReference ref, SimpleShape shape) {
+ if ((m_context.getBundle(0).getState() & (Bundle.STARTING | Bundle.ACTIVE)) == 0) {
+ return;
+ }
+
+ try {
+ if (SwingUtilities.isEventDispatchThread()) {
+ processShape(action, ref, shape);
+ } else {
+ SwingUtilities.invokeAndWait(new ShapeRunnable(action, ref, shape));
+ }
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ /**
+ * Actually performs the processing of the service notification. Invokes the
+ * appropriate callback method on the application object depending on the
+ * action type of the notification.
+ *
+ * @param action The type of action associated with the notification.
+ * @param ref The service reference of the corresponding service.
+ * @param shape The service object of the corresponding service.
+ **/
+ private void processShape(int action, ServiceReference ref, SimpleShape shape) {
+ String name = (String) ref.getProperty(SimpleShape.NAME_PROPERTY);
+
+ switch (action) {
+ case MODIFIED:
+ m_frame.removeShape(name);
+ // Purposely let this fall through to the 'add' case to
+ // reload the service.
+
+ case ADDED:
+ Icon icon = (Icon) ref.getProperty(SimpleShape.ICON_PROPERTY);
+ m_frame.addShape(name, icon, shape);
+ break;
+
+ case REMOVED:
+ m_frame.removeShape(name);
+ break;
+ }
+ }
+
+ /**
+ * Simple class used to process service notification handling on the Swing
+ * event thread.
+ **/
+ private class ShapeRunnable implements Runnable {
+ private int m_action;
+ private ServiceReference m_ref;
+ private SimpleShape m_shape;
+
+ /**
+ * Constructs an object with the specified action, service reference, and
+ * service object for processing on the Swing event thread.
+ *
+ * @param action The type of action associated with the notification.
+ * @param ref The service reference of the corresponding service.
+ * @param shape The service object of the corresponding service.
+ **/
+ public ShapeRunnable(int action, ServiceReference ref, SimpleShape shape) {
+ m_action = action;
+ m_ref = ref;
+ m_shape = shape;
+ }
+
+ /**
+ * Calls the processShape() method.
+ **/
+ public void run() {
+ processShape(m_action, m_ref, m_shape);
+ }
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/palette.png b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/palette.png
new file mode 100644
index 0000000..29ffc24
Binary files /dev/null and b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/palette.png differ
diff --git a/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/underc.png b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/underc.png
new file mode 100644
index 0000000..425cdb9
Binary files /dev/null and b/chapter08/debugging-bundles/org.foo.paint/src/org/foo/paint/underc.png differ
diff --git a/chapter08/debugging-bundles/org.foo.shape.circle/build.properties b/chapter08/debugging-bundles/org.foo.shape.circle/build.properties
new file mode 100644
index 0000000..bcd66be
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.circle/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Paint Example - Circle Shape
+#-------------------------------------------------
+
+module=org.foo.shape.circle
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.foo.shape;version="[6.0,7.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter08/debugging-bundles/org.foo.shape.circle/build.xml b/chapter08/debugging-bundles/org.foo.shape.circle/build.xml
new file mode 100644
index 0000000..1f9cb3b
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.circle/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/Activator.java b/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/Activator.java
new file mode 100644
index 0000000..87376f5
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/Activator.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.circle;
+
+import java.util.Hashtable;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * This class implements a simple bundle activator for the circle
+ * SimpleShape service. This activator simply creates an instance of
+ * the circle service object and registers it with the service registry along
+ * with the service properties indicating the service's name and icon.
+ **/
+public class Activator implements BundleActivator {
+ private BundleContext m_context = null;
+
+ /**
+ * Implements the BundleActivator.start() method, which registers the
+ * circle SimpleShape service.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void start(BundleContext context) {
+ m_context = context;
+ Hashtable dict = new Hashtable();
+ dict.put(SimpleShape.NAME_PROPERTY, "Circle");
+ dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass().getResource("circle.png")));
+ m_context.registerService(SimpleShape.class.getName(), new Circle(), dict);
+ }
+
+ /**
+ * Implements the BundleActivator.start() method, which does nothing.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/Circle.java b/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/Circle.java
new file mode 100644
index 0000000..f6f6e16
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/Circle.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.circle;
+
+import java.awt.*;
+import java.awt.geom.Ellipse2D;
+import org.foo.shape.SimpleShape;
+
+public class Circle implements SimpleShape {
+
+ Color m_color = Color.RED;
+
+ /**
+ * Implements the SimpleShape.draw() method for painting the shape.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ int x = p.x - 25;
+ int y = p.y - 25;
+ GradientPaint gradient = new GradientPaint(x, y, m_color, x + 50, y, Color.WHITE);
+ g2.setPaint(gradient);
+ g2.fill(new Ellipse2D.Double(x, y, 50, 50));
+ BasicStroke wideStroke = new BasicStroke(2.0f);
+ g2.setColor(Color.black);
+ g2.setStroke(wideStroke);
+ g2.draw(new Ellipse2D.Double(x, y, 50, 50));
+ }
+
+ public void setColor(Color color) {
+ m_color = color;
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/circle.png b/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/circle.png
new file mode 100644
index 0000000..3d4887e
Binary files /dev/null and b/chapter08/debugging-bundles/org.foo.shape.circle/src/org/foo/shape/circle/circle.png differ
diff --git a/chapter08/debugging-bundles/org.foo.shape.square/build.properties b/chapter08/debugging-bundles/org.foo.shape.square/build.properties
new file mode 100644
index 0000000..d8ee81b
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.square/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Paint Example - Square Shape
+#-------------------------------------------------
+
+module=org.foo.shape.square
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.foo.shape;version="[6.0,7.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter08/debugging-bundles/org.foo.shape.square/build.xml b/chapter08/debugging-bundles/org.foo.shape.square/build.xml
new file mode 100644
index 0000000..25bf2d6
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.square/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/Activator.java b/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/Activator.java
new file mode 100644
index 0000000..5ad618c
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/Activator.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.square;
+
+import java.util.Hashtable;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * This class implements a simple bundle activator for the square
+ * SimpleShape service. This activator simply creates an instance of
+ * the square service object and registers it with the service registry along
+ * with the service properties indicating the service's name and icon.
+ **/
+public class Activator implements BundleActivator {
+ private BundleContext m_context = null;
+
+ /**
+ * Implements the BundleActivator.start() method, which registers the
+ * square SimpleShape service.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void start(BundleContext context) {
+ m_context = context;
+ Hashtable dict = new Hashtable();
+ dict.put(SimpleShape.NAME_PROPERTY, "Square");
+ dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass().getResource("square.png")));
+ m_context.registerService(SimpleShape.class.getName(), new Square(), dict);
+ }
+
+ /**
+ * Implements the BundleActivator.start() method, which does nothing.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/Square.java b/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/Square.java
new file mode 100644
index 0000000..0662993
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/Square.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.square;
+
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+import org.foo.shape.SimpleShape;
+
+public class Square implements SimpleShape {
+
+ Color m_color = Color.BLUE;
+
+ /**
+ * Implements the SimpleShape.draw() method for painting the shape.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ int x = p.x - 25;
+ int y = p.y - 25;
+ GradientPaint gradient = new GradientPaint(x, y, m_color, x + 50, y, Color.WHITE);
+ g2.setPaint(gradient);
+ g2.fill(new Rectangle2D.Double(x, y, 50, 50));
+ BasicStroke wideStroke = new BasicStroke(2.0f);
+ g2.setColor(Color.black);
+ g2.setStroke(wideStroke);
+ g2.draw(new Rectangle2D.Double(x, y, 50, 50));
+ }
+
+ public void setColor(Color color) {
+ m_color = color;
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/square.png b/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/square.png
new file mode 100644
index 0000000..3f24cfc
Binary files /dev/null and b/chapter08/debugging-bundles/org.foo.shape.square/src/org/foo/shape/square/square.png differ
diff --git a/chapter08/debugging-bundles/org.foo.shape.triangle/build.properties b/chapter08/debugging-bundles/org.foo.shape.triangle/build.properties
new file mode 100644
index 0000000..ef18285
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.triangle/build.properties
@@ -0,0 +1,17 @@
+#-------------------------------------------------
+title=Paint Example - Triangle Shape
+#-------------------------------------------------
+
+module=org.foo.shape.triangle
+custom=true
+
+Bundle-Activator: ${module}.Activator
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.foo.shape;version="[6.0,7.0)", \
+ org.osgi.framework;version="[1.3,2.0)", \
+ javax.swing
+
diff --git a/chapter08/debugging-bundles/org.foo.shape.triangle/build.xml b/chapter08/debugging-bundles/org.foo.shape.triangle/build.xml
new file mode 100644
index 0000000..7b3faf8
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.triangle/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/Activator.java b/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/Activator.java
new file mode 100644
index 0000000..392f53a
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/Activator.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.triangle;
+
+import java.util.Hashtable;
+import javax.swing.ImageIcon;
+import org.foo.shape.SimpleShape;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * This class implements a simple bundle activator for the triangle
+ * SimpleShape service. This activator simply creates an instance of
+ * the triangle service object and registers it with the service registry along
+ * with the service properties indicating the service's name and icon.
+ **/
+public class Activator implements BundleActivator {
+ private BundleContext m_context = null;
+
+ /**
+ * Implements the BundleActivator.start() method, which registers the
+ * triangle SimpleShape service.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void start(BundleContext context) {
+ m_context = context;
+ Hashtable dict = new Hashtable();
+ dict.put(SimpleShape.NAME_PROPERTY, "Triangle");
+ dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass().getResource("triangle.png")));
+ m_context.registerService(SimpleShape.class.getName(), new Triangle(), dict);
+ }
+
+ /**
+ * Implements the BundleActivator.start() method, which does nothing.
+ *
+ * @param context The context for the bundle.
+ **/
+ public void stop(BundleContext context) {}
+}
diff --git a/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/Triangle.java b/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/Triangle.java
new file mode 100644
index 0000000..6cb6d55
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/Triangle.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape.triangle;
+
+import java.awt.*;
+import java.awt.geom.GeneralPath;
+import org.foo.shape.SimpleShape;
+
+public class Triangle implements SimpleShape {
+
+ Color m_color = Color.GREEN;
+
+ /**
+ * Implements the SimpleShape.draw() method for painting the shape.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the triangle.
+ **/
+ public void draw(Graphics2D g2, Point p) {
+ int x = p.x - 25;
+ int y = p.y - 25;
+ GradientPaint gradient = new GradientPaint(x, y, m_color, x + 50, y, Color.WHITE);
+ g2.setPaint(gradient);
+ int[] xcoords = { x + 25, x, x + 50 };
+ int[] ycoords = { y, y + 50, y + 50 };
+ GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, xcoords.length);
+ polygon.moveTo(x + 25, y);
+ for (int i = 0; i < xcoords.length; i++) {
+ polygon.lineTo(xcoords[i], ycoords[i]);
+ }
+ polygon.closePath();
+ g2.fill(polygon);
+ BasicStroke wideStroke = new BasicStroke(2.0f);
+ g2.setColor(Color.black);
+ g2.setStroke(wideStroke);
+ g2.draw(polygon);
+ }
+
+ public void setColor(Color color) {
+ m_color = color;
+ }
+}
diff --git a/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/triangle.png b/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/triangle.png
new file mode 100644
index 0000000..46a5288
Binary files /dev/null and b/chapter08/debugging-bundles/org.foo.shape.triangle/src/org/foo/shape/triangle/triangle.png differ
diff --git a/chapter08/debugging-bundles/org.foo.shape/build.properties b/chapter08/debugging-bundles/org.foo.shape/build.properties
new file mode 100644
index 0000000..9846301
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape/build.properties
@@ -0,0 +1,13 @@
+#-------------------------------------------------
+title=Paint Example - Shape API
+#-------------------------------------------------
+
+module=org.foo.shape
+custom=true
+
+Export-Package: \
+ ${module};version="6.0"
+
+Import-Package: \
+ ${module};version="[6.0,7.0)"
+
diff --git a/chapter08/debugging-bundles/org.foo.shape/build.xml b/chapter08/debugging-bundles/org.foo.shape/build.xml
new file mode 100644
index 0000000..8d0de0a
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/chapter08/debugging-bundles/org.foo.shape/src/org/foo/shape/SimpleShape.java b/chapter08/debugging-bundles/org.foo.shape/src/org/foo/shape/SimpleShape.java
new file mode 100644
index 0000000..eb4009e
--- /dev/null
+++ b/chapter08/debugging-bundles/org.foo.shape/src/org/foo/shape/SimpleShape.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.foo.shape;
+
+import java.awt.*;
+
+/**
+ * This interface defines the SimpleShape service. This service is used
+ * to draw shapes. It has two service properties:
+ *
+ * - simple.shape.name - A String name for the shape.
+ * - simple.shape.icon - An Icon for the shape.
+ *
+ **/
+public interface SimpleShape {
+
+ /**
+ * A service property for the name of the shape.
+ **/
+ public static final String NAME_PROPERTY = "simple.shape.name";
+
+ /**
+ * A service property for the icon of the shape.
+ **/
+ public static final String ICON_PROPERTY = "simple.shape.icon";
+
+ /**
+ * Draw this shape at the given position.
+ *
+ * @param g2 The graphics object used for painting.
+ * @param p The position to paint the shape.
+ **/
+ public void draw(Graphics2D g2, Point p);
+
+ /**
+ * Change the color used to shade the shape.
+ *
+ * @param color The color used to shade the shape.
+ **/
+ public void setColor(Color color);
+}
diff --git a/chapter08/memory-leaks/build.xml b/chapter08/memory-leaks/build.xml
new file mode 100644
index 0000000..03726ba
--- /dev/null
+++ b/chapter08/memory-leaks/build.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/memory-leaks/org.foo.leaky/build.properties b/chapter08/memory-leaks/org.foo.leaky/build.properties
new file mode 100644
index 0000000..ea78c86
--- /dev/null
+++ b/chapter08/memory-leaks/org.foo.leaky/build.properties
@@ -0,0 +1,16 @@
+#-------------------------------------------------
+title=Memory Leaks - Leaky bundle
+#-------------------------------------------------
+
+module=org.foo.leaky
+custom=true
+
+Bundle-Activator: ${module}.Activator
+Bundle-Version: 1.${build.number}
+
+Private-Package:\
+ ${module}
+
+Import-Package:\
+ org.osgi.framework;version="[1.3,2.0)"
+
diff --git a/chapter08/memory-leaks/org.foo.leaky/build.xml b/chapter08/memory-leaks/org.foo.leaky/build.xml
new file mode 100644
index 0000000..75c2471
--- /dev/null
+++ b/chapter08/memory-leaks/org.foo.leaky/build.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chapter08/memory-leaks/org.foo.leaky/src/org/foo/leaky/Activator.java b/chapter08/memory-leaks/org.foo.leaky/src/org/foo/leaky/Activator.java
new file mode 100644
index 0000000..8dd22c9
--- /dev/null
+++ b/chapter08/memory-leaks/org.foo.leaky/src/org/foo/leaky/Activator.java
@@ -0,0 +1,82 @@
+package org.foo.leaky;
+
+import org.osgi.framework.*;
+
+/**
+ * To run this example, first build it with ant then type:
+ *
+ * java -verbose:gc -jar launcher.jar bundles
+ *
+ * You should see the example start with GC trace enabled:
+ *
+ * [GC 896K->136K(5056K), 0.0029125 secs]
+ * ...
+ * ->
+ *
+ * If you try to update the bundle you should see a leak:
+ *
+ * [Full GC 320K->320K(5056K), 0.0280867 secs]
+ * -> update 1
+ * [GC 16979K->16717K(21444K), 0.0014941 secs]
+ * [Full GC 16717K->16714K(21444K), 0.0256443 secs]
+ * -> update 1
+ * [GC 33378K->33118K(46424K), 0.0009662 secs]
+ * [Full GC 33118K->33118K(46424K), 0.0268457 secs]
+ * -> update 1
+ * [GC 49777K->49514K(59424K), 0.0009663 secs]
+ * [Full GC 49514K->49463K(59424K), 0.0929226 secs]
+ * [Full GC 49463K->49452K(65088K), 0.0452441 secs]
+ * java.lang.OutOfMemoryError: Java heap space
+ * ->
+ *
+ * Uncomment the call to remove(), rebuild and try again:
+ *
+ * [Full GC 320K->320K(5056K), 0.0238252 secs]
+ * -> update 1
+ * [GC 16973K->16717K(21444K), 0.0021946 secs]
+ * [Full GC 16717K->16717K(21444K), 0.0296025 secs]
+ * -> update 1
+ * [GC 33373K->33114K(46428K), 0.0010646 secs]
+ * [Full GC[Unloading class org.foo.leaky.Activator$Data]
+ * [Unloading class org.foo.leaky.Activator$1]
+ * [Unloading class org.foo.leaky.Activator]
+ * 33114K->16721K(46428K), 0.0557759 secs]
+ *
+ * The leak is now gone and the classes are unloaded.
+ */
+public class Activator implements BundleActivator {
+
+ /*
+ * 8Mb data object
+ */
+ static class Data {
+ StringBuffer data = new StringBuffer(8 * 1024 * 1024);
+ }
+
+ /*
+ * This example relies on a "feature" of the Java5 ThreadLocal implementation
+ * where stale map entries are only cleared if set() or remove() is called on
+ * another ThreadLocal for the same thread - and in the worst case even this
+ * is not guaranteed to purge all stale map entries.
+ *
+ * As we shall soon see, missing out the remove() call in stop means that the
+ * data object will be kept alive indefinitely because we don't use any other
+ * ThreadLocal in our example. This in turn keeps our ClassLoader alive.
+ *
+ * Calling remove() in stop forces the underlying map entry to be cleared and
+ * means the bundle's ClassLoader can now be collected on each update/refresh.
+ */
+ static final ThreadLocal leak = new ThreadLocal() {
+ protected Object initialValue() {
+ return new Data();
+ };
+ };
+
+ public void start(BundleContext ctx) {
+ leak.get();
+ }
+
+ public void stop(BundleContext ctx) {
+ // leak.remove();
+ }
+}