You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by cz...@apache.org on 2018/04/16 12:59:01 UTC
svn commit: r1829267 - in /felix/trunk/eventadmin/impl: ./
src/main/java/org/apache/felix/eventadmin/impl/handler/
src/main/java/org/apache/felix/eventadmin/impl/util/
src/test/java/org/apache/felix/eventadmin/impl/
src/test/java/org/apache/felix/event...
Author: cziegeler
Date: Mon Apr 16 12:59:01 2018
New Revision: 1829267
URL: http://svn.apache.org/viewvc?rev=1829267&view=rev
Log:
FELIX-5738 : EventAdmin IgnoreTopic config. property doesn't support wildcards
Added:
felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java (with props)
felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/
felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/
felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java (with props)
Modified:
felix/trunk/eventadmin/impl/changelog.txt
felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java
felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerTracker.java
Modified: felix/trunk/eventadmin/impl/changelog.txt
URL: http://svn.apache.org/viewvc/felix/trunk/eventadmin/impl/changelog.txt?rev=1829267&r1=1829266&r2=1829267&view=diff
==============================================================================
--- felix/trunk/eventadmin/impl/changelog.txt (original)
+++ felix/trunk/eventadmin/impl/changelog.txt Mon Apr 16 12:59:01 2018
@@ -4,6 +4,8 @@ Changes in 1.5.0
* [FELIX-5733] - Update implementation to EventAdmin 1.4 (R7)
** Improvement
* [FELIX-5813] - EventAdmin async threads should be named
+** Bug
+ * [FELIX-5738] - EventAdmin IgnoreTopic config. property doesn't support wildcards
Changes from 1.4.6 to 1.4.8
Modified: felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java
URL: http://svn.apache.org/viewvc/felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java?rev=1829267&r1=1829266&r2=1829267&view=diff
==============================================================================
--- felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java (original)
+++ felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java Mon Apr 16 12:59:01 2018
@@ -18,10 +18,10 @@
*/
package org.apache.felix.eventadmin.impl.handler;
-import org.apache.felix.eventadmin.impl.handler.EventHandlerTracker.Matcher;
import org.apache.felix.eventadmin.impl.tasks.AsyncDeliverTasks;
import org.apache.felix.eventadmin.impl.tasks.DefaultThreadPool;
import org.apache.felix.eventadmin.impl.tasks.SyncDeliverTasks;
+import org.apache.felix.eventadmin.impl.util.Matchers;
import org.osgi.framework.BundleContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
@@ -51,7 +51,7 @@ public class EventAdminImpl implements E
private final SyncDeliverTasks m_sendManager;
// matchers for ignore topics
- private Matcher[] m_ignoreTopics;
+ private Matchers.Matcher[] m_ignoreTopics;
/**
* The constructor of the <tt>EventAdmin</tt> implementation.
@@ -76,7 +76,7 @@ public class EventAdminImpl implements E
this.tracker.open();
m_sendManager = new SyncDeliverTasks(syncPool, timeout);
m_postManager = new AsyncDeliverTasks(asyncPool, m_sendManager);
- m_ignoreTopics = EventHandlerTracker.createMatchers(ignoreTopics);
+ m_ignoreTopics = Matchers.createEventTopicMatchers(ignoreTopics);
}
/**
@@ -100,7 +100,7 @@ public class EventAdminImpl implements E
boolean result = true;
if ( this.m_ignoreTopics != null )
{
- for(final Matcher m : this.m_ignoreTopics)
+ for(final Matchers.Matcher m : this.m_ignoreTopics)
{
if ( m.match(event.getTopic()) )
{
@@ -169,7 +169,7 @@ public class EventAdminImpl implements E
this.tracker.update(ignoreTimeout, requireTopic);
this.m_sendManager.update(timeout);
this.tracker.open();
- this.m_ignoreTopics = EventHandlerTracker.createMatchers(ignoreTopics);
+ this.m_ignoreTopics = Matchers.createEventTopicMatchers(ignoreTopics);
}
/**
Modified: felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerTracker.java
URL: http://svn.apache.org/viewvc/felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerTracker.java?rev=1829267&r1=1829266&r2=1829267&view=diff
==============================================================================
--- felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerTracker.java (original)
+++ felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerTracker.java Mon Apr 16 12:59:01 2018
@@ -18,7 +18,6 @@
*/
package org.apache.felix.eventadmin.impl.handler;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@@ -27,6 +26,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
+import org.apache.felix.eventadmin.impl.util.Matchers;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.event.Event;
@@ -61,9 +61,9 @@ public class EventHandlerTracker extends
super(context, EventHandler.class.getName(), null);
// we start with empty collections
- this.matchingAllEvents = new CopyOnWriteArrayList<EventHandlerProxy>();
- this.matchingTopic = new ConcurrentHashMap<String, List<EventHandlerProxy>>();
- this.matchingPrefixTopic = new ConcurrentHashMap<String, List<EventHandlerProxy>>();
+ this.matchingAllEvents = new CopyOnWriteArrayList<>();
+ this.matchingTopic = new ConcurrentHashMap<>();
+ this.matchingPrefixTopic = new ConcurrentHashMap<>();
}
/**
@@ -71,38 +71,7 @@ public class EventHandlerTracker extends
* @param ignoreTimeout
*/
public void update(final String[] ignoreTimeout, final boolean requireTopic) {
- final Matcher[] ignoreTimeoutMatcher;
- if ( ignoreTimeout == null || ignoreTimeout.length == 0 )
- {
- ignoreTimeoutMatcher = null;
- }
- else
- {
- ignoreTimeoutMatcher = new Matcher[ignoreTimeout.length];
- for(int i=0;i<ignoreTimeout.length;i++)
- {
- String value = ignoreTimeout[i];
- if ( value != null )
- {
- value = value.trim();
- }
- if ( value != null && value.length() > 0 )
- {
- if ( value.endsWith(".") )
- {
- ignoreTimeoutMatcher[i] = new PackageMatcher(value.substring(0, value.length() - 1));
- }
- else if ( value.endsWith("*") )
- {
- ignoreTimeoutMatcher[i] = new SubPackageMatcher(value.substring(0, value.length() - 1));
- }
- else
- {
- ignoreTimeoutMatcher[i] = new ClassMatcher(value);
- }
- }
- }
- }
+ final Matchers.Matcher[] ignoreTimeoutMatcher = Matchers.createPackageMatchers(ignoreTimeout);
this.handlerContext = new HandlerContext(this.context, ignoreTimeoutMatcher, requireTopic);
}
@@ -146,7 +115,7 @@ public class EventHandlerTracker extends
{
return;
}
- proxies = new CopyOnWriteArrayList<EventHandlerProxy>();
+ proxies = new CopyOnWriteArrayList<>();
proxyListMap.put(key, proxies);
}
@@ -231,7 +200,7 @@ public class EventHandlerTracker extends
public Collection<EventHandlerProxy> getHandlers(final Event event) {
final String topic = event.getTopic();
- final Set<EventHandlerProxy> handlers = new HashSet<EventHandlerProxy>();
+ final Set<EventHandlerProxy> handlers = new HashSet<>();
// Add all handlers matching everything
this.checkHandlerAndAdd(handlers, this.matchingAllEvents, event);
@@ -275,125 +244,6 @@ public class EventHandlerTracker extends
}
}
- static Matcher[] createMatchers(final String[] config)
- {
- final Matcher[] matchers;
- if ( config == null || config.length == 0 )
- {
- matchers = null;
- }
- else
- {
- final List<Matcher> list = new ArrayList<EventHandlerTracker.Matcher>();
- for(int i=0;i<config.length;i++)
- {
- String value = config[i];
- if ( value != null )
- {
- value = value.trim();
- }
- if ( value != null && value.length() > 0 )
- {
- if ( value.endsWith(".") )
- {
- list.add(new PackageMatcher(value.substring(0, value.length() - 1)));
- }
- else if ( value.endsWith("*") )
- {
- if ( value.equals("*") )
- {
- return new Matcher[] {new MatcherAll()};
- }
- list.add(new SubPackageMatcher(value.substring(0, value.length() - 1)));
- }
- else
- {
- list.add(new ClassMatcher(value));
- }
- }
- }
- if ( list.size() > 0 )
- {
- matchers = list.toArray(new Matcher[list.size()]);
- }
- else
- {
- matchers = null;
- }
- }
- return matchers;
- }
-
- /**
- * The matcher interface for checking if timeout handling
- * is disabled for the handler.
- * Matching is based on the class name of the event handler.
- */
- static interface Matcher
- {
- boolean match(String className);
- }
-
- /** Match all. */
- private static final class MatcherAll implements Matcher
- {
- @Override
- public boolean match(final String className)
- {
- return true;
- }
- }
-
- /** Match a package. */
- private static final class PackageMatcher implements Matcher
- {
- private final String m_packageName;
-
- public PackageMatcher(final String name)
- {
- m_packageName = name;
- }
- @Override
- public boolean match(final String className)
- {
- final int pos = className.lastIndexOf('.');
- return pos > -1 && className.substring(0, pos).equals(m_packageName);
- }
- }
-
- /** Match a package or sub package. */
- private static final class SubPackageMatcher implements Matcher
- {
- private final String m_packageName;
-
- public SubPackageMatcher(final String name)
- {
- m_packageName = name + '.';
- }
- @Override
- public boolean match(final String className)
- {
- final int pos = className.lastIndexOf('.');
- return pos > -1 && className.substring(0, pos + 1).startsWith(m_packageName);
- }
- }
-
- /** Match a class name. */
- private static final class ClassMatcher implements Matcher
- {
- private final String m_className;
-
- public ClassMatcher(final String name)
- {
- m_className = name;
- }
- @Override
- public boolean match(final String className)
- {
- return m_className.equals(className);
- }
- }
-
/**
* The context object passed to the proxies.
*/
@@ -403,13 +253,13 @@ public class EventHandlerTracker extends
public final BundleContext bundleContext;
/** The matchers for ignore timeout handling. */
- public final Matcher[] ignoreTimeoutMatcher;
+ public final Matchers.Matcher[] ignoreTimeoutMatcher;
/** Is a topic required. */
public final boolean requireTopic;
public HandlerContext(final BundleContext bundleContext,
- final Matcher[] ignoreTimeoutMatcher,
+ final Matchers.Matcher[] ignoreTimeoutMatcher,
final boolean requireTopic)
{
this.bundleContext = bundleContext;
Added: felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java
URL: http://svn.apache.org/viewvc/felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java?rev=1829267&view=auto
==============================================================================
--- felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java (added)
+++ felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java Mon Apr 16 12:59:01 2018
@@ -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.eventadmin.impl.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class Matchers {
+
+ private static final char SEP_TOPIC = '/';
+ private static final char SEP_PCK = '.';
+
+ public static Matcher[] createEventTopicMatchers(final String[] config)
+ {
+ final Matcher[] matchers;
+ if ( config == null || config.length == 0 )
+ {
+ matchers = null;
+ }
+ else
+ {
+ final List<Matcher> list = new ArrayList<>();
+ for(int i=0;i<config.length;i++)
+ {
+ String value = config[i];
+ if ( value != null )
+ {
+ value = value.trim();
+ }
+ if ( value != null && value.length() > 0 )
+ {
+ if ( value.endsWith(".") )
+ {
+ list.add(new PackageMatcher(value.substring(0, value.length() - 1), SEP_TOPIC));
+ }
+ else if ( value.endsWith("*") )
+ {
+ if ( value.equals("*") )
+ {
+ return new Matcher[] {new MatcherAll()};
+ }
+ list.add(new SubPackageMatcher(value.substring(0, value.length() - 1), SEP_TOPIC));
+ }
+ else
+ {
+ list.add(new ClassMatcher(value));
+ }
+ }
+ }
+ if ( list.size() > 0 )
+ {
+ matchers = list.toArray(new Matcher[list.size()]);
+ }
+ else
+ {
+ matchers = null;
+ }
+ }
+ return matchers;
+ }
+
+ public static Matcher[] createPackageMatchers(final String[] ignoreTimeout)
+ {
+ final Matchers.Matcher[] ignoreTimeoutMatcher;
+ if ( ignoreTimeout == null || ignoreTimeout.length == 0 )
+ {
+ ignoreTimeoutMatcher = null;
+ }
+ else
+ {
+ ignoreTimeoutMatcher = new Matchers.Matcher[ignoreTimeout.length];
+ for(int i=0;i<ignoreTimeout.length;i++)
+ {
+ String value = ignoreTimeout[i];
+ if ( value != null )
+ {
+ value = value.trim();
+ }
+ if ( value != null && value.length() > 0 )
+ {
+ if ( value.endsWith(".") )
+ {
+ ignoreTimeoutMatcher[i] = new PackageMatcher(value.substring(0, value.length() - 1), SEP_PCK);
+ }
+ else if ( value.endsWith("*") )
+ {
+ ignoreTimeoutMatcher[i] = new SubPackageMatcher(value.substring(0, value.length() - 1), SEP_PCK);
+ }
+ else
+ {
+ ignoreTimeoutMatcher[i] = new ClassMatcher(value);
+ }
+ }
+ }
+ }
+ return ignoreTimeoutMatcher;
+ }
+
+ /**
+ * The matcher interface for checking if timeout handling
+ * is disabled for the handler.
+ * Matching is based on the class name of the event handler.
+ */
+ public interface Matcher
+ {
+ boolean match(String className);
+ }
+
+ /** Match all. */
+ private static final class MatcherAll implements Matcher
+ {
+ @Override
+ public boolean match(final String className)
+ {
+ return true;
+ }
+ }
+
+ /** Match a package. */
+ private static final class PackageMatcher implements Matcher
+ {
+ private final String packageName;
+
+ private final char sep;
+
+
+ public PackageMatcher(final String name, final char sep)
+ {
+ this.packageName = name;
+ this.sep = sep;
+ }
+ @Override
+ public boolean match(final String className)
+ {
+ final int pos = className.lastIndexOf(sep);
+ return pos > -1 && className.substring(0, pos).equals(packageName);
+ }
+ }
+
+ /** Match a package or sub package. */
+ private static final class SubPackageMatcher implements Matcher
+ {
+ private final String packageName;
+
+ private final char sep;
+
+ public SubPackageMatcher(final String name, final char sep)
+ {
+ this.packageName = name + sep;
+ this.sep = sep;
+ }
+
+ @Override
+ public boolean match(final String className)
+ {
+ final int pos = className.lastIndexOf(sep);
+ return pos > -1 && className.substring(0, pos + 1).startsWith(packageName);
+ }
+ }
+
+ /** Match a class name. */
+ private static final class ClassMatcher implements Matcher
+ {
+ private final String m_className;
+
+ public ClassMatcher(final String name)
+ {
+ m_className = name;
+ }
+ @Override
+ public boolean match(final String className)
+ {
+ return m_className.equals(className);
+ }
+ }
+}
Propchange: felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: felix/trunk/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java
------------------------------------------------------------------------------
svn:keywords = author date id revision rev url
Added: felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java?rev=1829267&view=auto
==============================================================================
--- felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java (added)
+++ felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java Mon Apr 16 12:59:01 2018
@@ -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.apache.felix.eventadmin.impl.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class MatchersTest {
+
+ @Test public void testPackageMatchersNoConfig()
+ {
+ assertNull(Matchers.createPackageMatchers(null));
+ assertNull(Matchers.createPackageMatchers(new String[0]));
+ }
+
+ @Test public void testPackageMatchersExact()
+ {
+ final Matchers.Matcher[] m = Matchers.createPackageMatchers(new String[] {"org.apache.felix.Foo"});
+ assertNotNull(m);
+ assertEquals(1, m.length);
+ assertNotNull(m[0]);
+
+ assertTrue(m[0].match("org.apache.felix.Foo"));
+ assertFalse(m[0].match("org.apache.felix.Bar"));
+ assertFalse(m[0].match("org.apache.felix.Foo$1"));
+ assertFalse(m[0].match("org.apache.felix.Foo.Test"));
+ assertFalse(m[0].match("org.apache.felix"));
+ }
+
+ @Test public void testPackageMatchersPackage()
+ {
+ final Matchers.Matcher[] m = Matchers.createPackageMatchers(new String[] {"org.apache.felix."});
+ assertNotNull(m);
+ assertEquals(1, m.length);
+ assertNotNull(m[0]);
+
+ assertTrue(m[0].match("org.apache.felix.Foo"));
+ assertTrue(m[0].match("org.apache.felix.Bar"));
+ assertTrue(m[0].match("org.apache.felix.Foo$1"));
+ assertFalse(m[0].match("org.apache.felix.Foo.Test"));
+ assertFalse(m[0].match("org.apache.felix"));
+ }
+
+ @Test public void testPackageMatchersSubPackage()
+ {
+ final Matchers.Matcher[] m = Matchers.createPackageMatchers(new String[] {"org.apache.felix*"});
+ assertNotNull(m);
+ assertEquals(1, m.length);
+ assertNotNull(m[0]);
+
+ assertTrue(m[0].match("org.apache.felix.Foo"));
+ assertTrue(m[0].match("org.apache.felix.Bar"));
+ assertTrue(m[0].match("org.apache.felix.Foo$1"));
+ assertTrue(m[0].match("org.apache.felix.Foo.Test"));
+ assertFalse(m[0].match("org.apache.felix"));
+ }
+
+ @Test public void testTopicMatchersNoConfig()
+ {
+ assertNull(Matchers.createEventTopicMatchers(null));
+ assertNull(Matchers.createEventTopicMatchers(new String[0]));
+ }
+
+ @Test public void testTopicMatchersExact()
+ {
+ final Matchers.Matcher[] m = Matchers.createEventTopicMatchers(new String[] {"org/apache/felix/Foo"});
+ assertNotNull(m);
+ assertEquals(1, m.length);
+ assertNotNull(m[0]);
+
+ assertTrue(m[0].match("org/apache/felix/Foo"));
+ assertFalse(m[0].match("org/apache/felix/Bar"));
+ assertFalse(m[0].match("org/apache/felix/Foo$1"));
+ assertFalse(m[0].match("org/apache/felix/Foo/Test"));
+ assertFalse(m[0].match("org/apache/felix"));
+ }
+
+ @Test public void testTopicMatchersPackage()
+ {
+ final Matchers.Matcher[] m = Matchers.createEventTopicMatchers(new String[] {"org/apache/felix."});
+ assertNotNull(m);
+ assertEquals(1, m.length);
+ assertNotNull(m[0]);
+
+ assertTrue(m[0].match("org/apache/felix/Foo"));
+ assertTrue(m[0].match("org/apache/felix/Bar"));
+ assertTrue(m[0].match("org/apache/felix/Foo$1"));
+ assertFalse(m[0].match("org/apache/felix/Foo/Test"));
+ assertFalse(m[0].match("org/apache/felix"));
+ }
+
+ @Test public void testTopicMatchersSubPackage()
+ {
+ final Matchers.Matcher[] m = Matchers.createEventTopicMatchers(new String[] {"org/apache/felix*"});
+ assertNotNull(m);
+ assertEquals(1, m.length);
+ assertNotNull(m[0]);
+
+ assertTrue(m[0].match("org/apache/felix/Foo"));
+ assertTrue(m[0].match("org/apache/felix/Bar"));
+ assertTrue(m[0].match("org/apache/felix/Foo$1"));
+ assertTrue(m[0].match("org/apache/felix/Foo/Test"));
+ assertFalse(m[0].match("org/apache/felix"));
+ }
+}
Propchange: felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: felix/trunk/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java
------------------------------------------------------------------------------
svn:keywords = author date id revision rev url