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: + * + **/ +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: + *

+ **/ +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(); + } +}